Skip to content

Clocks

Clock abstraction and per-node clock models for simulating time skew and drift.

Simulation clock for tracking current simulation time.

The Clock provides a shared time reference for all entities in a simulation. The Simulation advances the clock as it processes events. Entities access the current time via their injected clock reference.

Clock

Clock(start_time: Instant)

Tracks the current simulation time.

The simulation creates one Clock instance and shares it with all entities. As events are processed, the simulation calls update() to advance time. Entities query now to get the current time for their logic.

This indirection allows entities to access simulation time without direct coupling to the Simulation class.

Attributes:

Name Type Description
now Instant

The current simulation time.

Initialize clock at the given start time.

now property

now: Instant

The current simulation time.

update

update(time: Instant) -> None

Advance the clock to a new time. Called by the simulation loop.

Per-node clocks with skew and drift models.

In real distributed systems, every node has its own clock with skew and drift. NodeClock transforms true simulation time into perceived local time, enabling modeling of clock-sensitive protocols (leader election, lease expiry, cache TTLs, distributed tracing).

Key insight: NodeClock is a view layer over the shared Clock. Events are still ordered by true simulation time (the global Clock). NodeClock only transforms the read side — what an entity perceives as "now". This avoids causality issues.

Usage::

from happysimulator import NodeClock, FixedSkew, LinearDrift, Duration

# Fixed offset: node clock is 50ms ahead of true time
clock = NodeClock(FixedSkew(Duration.from_seconds(0.05)))

# Drifting clock: 1000 ppm = 1ms drift per second
clock = NodeClock(LinearDrift(rate_ppm=1000))


# In an entity:
class RaftNode(Entity):
    def __init__(self, name, node_clock):
        super().__init__(name)
        self._node_clock = node_clock

    def set_clock(self, clock):
        super().set_clock(clock)
        self._node_clock.set_clock(clock)

    @property
    def local_now(self):
        return self._node_clock.now

ClockModel

Bases: Protocol

Protocol for time transformation models.

A ClockModel transforms true simulation time into perceived local time. Implementations define how a node's clock deviates from the global clock.

read

read(true_time: Instant) -> Instant

Transform true simulation time into perceived local time.

Parameters:

Name Type Description Default
true_time Instant

The actual simulation time from the global clock.

required

Returns:

Type Description
Instant

The time as perceived by the node.

FixedSkew

FixedSkew(offset: Duration)

Constant time offset — clock is always ahead or behind by a fixed amount.

A positive offset means the clock reads ahead of true time (fast clock). A negative offset means the clock reads behind true time (slow clock).

Parameters:

Name Type Description Default
offset Duration

The constant offset to apply. Positive = ahead, negative = behind.

required

offset property

offset: Duration

The fixed offset applied by this model.

read

read(true_time: Instant) -> Instant

Return true_time shifted by the fixed offset.

LinearDrift

LinearDrift(rate_ppm: float)

Clock that runs faster or slower than true time, accumulating drift.

Drift is specified in parts per million (ppm). A rate of 1000 ppm means the clock gains 1ms per second of elapsed true time. Negative ppm means the clock runs slow.

The drift accumulates linearly: at true time T seconds from epoch, the perceived time is T + (T * rate_ppm / 1_000_000) seconds.

Parameters:

Name Type Description Default
rate_ppm float

Drift rate in parts per million. Positive = fast, negative = slow.

required

rate_ppm property

rate_ppm: float

The drift rate in parts per million.

read

read(true_time: Instant) -> Instant

Return true_time with accumulated linear drift.

NodeClock

NodeClock(model: ClockModel | None = None)

Per-node clock that transforms true simulation time via a ClockModel.

NodeClock wraps a base Clock (the shared simulation clock) and applies a ClockModel to transform what the node perceives as "now". The base clock is injected via set_clock(), typically forwarded from Entity.set_clock().

This is a plain class, NOT an Entity. Entities hold a NodeClock reference and forward clock injection to it.

Parameters:

Name Type Description Default
model ClockModel | None

The clock model defining how time is transformed. If None, the node clock returns true time (identity).

None

now property

now: Instant

The perceived local time, transformed by the clock model.

Returns true time if no model is set.

Raises:

Type Description
RuntimeError

If accessed before clock injection.

model property

model: ClockModel | None

The clock model, or None for identity.

set_clock

set_clock(clock: Clock) -> None

Inject the base simulation clock.

Parameters:

Name Type Description Default
clock Clock

The shared simulation clock to read true time from.

required