feat: add distributed Locker client (rueidislock port)#373
Open
gtoxlili wants to merge 3 commits into
Open
Conversation
A Rust port of Go's [`rueidislock`](https://pkg.go.dev/github.com/redis/rueidis/rueidislock). Each acquired lock spawns a background task that extends the TTL on a cadence and also listens for RESP3 invalidation pushes on the lock key, so the holder learns about takeovers and evictions without waiting for the next extension tick. The returned `LockGuard` exposes a `cancelled()` future for use in `tokio::select!` and a `release()` that runs a CAS delete on the way out. Correctness notes: * `init()` serializes concurrent callers via an async mutex and only publishes `initialized = true` after every fallible step succeeds, so a failed init (e.g. RESP2 with caching enabled) is retryable and a race cannot spawn duplicate dispatchers. * `init()` installs an `on_reconnect` handler that re-arms `CLIENT TRACKING` after every reconnect — fred doesn't restore tracking automatically, and without this hook the locker would silently stop receiving invalidations after the first connection bounce. * The dispatcher and reconnect handler hold a `Weak<LockerInner>` so the background tasks don't form an `Arc` cycle with the locker they belong to. Dropping the last `Locker` clone aborts both via `Drop for LockerInner` and the tasks exit cleanly. * `Cancellation::cancelled` uses `Notify::notify_waiters` with a pinned `Notified::enable()` so a `cancel` racing between the flag check and the future registration isn't lost. Per-gate signals use `notify_one`, matching `rueidislock`'s buffered-channel-of-1 semantics. * `register_outstanding` and `close` are serialized through the same mutex, so an acquire racing with close either gets registered (and promptly cancelled) or refuses to register and runs a CAS-delete on the key it just wrote — no stranded keys. * `release()` and `Drop` are partitioned: the explicit path awaits the extender's DEL and logs; `Drop` always runs `notify_gate` and `drop_gate` exactly once, so the gate's ref-count never wraps. * `Drop` is sync — it signals cancellation but never spawns, since callers may drop guards outside of a tokio context. Single-key only at this revision; the config shape leaves room for a multi-key Redlock variant later (`LockerConfig::key_majority`) without breaking the public API. Hidden behind a new `locker` feature that pulls in `i-keys`, `i-scripts`, `i-tracking`, and `sha-1`.
Mirrors `build_pool` / `build_exclusive_pool` — pulls in the same `Config` / `PerformanceConfig` / `ConnectionConfig` and hands back an un-initialized `Locker` that the caller wires up with `client.init().await` followed by `locker.init().await`.
Covers acquire / release, `try_acquire` on a contended lock, in-process waiting for a release, auto-extension past validity, the force-takeover path (verifies `cancelled()` fires within the invalidation push window rather than the next extend tick), tracking re-arm after a forced reconnection, and the close-cancels-outstanding- guards behaviour. Tests are wired into both the centralized and clustered harnesses; RESP2 runs no-op out since the locker defaults require RESP3.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds a
clients::Lockerinterface modelled onGo's
rueidislock.It uses RESP3 client-side-cache invalidation pushes so a holder learns about
takeovers and evictions the moment they happen, plus a background extender that
bumps
PEXPIREATon a cadence (defaultvalidity / 2).Public surface
What's in
Locker+LockGuard+LockerConfigbehind a newlockerfeature flag(pulls in
i-keys,i-scripts,i-tracking,sha-1).Builder::build_lockerfor the standard entry point.on_reconnecthook that re-armsCLIENT TRACKING— fred doesn't restore itautomatically and without this every connection bounce would silently drop
invalidation pushes for active holders.
Weak<LockerInner>in the dispatcher + reconnect handler so neither taskkeeps the locker alive after the last
Lockerclone drops;Drop for LockerInneraborts both for prompt cleanup.acqat/acqms/fcqat/fcqms/
extend/delkey), each with the trailingGETthat arms tracking onthe lock key.
Notifyusage:Cancellation::cancelledpins theNotifiedandcalls
enable()before the second flag check; per-gate signals usenotify_one(matching rueidislock's buffered chan size=1).init()serializes concurrent callers via an async mutex and only publishesinitialized = trueafter every fallible step succeeds, so a failed init isretryable and a race cannot spawn duplicate dispatchers.
register_outstandingandcloseare serialized through the same mutex, soan acquire racing with close either gets registered (and promptly cancelled)
or refuses to register and runs a CAS-delete on the key it just wrote — no
stranded keys.
release, contention, in-process waiting, auto-extension past validity,
force-takeover detection via push, tracking re-arm after
force_reconnection,close-cancels-outstanding-guards.
What's not (out of scope, follow-ups)
KeyMajority > 1). The config shape leaves room forLockerConfig::key_majority(n)without breaking changes.PipelineMultiplex = -1. fred'sstart_trackingbroadcasts and EVAL routes per-key, so forsingle-key locks this works in practice, but a multi-key variant would need
extra care.
metrics/mocksintegration — rueidislock doesn't ship those either.Happy to split this into smaller pieces if that would help review.