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,
    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)):
        return  # already started or completed

    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"},
        )
        guard.mark_done(key)
    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.2)
Safety if the app writes without presenting the token No

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.2 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.2.0.tar.gz (22.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.2.0-py3-none-any.whl (16.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: fencekit-0.2.0.tar.gz
  • Upload date:
  • Size: 22.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.2.0.tar.gz
Algorithm Hash digest
SHA256 829750d1f87628b2c400fa4eb2e17fa24150008a4f4bf7c50c0fb3721a13b7cc
MD5 22f6d6391feeca9e8bf0beb0b3a4921b
BLAKE2b-256 8f766b3119b746b69ef724a8eb17093c64b0df07e003f77cf45a6f5ad0b614e5

See more details on using hashes here.

Provenance

The following attestation bundles were made for fencekit-0.2.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.2.0-py3-none-any.whl.

File metadata

  • Download URL: fencekit-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 16.4 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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8c5e25445c74643e32d60cb1b40af910b28ebd6d361ac706a6e0d9c9f205fc9e
MD5 cfe40096b44b69549815cd10957de514
BLAKE2b-256 9b8050ce17c155dd9ee6d48dd403cb36203abdbc98ecc4c805523251b8df3a15

See more details on using hashes here.

Provenance

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