Skip to content

Logical Clocks

Lamport, Vector, and Hybrid Logical Clocks for causal ordering in distributed systems.

Logical clocks for distributed systems simulation.

Provides three classic logical clock algorithms used in distributed systems:

  • LamportClock: Monotonic counter for total ordering (Lamport 1978).
  • VectorClock: Per-node counters for causal ordering (Fidge/Mattern 1988).
  • HybridLogicalClock: Physical + logical components (Kulkarni et al. 2014).

These are pure algorithm classes — not Entities. Entities store them as fields and call their methods during event handling, exactly like NodeClock and FixedSkew/LinearDrift.

Usage::

from happysimulator import LamportClock, VectorClock, HybridLogicalClock

# Lamport: simple monotonic counter
clock = LamportClock()
clock.tick()  # Local event
ts = clock.send()  # Returns timestamp to embed in message
clock.receive(ts)  # max(local, remote) + 1

# Vector clock: per-node counters for causal ordering
vc = VectorClock("node-1", ["node-1", "node-2", "node-3"])
vc.tick()
snapshot = vc.send()  # dict[str, int] to embed in message
vc.receive(snapshot)  # Element-wise max + increment own

# HLC: physical + logical (CockroachDB/Spanner-style)
hlc = HybridLogicalClock("node-1", physical_clock=node_clock)
ts = hlc.now()  # HLCTimestamp(physical_ns, logical, node_id)
hlc.receive(remote_ts)

LamportClock

LamportClock(initial: int = 0)

Monotonic counter for establishing total ordering of events.

Each local event increments the counter. When sending a message, the current counter is included. On receive, the counter advances to max(local, remote) + 1, ensuring causal ordering.

Parameters:

Name Type Description Default
initial int

Starting counter value (default 0).

0

time property

time: int

Current counter value.

tick

tick() -> None

Record a local event (increment counter).

send

send() -> int

Increment counter and return timestamp to embed in a message.

receive

receive(remote_ts: int) -> None

Update counter on receiving a message.

Sets counter to max(local, remote) + 1.

Parameters:

Name Type Description Default
remote_ts int

The Lamport timestamp from the received message.

required

VectorClock

VectorClock(node_id: str, node_ids: list[str])

Per-node counters for determining causal ordering between events.

Each node maintains a vector of counters (one per node in the system). The happened_before relation enables detecting causally related vs. concurrent events — fundamental to conflict detection in replicated datastores (Dynamo, Riak) and consistency verification.

Parameters:

Name Type Description Default
node_id str

This node's identifier.

required
node_ids list[str]

All node identifiers in the system (including this node).

required

node_id property

node_id: str

This node's identifier.

tick

tick() -> None

Record a local event (increment own counter).

send

send() -> dict[str, int]

Increment own counter and return a snapshot for embedding in a message.

receive

receive(remote: dict[str, int]) -> None

Update vector on receiving a message.

Takes the element-wise max of local and remote vectors, then increments own counter.

Parameters:

Name Type Description Default
remote dict[str, int]

The vector clock snapshot from the received message.

required

snapshot

snapshot() -> dict[str, int]

Return a frozen copy of the current vector.

happened_before

happened_before(other: VectorClock) -> bool

Test if this clock causally precedes other.

Returns True if all components of this vector are <= the corresponding components of other, and at least one is strictly less.

Parameters:

Name Type Description Default
other VectorClock

The vector clock to compare against.

required

is_concurrent

is_concurrent(other: VectorClock) -> bool

Test if this clock and other are concurrent (neither happened-before).

Parameters:

Name Type Description Default
other VectorClock

The vector clock to compare against.

required

merge

merge(other: VectorClock) -> VectorClock

Create a new VectorClock with element-wise max (no increment).

Unlike receive(), this does NOT increment the local counter. Useful for read-only merges (e.g., computing a combined view).

Parameters:

Name Type Description Default
other VectorClock

The vector clock to merge with.

required

Returns:

Type Description
VectorClock

A new VectorClock with the merged vector.

HLCTimestamp dataclass

HLCTimestamp(physical_ns: int, logical: int, node_id: str)

Immutable timestamp from a Hybrid Logical Clock.

Total ordering is defined by (physical_ns, logical, node_id).

Attributes:

Name Type Description
physical_ns int

Nanosecond wall-clock component.

logical int

Logical counter within the same physical tick.

node_id str

Originating node identifier.

to_dict

to_dict() -> dict

Serialize to a plain dict for embedding in event contexts.

from_dict classmethod

from_dict(d: dict) -> HLCTimestamp

Deserialize from a plain dict.

Parameters:

Name Type Description Default
d dict

Dict with keys physical_ns, logical, node_id.

required

HybridLogicalClock

HybridLogicalClock(
    node_id: str,
    physical_clock: NodeClock | None = None,
    wall_time: Callable[[], Instant] | None = None,
)

Hybrid Logical Clock combining physical and logical components.

HLC provides the best of both worlds: timestamps that respect causality (like Lamport clocks) while staying close to physical time (enabling bounded-staleness reads). Used in CockroachDB, Spanner, and similar systems.

The physical component comes from either a NodeClock (for skew modeling) or a plain callable returning an Instant.

Algorithm (Kulkarni et al. 2014):

  • now/send: Read physical time pt. If pt > last.physical_ns, reset logical to 0. Otherwise keep physical from last and increment logical. Return (pt, logical, node_id).
  • receive: Take max of physical time, last physical, and remote physical. Adjust logical based on which components tied.

Parameters:

Name Type Description Default
node_id str

This node's identifier.

required
physical_clock NodeClock | None

A NodeClock to read perceived time from. Mutually exclusive with wall_time.

None
wall_time Callable[[], Instant] | None

A callable returning the current Instant. Mutually exclusive with physical_clock.

None

Raises:

Type Description
ValueError

If both or neither time source is provided.

node_id property

node_id: str

This node's identifier.

now

now() -> HLCTimestamp

Generate a timestamp for the current instant.

Reads the physical clock and advances the logical component if the physical time hasn't changed since the last call.

send

send() -> HLCTimestamp

Generate a timestamp to embed in an outgoing message.

Equivalent to now() — included for API symmetry with LamportClock and VectorClock.

receive

receive(remote: HLCTimestamp) -> None

Update clock state on receiving a remote timestamp.

Implements the HLC receive algorithm: takes the max of physical time, last local physical, and remote physical, then adjusts the logical component based on which values tied.

Parameters:

Name Type Description Default
remote HLCTimestamp

The HLC timestamp from the received message.

required