The cluster below has one leader (L) and two followers (F1, F2). Broadcast messages one at a time to see how the leader sends a PROPOSAL, waits for a quorum ACK, then sends COMMIT — ensuring every follower delivers messages in the same order.
<!-- ZAB Protocol demo: cluster nodes + log + controls -->
<div class="hint-box">Broadcast messages to see ZAB’s two-phase protocol (PROPOSAL → ACK → COMMIT). Then crash the leader to watch the recovery phase synchronize zxids before the new leader accepts writes.</div>
<div id="cluster" class="cluster">
<div class="node leader" id="node-L">
<span class="node-label">L (leader)</span>
<span class="node-zxid" id="zxid-L">zxid: 0</span>
</div>
<div class="node follower" id="node-F1">
<span class="node-label">F1</span>
<span class="node-zxid" id="zxid-F1">zxid: 0</span>
</div>
<div class="node follower" id="node-F2">
<span class="node-label">F2</span>
<span class="node-zxid" id="zxid-F2">zxid: 0</span>
</div>
</div>
<div id="log-area" class="log-area"></div>
<div class="btns">
<button id="btn-broadcast" type="button">Broadcast msg</button>
<button id="btn-crash" type="button" class="danger">Crash leader</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* ZAB demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint-box { font-size: .87rem; color: #444; margin-bottom: .7rem; line-height: 1.45; }
.cluster { display: flex; gap: 12px; margin-bottom: .6rem; flex-wrap: wrap; }
.node { display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 80px; height: 80px; border-radius: 12px; border: 2px solid #adb1b8;
background: #e8eef3; transition: background .25s, border-color .25s; }
.node.leader { border-color: #1d3557; background: #1d3557; color: #fff; }
.node.crashed { border-color: #c92f3c; background: #fde8ea; color: #c92f3c; opacity: .7; }
.node.recovering { border-color: #e67e22; background: #fef3e2; color: #c0661e; }
.node-label { font-weight: 700; font-size: 1.05rem; }
.node-zxid { font-size: .72rem; margin-top: 3px; opacity: .85; }
.log-area { border: 1px solid #cdd9e3; border-radius: 8px; background: #f7f9fb;
min-height: 120px; max-height: 160px; overflow-y: auto;
padding: .5rem .7rem; font-size: .82rem; margin-bottom: .6rem; }
.log-entry { margin: 2px 0; line-height: 1.4; }
.log-entry.phase-propose { color: #1d3557; }
.log-entry.phase-ack { color: #0a7d33; }
.log-entry.phase-commit { color: #0a7d33; font-weight: 600; }
.log-entry.phase-crash { color: #c92f3c; font-weight: 700; }
.log-entry.phase-recover { color: #c0661e; }
.log-entry.phase-info { color: #555; font-style: italic; }
.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.danger { background: #c92f3c; border-color: #c92f3c; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ZAB (ZooKeeper Atomic Broadcast) demo — simplified two-phase broadcast + leader-change recovery.
// Phase 1: Leader sends PROPOSAL to all followers.
// Phase 2: Leader waits for quorum ACK, then sends COMMIT.
// Cluster state
let epoch = 1;
let counter = 0;
let leaderAlive = true;
let msgQueue = []; // unused in this simplified model
let broadcasting = false;
// zxid encoding: epoch * 1000 + counter (simplified; real ZAB uses 32+32 bit split)
function makeZxid(ep, cnt) { return ep * 1000 + cnt; }
function zxidStr(ep, cnt) { return ep + '-' + cnt; }
// Node identifiers
const NODES = ['L', 'F1', 'F2'];
const zxids = { L: 0, F1: 0, F2: 0 }; // highest committed zxid per node
function updateNodeUI() {
NODES.forEach(id => {
const el = document.getElementById('node-' + id);
const zxidEl = document.getElementById('zxid-' + id);
if (!el) return;
// update visual state
if (id === 'L') {
if (!leaderAlive) {
el.className = 'node crashed';
el.querySelector('.node-label').textContent = 'L ✗';
} else {
el.className = 'node leader';
el.querySelector('.node-label').textContent = 'L (leader)';
}
}
const ep = Math.floor(zxids[id] / 1000);
const cnt = zxids[id] % 1000;
zxidEl.textContent = 'zxid: ' + zxidStr(ep, cnt);
});
}
function log(msg, phase) {
const el = document.getElementById('log-area');
const d = document.createElement('div');
d.className = 'log-entry phase-' + phase;
d.textContent = msg;
el.appendChild(d);
el.scrollTop = el.scrollHeight;
}
// Two-phase broadcast: PROPOSAL -> quorum ACK -> COMMIT
function broadcast() {
if (!leaderAlive) { log('No leader — recovery in progress or cluster reset needed.', 'crash'); return; }
if (broadcasting) return;
broadcasting = true;
counter++;
const ep = epoch, cnt = counter;
const zx = makeZxid(ep, cnt);
const label = fmt('tx:%zxidStr%', { zxidStr: zxidStr(ep, cnt) });
// Phase 1: PROPOSAL
log('L → F1, F2: PROPOSAL(' + label + ')', 'phase-propose');
// Phase 2: quorum ACK (both followers acknowledge)
log('F1 → L: ACK(' + label + ')', 'phase-ack');
log('F2 → L: ACK(' + label + ')', 'phase-ack');
zxids['F1'] = zx;
zxids['F2'] = zx;
// Phase 3: COMMIT after quorum
log('L → F1, F2: COMMIT(' + label + ')', 'phase-commit');
zxids['L'] = zx;
updateNodeUI();
broadcasting = false;
}
// Crash the leader and run ZAB recovery
function crashLeader() {
if (!leaderAlive) { log('Leader is already down.', 'crash'); return; }
leaderAlive = false;
log('⚡ Leader L crashed! Starting leader election and recovery...', 'crash');
updateNodeUI();
// Simulate recovery delay
setTimeout(() => {
log('Recovery phase: collecting highest zxid from quorum...', 'recover');
// Find highest zxid in the surviving quorum
const maxZx = Math.max(zxids['F1'], zxids['F2']);
const maxEp = Math.floor(maxZx / 1000);
const maxCnt = maxZx % 1000;
log(fmt('Highest committed zxid in quorum: %zxidStr%', { zxidStr: zxidStr(maxEp, maxCnt) }), 'recover');
// Bump epoch for new leader
epoch++;
counter = maxCnt;
zxids['F1'] = makeZxid(epoch, counter);
zxids['F2'] = makeZxid(epoch, counter);
// F1 becomes new leader (simplified: reuse L node visually)
const leaderEl = document.getElementById('node-L');
leaderEl.className = 'node recovering';
leaderEl.querySelector('.node-label').textContent = 'F1*';
const zxEl = document.getElementById('zxid-L');
zxids['L'] = makeZxid(epoch, counter);
zxEl.textContent = 'zxid: ' + zxidStr(epoch, counter);
log(fmt('NEW_EPOCH(%epoch%) → all followers synchronized', { epoch: epoch }), 'recover');
log('Recovery complete. New leader is ready to accept writes.', 'phase-info');
leaderAlive = true;
NODES.forEach(id => {
const el = document.getElementById('node-' + id);
if (!el) return;
if (el.classList.contains('crashed')) el.className = 'node follower';
if (el.classList.contains('recovering')) el.className = 'node leader';
});
updateNodeUI();
}, 800);
}
function reset() {
epoch = 1; counter = 0; leaderAlive = true; broadcasting = false;
NODES.forEach(id => { zxids[id] = 0; });
document.getElementById('log-area').innerHTML = '';
const leaderEl = document.getElementById('node-L');
leaderEl.className = 'node leader';
leaderEl.querySelector('.node-label').textContent = 'L (leader)';
updateNodeUI();
log('Cluster reset. Epoch 1, all zxids at 0.', 'phase-info');
}
document.getElementById('btn-broadcast').addEventListener('click', broadcast);
document.getElementById('btn-crash').addEventListener('click', crashLeader);
document.getElementById('btn-reset').addEventListener('click', reset);
updateNodeUI();
log('Cluster ready. Click ’Broadcast msg’ to start.', 'phase-info');
<!-- ZAB Protocol demo: cluster nodes + log + controls -->
<div class="hint-box">Broadcast messages to see ZAB’s two-phase protocol (PROPOSAL → ACK → COMMIT). Then crash the leader to watch the recovery phase synchronize zxids before the new leader accepts writes.</div>
<div id="cluster" class="cluster">
<div class="node leader" id="node-L">
<span class="node-label">L (leader)</span>
<span class="node-zxid" id="zxid-L">zxid: 0</span>
</div>
<div class="node follower" id="node-F1">
<span class="node-label">F1</span>
<span class="node-zxid" id="zxid-F1">zxid: 0</span>
</div>
<div class="node follower" id="node-F2">
<span class="node-label">F2</span>
<span class="node-zxid" id="zxid-F2">zxid: 0</span>
</div>
</div>
<div id="log-area" class="log-area"></div>
<div class="btns">
<button id="btn-broadcast" type="button">Broadcast msg</button>
<button id="btn-crash" type="button" class="danger">Crash leader</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* ZAB demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint-box { font-size: .87rem; color: #444; margin-bottom: .7rem; line-height: 1.45; }
.cluster { display: flex; gap: 12px; margin-bottom: .6rem; flex-wrap: wrap; }
.node { display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 80px; height: 80px; border-radius: 12px; border: 2px solid #adb1b8;
background: #e8eef3; transition: background .25s, border-color .25s; }
.node.leader { border-color: #1d3557; background: #1d3557; color: #fff; }
.node.crashed { border-color: #c92f3c; background: #fde8ea; color: #c92f3c; opacity: .7; }
.node.recovering { border-color: #e67e22; background: #fef3e2; color: #c0661e; }
.node-label { font-weight: 700; font-size: 1.05rem; }
.node-zxid { font-size: .72rem; margin-top: 3px; opacity: .85; }
.log-area { border: 1px solid #cdd9e3; border-radius: 8px; background: #f7f9fb;
min-height: 120px; max-height: 160px; overflow-y: auto;
padding: .5rem .7rem; font-size: .82rem; margin-bottom: .6rem; }
.log-entry { margin: 2px 0; line-height: 1.4; }
.log-entry.phase-propose { color: #1d3557; }
.log-entry.phase-ack { color: #0a7d33; }
.log-entry.phase-commit { color: #0a7d33; font-weight: 600; }
.log-entry.phase-crash { color: #c92f3c; font-weight: 700; }
.log-entry.phase-recover { color: #c0661e; }
.log-entry.phase-info { color: #555; font-style: italic; }
.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.danger { background: #c92f3c; border-color: #c92f3c; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ZAB (ZooKeeper Atomic Broadcast) demo — simplified two-phase broadcast + leader-change recovery.
// Phase 1: Leader sends PROPOSAL to all followers.
// Phase 2: Leader waits for quorum ACK, then sends COMMIT.
// Cluster state
let epoch = 1;
let counter = 0;
let leaderAlive = true;
let msgQueue = []; // unused in this simplified model
let broadcasting = false;
// zxid encoding: epoch * 1000 + counter (simplified; real ZAB uses 32+32 bit split)
function makeZxid(ep, cnt) { return ep * 1000 + cnt; }
function zxidStr(ep, cnt) { return ep + '-' + cnt; }
// Node identifiers
const NODES = ['L', 'F1', 'F2'];
const zxids = { L: 0, F1: 0, F2: 0 }; // highest committed zxid per node
function updateNodeUI() {
NODES.forEach(id => {
const el = document.getElementById('node-' + id);
const zxidEl = document.getElementById('zxid-' + id);
if (!el) return;
// update visual state
if (id === 'L') {
if (!leaderAlive) {
el.className = 'node crashed';
el.querySelector('.node-label').textContent = 'L ✗';
} else {
el.className = 'node leader';
el.querySelector('.node-label').textContent = 'L (leader)';
}
}
const ep = Math.floor(zxids[id] / 1000);
const cnt = zxids[id] % 1000;
zxidEl.textContent = 'zxid: ' + zxidStr(ep, cnt);
});
}
function log(msg, phase) {
const el = document.getElementById('log-area');
const d = document.createElement('div');
d.className = 'log-entry phase-' + phase;
d.textContent = msg;
el.appendChild(d);
el.scrollTop = el.scrollHeight;
}
// Two-phase broadcast: PROPOSAL -> quorum ACK -> COMMIT
function broadcast() {
if (!leaderAlive) { log('No leader — recovery in progress or cluster reset needed.', 'crash'); return; }
if (broadcasting) return;
broadcasting = true;
counter++;
const ep = epoch, cnt = counter;
const zx = makeZxid(ep, cnt);
const label = fmt('tx:%zxidStr%', { zxidStr: zxidStr(ep, cnt) });
// Phase 1: PROPOSAL
log('L → F1, F2: PROPOSAL(' + label + ')', 'phase-propose');
// Phase 2: quorum ACK (both followers acknowledge)
log('F1 → L: ACK(' + label + ')', 'phase-ack');
log('F2 → L: ACK(' + label + ')', 'phase-ack');
zxids['F1'] = zx;
zxids['F2'] = zx;
// Phase 3: COMMIT after quorum
log('L → F1, F2: COMMIT(' + label + ')', 'phase-commit');
zxids['L'] = zx;
updateNodeUI();
broadcasting = false;
}
// Crash the leader and run ZAB recovery
function crashLeader() {
if (!leaderAlive) { log('Leader is already down.', 'crash'); return; }
leaderAlive = false;
log('⚡ Leader L crashed! Starting leader election and recovery...', 'crash');
updateNodeUI();
// Simulate recovery delay
setTimeout(() => {
log('Recovery phase: collecting highest zxid from quorum...', 'recover');
// Find highest zxid in the surviving quorum
const maxZx = Math.max(zxids['F1'], zxids['F2']);
const maxEp = Math.floor(maxZx / 1000);
const maxCnt = maxZx % 1000;
log(fmt('Highest committed zxid in quorum: %zxidStr%', { zxidStr: zxidStr(maxEp, maxCnt) }), 'recover');
// Bump epoch for new leader
epoch++;
counter = maxCnt;
zxids['F1'] = makeZxid(epoch, counter);
zxids['F2'] = makeZxid(epoch, counter);
// F1 becomes new leader (simplified: reuse L node visually)
const leaderEl = document.getElementById('node-L');
leaderEl.className = 'node recovering';
leaderEl.querySelector('.node-label').textContent = 'F1*';
const zxEl = document.getElementById('zxid-L');
zxids['L'] = makeZxid(epoch, counter);
zxEl.textContent = 'zxid: ' + zxidStr(epoch, counter);
log(fmt('NEW_EPOCH(%epoch%) → all followers synchronized', { epoch: epoch }), 'recover');
log('Recovery complete. New leader is ready to accept writes.', 'phase-info');
leaderAlive = true;
NODES.forEach(id => {
const el = document.getElementById('node-' + id);
if (!el) return;
if (el.classList.contains('crashed')) el.className = 'node follower';
if (el.classList.contains('recovering')) el.className = 'node leader';
});
updateNodeUI();
}, 800);
}
function reset() {
epoch = 1; counter = 0; leaderAlive = true; broadcasting = false;
NODES.forEach(id => { zxids[id] = 0; });
document.getElementById('log-area').innerHTML = '';
const leaderEl = document.getElementById('node-L');
leaderEl.className = 'node leader';
leaderEl.querySelector('.node-label').textContent = 'L (leader)';
updateNodeUI();
log('Cluster reset. Epoch 1, all zxids at 0.', 'phase-info');
}
document.getElementById('btn-broadcast').addEventListener('click', broadcast);
document.getElementById('btn-crash').addEventListener('click', crashLeader);
document.getElementById('btn-reset').addEventListener('click', reset);
updateNodeUI();
log('Cluster ready. Click ’Broadcast msg’ to start.', 'phase-info');
When you crash the leader, a new leader is elected and runs ZAB's recovery phase: it synchronizes the highest committed transaction ID (zxid) across the surviving followers before any new message can be broadcast. Notice that messages in-flight during the crash are handled safely — ZAB's primary-order guarantee means a committed message is never lost and a not-yet-committed message is never half-delivered.
Comments
Loading comments...