- Docs
- Security
- Locks and idempotency
Locks and idempotency
Guard critical sections with distributed locks and make retried jobs and webhooks take effect once.
Use lock for mutual exclusion and idempotency for dedupe. Locker offers TryAcquire, blocking Acquire (polls every retry_interval, default 50ms), Extend, and ownership-checked Unlock. Store reserves a key with Begin and records the outcome with Complete, so retries replay the stored result.
Quick start
Section titled “Quick start”Both default to memory: single process only. Register the adapters before resolving, since the container wires nothing.
package main
import ( "context" "fmt" "time"
idemmemory "github.com/zenta-dev/zever/adapters/idempotency/memory" lockmemory "github.com/zenta-dev/zever/adapters/lock/memory" "github.com/zenta-dev/zever/config" "github.com/zenta-dev/zever/container" "github.com/zenta-dev/zever/core/idempotency")
func main() { lockmemory.Register() idemmemory.Register() c := container.New(config.Default()) lk, err := c.Lock() if err != nil { panic(err) } idem, err := c.Idempotency() if err != nil { panic(err) }
ctx := context.Background() lease, ok, err := lk.TryAcquire(ctx, "booking:room-1:2026-01-01:2026-01-03", 30*time.Second) fmt.Println(ok, err) if ok { defer lease.Unlock(ctx) } out, err := idem.Begin(ctx, "booking:req-1", idempotency.BeginOptions{Fingerprint: []byte("guest|room-1")}) fmt.Println(out.Replay, err) if !out.Replay { _ = idem.Complete(ctx, "booking:req-1", []byte("guest|room-1"), []byte(`{"status":"confirmed"}`)) }}true <nil>false <nil>A second Begin with the same key and fingerprint prints true <nil> with the stored bytes.
Guard a booking path
Section titled “Guard a booking path”The bookings handler runs RateLimit.Allow, then Idempotency.Begin (booking:<key>, fingerprint guest|space|start|end), then Lock.TryAcquire on booking:<space>:<start>:<end> with a 30s TTL while the overlap check, charge, and insert run.
| Outcome | Status |
|---|---|
| Replay of a finished request | Stored bytes returned. |
ErrInProgress |
409. |
ErrKeyMismatch |
400. |
Key the lock on the contended range and the idempotency key on a stable external ID (webhook event ID, job dedupe key), never raw payload bytes. Idempotency fails closed.
Share state across replicas
Section titled “Share state across replicas”memory is invisible to other replicas, so a second instance can double-book or duplicate webhook effects. Move both to redis, or idempotency to db.
LOCK_ADAPTER=redisLOCK_TTL=30sLOCK_RETRY_INTERVAL=50msLOCK_PREFIX=bookings:IDEMPOTENCY_ADAPTER=redisIDEMPOTENCY_TTL=24hLOCK_ADAPTER=redisLOCK_TTL=30sLOCK_RETRYINTERVAL=50msIDEMPOTENCY_ADAPTER=redisIDEMPOTENCY_TTL=24hThe lock takes Redis connection keys at the top level (addr, url, password, db, tls), while idempotency nests them under redis: (redis.addr, redis.prefix). Lock Validate is a no-op; each adapter validates its own fields. Idempotency rejects a negative ttl (zero means 24h) and bad Redis addr/prefix. Keys cap at 255 bytes (MaxKeyLen). Adapters: adapters/lock/{memory,redis} and adapters/idempotency/{memory,redis,db} (db via dsn, dedicated_pool). Keys: lock, idempotency.
Production checklist
Section titled “Production checklist”Lock.Close(ctx)takes a context;Store.Close()does not. Neither releases held leases or reservations; locks expire on their TTL (default 30s), so plan expiry.- Fingerprints may carry PII; keep them and raw option maps out of logs (
cfg.RedactedServices()).
Troubleshooting
Section titled “Troubleshooting”409 dates already booked with no real overlap
A prior attempt holds the lock until its 30s TTL lapses, or a crashed worker never unlocked. Wait out the TTL or scope the key tighter; always defer Unlock.
409 booking in progress never clears
A Begin without a matching Complete stays pending until the TTL. Run Complete on every post-reservation path, or Forget the key.
400 idempotency key mismatch
The same key arrived with a different fingerprint (key reuse across payloads). Key on a per-attempt ID (header or body idempotency_key), not guest plus dates.