Deterministic lease timeline
Logical ticks explain ordering; they do not model Redis latency.
Only a candidate with the current token may enter this boundary.
Latest ownership events
| Tick | Candidate | Operation | Token | TTL | Outcome |
|---|
Visual companion · Release 0.4.0 · Redis Lettuce
Follow a blocking LeaderElector call from contention to token ownership,
lease TTL, protected work, extension, expiry, and token-checked release.
Logical ticks explain ordering; they do not model Redis latency.
Only a candidate with the current token may enter this boundary.
| Tick | Candidate | Operation | Token | TTL | Outcome |
|---|
Step 01 · Mental model
The Redis drawing is conceptual: lockName → { owner, token, TTL }.
It explains the contract without promising a particular serialized key layout.
A missing lock becomes owned by one successful candidate.
Release and extension must present the token that currently owns the lock.
Ownership disappears after leaseTime unless it is validly extended.
The library runs the action only after acquisition succeeds.
A contender that reaches its wait deadline is skipped and the protected action is not called.
Step 02 · Settings
Change action duration and leaseTime above; scenario presets control the other values.
How long a contender may keep trying before it skips. This simulation uses 2 ticks.
How long the current token remains valid after acquire or extension.
How long a successful lock remains held even when the action finishes early. It cannot exceed leaseTime.
How long business work continues. It may be shorter or longer than the lease.
The extension preset renews the lease periodically while the elected action remains active.
Step 03 · Direct API
val value = elector.runIfLeader("invoice-close") {
closeInvoices()
}
// elected: action value
// contention: null
when (val result =
elector.runIfLeaderResult("invoice-close") { closeInvoices() }) {
is LeaderRunResult.Elected -> showCompleted(result.value)
LeaderRunResult.Skipped -> showSkipped()
is LeaderRunResult.ActionFailed -> showFailure(result.cause)
}
The lock was acquired and the action returned, even when its value is null.
The lock was not acquired during normal contention.
Acquisition succeeded, but the protected action failed.
Backend or configuration failures are not relabeled as normal contention.
Step 04 · Spring Boot
@LeaderElection(
name = "invoice-close",
waitTime = "PT0.5S",
leaseTime = "PT10S",
minLeaseTime = "PT1S",
autoExtend = true,
)
fun closeInvoices(): CloseSummary? = service.closeInvoices()
Do not add @EnableAspectJAutoProxy. Kotlin methods do not need to be
open, while private methods are not intercepted. A dynamic name must be
valid SpEL such as "'invoice-' + #tenantId".
This companion animates the synchronous boundary. The release also supports suspend values,
Mono, Flux, and Kotlin Flow. Long streams require
autoExtend = true; use streamBounded = true only when completion
is guaranteed inside the lease.
Step 05 · Failure and recovery
Run “Expiry + takeover.” The first action becomes stale after its token expires, while a later candidate acquires a new token and begins valid work.
A stale process may still execute code after expiry. Its old token cannot release the successor's lock, but leader election alone cannot undo an external write. Make actions idempotent and use a domain-appropriate fencing or claim mechanism when stale writes must be rejected.
In the extension scenario, the valid owner renews before expiry. In every scenario, release is token-checked: an old owner cannot delete a newer owner's lock.
Release evidence
Claims and links below target release commit 17ab7f8.