Currently in testing: the GitHub code will be opened once complete.

Request × Key-State Matrix

A (Acquire)

Key stateServer actionResponseTiming
Not registeredRegister (expiry = now+lease), issue tokenA+token+owner+keyImmediately
Registered + expiredRenew (now+lease), new tokenA+token+owner+keyImmediately
Registered + valid holder + wait=0Do not enqueueT+owner+keyImmediately
Registered + valid holder + wait>0Enqueue, hold responseA or T+owner+keyAcquired via release/expiry, or T when wait expires
Queue or server capacity at its capDo not enqueueB+owner+keyImmediately
  • The release path is FIFO-fair: when the holder does R, it's handed directly to the waiter at the front of the queue (no re-contention, a new token).
  • Only the expiry path is non-FIFO.
  • Matching the current holder's owner receives no special treatment. It is not authority to return an active token or renew a lease; the ordinary held-key rules apply.
  • Responses can arrive out of request order, so clients must match by the echoed owner.

R (Release)

Key stateServer actionResponse
Registered + token matches, has waitersHand off directly to the waiter at the front (new token)R+token+key
Registered + token matches, no waitersDelete the keyR+token+key
Registered + token mismatchNothing (lock stays held)N+token+key
Not registeredNothingN+token+key

When Expiry Is Processed

  • Single mode processes expiry lazily — a key past its expiry is taken over on the spot by the next acquire (the second row above), and a separate 5-second sweep cleans up expired keys with no waiters. So a release on a key that's expired but not yet swept can get R instead of N.
  • Cluster mode waits until the next token-validated deadline in a min-heap, then requires the leader to commit an Expire command through consensus before the key disappears. This keeps clock differences between nodes from splitting state.

The meaning above still holds in cluster mode — except the response only goes out after the state change commits via majority consensus, and a non-leader node responds with M (moved) or E no_leader instead of processing the request.