Redis-backed idempotent task primitives: fencing tokens and distributed locks
Project description
fencekit
Redis-backed idempotency and fenced locks for background jobs. A destination that atomically checks the fencing token can reject writes from a stale lock holder.
Why
ChessMate (chess-mate.online) runs imported games through Stockfish before writing a coaching report. With Redis as the Celery broker and late acknowledgements enabled, a worker crash can cause the same job to be delivered again. I saw batches progress twice. That wasted Stockfish CPU and left the recorded progress ambiguous.
Celery requires late-ack tasks to be idempotent, but each task still needs code to enforce that property. fencekit collects the Redis operations I used in ChessMate. Its tests reproduce worker death and lock-expiry races.
Install
pip install fencekit
# optional Django helper tests / apps that want the extra declared:
pip install "fencekit[django]"
# or
uv add fencekit
Requires Redis 6+ (tested with Redis 7) and Python 3.10+.
30-second 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) -> None:
key = idempotency_key(
{"game_ids": ["abc", "def"], "engine": "sf16"},
namespace="analysis",
)
if not guard.try_begin(key, ttl=timedelta(hours=24)):
# Already started or completed; return a prior memo when present.
try:
return guard.get_result(key)
except IdempotencyResultMissing:
return
handle = lock.acquire(f"analysis:{job.pk}", ttl=timedelta(minutes=5))
try:
# Redis progress (optional):
fence.set_if_fresh(handle.token, f"analysis:{job.pk}:status", "running")
# Durable Postgres / Django row (required for ChessMate-style state):
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)
Guarantees
| Claim | Status |
|---|---|
| At-most-once start within the idempotency TTL (Redis available) | Yes (SET NX) |
| Mutual exclusion while lock TTL held (single Redis primary) | Best-effort lease |
Stale holder cannot overwrite via atomic FenceGate.set_if_fresh |
Yes (Redis strings) |
Stale holder cannot overwrite via fenced_update / equivalent SQL |
Yes (when used) |
| Exactly-once delivery | No |
| Safety under Redis failover / split brain | No (v0.3) |
| Safety if the app writes without presenting the token | No |
Memoized result available after mark_done(..., result=...) |
Yes (within TTL) |
See DESIGN.md for the threat model, Lua algorithms, TTL guidance, and crash/restart semantics. fencekit does not implement Redlock.
Fencing covers stale overwrites only when the destination enforces the token in
the same operation as the write. Redis helpers do not protect Postgres rows;
use fenced_update (or the raw SQL pattern in DESIGN.md) for Django models.
Running tests
REM CMD (Windows), no uv required
cd fencekit
python -m pip install -e ".[dev]"
python -m ruff check --fix src tests
python -m ruff check src tests
python -m mypy src
python -m pytest -m "not integration" -q
The full suite needs Redis on localhost:6379:
set FENCEKIT_REDIS_URL=redis://localhost:6379/15
python -m pytest -q
Integration tests skip cleanly when Redis is unreachable or FENCEKIT_REDIS_URL is unset.
Django ORM fencing tests run whenever Django is installed (pip install -e ".[dev]"
or .[django]).
# Linux/macOS with uv + Docker
uv sync --extra dev
uv run pytest -m "not integration"
docker run -d -p 6379:6379 --name fencekit-redis redis:7
export FENCEKIT_REDIS_URL=redis://localhost:6379/15
uv run pytest
v0.3 has no Celery adapter. A later release may add one as an optional extra.
License
MIT
Project details
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.0.tar.gz.
File metadata
- Download URL: fencekit-0.3.0.tar.gz
- Upload date:
- Size: 24.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4bf1d30b00297b3b4fb6db9c51e864cdd1bc42cfd6036c474e80f00aa9ae0c54
|
|
| MD5 |
f422dc61ee86adb6d16d3ae1b23961fc
|
|
| BLAKE2b-256 |
69e9b5d08358ef18f6bfdfe236f76ddb4c9d9cce8164c35996779eba59e57b67
|
Provenance
The following attestation bundles were made for fencekit-0.3.0.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.0.tar.gz -
Subject digest:
4bf1d30b00297b3b4fb6db9c51e864cdd1bc42cfd6036c474e80f00aa9ae0c54 - Sigstore transparency entry: 2190887684
- Sigstore integration time:
-
Permalink:
ahmed5145/fencekit@a162ee13c14877eedf3bb5ac901381ee440b2e9c -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/ahmed5145
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a162ee13c14877eedf3bb5ac901381ee440b2e9c -
Trigger Event:
release
-
Statement type:
File details
Details for the file fencekit-0.3.0-py3-none-any.whl.
File metadata
- Download URL: fencekit-0.3.0-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 |
044b2708a0f71a364ba6b2c98484a17171cc6a784e457620508c97d0ad14af0c
|
|
| MD5 |
86ed1c80786a62477f672524c459aad3
|
|
| BLAKE2b-256 |
ddc427c7dec355c032f766615be255ea677a6b7c278c2103ce140ce15567589e
|
Provenance
The following attestation bundles were made for fencekit-0.3.0-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.0-py3-none-any.whl -
Subject digest:
044b2708a0f71a364ba6b2c98484a17171cc6a784e457620508c97d0ad14af0c - Sigstore transparency entry: 2190887848
- Sigstore integration time:
-
Permalink:
ahmed5145/fencekit@a162ee13c14877eedf3bb5ac901381ee440b2e9c -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/ahmed5145
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a162ee13c14877eedf3bb5ac901381ee440b2e9c -
Trigger Event:
release
-
Statement type: