Skip to content

Feat/cache distributed lock - #27

Open
swsarancodes wants to merge 2 commits into
LYZR-OSS:mainfrom
swsarancodes:feat/cache-distributed-lock
Open

Feat/cache distributed lock#27
swsarancodes wants to merge 2 commits into
LYZR-OSS:mainfrom
swsarancodes:feat/cache-distributed-lock

Conversation

@swsarancodes

Copy link
Copy Markdown

Adds distributed locking to the cache using Redis, so only one process can run a critical section at a time.

Includes safe token-based release/extend, auto TTL refresh, manual lock controls, and 12 new tests covering contention, expiry, exceptions, and watchdog behavior.

Also fixes a Windows test issue where moto fixtures were connecting to 0.0.0.0 instead of 127.0.0.1.

ThreadedMotoServer binds 0.0.0.0 when given port=0, and every moto-backed
test fixture built its endpoint_url straight from that bind address. Linux
treats 0.0.0.0 as reachable from a client socket; Windows does not, so
every SQS/S3/KMS/SES/SNS/Secrets-Manager test using this pattern hung or
failed with EndpointConnectionError on Windows.
Adds cross-instance mutual exclusion to the cache abstraction - useful for
leader election, cron dedup across replicas, and idempotent job claiming,
none of which cloudrift previously had an answer for.

    async with cache.lock("invoice:42:close"):
        ...  # only one process/replica runs this at a time

Implemented once in _RedisMixin (cache/base.py), so all three Redis
backends - StandaloneRedisBackend, AWSElastiCacheBackend,
AzureRedisCacheBackend - get it for free, matching how every other Redis
command in this codebase is implemented once and shared.

Design notes:
- Every acquisition gets a random fencing token (SET key token NX PX ttl).
  Release and extend are conditional on that token via WATCH/MULTI
  compare-and-mutate, not a bare DEL/PEXPIRE - so a caller can only ever
  act on a lock it currently holds, never one that expired and was
  re-acquired by someone else. This closes the classic bug naive
  set-then-delete locks have.
- WATCH/MULTI instead of a Lua script (EVAL) so the same code path works
  against managed Redis offerings that restrict scripting and against
  fakeredis in tests (fakeredis has no EVAL support at all).
- auto_extend=True (default) runs a background watchdog that refreshes the
  TTL at roughly ttl/3 for as long as the `async with` block is running,
  so a critical section that runs longer than expected doesn't have its
  lock expire and get stolen mid-operation. The watchdog is cancelled the
  moment the block exits, on success or exception.
- extend_lock()/release_lock() are exposed directly for callers managing a
  lock's lifetime by hand instead of via the context manager.
- Backends that can't offer atomic conditional writes fail loudly with
  NotImplementedError, matching the existing fail-loud convention for
  abstraction gaps (see Azure Service Bus delete()).

12 new tests cover: release-on-exit, release-on-exception, contention,
blocking handoff between two waiters, lock-key/cache-key namespace
isolation, fencing-token rejection of a stale holder, idempotent release,
manual extend, auto-extend surviving a critical section longer than its
base ttl, watchdog cancellation on release, and the default
NotImplementedError contract. All run against fakeredis - no real Redis
needed.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant