Distributed Systems · Coordination Services

The Lease as a Time-Bounded Grant: Why Expiry Needs No Message

How a lease replaces an explicit release with a deadline, why the holder must stop before the server reassigns, and what the gap between the two clocks costs.

A lock released by an explicit message is only as reliable as the sender. When the holder crashes, loses its network path, or is stopped mid-operation, the release never arrives and the resource remains locked for the lifetime of the system. Every design that grants exclusive access has to answer one question: who is allowed to conclude that the current holder is gone? A lease answers it in advance, by writing the answer down as a timestamp.

A grant with a deadline attached

A lease is a grant of a resource for a bounded interval. The server records the holder and the instant at which the grant lapses. The client is told the interval’s length, or the absolute deadline, and treats that deadline as the end of its right regardless of what it hears afterwards. Nothing needs to be sent when the interval ends. Expiry is not an event that propagates; it is an arithmetic fact that both sides can evaluate on their own.

That single change removes the failure mode that breaks message-based release. A holder that has been unreachable since before the deadline is, by construction, not a holder any more. The server does not need to distinguish “crashed” from “partitioned” from “paused”; it only needs to know whether the interval has run out on the clock it is using for grants. Failure detection, which is otherwise an unbounded and ambiguous problem, collapses into a timer.

The client side has an obligation that is easy to state and easy to violate: it must stop using the resource at its own deadline, not at the moment it learns that the server has taken the resource away. A lease is therefore not a courtesy notification about who currently owns something. It is a self-enforcing contract in which the holder polices itself.

type Grant struct {
	Holder  string
	Expires time.Time // deadline as the holder computed it from the grant
}

// The holder's rule: past the deadline the grant is void, whether or not
// anything has been said about it. Nothing here consults the server.
func (g Grant) Covers(now time.Time, who string) bool {
	return g.Holder == who && now.Before(g.Expires)
}

Why the server waits longer than the client

If both sides used the same clock, the deadline would be exact and the server could reassign the resource at the same instant the holder’s right ended. They do not use the same clock. The client’s deadline is computed from the duration it was handed plus its own notion of the current time, and that notion differs from the server’s by the offset between the two clocks plus whatever drift accumulates over the interval.

The safe arrangement is asymmetric. The server hands out a duration D, and it refuses to grant the resource to a new holder until it has observed D plus a margin elapse since the grant. The margin must cover the worst-case difference between the two clocks over one interval: the initial offset, the drift rate of both oscillators, and the resolution of both timers. The client, for its part, treats its own computed deadline as final, which is the earlier of the two instants whenever the client’s clock runs ahead. The result is a window in which nobody believes they hold the resource, and that window is what makes the scheme safe rather than merely convenient.

This is the mechanism described in the original leases paper by Gray and Cheriton, where the duration is chosen so that a client’s lease expires at the server no earlier than at the client. The arithmetic of the margin is the subject of lease safety margins under clock drift, and it is the part most often skipped when leases are adopted casually.

The pause that neither side can see

The interesting failure is not a crash. A crashed holder stops issuing requests, so the only cost is the wait for the interval to lapse. The expensive failure is a holder that is alive but not running: a process stopped by a debugger, suspended by a hypervisor, stalled in a garbage collection pause longer than the lease, or blocked on a page fault in a swapped-out heap. Such a process misses its own deadline while retaining every intention of using the resource when it wakes.

When it wakes, its memory still says it holds the lease. If the server has already reassigned the resource, two actors now believe they have exclusive access, and the second one may have written data the first is about to overwrite. Leases do not prevent this; they bound how long the overlap can last and make it detectable. The standard remedy is to attach a monotonically increasing token to each grant and require the protected resource to check the token on every write, rejecting any token lower than the highest it has seen. A client that wakes from a long pause presents a stale token and is refused. This is why a lease is a building block for exclusion rather than exclusion itself: the guarantee lives at the resource, not at the lock service.

ZooKeeper’s session model arrives at the same asymmetry from a different direction. A session that expires causes the server to delete the ephemeral nodes created under it, which is a lease-like expiry of the client’s membership; but a client that merely lost its connection, without the session expiring, may still reconnect and continue. The two cases are indistinguishable from the client’s side at the moment the connection breaks, which is why session semantics need explicit handling in any client that mutates shared state.

Choosing the duration

The duration is a direct trade between recovery latency and renewal cost. A long lease means a failed holder blocks the resource for that long before anyone else can take it. A short lease means the holder must renew frequently, and every renewal is a round trip that must complete before the deadline; a burst of latency or a brief network hiccup then causes a spurious expiry, and the holder loses a resource it was using correctly. Since renewal intervals are usually set to a fraction of the duration, a one-second lease might renew every few hundred milliseconds, which is a meaningful fraction of a busy client’s request budget.

The shape of the workload decides the balance. A lock protecting a batch job that runs for minutes can tolerate a lease measured in tens of seconds, because a false expiry costs a retry and a true expiry costs at most one lease period of idleness. A lock guarding a hot metadata record needs a duration short enough that a dead holder does not stall a request path, which pushes the design toward frequent renewal and toward a client that can renew from a background thread independent of the work it is doing.

There is a second, less obvious cost: the server must hold state for every outstanding lease, and it must expire them on a timer. A coordination service with a million active sessions has a million deadlines to track, and the granularity of its expiry mechanism becomes a design constraint on the whole system. Bucketing deadlines into coarse time slots, and sweeping a bucket at a time, turns per-lease timer management into a bounded amount of work per tick at the cost of expiring some leases later than their nominal deadline. That lateness is safe in one direction only: expiring late delays reassignment, while expiring early breaks the guarantee outright. Any implementation that rounds must round toward the conservative side.

What the lease does not decide

A lease says when a claim ends. It says nothing about whether the claim was worth having, whether the holder did its work correctly, or whether the resource is in a consistent state when the next holder arrives. Those remain the responsibility of the protocol around the lease. The useful mental model is that a lease converts an unbounded failure-detection problem into a bounded waiting problem, and that the conversion is only sound if the holder is disciplined about its own deadline and the resource is disciplined about the tokens it accepts.

References

  1. Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency — Cary G. Gray, David R. Cheriton, SOSP 1989
  2. The Chubby lock service for loosely-coupled distributed systems — Mike Burrows, OSDI 2006
  3. ZooKeeper: Wait-free coordination for Internet-scale systems — Patrick Hunt et al., USENIX ATC 2010