File: lamport-clocks/lamport.py

Date: 2026-05-29

Time: 08:57

lamport-clocks/lamport.py — Lamport Logical Clocks

Purpose

This file implements Lamport logical clocks and their primary application: distributed mutual exclusion. It's a reference implementation of two foundational algorithms from Leslie Lamport's 1978 paper "Time, Clocks, and the Ordering of Events in a Distributed System" — the logical clock protocol itself, and the mutex algorithm built on top of it. The file owns all event ordering logic: clock advancement, causal tracking via the happens-before relation, and total ordering of events across nodes.

Key Components

Data Classes

Event — Represents a single event (LOCAL, SEND, or RECEIVE) at a node. Beyond the visible fields (nodeid, eventtype, timestamp, description, message_id), it carries two hidden graph edges:

These two edges form the happens-before graph that _reaches() traverses.

Message — A simple envelope carrying senderid, sendertimestamp, message_id, and payload. The timestamp piggybacks on the message so the receiver can update its clock.

LamportClock

A single integer counter with three operations:

Node

A process in the distributed system. Each node owns a LamportClock and an append-only eventlog. The three event methods mirror the three clock operations:

LamportMutex

Implements Lamport's distributed mutex algorithm. The protocol:

1. request(node) — The requesting node ticks its clock, adds (timestamp, node_id) to the shared request queue, then broadcasts REQUEST messages to all other nodes. Each recipient immediately sends an ACK back. The mutex tracks which ACKs have been received per requesting node.

2. can_enter(node) — A node can enter the critical section when:

3. release(node) — Removes the node's request from the queue and broadcasts RELEASE to all others.

The request queue is sorted by (timestamp, node_id), which gives a deterministic total order even when timestamps collide.

Free Functions

totalorder(events) — Sorts events by (timestamp, nodeid). This is the standard Lamport total order — timestamps provide causal ordering, node IDs break ties deterministically.

happensbefore(eventa, eventb, allevents) — Determines the causal relationship: True if a→b, False if b→a, None if concurrent. Note that the allevents parameter is accepted but unused — the implementation relies entirely on the parent/_cause graph edges.

reaches(source, target) — BFS backward from target through parent and _cause edges to check if source is reachable. This is the operational definition of happens-before: a→b iff there's a path from a to b in the causal graph.

Patterns

Dependencies

Imports: Standard library only — dataclasses, typing, collections.deque, itertools. No external dependencies.

Imported by: test_lamport.py — the test suite that exercises clock behavior, message passing, mutex correctness, and happens-before reasoning.

Flow

A typical interaction:

1. Create several Node instances

2. Nodes perform local_event() calls (clock ticks: 1, 2, 3...)

3. Node A calls send_message(B, "hello"):

4. total_order() can sort all events from all nodes into a single consistent sequence

5. happensbefore() can determine if any two events are causally related by walking the parent/_cause graph

For mutual exclusion, the flow adds:

1. Wrap nodes in a LamportMutex

2. mutex.request(node) broadcasts REQUEST, collects ACKs

3. mutex.can_enter(node) checks queue head + ACK completeness

4. After critical section work, mutex.release(node) broadcasts RELEASE

Invariants

Error Handling

There is essentially none. The code assumes:

No exceptions are raised or caught anywhere. This is appropriate for a reference implementation — it models the algorithm's happy path, not network failure modes.

Topics to Explore

Beliefs