TimeWarp.Model behaviour (TimeWarp v0.2.0)

Copy Markdown View Source

What a domain model implements. The engine handles all Time Warp mechanics — rollback, anti-messages, GVT, fossil collection.

The purity contract (hard requirement)

handle_event/3 MUST be pure: no side effects, no wall-clock reads, no randomness outside a seeded PRNG carried inside model_state. A model that violates this produces non-reproducible, silently-wrong results.

Two engine features defend the contract:

  • TimeWarp.Rand — a seeded PRNG carried in model_state, so rollback restores the PRNG position for free with the state snapshot.
  • The Divergence Detector (check_purity: true) — exploits the fact that rollback re-executes events, and asserts a re-execution emits the identical events it did the first time. Impurity surfaces immediately instead of corrupting results.

PRNG discipline and lazy cancellation

Lazy cancellation collapses the rollback storm by suppressing an anti-message when re-execution re-emits a byte-identical event. Whether it pays is a property of the model, and the deciding factor is precise: does the event that a straggler interleaves consume a PRNG draw? If it does, every subsequently re-executed event draws different numbers, so its emission changes in timestamp/payload and the match fails — even when the logical decision is identical (same routing choice, different drawn travel time is enough to kill it). A purely random model (see TimeWarp.Examples.PHOLD) therefore gets a zero match rate and lazy cannot help it.

This makes PRNG placement a design lever: keep bookkeeping events (occupancy updates, state transitions with no stochastic duration) PRNG-free, and concentrate draws in as few event types as possible. Stragglers landing on PRNG-free events re-emit identically and lazy pays; the lazy_matched stat measures whether that discipline held.

Summary

Types

An event to schedule: to target, firing at virtual time at, carrying payload.

Callbacks

Called ONLY by fossil collection, once GVT has passed this event's timestamp. The single sanctioned place for externally-visible effects. Optional.

Process one event. MUST be pure. Returns the new model state and any events to schedule at strictly future virtual times (at > at_of_current_event); target may be the LP itself.

Build the initial model state for one LP from its config args.

Types

emission()

@type emission() :: {target :: lp_id(), at :: vtime(), payload :: term()}

An event to schedule: to target, firing at virtual time at, carrying payload.

lp_id()

@type lp_id() :: TimeWarp.Event.lp_id()

vtime()

@type vtime() :: TimeWarp.Event.vtime()

Callbacks

commit(event, model_state)

(optional)
@callback commit(event :: TimeWarp.Event.t(), model_state :: term()) ::
  :ok | {:ok, new_model_state :: term()}

Called ONLY by fossil collection, once GVT has passed this event's timestamp. The single sanctioned place for externally-visible effects. Optional.

Return :ok to leave the model state untouched, or {:ok, new_model_state} to release the state this event's commit has made unreachable. A model that accumulates per-event state and never returns {:ok, _} grows without bound: handle_event/3 cannot prune, because it never learns GVT and so cannot know what is safe to drop. Commit is the only callback that does.

Events are committed in ascending virtual-time order and the state is threaded through, so a fold across one fossil-collection pass sees each commit in order.

Pruning is bounded, not immediate

Rollback restores a state snapshot from history, and snapshots at or above GVT were taken before the prune, so they still reference the released terms. Structural sharing means those terms survive only as long as the snapshots holding them, which the same fossil-collection pass drops once GVT moves past. Pruning converts unbounded growth into growth bounded by the GVT lag — it does not free the term the instant commit/2 returns.

handle_event(payload, at, model_state)

@callback handle_event(payload :: term(), at :: vtime(), model_state :: term()) ::
  {new_model_state :: term(), [emission()]}

Process one event. MUST be pure. Returns the new model state and any events to schedule at strictly future virtual times (at > at_of_current_event); target may be the LP itself.

init(args)

@callback init(args :: term()) :: model_state :: term()

Build the initial model state for one LP from its config args.