File: fencing-tokens/fencing_tokens.py

Date: 2026-05-29

Time: 08:54

Purpose

This file implements fencing tokens for distributed locking, the mechanism described in DDIA Chapters 8–9 for preventing stale lock holders from corrupting shared resources. The core problem: a client acquires a lock, gets paused (GC, network delay), the lock expires, another client acquires it, and now two clients think they hold the lock. Fencing tokens solve this by attaching a monotonically increasing integer to each lock acquisition — resource servers reject writes from tokens lower than the highest they've seen.

The file provides all four actors in this protocol: the token itself, the lock service that issues them, the resource servers (fenced and unfenced for comparison), and the client that ties them together.

Key Components

FencingToken

A value object representing an issued lock grant. Carries five fields: the monotonic token integer, lockname, clientid, issuedat timestamp, and ttl. The only behavior is isexpired(currenttime), which checks currenttime >= issuedat + ttl. Note the >= — expiry is inclusive of the boundary, meaning a token with issuedat=0, ttl=10 is expired at time 10.

LockService

The central lock authority. Maintains a global _counter (starts at 1) and a map of lock names to their current FencingToken.

FencedResourceServer

A key-value store organized as resource → key → value. The critical invariant: it tracks highesttoken per resource and rejects any write whose fencing_token < highest. This is the server-side enforcement that makes fencing work — a slow client whose lock expired and was re-acquired by another client will be rejected when it finally tries to write.

UnfencedResourceServer

Identical storage structure but accepts all writes unconditionally. Exists as a pedagogical counterexample — tests can demonstrate the data corruption that happens without fencing.

Client

Convenience wrapper that binds a clientid to a LockService and caches acquired tokens locally. writeto_resource() automatically attaches the correct fencing token to writes, so callers don't have to thread tokens manually.

Patterns

Dependencies

No imports. Imported by testfencingtokens.py and testertestfencing_tokens.py for testing.

Flow

A typical fencing scenario plays out like this:

1. Client A calls acquire_lock("db", time=0, ttl=10) → gets token 1

2. Client A writes to FencedResourceServer with token 1 → accepted, highest becomes 1

3. Client A stalls (simulated by advancing time past TTL)

4. Client B calls acquire_lock("db", time=15) → lock expired, gets token 2

5. Client B writes with token 2 → accepted, highest becomes 2

6. Client A wakes up, tries to write with token 1 → rejected (1 < 2)

The UnfencedResourceServer path: step 6 would succeed, silently overwriting Client B's data.

Invariants

1. Token monotonicity_counter only increments. Every acquire() call that succeeds produces a strictly higher token than all previous ones. This is never reset.

2. Fencing rejectionFencedResourceServer.write() enforces fencing_token >= highest. Equal tokens are accepted (same holder writing multiple keys).

3. Holder-only release/renew — Both release() and renew() verify client_id matches the current holder. No other client can release or extend your lock.

4. Renewal preserves token valuerenew() mutates issued_at and ttl on the existing FencingToken but does not change token. The fencing token number is stable across renewals.

5. Re-entrant acquire issues new token — If the same client re-acquires a lock it already holds, it gets a *new* (higher) token number. This differs from renew().

Error Handling

Errors are returned as values, never raised:

No case throws an exception. This is consistent with modeling distributed system responses where "denied" is a normal outcome.

Topics to Explore

Beliefs