<p class="hint">Three nodes — <b>A → B → C</b> — pass tokens continuously. Click <b>Step</b> to advance time; click <b>Snapshot!</b> to trigger the Chandy-Lamport marker protocol. Watch how each node records its local state and the in-transit tokens become the channel state.</p>
<div id="scene"></div>
<div id="log"></div>
<div class="btns">
<button id="btn-step">Step</button>
<button id="btn-snap">Snapshot!</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#scene { display: flex; align-items: center; gap: 0; margin: .5rem 0 .6rem; flex-wrap: nowrap; }
.node { width: 72px; height: 72px; border-radius: 12px; border: 2px solid #1d3557;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: #e8eef3; flex-shrink: 0; transition: background .3s; }
.node.recorded { background: #c7f0d8; border-color: #0a7d33; }
.node-label { font-weight: 700; font-size: 1.1rem; color: #1d3557; }
.node-tokens { font-size: .78rem; color: #444; margin-top: 2px; }
.node-state { font-size: .7rem; color: #0a7d33; font-weight: 600; min-height: 1em; }
.channel { flex: 1; display: flex; flex-direction: column; align-items: center;
justify-content: center; position: relative; min-width: 60px; }
.channel-arrow { font-size: 1.1rem; color: #888; }
.channel-msgs { display: flex; gap: 3px; justify-content: center; flex-wrap: wrap;
min-height: 20px; margin-top: 2px; }
.token { width: 18px; height: 18px; border-radius: 50%; background: #f4a261;
border: 1.5px solid #c97a3b; display: flex; align-items: center;
justify-content: center; font-size: .6rem; font-weight: 700; color: #fff; }
.token.marker { background: #1d3557; border-color: #0a2340; }
.channel-state { font-size: .68rem; color: #0a7d33; font-weight: 600;
min-height: 1em; text-align: center; }
#log { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 8px;
padding: .5rem .7rem; font-size: .8rem; line-height: 1.6; max-height: 150px;
overflow-y: auto; margin-bottom: .6rem; font-family: ui-monospace, monospace; }
#log .ok { color: #0a7d33; font-weight: 600; }
#log .info { color: #1d3557; }
#log .mark { color: #c97a3b; font-weight: 600; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Chandy-Lamport global snapshot demo.
// System: 3 nodes A→B→C with FIFO channels. Tokens flow A→B and B→C each step.
// Snapshot: initiated by A. Marker travels A→B→C; each node records state on
// first marker receipt; channel state = tokens that arrived before marker closed.
const NODES = ['A', 'B', 'C'];
function initState() {
return {
// tokens held at each node
held: { A: 3, B: 2, C: 1 },
// in-transit queues for channels A→B and B→C (each is an array of items)
// items are either 'token' or 'marker'
ch: { AB: [], BC: [] },
// snapshot recording per node: null = not recorded, else { tokens, step }
snap: { A: null, B: null, C: null },
// channel state recording: recorded after marker passes, captures pre-marker tokens
chanSnap: { AB: null, BC: null },
// which channels are "open" (still accepting tokens into channel state)
// After a node records, it starts watching incoming channels for channel state
watching: { AB: false, BC: false },
// tokens captured for channel state (tokens that arrived while watching)
chanCapture: { AB: [], BC: [] },
snapshotStarted: false,
snapshotDone: false,
step: 0
};
}
let S = initState();
const logEl = document.getElementById('log');
function log(msg, cls = 'info') {
const d = document.createElement('div');
d.className = cls;
d.textContent = fmt('[step %step%] %msg%', { step: S.step, msg: msg });
logEl.appendChild(d);
logEl.scrollTop = logEl.scrollHeight;
}
function render() {
const scene = document.getElementById('scene');
scene.innerHTML = '';
// Helper: render a node box
function nodeEl(id) {
const div = document.createElement('div');
div.className = 'node' + (S.snap[id] ? ' recorded' : '');
const lbl = document.createElement('div');
lbl.className = 'node-label';
lbl.textContent = id;
const tok = document.createElement('div');
tok.className = 'node-tokens';
tok.textContent = fmt('tokens: %v%', { v: S.held[id] });
const st = document.createElement('div');
st.className = 'node-state';
if (S.snap[id]) st.textContent = fmt('snap@%step%: %tokens%', { step: S.snap[id].step, tokens: S.snap[id].tokens });
div.appendChild(lbl); div.appendChild(tok); div.appendChild(st);
return div;
}
// Helper: render a channel
function chanEl(key) {
const div = document.createElement('div');
div.className = 'channel';
const arr = document.createElement('div');
arr.className = 'channel-arrow';
arr.textContent = '→';
const msgs = document.createElement('div');
msgs.className = 'channel-msgs';
S.ch[key].forEach(item => {
const t = document.createElement('div');
t.className = 'token' + (item === 'marker' ? ' marker' : '');
t.textContent = item === 'marker' ? 'M' : '●';
msgs.appendChild(t);
});
const cst = document.createElement('div');
cst.className = 'channel-state';
if (S.chanSnap[key] !== null) {
cst.textContent = fmt('ch: %v%', { v: S.chanSnap[key] });
}
div.appendChild(arr); div.appendChild(msgs); div.appendChild(cst);
return div;
}
scene.appendChild(nodeEl('A'));
scene.appendChild(chanEl('AB'));
scene.appendChild(nodeEl('B'));
scene.appendChild(chanEl('BC'));
scene.appendChild(nodeEl('C'));
document.getElementById('btn-snap').disabled = S.snapshotStarted;
}
function doStep() {
S.step++;
// 1. Deliver one item from each channel (FIFO)
function deliver(ch, to) {
if (S.ch[ch].length === 0) return;
const item = S.ch[ch].shift();
if (item === 'marker') {
// The receiving node processes the marker
if (!S.snap[to]) {
// First marker: record state
S.snap[to] = { tokens: S.held[to], step: S.step };
log(fmt('Node %to% received marker → records state: tokens=%v%', { to: to, v: S.held[to] }), 'ok');
// Start watching incoming channel for channel state (already captured what arrived before)
S.chanSnap[ch] = S.chanCapture[ch].length;
log(fmt('Channel %ch% state sealed: %length% in-transit token(s)', { ch: ch, length: S.chanCapture[ch].length }), 'mark');
S.watching[ch] = false;
// Forward marker if not the last node
if (to === 'B') { S.ch['BC'].push('marker'); log('Node B forwards marker → BC', 'mark'); }
if (to === 'C') {
// Done
S.snapshotDone = true;
log(fmt('Snapshot complete! Global state = A:%tokens% B:%tokens2% C:%tokens3% | ch AB:%AB% ch BC:%BC%', { tokens: S.snap.A.tokens, tokens2: S.snap.B.tokens, tokens3: S.snap.C.tokens, AB: S.chanSnap.AB, BC: S.chanSnap.BC }), 'ok');
}
} else {
// Subsequent marker on same channel: just seal channel state
S.chanSnap[ch] = S.chanCapture[ch].length;
log(fmt('Duplicate marker on %ch% ignored; ch state sealed: %length%', { ch: ch, length: S.chanCapture[ch].length }), 'mark');
S.watching[ch] = false;
}
} else {
// Regular token delivered
if (S.watching[ch]) {
// This token arrived before the marker on this channel → channel state
S.chanCapture[ch].push('token');
}
S.held[to]++;
log(fmt('Token delivered on %ch% → %to% now holds %v%', { ch: ch, to: to, v: S.held[to] }));
}
}
// Deliver from BC first, then AB (order within step doesn't matter for FIFO)
deliver('BC', 'C');
deliver('AB', 'B');
// 2. Node A sends a token to B each step (normal operation)
if (S.held.A > 0) {
S.held.A--;
S.ch.AB.push('token');
}
// 3. Node B passes one token to C each step if it has tokens
if (S.held.B > 0) {
S.held.B--;
S.ch.BC.push('token');
}
render();
}
function doSnapshot() {
if (S.snapshotStarted) return;
S.snapshotStarted = true;
S.step++;
// A records its own state immediately
S.snap.A = { tokens: S.held.A, step: S.step };
log(fmt('Snapshot initiated by A → records state: tokens=%A%', { A: S.held.A }), 'ok');
// A sends marker on AB; start watching AB for channel state
S.ch.AB.push('marker');
S.watching.AB = true;
log('A sends marker on channel AB', 'mark');
render();
}
function doReset() {
S = initState();
logEl.innerHTML = '';
render();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-snap').addEventListener('click', doSnapshot);
document.getElementById('btn-reset').addEventListener('click', doReset);
render();
<p class="hint">Three nodes — <b>A → B → C</b> — pass tokens continuously. Click <b>Step</b> to advance time; click <b>Snapshot!</b> to trigger the Chandy-Lamport marker protocol. Watch how each node records its local state and the in-transit tokens become the channel state.</p>
<div id="scene"></div>
<div id="log"></div>
<div class="btns">
<button id="btn-step">Step</button>
<button id="btn-snap">Snapshot!</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#scene { display: flex; align-items: center; gap: 0; margin: .5rem 0 .6rem; flex-wrap: nowrap; }
.node { width: 72px; height: 72px; border-radius: 12px; border: 2px solid #1d3557;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: #e8eef3; flex-shrink: 0; transition: background .3s; }
.node.recorded { background: #c7f0d8; border-color: #0a7d33; }
.node-label { font-weight: 700; font-size: 1.1rem; color: #1d3557; }
.node-tokens { font-size: .78rem; color: #444; margin-top: 2px; }
.node-state { font-size: .7rem; color: #0a7d33; font-weight: 600; min-height: 1em; }
.channel { flex: 1; display: flex; flex-direction: column; align-items: center;
justify-content: center; position: relative; min-width: 60px; }
.channel-arrow { font-size: 1.1rem; color: #888; }
.channel-msgs { display: flex; gap: 3px; justify-content: center; flex-wrap: wrap;
min-height: 20px; margin-top: 2px; }
.token { width: 18px; height: 18px; border-radius: 50%; background: #f4a261;
border: 1.5px solid #c97a3b; display: flex; align-items: center;
justify-content: center; font-size: .6rem; font-weight: 700; color: #fff; }
.token.marker { background: #1d3557; border-color: #0a2340; }
.channel-state { font-size: .68rem; color: #0a7d33; font-weight: 600;
min-height: 1em; text-align: center; }
#log { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 8px;
padding: .5rem .7rem; font-size: .8rem; line-height: 1.6; max-height: 150px;
overflow-y: auto; margin-bottom: .6rem; font-family: ui-monospace, monospace; }
#log .ok { color: #0a7d33; font-weight: 600; }
#log .info { color: #1d3557; }
#log .mark { color: #c97a3b; font-weight: 600; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Chandy-Lamport global snapshot demo.
// System: 3 nodes A→B→C with FIFO channels. Tokens flow A→B and B→C each step.
// Snapshot: initiated by A. Marker travels A→B→C; each node records state on
// first marker receipt; channel state = tokens that arrived before marker closed.
const NODES = ['A', 'B', 'C'];
function initState() {
return {
// tokens held at each node
held: { A: 3, B: 2, C: 1 },
// in-transit queues for channels A→B and B→C (each is an array of items)
// items are either 'token' or 'marker'
ch: { AB: [], BC: [] },
// snapshot recording per node: null = not recorded, else { tokens, step }
snap: { A: null, B: null, C: null },
// channel state recording: recorded after marker passes, captures pre-marker tokens
chanSnap: { AB: null, BC: null },
// which channels are "open" (still accepting tokens into channel state)
// After a node records, it starts watching incoming channels for channel state
watching: { AB: false, BC: false },
// tokens captured for channel state (tokens that arrived while watching)
chanCapture: { AB: [], BC: [] },
snapshotStarted: false,
snapshotDone: false,
step: 0
};
}
let S = initState();
const logEl = document.getElementById('log');
function log(msg, cls = 'info') {
const d = document.createElement('div');
d.className = cls;
d.textContent = fmt('[step %step%] %msg%', { step: S.step, msg: msg });
logEl.appendChild(d);
logEl.scrollTop = logEl.scrollHeight;
}
function render() {
const scene = document.getElementById('scene');
scene.innerHTML = '';
// Helper: render a node box
function nodeEl(id) {
const div = document.createElement('div');
div.className = 'node' + (S.snap[id] ? ' recorded' : '');
const lbl = document.createElement('div');
lbl.className = 'node-label';
lbl.textContent = id;
const tok = document.createElement('div');
tok.className = 'node-tokens';
tok.textContent = fmt('tokens: %v%', { v: S.held[id] });
const st = document.createElement('div');
st.className = 'node-state';
if (S.snap[id]) st.textContent = fmt('snap@%step%: %tokens%', { step: S.snap[id].step, tokens: S.snap[id].tokens });
div.appendChild(lbl); div.appendChild(tok); div.appendChild(st);
return div;
}
// Helper: render a channel
function chanEl(key) {
const div = document.createElement('div');
div.className = 'channel';
const arr = document.createElement('div');
arr.className = 'channel-arrow';
arr.textContent = '→';
const msgs = document.createElement('div');
msgs.className = 'channel-msgs';
S.ch[key].forEach(item => {
const t = document.createElement('div');
t.className = 'token' + (item === 'marker' ? ' marker' : '');
t.textContent = item === 'marker' ? 'M' : '●';
msgs.appendChild(t);
});
const cst = document.createElement('div');
cst.className = 'channel-state';
if (S.chanSnap[key] !== null) {
cst.textContent = fmt('ch: %v%', { v: S.chanSnap[key] });
}
div.appendChild(arr); div.appendChild(msgs); div.appendChild(cst);
return div;
}
scene.appendChild(nodeEl('A'));
scene.appendChild(chanEl('AB'));
scene.appendChild(nodeEl('B'));
scene.appendChild(chanEl('BC'));
scene.appendChild(nodeEl('C'));
document.getElementById('btn-snap').disabled = S.snapshotStarted;
}
function doStep() {
S.step++;
// 1. Deliver one item from each channel (FIFO)
function deliver(ch, to) {
if (S.ch[ch].length === 0) return;
const item = S.ch[ch].shift();
if (item === 'marker') {
// The receiving node processes the marker
if (!S.snap[to]) {
// First marker: record state
S.snap[to] = { tokens: S.held[to], step: S.step };
log(fmt('Node %to% received marker → records state: tokens=%v%', { to: to, v: S.held[to] }), 'ok');
// Start watching incoming channel for channel state (already captured what arrived before)
S.chanSnap[ch] = S.chanCapture[ch].length;
log(fmt('Channel %ch% state sealed: %length% in-transit token(s)', { ch: ch, length: S.chanCapture[ch].length }), 'mark');
S.watching[ch] = false;
// Forward marker if not the last node
if (to === 'B') { S.ch['BC'].push('marker'); log('Node B forwards marker → BC', 'mark'); }
if (to === 'C') {
// Done
S.snapshotDone = true;
log(fmt('Snapshot complete! Global state = A:%tokens% B:%tokens2% C:%tokens3% | ch AB:%AB% ch BC:%BC%', { tokens: S.snap.A.tokens, tokens2: S.snap.B.tokens, tokens3: S.snap.C.tokens, AB: S.chanSnap.AB, BC: S.chanSnap.BC }), 'ok');
}
} else {
// Subsequent marker on same channel: just seal channel state
S.chanSnap[ch] = S.chanCapture[ch].length;
log(fmt('Duplicate marker on %ch% ignored; ch state sealed: %length%', { ch: ch, length: S.chanCapture[ch].length }), 'mark');
S.watching[ch] = false;
}
} else {
// Regular token delivered
if (S.watching[ch]) {
// This token arrived before the marker on this channel → channel state
S.chanCapture[ch].push('token');
}
S.held[to]++;
log(fmt('Token delivered on %ch% → %to% now holds %v%', { ch: ch, to: to, v: S.held[to] }));
}
}
// Deliver from BC first, then AB (order within step doesn't matter for FIFO)
deliver('BC', 'C');
deliver('AB', 'B');
// 2. Node A sends a token to B each step (normal operation)
if (S.held.A > 0) {
S.held.A--;
S.ch.AB.push('token');
}
// 3. Node B passes one token to C each step if it has tokens
if (S.held.B > 0) {
S.held.B--;
S.ch.BC.push('token');
}
render();
}
function doSnapshot() {
if (S.snapshotStarted) return;
S.snapshotStarted = true;
S.step++;
// A records its own state immediately
S.snap.A = { tokens: S.held.A, step: S.step };
log(fmt('Snapshot initiated by A → records state: tokens=%A%', { A: S.held.A }), 'ok');
// A sends marker on AB; start watching AB for channel state
S.ch.AB.push('marker');
S.watching.AB = true;
log('A sends marker on channel AB', 'mark');
render();
}
function doReset() {
S = initState();
logEl.innerHTML = '';
render();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-snap').addEventListener('click', doSnapshot);
document.getElementById('btn-reset').addEventListener('click', doReset);
render();
Comments
Loading comments...