Skip to main content

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

fencekit-0.3.1.tar.gz (24.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fencekit-0.3.1-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

File details

Details for the file fencekit-0.3.1.tar.gz.

File metadata

  • Download URL: fencekit-0.3.1.tar.gz
  • Upload date:
  • Size: 24.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for fencekit-0.3.1.tar.gz
Algorithm Hash digest
SHA256 d84250e7ab5e7f924dd6242fb057743aa547aac9841e6481fc5b40e0fbac401f
MD5 4bd5dcf016e488bc6afa7ee9d4c8fe22
BLAKE2b-256 82ef8885a03993ea763f56863f24ff0f75c6bbf77798bfaaf12fc5ce9ca73457

See more details on using hashes here.

Provenance

The following attestation bundles were made for fencekit-0.3.1.tar.gz:

Publisher: release.yml on ahmed5145/fencekit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fencekit-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: fencekit-0.3.1-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

Hashes for fencekit-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e274957b837d0a0b3dc741a7eac3926da0ca8a9bcda3653e3217d114df17da0b
MD5 28dadb40ad06e764e1bbf50982a6ccb9
BLAKE2b-256 900e72adf6515ab084d073f8a6c6a5d678e59757532bdb7a1e82a7056b97efda

See more details on using hashes here.

Provenance

The following attestation bundles were made for fencekit-0.3.1-py3-none-any.whl:

Publisher: release.yml on ahmed5145/fencekit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page