Below is a coordinator and three participants (think of them as three databases). Pick how each one will vote, then press Run protocol. In Phase 1 the coordinator asks everyone to prepare and collects votes. In Phase 2 it broadcasts the verdict.
<p class="hint">A coordinator wants three databases to commit one transaction <b>atomically</b>. Set each participant's vote, then run the protocol. It commits only if <b>everyone</b> votes YES — a single NO rolls them all back.</p>
<div class="coord" id="coord">Coordinator: idle</div>
<div class="parts" id="parts"></div>
<div class="status" id="status">Set the votes, then run the protocol.</div>
<div class="btns">
<button id="run" type="button">Run protocol</button>
<button id="reset" type="button" class="ghost">Reset (all YES)</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.coord { font: 700 14px system-ui, sans-serif; background: #1d3557; color: #fff;
padding: .55rem .8rem; border-radius: 8px; text-align: center; margin-bottom: .7rem; }
.coord.commit { background: #0a7d33; }
.coord.abort { background: #c92f3c; }
.parts { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; }
.part { border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem; background: #f4f7fa; }
.part h4 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; }
.vote { display: flex; gap: .3rem; margin-bottom: .5rem; }
.vote button { flex: 1; font: 600 12px system-ui; padding: .3rem; border: 1px solid #adb1b8;
background: #fff; color: #444; border-radius: 6px; cursor: pointer; }
.vote button.sel-yes { background: #0a7d33; color: #fff; border-color: #0a7d33; }
.vote button.sel-no { background: #c92f3c; color: #fff; border-color: #c92f3c; }
.state { font: 700 13px ui-monospace, monospace; text-align: center; padding: .35rem;
border-radius: 6px; background: #e8eef3; color: #1d3557; min-height: 1.6em; }
.state.committed { background: #d6f0de; color: #0a7d33; }
.state.aborted { background: #f7d9dc; color: #c92f3c; }
.status { font-size: 1rem; font-weight: 600; margin: .7rem 0 .5rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button.act { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
#run { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
#reset { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 8px; cursor: pointer; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Two-phase commit (2PC). Phase 1: coordinator asks each participant to PREPARE
// and collects a YES/NO vote. Phase 2: it COMMITs iff EVERY vote is YES; a single
// NO forces a global ABORT and all participants roll back — atomicity, all-or-nothing.
const NAMES = ['DB Alpha', 'DB Beta', 'DB Gamma'];
let votes = ['YES', 'YES', 'YES']; // player-controlled votes
const sleep = ms => new Promise(r => setTimeout(r, ms));
function renderParts(states){
const wrap = document.getElementById('parts'); wrap.innerHTML = '';
NAMES.forEach((name, i) => {
const cell = document.createElement('div'); cell.className = 'part';
const st = states ? states[i] : '—';
const cls = st === 'committed' ? ' committed' : st === 'aborted' ? ' aborted' : '';
cell.innerHTML =
'<h4>'+name+'</h4>' +
'<div class="vote">' +
'<button data-i="'+i+'" data-v="YES" class="'+(votes[i]==='YES'?'sel-yes':'')+'">YES</button>' +
'<button data-i="'+i+'" data-v="NO" class="'+(votes[i]==='NO'?'sel-no':'')+'">NO</button>' +
'</div>' +
'<div class="state'+cls+'">'+(states?states[i]:'ready')+'</div>';
wrap.appendChild(cell);
});
wrap.querySelectorAll('.vote button').forEach(b => b.addEventListener('click', () => {
votes[+b.dataset.i] = b.dataset.v;
setCoord('Coordinator: idle', '');
setStatus('Set the votes, then run the protocol.', '');
renderParts(null);
}));
}
function setCoord(t, cls){ const c = document.getElementById('coord'); c.textContent = t; c.className = 'coord '+cls; }
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t; s.className = 'status '+cls; }
async function run(){
document.getElementById('run').disabled = true;
// Phase 1: prepare + collect votes
setCoord('Phase 1 — asking everyone to PREPARE…', '');
renderParts(NAMES.map(() => 'preparing…'));
await sleep(700);
renderParts(votes.map(v => fmt('voted %v%', { v: v })));
const allYes = votes.every(v => v === 'YES');
await sleep(700);
// Phase 2: decide + broadcast. COMMIT iff every vote is YES.
const decision = allYes ? 'COMMIT' : 'ABORT';
setCoord(fmt('Phase 2 — decision: %decision%', { decision: decision }), allYes ? 'commit' : 'abort');
const finalStates = NAMES.map(() => allYes ? 'committed' : 'aborted');
renderParts(finalStates);
if (allYes) setStatus('✓ Unanimous YES — transaction COMMITTED everywhere.', 'ok');
else setStatus('✗ At least one NO — global ABORT, every participant rolled back.', 'bad');
console.log('votes:', votes.join(', '), '→ decision:', decision,
'| all participants:', finalStates.every(s => s === finalStates[0]) ? 'AGREE (atomic)' : 'DIVERGED');
document.getElementById('run').disabled = false;
}
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', () => {
votes = ['YES','YES','YES']; setCoord('Coordinator: idle',''); setStatus('Set the votes, then run the protocol.',''); renderParts(null);
});
renderParts(null);
<p class="hint">A coordinator wants three databases to commit one transaction <b>atomically</b>. Set each participant's vote, then run the protocol. It commits only if <b>everyone</b> votes YES — a single NO rolls them all back.</p>
<div class="coord" id="coord">Coordinator: idle</div>
<div class="parts" id="parts"></div>
<div class="status" id="status">Set the votes, then run the protocol.</div>
<div class="btns">
<button id="run" type="button">Run protocol</button>
<button id="reset" type="button" class="ghost">Reset (all YES)</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.coord { font: 700 14px system-ui, sans-serif; background: #1d3557; color: #fff;
padding: .55rem .8rem; border-radius: 8px; text-align: center; margin-bottom: .7rem; }
.coord.commit { background: #0a7d33; }
.coord.abort { background: #c92f3c; }
.parts { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; }
.part { border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem; background: #f4f7fa; }
.part h4 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; }
.vote { display: flex; gap: .3rem; margin-bottom: .5rem; }
.vote button { flex: 1; font: 600 12px system-ui; padding: .3rem; border: 1px solid #adb1b8;
background: #fff; color: #444; border-radius: 6px; cursor: pointer; }
.vote button.sel-yes { background: #0a7d33; color: #fff; border-color: #0a7d33; }
.vote button.sel-no { background: #c92f3c; color: #fff; border-color: #c92f3c; }
.state { font: 700 13px ui-monospace, monospace; text-align: center; padding: .35rem;
border-radius: 6px; background: #e8eef3; color: #1d3557; min-height: 1.6em; }
.state.committed { background: #d6f0de; color: #0a7d33; }
.state.aborted { background: #f7d9dc; color: #c92f3c; }
.status { font-size: 1rem; font-weight: 600; margin: .7rem 0 .5rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button.act { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
#run { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
#reset { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 8px; cursor: pointer; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Two-phase commit (2PC). Phase 1: coordinator asks each participant to PREPARE
// and collects a YES/NO vote. Phase 2: it COMMITs iff EVERY vote is YES; a single
// NO forces a global ABORT and all participants roll back — atomicity, all-or-nothing.
const NAMES = ['DB Alpha', 'DB Beta', 'DB Gamma'];
let votes = ['YES', 'YES', 'YES']; // player-controlled votes
const sleep = ms => new Promise(r => setTimeout(r, ms));
function renderParts(states){
const wrap = document.getElementById('parts'); wrap.innerHTML = '';
NAMES.forEach((name, i) => {
const cell = document.createElement('div'); cell.className = 'part';
const st = states ? states[i] : '—';
const cls = st === 'committed' ? ' committed' : st === 'aborted' ? ' aborted' : '';
cell.innerHTML =
'<h4>'+name+'</h4>' +
'<div class="vote">' +
'<button data-i="'+i+'" data-v="YES" class="'+(votes[i]==='YES'?'sel-yes':'')+'">YES</button>' +
'<button data-i="'+i+'" data-v="NO" class="'+(votes[i]==='NO'?'sel-no':'')+'">NO</button>' +
'</div>' +
'<div class="state'+cls+'">'+(states?states[i]:'ready')+'</div>';
wrap.appendChild(cell);
});
wrap.querySelectorAll('.vote button').forEach(b => b.addEventListener('click', () => {
votes[+b.dataset.i] = b.dataset.v;
setCoord('Coordinator: idle', '');
setStatus('Set the votes, then run the protocol.', '');
renderParts(null);
}));
}
function setCoord(t, cls){ const c = document.getElementById('coord'); c.textContent = t; c.className = 'coord '+cls; }
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t; s.className = 'status '+cls; }
async function run(){
document.getElementById('run').disabled = true;
// Phase 1: prepare + collect votes
setCoord('Phase 1 — asking everyone to PREPARE…', '');
renderParts(NAMES.map(() => 'preparing…'));
await sleep(700);
renderParts(votes.map(v => fmt('voted %v%', { v: v })));
const allYes = votes.every(v => v === 'YES');
await sleep(700);
// Phase 2: decide + broadcast. COMMIT iff every vote is YES.
const decision = allYes ? 'COMMIT' : 'ABORT';
setCoord(fmt('Phase 2 — decision: %decision%', { decision: decision }), allYes ? 'commit' : 'abort');
const finalStates = NAMES.map(() => allYes ? 'committed' : 'aborted');
renderParts(finalStates);
if (allYes) setStatus('✓ Unanimous YES — transaction COMMITTED everywhere.', 'ok');
else setStatus('✗ At least one NO — global ABORT, every participant rolled back.', 'bad');
console.log('votes:', votes.join(', '), '→ decision:', decision,
'| all participants:', finalStates.every(s => s === finalStates[0]) ? 'AGREE (atomic)' : 'DIVERGED');
document.getElementById('run').disabled = false;
}
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', () => {
votes = ['YES','YES','YES']; setCoord('Coordinator: idle',''); setStatus('Set the votes, then run the protocol.',''); renderParts(null);
});
renderParts(null);
The rule is brutally simple: the transaction commits only if every participant votes YES. Flip even one vote to NO and watch the coordinator order a global ABORT — every machine rolls back, including the ones that were perfectly happy to commit. That all-or-nothing guarantee is the whole point.
Comments
Loading comments...