Add optional fencing token minted atomically on Obtain - #94
Conversation
Set Options.Fence to mint a fencing token with the lock: a strictly increasing value, incremented atomically inside obtain.lua on each new acquisition and returned by Lock.FenceToken. Stamp writes to the protected resource with the token and reject any write carrying an older one, fencing out a stale lock holder per Kleppmann's "How to do distributed locking". The token is minted only on a genuinely new acquisition; a re-entrant override by the current holder returns the existing token without incrementing. The counter is stored at "<key>:fence" and persists across release so it keeps increasing. Without Options.Fence the script returns the original "OK" status, preserving backward compatibility.
dim
left a comment
There was a problem hiding this comment.
LGTM, only a minor comment, open to debate
|
|
||
| // FenceToken returns the lock's fencing token, or false if it was obtained | ||
| // without Options.Fence. | ||
| func (l *Lock) FenceToken() (int64, bool) { |
There was a problem hiding this comment.
I would simplify this and always return 0 if not fenced. you can mention this in the method documentation, seems the better choice
There was a problem hiding this comment.
Good call — done. FenceToken now returns int64 with 0 meaning unfenced; since tokens start at 1 the sentinel is unambiguous, and I noted it on the method doc. Thanks!
Per review: FenceToken now returns a single int64 instead of (int64, bool). Tokens start at 1, so 0 is an unambiguous "not fenced" sentinel; documented on the method. Drops the now-redundant fenced field.
dim
left a comment
There was a problem hiding this comment.
Sorry, one more thing I thought of
| ctx := context.Background() | ||
|
|
||
| // Obtain a lock with a fencing token. | ||
| lock, err := locker.Obtain(ctx, "my-key", time.Second, &redislock.Options{Fence: true}) |
There was a problem hiding this comment.
one little concern left: I don't like the fact that we are automatically generating key names for the fence key, I always prefer to delegate this to the user as there are sometimes non-trivial implications, e.g. redis cluster or other redis implementations where key names matter. I would therefore suggest:
lock, err := locker.Obtain(ctx, "my-key", time.Second, &redislock.Options{FenceKey: "my-key:fence"})what do you think?
There was a problem hiding this comment.
Agreed — auto-generating the key name hides exactly the kind of Cluster/key-placement footgun you're describing, so letting the caller own it is the right call. Done: Options.FenceKey is now caller-supplied.
I also took it one step further for Cluster safety — the fence key now goes in KEYS (not ARGV), as the last entry when fencing is on, with an ARGV flag telling the script to treat it specially. That way Redis validates the slot up front and a misplaced fence key fails with CROSSSLOT before the script runs, rather than mid-execution.
Per review: replace the auto-derived fence key (keys[0]+":fence") with a caller-supplied Options.FenceKey, so the caller controls its placement — important on Redis Cluster, where the fence key must hash to the same slot as the lock key(s). The fence key is also passed in KEYS rather than ARGV (as the last entry, flagged by ARGV[4]) so Redis validates the slot up front: a misplaced fence key fails with CROSSSLOT before the script runs instead of mid-execution.
|
Thanks!! |
Closes #93.
Adds an opt-in fencing token to
Obtain: a strictly increasing value, incremented atomically insideobtain.luaon each new acquisition and returned byLock.FenceToken. Callers stamp writes to the protected resource with the token and reject any write carrying an older one, fencing out a stale lock holder per Martin Kleppmann's How to do distributed locking.API
Options.FenceKey string— the key the fence counter lives at; non-empty opts in (default off).Lock.FenceToken() int64— the token, or0when obtained withoutFenceKey. Tokens start at 1, so 0 is an unambiguous "unfenced" sentinel.Behaviour
msetnxpath). A re-entrant override by the current holder returns the existing token without incrementing, so refreshing-via-obtain does not inflate it.KEYS(the last entry when fencing is on, flagged viaARGV), so on Redis Cluster a fence key that does not share a slot with the lock key fails withCROSSSLOTup front rather than mid-execution.Release, so it keeps increasing.FenceKey,obtain.luareturns the original"OK"status andFenceTokenreturns0. Existing tests pass unchanged.Caveat (documented in the README)
The token is only as monotonic as the underlying Redis. On a single instance it is strict; on a Sentinel/Cluster failover that loses the
INCR, it can regress. For strict cross-failover monotonicity, source the token from a linearizable store.Tests
TestObtain_fence— token minted, monotonic across terms, absent withoutFenceKey.TestObtain_fence_reentrant— re-entrant override does not advance the token.