fencekit
Redis-backed idempotency and fenced distributed locks for background jobs.
Celery with acks_late redelivers work after a worker crash. Redis gives you
primitives (SET NX, Lua). fencekit wires them into three tested pieces:
| Piece | Problem it solves |
|---|---|
IdempotencyGuard |
Double-start / redelivery of the same logical job |
DistributedLock + fencing token |
Two workers on the same resource at once |
FenceGate / fenced_update |
Stale lock holder overwriting newer state after TTL expiry |
Extracted from ChessMate (Celery + Redis + Postgres). See DESIGN.md for guarantees, non-guarantees, and crash semantics.
Install
pip install fencekit
pip install "fencekit[django]" # optional Django QuerySet helper
Requires Redis 6+ (tested with Redis 7) and Python 3.10+.
Quick example
from datetime import timedelta
from redis import Redis
from fencekit import (
DistributedLock,
FenceGate,
IdempotencyGuard,
IdempotencyResultMissing,
fenced_update,
idempotency_key,
)
r = Redis.from_url("redis://localhost:6379/0", decode_responses=True)
guard = IdempotencyGuard(r)
lock = DistributedLock(r)
fence = FenceGate(r)
def analyze_batch(job) -> dict | None:
key = idempotency_key(
{"game_ids": ["abc", "def"], "engine": "sf16"},
namespace="analysis",
)
if not guard.try_begin(key, ttl=timedelta(hours=24)):
try:
return guard.get_result(key) # prior outcome on redelivery
except IdempotencyResultMissing:
return None # still pending or done without a memo
handle = lock.acquire(f"analysis:{job.pk}", ttl=timedelta(minutes=5))
try:
fence.set_if_fresh(handle.token, f"analysis:{job.pk}:status", "running")
fenced_update(
type(job).objects.filter(pk=job.pk),
handle.token,
updates={"progress": 50, "status": "running"},
)
result = {"report_id": job.pk, "status": "done"}
guard.mark_done(key, result=result, ttl=timedelta(hours=24))
return result
finally:
lock.release(handle)
API overview
idempotency_key(payload, namespace=...)— deterministic key from JSON-canonicalized payloadIdempotencyGuard.try_begin/mark_done/get_result— at-most-once start; optional JSON memo on completionDistributedLock.acquire/release/extend— lease + monotonic fencing token (Lua)FenceGate.set_if_fresh— atomic fenced Redis string writesfenced_update(queryset, token, updates=...)— fenced Django/PostgresUPDATEin one statement
Typed public API (py.typed). No Celery adapter yet; wire the guard in your task body for now.
Guarantees (honest)
| Claim | Status |
|---|---|
| At-most-once start within idempotency TTL | Yes (SET NX) |
Memoized result after mark_done(..., result=...) |
Yes (within TTL) |
| Mutual exclusion while lock TTL held (single Redis primary) | Best-effort lease |
Stale holder blocked via FenceGate / fenced_update |
Yes (when used) |
| Exactly-once delivery | No |
| Safety under Redis failover / split brain | No |
| Writes that skip the fencing token | No |
fencekit does not implement Redlock. Fencing only works when the storage layer checks the token in the same operation as the write.
Development
REM Windows (CMD)
python -m pip install -e ".[dev]"
python -m ruff check src tests
python -m mypy src
python -m pytest -m "not integration" -q
Full suite (Redis on localhost:6379):
set FENCEKIT_REDIS_URL=redis://localhost:6379/15
python -m pytest -q
# Linux/macOS with uv
uv sync --extra dev
uv run pytest
CI runs lint, type-check, and tests on Python 3.10–3.13 with Redis. Releases publish to PyPI via Trusted Publishing (OIDC, no long-lived token).
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fencekit-0.3.2.tar.gz.
File metadata
- Download URL: fencekit-0.3.2.tar.gz
- Upload date:
- Size: 24.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c9e1ad2a0b5b201cd071269cf987f1f5fd9f179aaa77508e3238c337fa92d349
|
|
| MD5 |
ccaef10a7808c3f19e27428408da7f74
|
|
| BLAKE2b-256 |
7796b8ef0c9b6273f2d3c25acaae497d3ac6ef1cafe8628c632d7c53aa442d42
|
Provenance
The following attestation bundles were made for fencekit-0.3.2.tar.gz:
Publisher:
release.yml on ahmed5145/fencekit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fencekit-0.3.2.tar.gz -
Subject digest:
c9e1ad2a0b5b201cd071269cf987f1f5fd9f179aaa77508e3238c337fa92d349 - Sigstore transparency entry: 2206480257
- Sigstore integration time:
-
Permalink:
ahmed5145/fencekit@579267390aa7a47772e13fff73985310502e2465 -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/ahmed5145
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@579267390aa7a47772e13fff73985310502e2465 -
Trigger Event:
release
-
Statement type:
File details
Details for the file fencekit-0.3.2-py3-none-any.whl.
File metadata
- Download URL: fencekit-0.3.2-py3-none-any.whl
- Upload date:
- Size: 17.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d1576b184f75ae2f925485fd2f5173195797761ef81916248311c05ea42ddb0
|
|
| MD5 |
c863783acb5faf315632f3a9a3bf0727
|
|
| BLAKE2b-256 |
d4b6a6df2236fca47566fe039fa7620ca365480cbf0f70f048d300082b366d0b
|
Provenance
The following attestation bundles were made for fencekit-0.3.2-py3-none-any.whl:
Publisher:
release.yml on ahmed5145/fencekit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fencekit-0.3.2-py3-none-any.whl -
Subject digest:
9d1576b184f75ae2f925485fd2f5173195797761ef81916248311c05ea42ddb0 - Sigstore transparency entry: 2206480282
- Sigstore integration time:
-
Permalink:
ahmed5145/fencekit@579267390aa7a47772e13fff73985310502e2465 -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/ahmed5145
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@579267390aa7a47772e13fff73985310502e2465 -
Trigger Event:
release
-
Statement type: