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.0.tar.gz (24.1 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.0-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

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

Hashes for fencekit-0.3.0.tar.gz
Algorithm Hash digest
SHA256 4bf1d30b00297b3b4fb6db9c51e864cdd1bc42cfd6036c474e80f00aa9ae0c54
MD5 f422dc61ee86adb6d16d3ae1b23961fc
BLAKE2b-256 69e9b5d08358ef18f6bfdfe236f76ddb4c9d9cce8164c35996779eba59e57b67

See more details on using hashes here.

Provenance

The following attestation bundles were made for fencekit-0.3.0.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.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

Hashes for fencekit-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 044b2708a0f71a364ba6b2c98484a17171cc6a784e457620508c97d0ad14af0c
MD5 86ed1c80786a62477f672524c459aad3
BLAKE2b-256 ddc427c7dec355c032f766615be255ea677a6b7c278c2103ce140ce15567589e

See more details on using hashes here.

Provenance

The following attestation bundles were made for fencekit-0.3.0-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