etcd's leases are its mechanism for self-cleaning state: attach keys to a lease, and when the lease's TTL runs out without being renewed, everything attached to it is considered gone -- no manual cleanup needed. This is what powers etcd-based service discovery: a service registers itself under a lease and keeps renewing it (a heartbeat); if the service crashes and stops renewing, its registration disappears automatically once the TTL elapses.
now is always caller-supplied, never a real clock (same as the Token
Bucket / Circuit Breaker exercises). grant() and attach() are done for
you. Implement:
renew(lease_id, now, ttl): refresh an existing lease's expiry tonow + ttl. No-op if the lease doesn't exist, OR if it has already expired as ofnow-- an expired lease can't be renewed back to life, it's gone.revoke(lease_id): explicitly kill a lease RIGHT NOW, regardless of its TTL -- real etcd clients call this on a graceful shutdown ("I'm intentionally leaving, don't wait for my heartbeat to time out"). Every key currently attached to this lease becomes dead immediately. No-op if the lease doesn't exist. Afterward, the lease_id must behave exactly like an already-expired one --renew()on it is also a no-op.is_alive(key, now): iskeystill alive at timenow?Falseif it was never attached to a lease, the lease doesn't exist, or the lease has expired (now >= its expiry time).
The missing link: "expired" and "revoked" must be indistinguishable
A lease can stop being valid two different ways -- its TTL quietly runs out,
or someone explicitly calls revoke() -- but from every OTHER method's point
of view, those two must look identical. is_alive doesn't need a separate
"was this revoked?" flag, and renew doesn't need special-case revoked
handling: if revoke fully removes the lease's bookkeeping (the same
bookkeeping an expired lease already lacks), every other method's existing
"does this lease_id exist" check already does the right thing for free.
Example
lm = LeaseManager()
lm.grant("lease1", now=0, ttl=10) # expires at time 10
lm.attach("key1", "lease1")
lm.is_alive("key1", now=5) # -> True
lm.revoke("lease1")
lm.is_alive("key1", now=5) # -> False, immediately, TTL notwithstanding
Constraints
- Expiry is exclusive:
is_aliveisFalseoncenowreaches the expiry time, not just after it. - Renewing an expired lease must be a complete no-op -- it doesn't come back to life, and its expiry time doesn't change.
- A revoked lease stays revoked: calling
renew()on it afterward must also be a no-op, exactly as if it had expired.
Real-world grounding
- etcd's lease implementation:
server/lease/lessor.go
Real etcd leases also support attaching MULTIPLE keys to one lease, so a
whole group of related keys expires (or is revoked) together -- this
exercise's keys mapping already supports that (many keys, one shared
lease_id), which is exactly why revoke only has to touch self.leases
once and every attached key goes dark automatically.