Distributed Systems Playground Open the Playground

SPEC — Distributed Systems Playground#

Status: this is the original design document. What the implementation does today, and where it departs from this text, is in Appendix B.

1. Purpose#

A fully client-side web app, deployable on GitHub Pages, to:

  1. draw a topology of processes and channels interactively;
  2. write distributed algorithms in executable pseudocode in the upon event / trigger style of Cachin, Guerraoui and Rodrigues;
  3. run them in a deterministic, reproducible discrete-event simulation, and watch the run as an animation in which messages visibly travel between processes;
  4. compare the timing model the algorithm assumes with the timing model the network actually follows, and show the consequences of the gap.

Primary audience: teaching (students and instructors). Secondary goal: experimenting with protocols under realistic conditions.

1.1 Non-goals (v1)#


2. Core principle: assumed model vs actual model#

Teaching often starts from the synchronous model, but a real network never is synchronous. The simulator makes the distinction explicit with two separate configurations:

ConfigurationVisible toDefines
Assumed model (assumed)the algorithm (DSL)the guarantees the algorithm may rely on: rounds, DELTA, PHI, GST
Actual model (actual)the simulator onlyhow channels, processes and clocks really behave

The assumed model determines which constructs are legal in the code (static checks, §5.6). The actual model determines what happens during the run.

When the two coincide, the run is the textbook one. When they differ, every violation of the assumption (a message beyond DELTA, a step beyond PHI, clock drift beyond RHO) is:

The key teaching point: in a system that believes it is synchronous, a timing violation turns into an omission (or performance) failure that the algorithm did not plan for.


3. Parametric timing model#

Synchrony models are not rigid modes but points in a parameter space. The classical models are presets.

3.1 Virtual time#

3.2 Actual model parameters (actual)#

Channels (global default, overridable per link):

ParameterTypeMeaning
delaydistribution (§3.4)transmission delay
boundduration | noneupper truncation of the delay; none = unbounded
loss[0,1]loss probability
dup[0,1]duplication probability
fifoboolif true, no overtaking on the same link
spikeProb, spikeExtraprobability, durationoccasional delay spikes added to delay

Processes (global default, overridable per node in later versions):

ParameterTypeMeaning
stepdistributiontime taken to execute one handler step
pauses{prob, duration}arbitrary pauses such as stop-the-world GC (roadmap)

Local clocks (per node): C_i(t) = offset_i + (1 + rho_i) · t

ParameterTypeMeaning
offsetdistributioninitial offset
rhodistributiondrift, sampled once per node

Timers (starttimer) are expressed in local time: with non-zero drift, a timeout of DELTA is not exactly DELTA of global time.

Stabilization (partial synchrony, DLS model):

ParameterTypeMeaning
gstduration | noneGlobal Stabilization Time
preGstDelaydistributiondelay before GST (e.g. heavy-tailed)

After GST the regular delay and bound apply. Messages sent before GST still arrive by GST + bound.

3.3 Assumed model parameters (assumed)#

ParameterValuesEffect in the DSL
timingsynchronous-rounds | synchronous-timed | partial | asynchronousenables or disables constructs
DELTAduration | unknownexposes the constant DELTA if known
PHIduration | unknownexposes the constant PHI if known
RHOnumber | unknownexposes the constant RHO if known
failuresnone | crash | crash-recovery | omissioninformative; used for invariants and warnings (roadmap)

3.4 Distributions#

const(d) · uniform(a, b) · exp(mean) · normal(mu, sigma) (truncated at 0) · lognormal(mu, sigma) (in log ms) · pareto(xm, alpha) · empirical(d1, d2, ...)

Unitless arguments are milliseconds, except shape parameters. Every sample comes from a dedicated PRNG substream derived from (seed, source), where the source is a link, a node or a clock. Changing one link does not alter the randomness of the others, so runs stay comparable.

3.5 Violation policies#

Applied when actual exceeds assumed:

PolicyBehavior
deliver-latethe message is delivered anyway, late (default for timed models)
dropthe late message is discarded and recorded as an omission
next-roundrounds only: deliver in the next round (with emulated rounds, deliver-late already has this effect)
haltthe run stops at the first violation (useful in class)

3.6 Presets#

Presetassumed.timingactualUse
Ideal synchronoussynchronous-roundsperfect lockstep, no violation possibleexplain the algorithm
Realistic synchronoussynchronous-roundsrounds emulated on local clocks; lognormal delays with spikes; rho ≠ 0show what breaks
Timed synchronoussynchronous-timedbound = DELTA, rho = 0algorithms with exact timeouts
Partially synchronouspartial (DELTA unknown)gst = 3s, heavy-tailed before, bounded after◇P, Paxos, Raft
Asynchronousasynchronousbound: noneFLP, reliable broadcast

In the UI a preset is only a starting point: every parameter stays editable, and any edit turns the preset into "Custom".

3.7 Round execution#


4. Simulation engine#

4.1 Faults#

TypeFieldsSemantics
crashnode, atthe node stops; deliveries to it are recorded as lost-crash; pending timers are discarded
recovernode, atthe node restarts: non-stable state variables are re-initialized, timers are cleared, and each instance receives Recovery if it handles it, Init otherwise, bottom-up; ignored with a warning if the node is running
linka, b, from, tothe channel between a and b is down in both directions during [from, to); to empty means forever
partitiongroups, from, toduring [from, to) messages between different groups are dropped; nodes not listed form one more group

5. Language (DSL)#

5.1 Lexical structure#

Every Unicode symbol has an ASCII equivalent:

UnicodeASCIIUnicodeASCII
⟨ ⟩< >∈ ∉in notin
∪ ∩ \union inter minus∅{}
≠ ≤ ≥!= <= >=∧ ∨ ¬and or not
ΠProcs⊆subseteq
← / :=:=∀forall

5.2 Grammar (EBNF)#

program        = { interface | algorithm } ;

(* ---------- Interfaces ---------- *)
interface      = "interface" IDENT { iface_event } "end" ;
iface_event    = ( "request" | "indication" ) IDENT [ "(" [ ident_list ] ")" ] ;

(* ---------- Algorithms ---------- *)
algorithm      = "algorithm" IDENT
                 "implements" IDENT "as" IDENT
                 { "uses" IDENT "as" IDENT [ "via" IDENT ] }
                 [ params ] [ state ]
                 { handler | function }
                 "end" ;

function       = "function" IDENT "(" [ ident_list ] ")" block "end" ;

params         = "params" { IDENT ":=" expr [ ";" ] } ;
state          = "state" { [ "stable" ] IDENT ":=" expr [ ";" ] } ;

handler        = "upon" trigger_cond "do" block "end" ;
trigger_cond   = "event" event_pat [ "where" expr ]
               | "condition" expr
               | "exists" IDENT "in" expr "where" expr ;

event_pat      = LANGLE IDENT "," IDENT [ "|" pattern { "," pattern } ] RANGLE ;
pattern        = "_" | IDENT | literal
               | "[" [ pattern { "," pattern } ] "]" ;

(* ---------- Statements ---------- *)
block          = { stmt [ ";" ] } ;
property       = "property" IDENT ( "always" | "eventually" ) expr "end" ;

stmt           = assign | trigger | if | forall | while
               | timer | assert | log | "skip"
               | "call" IDENT "(" [ expr_list ] ")"
               | "return" [ expr ] ;                        (* only inside functions *)
assign         = IDENT { "[" expr "]" } ":=" expr ;
trigger        = "trigger" LANGLE IDENT "," IDENT [ "|" expr { "," expr } ] RANGLE ;
if             = "if" expr "then" block
                 { "elif" expr "then" block }
                 [ "else" block ] "end" ;
forall         = "forall" IDENT "in" expr [ "where" expr ] "do" block "end" ;
while          = "while" expr "do" block "end" ;
timer          = "starttimer" "(" IDENT "," expr ")"
               | "canceltimer" "(" IDENT ")" ;
assert         = "assert" expr [ "," expr ] ;
log            = "log" expr { "," expr } ;

(* ---------- Expressions (increasing precedence) ---------- *)
expr           = or_expr ;
or_expr        = and_expr { "or" and_expr } ;
and_expr       = not_expr { "and" not_expr } ;
not_expr       = "not" not_expr | cmp_expr ;
cmp_expr       = set_expr [ cmp_op set_expr ] ;          (* no chaining *)
cmp_op         = "=" | "!=" | "<" | "<=" | ">" | ">="
               | "in" | "notin" | "subseteq" ;
set_expr       = add_expr { ( "union" | "inter" | "minus" ) add_expr } ;
add_expr       = mul_expr { ( "+" | "-" ) mul_expr } ;
mul_expr       = unary { ( "*" | "/" | "%" ) unary } ;
unary          = ( "-" | "#" ) unary | postfix ;          (* # = cardinality *)
postfix        = primary { "[" expr "]" | "(" [ expr_list ] ")" } ;
primary        = literal | IDENT | "(" expr ")"
               | "{" [ expr_list ] "}"                     (* set *)
               | "{" IDENT "in" expr "where" expr "}"      (* comprehension *)
               | "[" [ expr_list ] "]" ;                   (* tuple / message *)

literal        = NUMBER | DURATION | STRING | "true" | "false" | "nil" ;
expr_list      = expr { "," expr } ;
ident_list     = IDENT { "," IDENT } ;

(* ---------- Terminals ---------- *)
DURATION       = NUMBER ( "us" | "ms" | "s" ) ;
LANGLE         = "<" | "⟨" ;
RANGLE         = ">" | "⟩" ;

Implementation note: inside a trigger, the ASCII > closes the event, so comparisons in trigger arguments must be wrapped in parentheses. With ⟨ ⟩ there is no ambiguity. Error messages mention this.

5.3 Semantics#

5.4 Built-ins#

NameAvailabilityMeaning
selfalwaysnode id
Π / Procsalwaysset of processes
neighborsalwaysout-neighbors in the topology
Nalwaysnumber of processes
now()timed modelslocal clock
DELTA, PHI, RHOif known in assumedsynchrony constants
roundsynchronous-roundscurrent round
random(a, b)alwaysfrom the node's PRNG substream
min, max, choose, size, keys, values, map, append, toset, str, absalwayspure functions
head, last, tail, sort, reverse, slice, range, get, remove, sum, mean, argmin, argmax, sqrt, ln, exp, pow, floor, ceil, roundalwayspure functions on sequences, collections and numbers
pick(S)alwaysrandom element from the node's PRNG substream

5.5 Modules provided by the simulator#

Every higher abstraction (stubborn link, perfect link, broadcast, failure detector, consensus) is written in the DSL. Its source code is part of the teaching material.

5.7 Batch runner, workers and command line#

src/runner.js runs a scenario several times without the page: it clones the scenario, applies optional field overrides and a timing preset, runs one simulation per seed and returns a compact summary of each (counts of messages by status, violations, failed assertions, outputs with their times, stop reason, error) plus an aggregate and, on request, the runs grouped by the outputs they produced. bin/dsp.mjs is a dependency-free command line front end for it, with exit codes suitable for continuous integration. The page uses the same module inside Web Workers, whose source (engine, library, examples and runner) is inlined in the bundle as a plain text script and turned into a blob URL at run time; where workers are refused, batches run on the page one seed at a time. Both are described in Running scenarios outside the browser.

5.8 Module library#

src/library.js holds modules written in Upon, each with the interface it implements, the interfaces it uses, a summary and its guarantees: RetransmitLinks (stubborn links, bounded retransmissions), EliminateDuplicates and AckLinks (perfect links), SequencedFifoLinks (FIFO perfect links), BasicBroadcast, EagerReliableBroadcast, MajorityAckURB, BroadcastWithSequenceNumber (FIFO reliable broadcast), WaitingCausalBroadcast (vector clocks) and EagerGossip. Adding a module inserts the missing interfaces and, for every used interface that the program does not implement yet, a default module that does.

5.6 Static checks#


6. Scenario (JSON format)#

A scenario is self-contained and can be shared as a file or as a URL (deflate-compressed, base64url).

{
  "version": 1,
  "seed": 5,
  "nodes": [{ "id": 1, "x": 330, "y": 80 }, { "id": 2, "x": 480, "y": 230 }],
  "links": [{ "a": 1, "b": 2, "directed": false, "enabled": true, "loss": "", "delay": "" }],
  "code": "interface … algorithm … end",
  "top": "IncreasingTimeout",
  "inputs": "0ms * Start",
  "faults": [
    { "type": "crash", "node": 2, "at": "6s" },
    { "type": "recover", "node": 2, "at": "8s" },
    { "type": "link", "a": 1, "b": 2, "from": "1s", "to": "2s" },
    { "type": "partition", "groups": "1 2 | 3", "from": "3s", "to": "" }
  ],
  "preset": "partial",
  "assumed": { "timing": "partial", "DELTA": "unknown", "PHI": "unknown", "RHO": "unknown" },
  "actual": {
    "delay": "uniform(5ms, 40ms)", "bound": "50ms", "loss": 0, "dup": 0, "fifo": true,
    "spikeProb": 0, "spikeExtra": "0ms", "step": "uniform(100us, 1ms)",
    "offset": "uniform(0ms, 20ms)", "rho": "uniform(-0.0001, 0.0001)",
    "gst": "3s", "preGstDelay": "pareto(20ms, 1.1)", "roundMode": "lockstep", "roundLen": "1s"
  },
  "violationPolicy": "deliver-late",
  "tieBreak": "stable",
  "haltOnAssert": false,
  "stopAt": "12s"
}

7. User interface#

  1. Topology editor: add and remove processes and links, drag, generators (ring, grid, complete, star, line, binary tree, random), a properties bar per node and per link.
  2. Code editor: syntax highlighting, inline diagnostics, a symbol palette, a quick reference.
  3. Timing panel: presets, assumed and actual parameters, a delay histogram with bound and DELTA markers and the share of messages exceeding DELTA.
  4. Animated execution on the topology:
    • messages travel along links as labeled packets (the first atom of the payload, e.g. FLOOD) with a trail; red for violations, amber for messages the network will lose, gray for messages to a crashed process;
    • arrival ripples on the recipient, ✕ marks where a message is lost or discarded;
    • processing glow while a node executes a step, a timer icon when a timeout fires, a round badge;
    • an output bubble when a process emits an indication, a flash when it crashes or recovers;
    • links interrupted by a failure or a partition drawn as dashed red lines with a ✂ mark;
    • clicking a packet pauses and shows its details, with shortcuts to its send and arrival times.
  5. Playback: play, pause, previous and next event, time scrubber; speeds from 2 ms to 5 s of simulated time per second, "auto" (the interesting part of the run in about 25 s), and event by event (each step animated at a constant pace regardless of the time scale). Optional autoplay after each run.
  6. Interaction during playback: with a process selected, inject an event (a request of the main algorithm), a crash, a recovery or an isolation at the cursor time; with a link selected, take it down for a given duration. The simulation re-runs and continues from that point.
  7. Faults list in the Scenario tab, with a form for every fault type.
  8. Stack view: the module tree of a process with the application on top and the network at the bottom; the latest requests (down) and indications (up) between modules at the cursor, event counts per module, and a list of recent local events.
  9. Layer colors: messages and packets colored by the module that originated them, with a legend.
  10. Causality mode: clicking an event computes its causal past and future (the happened-before relation over delivered messages); the diagram shades both, fades unrelated messages and reports how many events precede, follow or are concurrent with it; the graph marks the processes the event has reached at the cursor.
  11. Space-time diagram synchronized with the cursor: processing bars, messages, violations, outputs, round lines, GST, down intervals with crash and recovery markers, shaded partitions and link failures; pan, zoom, "Action" and "All" views; hovering shows message details, clicking selects the message.
  12. State inspector: local clock, round and every variable of every instance at the cursor, with changes highlighted.
  13. Event log: filterable; clicking an entry moves the cursor.
  14. Comparison of two runs side by side (roadmap).

8. Architecture and repository#

Target architecture (see Appendix B for what the implementation does instead):

The parser and the engine have no DOM dependencies and also run under Node, which the CI uses for automated tests.


9. Acceptance criteria (MVP)#

10. Roadmap#

VersionContent
v0.1DSL, Net/timer/Rounds, crashes, presets, space-time diagram, time travel
v0.2English UI, animated execution on the topology, event-by-event playback, interactive injection
v0.3crash-recovery with stable, link failures and partitions over time, fault injection at the cursor
v0.4responsive layouts and touch support, faster rendering of large runs, robustness fixes
v0.5functions and via in Upon, more built-ins, module library for links and broadcast, stack view, layer colors, causality mode, full user documentation
v0.6visual identity, settings (theme, palette, language), presentation mode, documentation site, batch runner and command line tool
v0.7batch runner and command line tool
v0.8global invariants checked across processes, reported in the interface and by the batch runner
v0.9one-way link failures, process pauses and omission faults
v0.10batch runs over many seeds in workers, browser suites in the repository, interface split by area
v0.11generated fault schedules for batch runs
v0.12counterexample minimization
v0.13catalog algorithms with their properties: P, Ω, mutual exclusion, two-phase commit
v0.18writing help: contextual completion and quick fixes
v0.19catalog: logical clocks and the Chandy-Lamport snapshot
v0.20–0.23catalog: quorum register, total order broadcast, Paxos, Ben-Or
v1.0suite saved with the scenario, Markdown reports, npm package, accessibility
v1.1failure detectors (P, ◇P ping-pong, Ω, φ-accrual, SWIM) with suspicion matrix and quality metrics
v1.2algorithm catalog (clocks, snapshots, elections, mutual exclusion, consensus, replication, 2PC) with the properties they must satisfy
v1.1global invariants, node pauses and omissions, one-way link failures, side-by-side comparison
v1.2failure detector oracles (P, ◇P, Ω) as provided modules, to study consensus on top of the abstraction
v2Byzantine faults (adversarial nodes written in the DSL), systematic exploration of interleavings, trace export

Appendix A — Examples#

A.1 FloodSet (synchronous rounds, complete graph)#

interface Consensus
  request Propose(v)
  indication Decide(v)
end

algorithm FloodSet
  implements Consensus as c
  uses Rounds as net
  params
    f := 1
  state
    W := ∅
    decided := false

  upon event ⟨c, Propose | v⟩ do
    W := W ∪ {v}
  end

  upon event ⟨net, RoundStart | r⟩ where r ≤ f + 1 do
    forall q in neighbors do
      trigger ⟨net, Send | q, [VALUES, W]⟩
    end
  end

  upon event ⟨net, Deliver | p, [VALUES, V]⟩ do
    W := W ∪ V
  end

  upon event ⟨net, RoundEnd | r⟩ where r = f + 1 and not decided do
    decided := true
    trigger ⟨c, Decide | min(W)⟩
  end
end

A.2 Eventually perfect failure detector with increasing timeout (partial synchrony)#

interface EventuallyPerfectFailureDetector
  indication Suspect(p)
  indication Restore(p)
end

algorithm IncreasingTimeout
  implements EventuallyPerfectFailureDetector as epfd
  uses PerfectLinks as pl
  params
    DELTA0 := 100ms
  state
    alive := Π
    suspected := ∅
    delay := DELTA0

  upon event ⟨epfd, Init⟩ do
    starttimer(t, delay)
  end

  upon event ⟨timer, Timeout | t⟩ do
    if alive ∩ suspected ≠ ∅ then
      delay := delay + DELTA0
    end
    forall p in Π do
      if p ∉ alive and p ∉ suspected then
        suspected := suspected ∪ {p}
        trigger ⟨epfd, Suspect | p⟩
      elif p ∈ alive and p ∈ suspected then
        suspected := suspected \ {p}
        trigger ⟨epfd, Restore | p⟩
      end
      trigger ⟨pl, Send | p, [HEARTBEAT_REQUEST]⟩
    end
    alive := ∅
    starttimer(t, delay)
  end

  upon event ⟨pl, Deliver | q, [HEARTBEAT_REQUEST]⟩ do
    trigger ⟨pl, Send | q, [HEARTBEAT_REPLY]⟩
  end

  upon event ⟨pl, Deliver | p, [HEARTBEAT_REPLY]⟩ do
    alive := alive ∪ {p}
  end
end

IncreasingTimeout does not use DELTA, so it compiles with DELTA: unknown, which is exactly the point of the algorithm under partial synchrony.


Appendix B — Implementation status (version 1.x)#

Implemented#

Differences from the specification#

Specificationversion 1.xReason
TypeScript, Vite, React, Lezer, CodeMirror, React Flowdependency-free JavaScript, SVG and canvas, custom highlighting editorone static file, no build step needed to use it
Engine in a Web Workerthe interactive run is on the main thread, capped at 150,000 events; batches and profiles run in a pool of workersthe single run stays simple to reason about, the heavy work does not block the page
Canvas for animation beyond a few hundred nodesSVG with a static layer redrawn only on topology changes, a separate composited layer for animations, pooled elements, and batched canvas drawing for the diagramkeeps editing simple while playing large runs smoothly
Periodic snapshots for time travelthe whole run is computed first and then replayed; state recorded at every step that changes itequivalent thanks to determinism, and simpler
Explicit stack in the scenariobinding in the code: via when needed, otherwise the first algorithm implementing Xthe choice lives next to the code that depends on it
Standard library in .dalg filesa library of Upon modules in library.js, added from the editor; RetransmitLinks retransmits a bounded number of timestextbook stubborn links retransmit forever and make traces grow too much
Properties over the whole systemproperty … always/eventually over the state of the main algorithm of every process; deeper modules must expose what they want observedthe property language stays the expression language, with no new syntax

What is still missing#

  1. Byzantine processes: a process running a different algorithm, lying or saying different things to different processes. This is the one gap in the fault model, and it needs a mechanism the engine does not have.
  2. Message corruption, clock jumps and partial slowdowns (a pause is all or nothing).
  3. Side-by-side comparison of two runs.
  4. The interactive run in a worker, so that a single heavy run cannot block the page.
  5. A configurable grid for the behaviour profile.
Generated from docs/SPEC.md. Released under the MIT License.