Moxy is a reliability layer for Redis-style queues. It adds leases, ACKs, visibility timeouts, and expiration recovery so tasks do not silently disappear when a worker dies.
READY -> PROCESSING -> ACKED
READY -> PROCESSING -> EXPIRED -> REQUEUED -> READY
Moxy is an early-stage alpha project being built from the inside out: first the correctness model, then the network/protocol layer, and only later transparent Redis proxying and persistence. The current repo is already useful as a compact Go reference for reliable queue internals.
Moxy is v0.1.0-alpha software. It is useful for learning, experiments, and
small reliability-model prototypes, but it is not production-hardened yet.
Plain Redis list consumption often starts with LPOP. It is fast, simple, and
dangerous:
- A worker pops a task.
- The worker crashes before completing it.
- Redis has already removed the task.
- The task is gone.
Moxy replaces pop-and-pray delivery with leased delivery. A worker receives a temporary lease; if it ACKs, the task is completed. If it vanishes, the task is requeued after the lease expires.
| Capability | Standard Redis List Consumer | Moxy |
|---|---|---|
| Worker crash after fetch | Task can disappear | Task remains recoverable |
| Visibility timeout | Build it yourself | Native lease expiration |
| ACK semantics | Build it yourself | MOXY.ACK lease_id |
| Expired task recovery | Manual scripts or side tables | Built into the engine |
| Testable core logic | Usually buried in worker code | Dedicated core package |
| Backend choices | Redis list only | MemoryQueue and RedisQueue |
| Protocol status | Redis commands today | RESP command path for Moxy commands |
Run the local recovery demo:
go run ./cmd/moxy-recovery-demoIt simulates a worker fetching a task, disappearing before ACK, and the reaper putting that task back into ready storage.
To render a terminal GIF with VHS:
vhs demo/recovery.tapeThe tape writes docs/assets/recovery.gif.
- In-memory queue backend with
READYandPROCESSINGstorage. - Redis queue backend using
go-redis/v9. - Atomic Redis
CompleteandRequeueoperations with Lua scripts. - Single-queue lease coordinator in
internal/core. - Multi-queue service layer in
internal/service. - Protocol-neutral command handler in
internal/command. - RESP2 reader/writer in
internal/resp. - Redis-compatible TCP command server for Moxy commands and
PING. - Background expiration reaper.
- Shared backend contract tests for MemoryQueue and RedisQueue.
- Opt-in Redis integration tests.
flowchart LR
Client["RESP client<br/>redis-cli / Go client"]
Server["internal/server<br/>TCP accept loop"]
RESP["internal/resp<br/>RESP2 reader/writer"]
Protocol["internal/protocol<br/>RESP adapter"]
Command["internal/command<br/>Protocol-neutral commands"]
Service["internal/service<br/>Multiple named queues"]
Core["internal/core<br/>Lease coordination"]
Queue["internal/queue<br/>Backend interface"]
Memory["MemoryQueue"]
Redis["RedisQueue"]
Client --> Server
Server --> RESP
Server --> Protocol
Protocol --> Command
Command --> Service
Service --> Core
Core --> Queue
Queue --> Memory
Queue --> Redis
core.Engine coordinates one queue. service.Service owns the map of queue names
to engines. Queue backends own task storage; the core engine owns lease metadata and
expiration scheduling.
See ARCHITECTURE.md for the deeper system notes. See docs/redis-production-caveats.md before using the Redis backend for anything beyond experimentation.
The command layer is protocol-neutral Go code. The TCP/RESP server translates wire input into these commands and translates command responses back to RESP.
MOXY.ENQUEUE queue payload
MOXY.FETCH queue timeout_ms
MOXY.ACK lease_id
MOXY.STATS queue
Example:
svc := service.New(func(queueName string) queue.Backend {
return queue.NewMemoryQueue()
})
handler := command.NewHandler(svc)
enqueue, _ := handler.Handle(command.Command{
Name: command.EnqueueName,
Args: []string{"emails", "send welcome email"},
})
fetch, _ := handler.Handle(command.Command{
Name: command.FetchName,
Args: []string{"emails", "30000"},
})
_, _ = handler.Handle(command.Command{
Name: command.AckName,
Args: []string{fetch.LeaseID},
})
_ = enqueueMemoryQueue is the default backend used by core.NewEngine and the demo binary.
It is deterministic, easy to test, and useful for validating lease behavior.
RedisQueue stores ready and processing tasks in Redis lists:
moxy:{queue}:readymoxy:{queue}:processingmoxy:{queue}:dead
Acquire uses LMOVE ready processing RIGHT LEFT. Complete and Requeue use Lua
scripts so finding a task by ID and removing or moving it happens atomically inside
Redis.
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
backend := queue.NewRedisQueue(client, "emails")go test ./...
go run ./cmd/moxy --addr 127.0.0.1:6380 --backend memory
go run ./cmd/moxy-recovery-demoMemory backend:
go run ./cmd/moxy --addr 127.0.0.1:6380 --backend memoryRedis backend:
go run ./cmd/moxy --addr 127.0.0.1:6380 --backend redis --redis-addr localhost:6379Testing with redis-cli:
redis-cli -p 6380 PING
redis-cli -p 6380 MOXY.ENQUEUE jobs hello
redis-cli -p 6380 MOXY.FETCH jobs 30000
redis-cli -p 6380 MOXY.ACK <lease_id>
redis-cli -p 6380 MOXY.STATS jobsMOXY.FETCH returns a null bulk string when the named queue has no ready task.
That makes an empty queue a normal condition instead of a server failure.
This is not full Redis proxying yet. Only PING and MOXY.* commands are
supported. Normal Redis commands such as GET, SET, and INCR are not passed
through to Redis in this phase.
Redis tests are opt-in so normal development does not require a running Redis server.
PowerShell:
$env:MOXY_REDIS_INTEGRATION='1'
$env:MOXY_REDIS_ADDR='localhost:6379'
go test ./internal/queue -run Redis -count=1 -v
go test ./internal/core -run Redis -count=1 -vShell:
MOXY_REDIS_INTEGRATION=1 MOXY_REDIS_ADDR=localhost:6379 \
go test ./internal/queue -run Redis -count=1 -vgo mod tidy
go test ./...
go vet ./...
go test ./internal/core -count=100
go test ./internal/queue -count=100
go test ./internal/service -count=100
go test ./internal/command -count=100
go test ./internal/resp -count=100
go test ./internal/protocol -count=100
go test ./internal/server -count=100Race testing is deferred until the local Windows development environment has cgo/GCC available.
Moxy is still single-node and backend-adapter based. These are intentionally not implemented yet:
- Transparent Redis proxying
- Full Redis command pass-through
- WAL
- snapshots
- crash recovery
- Redis Streams
- distributed coordination
- additional Redis-compatible commands beyond
PINGandMOXY.*
- Keep hardening the command/service boundary.
- Add observability-friendly stats and structured errors.
- Add transparent Redis pass-through after the Moxy command path stays boring.
- Add persistence and crash recovery after the in-memory semantics remain boring.
Moxy is released under the MIT License.
The boring part is the point: a queue reliability layer should be legible, testable, and conservative before it becomes networked.