Below you can step through both protocols round by round. At any point click Crash coordinator to simulate a failure. Watch how 2PC blocks while 3PC recovers on its own.
<!-- Demo layout: protocol toggle, status banner, message-flow diagram, step/crash buttons, explanation text -->
<div id="app">
<div class="controls">
<label class="proto-label">Protocol:</label>
<div class="toggle-group">
<button id="btn2pc" class="toggle active" data-proto="2pc">2PC</button>
<button id="btn3pc" class="toggle" data-proto="3pc">3PC</button>
</div>
<button id="btnReset" class="ghost">Reset</button>
</div>
<div id="statusBanner" class="banner"></div>
<div id="diagram" class="diagram">
<div class="lane-labels">
<div class="lane-lbl">Coordinator</div>
<div class="lane-lbl">A</div>
<div class="lane-lbl">B</div>
<div class="lane-lbl">C</div>
</div>
<div id="timeline" class="timeline"></div>
</div>
<div class="action-row">
<button id="btnStep" class="primary">Next step →</button>
<button id="btnCrash" class="danger">Crash coordinator</button>
</div>
<p id="explanation" class="expl"></p>
</div>
/* Styles for the 3PC vs 2PC interactive demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; }
#app { display: flex; flex-direction: column; gap: .6rem; padding: .4rem; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.proto-label { font-weight: 600; }
.toggle-group { display: flex; border: 1px solid #1d3557; border-radius: 8px; overflow: hidden; }
.toggle { border: none; padding: .3rem .7rem; cursor: pointer; background: #fff; color: #1d3557; font-weight: 600; font-size: 13px; }
.toggle.active { background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; border: 1px solid #1d3557; border-radius: 8px; padding: .3rem .7rem; cursor: pointer; font-size: 13px; }
button.primary { background: #1d3557; color: #fff; border: 1px solid #1d3557; border-radius: 8px; padding: .4rem .9rem; cursor: pointer; font-weight: 600; font-size: 13px; }
button.danger { background: #c92f3c; color: #fff; border: 1px solid #c92f3c; border-radius: 8px; padding: .4rem .9rem; cursor: pointer; font-weight: 600; font-size: 13px; }
button:disabled { opacity: .45; cursor: default; }
.banner { min-height: 1.6em; font-weight: 700; font-size: .92rem; border-radius: 6px; padding: .25rem .6rem; }
.banner.ok { background: #d4edda; color: #0a7d33; }
.banner.bad { background: #fce4e6; color: #c92f3c; }
.banner.warn { background: #fff3cd; color: #856404; }
/* Message-flow diagram grid */
.diagram { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden; background: #f9fbfc; }
.lane-labels { display: grid; grid-template-columns: 80px repeat(3, 1fr); background: #e4eaf0; border-bottom: 1px solid #cdd9e3; }
.lane-lbl { text-align: center; font-weight: 700; font-size: 12px; padding: .3rem; color: #1d3557; }
.lane-lbl:first-child { border-right: 2px solid #1d3557; font-size: 11px; }
.timeline { padding: .4rem .3rem; display: flex; flex-direction: column; gap: 3px; max-height: 240px; overflow-y: auto; }
.row { display: grid; grid-template-columns: 80px repeat(3, 1fr); align-items: center; min-height: 28px; }
.row-label { font-size: 11px; color: #5a7088; padding-right: .3rem; text-align: right; border-right: 2px solid #e4eaf0; }
.msg { display: flex; align-items: center; justify-content: center; }
.pill { display: inline-block; font-size: 11px; font-weight: 700; border-radius: 12px; padding: 2px 7px; white-space: nowrap; }
.pill.send { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; }
.pill.recv { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
.pill.crash { background: #fce4e6; color: #c92f3c; border: 1px solid #f9a8b0; }
.pill.recover { background: #fff3cd; color: #856404; border: 1px solid #fcd34d; }
.action-row { display: flex; gap: .5rem; }
.expl { font-size: .88rem; color: #444; line-height: 1.5; min-height: 2.8em; }
// Step-through simulation of 2PC and 3PC with coordinator crash
// 2PC has 2 phases: vote (PREPARE / VOTE-YES) and commit (COMMIT / ACK)
const STEPS_2PC = [
{ phase: 'Vote', from: 0, to: [1,2,3], msg: 'PREPARE', dir: 'send', expl: 'Coordinator → participants: PREPARE. Each participant locks its resource and gets ready to vote.' },
{ phase: 'Vote', from: [1,2,3], to: 0, msg: 'VOTE-YES', dir: 'recv', expl: 'All participants voted YES. Coordinator now holds all votes but has NOT yet told anyone.' },
{ phase: 'Commit', from: 0, to: [1,2,3], msg: 'COMMIT', dir: 'send', expl: 'Coordinator → participants: COMMIT. If the coordinator crashes NOW, participants are stuck — they voted yes but don’t know the decision.' },
{ phase: 'Commit', from: [1,2,3], to: 0, msg: 'ACK', dir: 'recv', expl: 'All ACKs received. Transaction committed. Coordinator can release its log.' },
];
// 3PC has 3 phases: vote, pre-commit, and commit
const STEPS_3PC = [
{ phase: 'Vote', from: 0, to: [1,2,3], msg: 'PREPARE', dir: 'send', expl: 'Coordinator → participants: PREPARE. Same first step as 2PC.' },
{ phase: 'Vote', from: [1,2,3], to: 0, msg: 'VOTE-YES', dir: 'recv', expl: 'All participants voted YES. Now comes the extra 3PC step.' },
{ phase: 'Pre-commit', from: 0, to: [1,2,3], msg: 'PRE-COMMIT', dir: 'send', expl: 'Coordinator → participants: PRE-COMMIT. This is the key extra message. Every participant now knows ALL others voted yes. If coordinator crashes after this point, survivors can safely commit on their own.' },
{ phase: 'Pre-commit', from: [1,2,3], to: 0, msg: 'ACK', dir: 'recv', expl: 'All participants ACKed PRE-COMMIT. Coordinator can now safely send the final COMMIT.' },
{ phase: 'Commit', from: 0, to: [1,2,3], msg: 'COMMIT', dir: 'send', expl: 'Coordinator → participants: COMMIT. Transaction finalised.' },
{ phase: 'Commit', from: [1,2,3], to: 0, msg: 'ACK', dir: 'recv', expl: 'All ACKs received. Transaction committed.' },
];
const PARTICIPANT_NAMES = ['A','B','C'];
let proto = '2pc';
let stepIndex = 0;
let crashed = false;
let done = false;
function steps(){ return proto === '2pc' ? STEPS_2PC : STEPS_3PC; }
function renderRow(step, highlight){
// Build one timeline row for a protocol step
const row = document.createElement('div');
row.className = 'row' + (highlight ? ' current' : '');
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = step.phase;
row.appendChild(lbl);
for(let i = 0; i < 3; i++){
const cell = document.createElement('div');
cell.className = 'msg';
const from = step.from, to = step.to;
const fromCoord = from === 0;
const toArr = Array.isArray(to) ? to : [to];
const toCoord = toArr.includes(0);
const involvesParticipant = fromCoord ? toArr.includes(i+1) : (Array.isArray(from) ? from.includes(i+1) : from === i+1);
if(involvesParticipant){
const pill = document.createElement('span');
pill.className = 'pill ' + step.dir;
pill.textContent = step.msg;
cell.appendChild(pill);
}
row.appendChild(cell);
}
return row;
}
function renderCrashRow(atStep){
// Build a crash indicator row
const row = document.createElement('div');
row.className = 'row';
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = '💥';
row.appendChild(lbl);
for(let i=0;i<3;i++){
const cell = document.createElement('div');
cell.className = 'msg';
const pill = document.createElement('span');
pill.className = 'pill crash';
pill.textContent = 'CRASHED';
cell.appendChild(pill);
row.appendChild(cell);
}
return row;
}
function renderRecoverRow(canRecover){
// Build a recovery outcome row (green=can decide, red=blocked)
const row = document.createElement('div');
row.className = 'row';
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = '🔄';
row.appendChild(lbl);
for(let i=0;i<3;i++){
const cell = document.createElement('div');
cell.className = 'msg';
const pill = document.createElement('span');
pill.className = canRecover ? 'pill recover' : 'pill crash';
pill.textContent = canRecover ? 'COMMIT (decided)' : 'BLOCKED';
cell.appendChild(pill);
row.appendChild(cell);
}
return row;
}
function redrawTimeline(){
const tl = document.getElementById('timeline');
tl.innerHTML = '';
const allSteps = steps();
for(let i = 0; i < stepIndex; i++){
tl.appendChild(renderRow(allSteps[i], false));
}
if(crashed){
tl.appendChild(renderCrashRow(stepIndex));
// Recovery rule: 3PC recovers if coordinator crashed AFTER sending PRE-COMMIT (stepIndex>=3); 2PC always blocks.
// 3PC: if coordinator crashed AFTER sending PRE-COMMIT (step index >= 3),
// participants saw pre-commit and can commit on their own.
// 2PC: participants can never safely decide on their own.
const canRecover = proto === '3pc' && stepIndex >= 3;
tl.appendChild(renderRecoverRow(canRecover));
}
tl.scrollTop = tl.scrollHeight;
}
function setBanner(text, cls){
const b = document.getElementById('statusBanner');
b.textContent = text;
b.className = 'banner ' + cls;
}
function setExpl(text){
document.getElementById('explanation').textContent = text;
}
function updateButtons(){
const btnStep = document.getElementById('btnStep');
const btnCrash = document.getElementById('btnCrash');
btnStep.disabled = done || crashed;
btnCrash.disabled = done || crashed || stepIndex === 0;
}
function advance(){
if(done || crashed) return;
const allSteps = steps();
if(stepIndex < allSteps.length){
setExpl(allSteps[stepIndex].expl);
stepIndex++;
redrawTimeline();
if(stepIndex === allSteps.length){
done = true;
setBanner('✓ Transaction committed successfully.', 'ok');
setExpl('All participants acknowledged the final COMMIT. The transaction is durable and complete.');
}
}
updateButtons();
}
function crashCoordinator(){
if(done || crashed || stepIndex === 0) return;
crashed = true;
redrawTimeline();
const canRecover = proto === '3pc' && stepIndex >= 3;
if(canRecover){
setBanner('⚠ Coordinator crashed — but 3PC survivors recovered and committed.', 'warn');
setExpl('Because participants already received PRE-COMMIT, they know everyone voted YES. They elect a new coordinator and commit unanimously — no data loss, no blocking.');
} else {
setBanner('✗ Coordinator crashed — participants are blocked (cannot decide safely).', 'bad');
setExpl(proto === '2pc' ? 'Participants voted YES and locked their resources, but they have no idea whether the coordinator said COMMIT or ABORT before crashing. They must wait for the coordinator to recover — potentially forever.' : 'The coordinator crashed before sending PRE-COMMIT. Participants cannot tell if all votes were YES, so they must wait for recovery just like in 2PC.');
}
updateButtons();
}
function reset(){
stepIndex = 0; crashed = false; done = false;
redrawTimeline();
setBanner('', '');
setExpl('Choose a protocol above, then click ’Next step’ to advance. Click ’Crash coordinator’ at any point to simulate a failure.');
updateButtons();
}
function selectProto(p){
proto = p;
document.getElementById('btn2pc').classList.toggle('active', p === '2pc');
document.getElementById('btn3pc').classList.toggle('active', p === '3pc');
reset();
}
document.getElementById('btnStep').addEventListener('click', advance);
document.getElementById('btnCrash').addEventListener('click', crashCoordinator);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('btn2pc').addEventListener('click', () => selectProto('2pc'));
document.getElementById('btn3pc').addEventListener('click', () => selectProto('3pc'));
reset();
<!-- Demo layout: protocol toggle, status banner, message-flow diagram, step/crash buttons, explanation text -->
<div id="app">
<div class="controls">
<label class="proto-label">Protocol:</label>
<div class="toggle-group">
<button id="btn2pc" class="toggle active" data-proto="2pc">2PC</button>
<button id="btn3pc" class="toggle" data-proto="3pc">3PC</button>
</div>
<button id="btnReset" class="ghost">Reset</button>
</div>
<div id="statusBanner" class="banner"></div>
<div id="diagram" class="diagram">
<div class="lane-labels">
<div class="lane-lbl">Coordinator</div>
<div class="lane-lbl">A</div>
<div class="lane-lbl">B</div>
<div class="lane-lbl">C</div>
</div>
<div id="timeline" class="timeline"></div>
</div>
<div class="action-row">
<button id="btnStep" class="primary">Next step →</button>
<button id="btnCrash" class="danger">Crash coordinator</button>
</div>
<p id="explanation" class="expl"></p>
</div>
/* Styles for the 3PC vs 2PC interactive demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; }
#app { display: flex; flex-direction: column; gap: .6rem; padding: .4rem; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.proto-label { font-weight: 600; }
.toggle-group { display: flex; border: 1px solid #1d3557; border-radius: 8px; overflow: hidden; }
.toggle { border: none; padding: .3rem .7rem; cursor: pointer; background: #fff; color: #1d3557; font-weight: 600; font-size: 13px; }
.toggle.active { background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; border: 1px solid #1d3557; border-radius: 8px; padding: .3rem .7rem; cursor: pointer; font-size: 13px; }
button.primary { background: #1d3557; color: #fff; border: 1px solid #1d3557; border-radius: 8px; padding: .4rem .9rem; cursor: pointer; font-weight: 600; font-size: 13px; }
button.danger { background: #c92f3c; color: #fff; border: 1px solid #c92f3c; border-radius: 8px; padding: .4rem .9rem; cursor: pointer; font-weight: 600; font-size: 13px; }
button:disabled { opacity: .45; cursor: default; }
.banner { min-height: 1.6em; font-weight: 700; font-size: .92rem; border-radius: 6px; padding: .25rem .6rem; }
.banner.ok { background: #d4edda; color: #0a7d33; }
.banner.bad { background: #fce4e6; color: #c92f3c; }
.banner.warn { background: #fff3cd; color: #856404; }
/* Message-flow diagram grid */
.diagram { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden; background: #f9fbfc; }
.lane-labels { display: grid; grid-template-columns: 80px repeat(3, 1fr); background: #e4eaf0; border-bottom: 1px solid #cdd9e3; }
.lane-lbl { text-align: center; font-weight: 700; font-size: 12px; padding: .3rem; color: #1d3557; }
.lane-lbl:first-child { border-right: 2px solid #1d3557; font-size: 11px; }
.timeline { padding: .4rem .3rem; display: flex; flex-direction: column; gap: 3px; max-height: 240px; overflow-y: auto; }
.row { display: grid; grid-template-columns: 80px repeat(3, 1fr); align-items: center; min-height: 28px; }
.row-label { font-size: 11px; color: #5a7088; padding-right: .3rem; text-align: right; border-right: 2px solid #e4eaf0; }
.msg { display: flex; align-items: center; justify-content: center; }
.pill { display: inline-block; font-size: 11px; font-weight: 700; border-radius: 12px; padding: 2px 7px; white-space: nowrap; }
.pill.send { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; }
.pill.recv { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
.pill.crash { background: #fce4e6; color: #c92f3c; border: 1px solid #f9a8b0; }
.pill.recover { background: #fff3cd; color: #856404; border: 1px solid #fcd34d; }
.action-row { display: flex; gap: .5rem; }
.expl { font-size: .88rem; color: #444; line-height: 1.5; min-height: 2.8em; }
// Step-through simulation of 2PC and 3PC with coordinator crash
// 2PC has 2 phases: vote (PREPARE / VOTE-YES) and commit (COMMIT / ACK)
const STEPS_2PC = [
{ phase: 'Vote', from: 0, to: [1,2,3], msg: 'PREPARE', dir: 'send', expl: 'Coordinator → participants: PREPARE. Each participant locks its resource and gets ready to vote.' },
{ phase: 'Vote', from: [1,2,3], to: 0, msg: 'VOTE-YES', dir: 'recv', expl: 'All participants voted YES. Coordinator now holds all votes but has NOT yet told anyone.' },
{ phase: 'Commit', from: 0, to: [1,2,3], msg: 'COMMIT', dir: 'send', expl: 'Coordinator → participants: COMMIT. If the coordinator crashes NOW, participants are stuck — they voted yes but don’t know the decision.' },
{ phase: 'Commit', from: [1,2,3], to: 0, msg: 'ACK', dir: 'recv', expl: 'All ACKs received. Transaction committed. Coordinator can release its log.' },
];
// 3PC has 3 phases: vote, pre-commit, and commit
const STEPS_3PC = [
{ phase: 'Vote', from: 0, to: [1,2,3], msg: 'PREPARE', dir: 'send', expl: 'Coordinator → participants: PREPARE. Same first step as 2PC.' },
{ phase: 'Vote', from: [1,2,3], to: 0, msg: 'VOTE-YES', dir: 'recv', expl: 'All participants voted YES. Now comes the extra 3PC step.' },
{ phase: 'Pre-commit', from: 0, to: [1,2,3], msg: 'PRE-COMMIT', dir: 'send', expl: 'Coordinator → participants: PRE-COMMIT. This is the key extra message. Every participant now knows ALL others voted yes. If coordinator crashes after this point, survivors can safely commit on their own.' },
{ phase: 'Pre-commit', from: [1,2,3], to: 0, msg: 'ACK', dir: 'recv', expl: 'All participants ACKed PRE-COMMIT. Coordinator can now safely send the final COMMIT.' },
{ phase: 'Commit', from: 0, to: [1,2,3], msg: 'COMMIT', dir: 'send', expl: 'Coordinator → participants: COMMIT. Transaction finalised.' },
{ phase: 'Commit', from: [1,2,3], to: 0, msg: 'ACK', dir: 'recv', expl: 'All ACKs received. Transaction committed.' },
];
const PARTICIPANT_NAMES = ['A','B','C'];
let proto = '2pc';
let stepIndex = 0;
let crashed = false;
let done = false;
function steps(){ return proto === '2pc' ? STEPS_2PC : STEPS_3PC; }
function renderRow(step, highlight){
// Build one timeline row for a protocol step
const row = document.createElement('div');
row.className = 'row' + (highlight ? ' current' : '');
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = step.phase;
row.appendChild(lbl);
for(let i = 0; i < 3; i++){
const cell = document.createElement('div');
cell.className = 'msg';
const from = step.from, to = step.to;
const fromCoord = from === 0;
const toArr = Array.isArray(to) ? to : [to];
const toCoord = toArr.includes(0);
const involvesParticipant = fromCoord ? toArr.includes(i+1) : (Array.isArray(from) ? from.includes(i+1) : from === i+1);
if(involvesParticipant){
const pill = document.createElement('span');
pill.className = 'pill ' + step.dir;
pill.textContent = step.msg;
cell.appendChild(pill);
}
row.appendChild(cell);
}
return row;
}
function renderCrashRow(atStep){
// Build a crash indicator row
const row = document.createElement('div');
row.className = 'row';
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = '💥';
row.appendChild(lbl);
for(let i=0;i<3;i++){
const cell = document.createElement('div');
cell.className = 'msg';
const pill = document.createElement('span');
pill.className = 'pill crash';
pill.textContent = 'CRASHED';
cell.appendChild(pill);
row.appendChild(cell);
}
return row;
}
function renderRecoverRow(canRecover){
// Build a recovery outcome row (green=can decide, red=blocked)
const row = document.createElement('div');
row.className = 'row';
const lbl = document.createElement('div');
lbl.className = 'row-label';
lbl.textContent = '🔄';
row.appendChild(lbl);
for(let i=0;i<3;i++){
const cell = document.createElement('div');
cell.className = 'msg';
const pill = document.createElement('span');
pill.className = canRecover ? 'pill recover' : 'pill crash';
pill.textContent = canRecover ? 'COMMIT (decided)' : 'BLOCKED';
cell.appendChild(pill);
row.appendChild(cell);
}
return row;
}
function redrawTimeline(){
const tl = document.getElementById('timeline');
tl.innerHTML = '';
const allSteps = steps();
for(let i = 0; i < stepIndex; i++){
tl.appendChild(renderRow(allSteps[i], false));
}
if(crashed){
tl.appendChild(renderCrashRow(stepIndex));
// Recovery rule: 3PC recovers if coordinator crashed AFTER sending PRE-COMMIT (stepIndex>=3); 2PC always blocks.
// 3PC: if coordinator crashed AFTER sending PRE-COMMIT (step index >= 3),
// participants saw pre-commit and can commit on their own.
// 2PC: participants can never safely decide on their own.
const canRecover = proto === '3pc' && stepIndex >= 3;
tl.appendChild(renderRecoverRow(canRecover));
}
tl.scrollTop = tl.scrollHeight;
}
function setBanner(text, cls){
const b = document.getElementById('statusBanner');
b.textContent = text;
b.className = 'banner ' + cls;
}
function setExpl(text){
document.getElementById('explanation').textContent = text;
}
function updateButtons(){
const btnStep = document.getElementById('btnStep');
const btnCrash = document.getElementById('btnCrash');
btnStep.disabled = done || crashed;
btnCrash.disabled = done || crashed || stepIndex === 0;
}
function advance(){
if(done || crashed) return;
const allSteps = steps();
if(stepIndex < allSteps.length){
setExpl(allSteps[stepIndex].expl);
stepIndex++;
redrawTimeline();
if(stepIndex === allSteps.length){
done = true;
setBanner('✓ Transaction committed successfully.', 'ok');
setExpl('All participants acknowledged the final COMMIT. The transaction is durable and complete.');
}
}
updateButtons();
}
function crashCoordinator(){
if(done || crashed || stepIndex === 0) return;
crashed = true;
redrawTimeline();
const canRecover = proto === '3pc' && stepIndex >= 3;
if(canRecover){
setBanner('⚠ Coordinator crashed — but 3PC survivors recovered and committed.', 'warn');
setExpl('Because participants already received PRE-COMMIT, they know everyone voted YES. They elect a new coordinator and commit unanimously — no data loss, no blocking.');
} else {
setBanner('✗ Coordinator crashed — participants are blocked (cannot decide safely).', 'bad');
setExpl(proto === '2pc' ? 'Participants voted YES and locked their resources, but they have no idea whether the coordinator said COMMIT or ABORT before crashing. They must wait for the coordinator to recover — potentially forever.' : 'The coordinator crashed before sending PRE-COMMIT. Participants cannot tell if all votes were YES, so they must wait for recovery just like in 2PC.');
}
updateButtons();
}
function reset(){
stepIndex = 0; crashed = false; done = false;
redrawTimeline();
setBanner('', '');
setExpl('Choose a protocol above, then click ’Next step’ to advance. Click ’Crash coordinator’ at any point to simulate a failure.');
updateButtons();
}
function selectProto(p){
proto = p;
document.getElementById('btn2pc').classList.toggle('active', p === '2pc');
document.getElementById('btn3pc').classList.toggle('active', p === '3pc');
reset();
}
document.getElementById('btnStep').addEventListener('click', advance);
document.getElementById('btnCrash').addEventListener('click', crashCoordinator);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('btn2pc').addEventListener('click', () => selectProto('2pc'));
document.getElementById('btn3pc').addEventListener('click', () => selectProto('3pc'));
reset();
Comments
Loading comments...