A small reservation engine for unique inventory, written in Go.
The use case is the "add to cart locks the item, and the lock auto-releases if you don't check out" pattern. You're selling one-of-a-kind things, a cut diamond, a concert seat, a specific serial number, and two shoppers must never hold the same item at once. lockd is the lock primitive for that, built from scratch instead of leaning on Redis so the concurrency model is fully in your own process and your own control.
The whole engine is one key, one holder, qty=1. It is deliberately not a general key-value store. The API is four operations:
- acquire a key for a holder, with a TTL
- renew to extend the TTL while you still hold it
- release when you're done
- fence to read the current fencing token
If a holder goes quiet and the TTL lapses, the lease expires on its own and the next waiter in line gets the key.
In-process:
eng := lockd.New(lockd.Config{Shards: 16})
defer eng.Close()
ctx := context.Background()
g, err := eng.Acquire(ctx, "stone-42", "cart-7", 30*time.Second)
if err != nil {
// key is held by someone else, or ctx was cancelled while queued
}
// hold it, renew while the shopper is active
g, _ = eng.Renew("stone-42", "cart-7", g.Token, 30*time.Second)
// release on checkout (or just let the TTL expire)
eng.Release("stone-42", "cart-7", g.Token)Acquire blocks while the key is held and serves waiters in FIFO order. Pass a
context with a deadline or cancel to bound the wait; a cancelled acquire gives
up its place in the queue immediately.
go run ./cmd/lockd-server -addr :8080 -wal lockd.wal -metrics
curl -s -XPOST localhost:8080/v1/acquire \
-d '{"key":"stone-42","holder":"cart-7","ttl_ms":30000,"wait_ms":2000}'
# {"key":"stone-42","holder":"cart-7","token":1,"expires_at_unix_ms":...}
curl -s 'localhost:8080/v1/fence?key=stone-42'
# {"key":"stone-42","held":true,"token":1}
curl -s -XPOST localhost:8080/v1/release \
-d '{"key":"stone-42","holder":"cart-7","token":1}'
wait_ms of 0 (or omitted) makes acquire non-blocking: a held key returns 409
right away instead of queueing. With -wal set the server journals every grant
and replays the log on the next start.
Every grant carries a token that strictly increases per key. The token is the defense against a paused or partitioned client. If process A acquires the key, stalls long enough for its lease to expire, and process B then acquires the same key, A is holding a now-stale token. When A wakes up and tries to act on the resource it locked, it presents the old token; whatever sits downstream (the database write, the order placement) compares tokens and rejects the smaller one. That is how a single-node lock stays safe across client pauses.
lease/ lease model and per-key state machine (no I/O)
clock/ injectable clock, real and fake
. engine: sharded actors, timing wheel, FIFO queue, fencing
wal/ append-only write-ahead log
transport/httpapi/ HTTP/JSON server
metrics/ optional Prometheus collector
cmd/lockd-server/ runnable binary
go test ./... -race
go test . -bench . -benchmem
The suite includes table-driven unit tests, a property test over randomized operation interleavings, a crash-and-recover test against a file-backed WAL, and a linearizability check (via porcupine) that asserts the lease register admits a valid sequential ordering under concurrency.
See DESIGN.md for why it's built the way it is, and the limitations.