goblin_core — redis-py-shaped clients over Goblin's SBE transports

goblin_core is a Python client for goblin-core that talks to the server over typed SBE using shared-memory rings, optional RDMA or ExaSock, and optional Aeron UDP/IPC. The API mirrors redis-py, so existing code reads the same:

from goblin_core import Redis

r = Redis("/tmp/a", decode_responses=True)   # server: goblin-core --ring /tmp/a 64kb
r.set("user:42", "alice")
r.get("user:42")                              # 'alice'
r.zadd("board", {"alice": 10, "bob": 7})
r.zrange("board", 0, -1, withscores=True)     # [('bob', 7.0), ('alice', 10.0)]

An Aeron-enabled build adds two classes without changing the command surface:

from goblin_core import AeronIpcRedis, AeronUdpRedis, HAS_AERON

local = AeronIpcRedis(aeron_directory="/run/user/1000/aeron")
remote = AeronUdpRedis(
    "server.example:40123", "server.example:40124",
    aeron_directory="/run/user/1000/aeron",
)

The transport, busy-polling, and SBE encode/decode are a nanobind (>= 2.12) C++23 extension; the redis-py surface is a thin Python layer on top.

How the fast path works

What's implemented

Only the redis-py methods whose commands goblin-core actually implements are present — there is no lpush/sadd/xadd, because the server has no lists, sets, or streams. Covered: connection (ping, echo, info), strings (get/set with ex/px/nx/xx/keepttl/get, incr/decr/incrby/incrbyfloat, append, strlen, getrange/setrange, mset/mget, getset, setnx, getdel), keys/TTL (delete, exists, type, expire/pexpire/expireat/ pexpireat, ttl/pttl, persist, expiretime/pexpiretime), hashes (hset/hget/hmget/hgetall/hkeys/hvals/hdel/hlen/hexists/hstrlen/ hincrby/hsetnx), sorted sets (zadd, zcard, zrange/zrevrange with withscores/desc, zrank/zrevrank, zrem, zremrangebyscore, zscore), and scripting (eval, evalsha, script_load). execute_command(*args) reaches anything else.

GOBLIN.* native commands (no redis-py equivalent) are exposed as extension methods: cad, cas, caexpire, increx, incrbound, decrpos, hcad, hsetgt, zwindow, claim.

decode_responses=True returns str instead of bytes; RESP - errors raise goblin_core.ResponseError; a missing ring or a timed-out reply raises goblin_core.RingError.

Build

Needs nanobind >= 2.12 and a C++23 compiler.

python3 -m venv .venv
.venv/bin/pip install "nanobind>=2.12"

# plain CMake build (drops the extension next to goblin_core/__init__.py):
cmake -S python -B python/build -DPython_EXECUTABLE=$(.venv/bin/python -c 'import sys;print(sys.executable)')
cmake --build python/build

# or a pip install (scikit-build-core):
.venv/bin/pip install ./python

For Aeron, first build the dependency with ../scripts/build-aeron.sh, then add:

cmake -S python -B python/build-aeron \
  -DPython_EXECUTABLE=$(.venv/bin/python -c 'import sys;print(sys.executable)') \
  -DGOBLIN_CORE_ENABLE_AERON=ON \
  -DGOBLIN_CORE_AERON_ROOT="$HOME/opt/aeron-1.51.0-$(uname -m)"
cmake --build python/build-aeron

Test

Start nothing by hand — the test launches a server itself:

PYTHONPATH=python .venv/bin/python python/tests/test_ring_roundtrip.py ./build/goblin-core

PYTHONPATH=python .venv/bin/python python/tests/test_aeron_roundtrip.py \
  ./build-aeron/goblin-core "$AERON_PREFIX/bin/aeronmd_s"

Caveats