Below are 5 nodes in a cluster. Any subset of 3 or more nodes forms a valid quorum (a majority quorum, the most common choice). Select which nodes handle a read and which handle a write, then check whether they overlap.
<p class="hint">
A cluster of 5 nodes. Any <b>3 or more</b> nodes form a valid quorum.
Toggle nodes into the <b>Read</b> or <b>Write</b> quorum (or neither), then
press <b>Check Overlap</b> to verify the intersection property.
</p>
<div id="nodes-container"></div>
<div class="legend">
<span class="dot read-dot"></span> Read quorum
<span class="dot write-dot"></span> Write quorum
<span class="dot both-dot"></span> Both (overlap)
</div>
<div class="status" id="status">Select nodes for each quorum.</div>
<div class="btns">
<button id="check" type="button">Check Overlap</button>
<button id="random" type="button">Random Quorums</button>
<button id="reset" type="button" class="ghost">Clear</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 .8rem; line-height: 1.45; }
#nodes-container {
display: flex; gap: 12px; flex-wrap: wrap;
margin: .6rem 0 .5rem;
}
.node-card {
width: 80px; height: 100px;
border: 2px solid #cdd9e3; border-radius: 12px;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 6px; cursor: default; user-select: none;
background: #f2f5f8; transition: all .15s;
position: relative;
}
.node-label {
font-weight: 700; font-size: 1.1rem; color: #1d3557;
}
.node-btns {
display: flex; gap: 4px;
}
.node-btn {
font-size: 0.65rem; font-weight: 700;
padding: 2px 6px; border-radius: 6px; cursor: pointer;
border: 1px solid #aaa; background: #e8eef3; color: #444;
transition: all .1s;
}
.node-btn.active-r { background: #457b9d; border-color: #1d3557; color: #fff; }
.node-btn.active-w { background: #e63946; border-color: #c92f3c; color: #fff; }
.node-card.in-read { background: #d0e6f5; border-color: #457b9d; }
.node-card.in-write { background: #fde8ea; border-color: #e63946; }
.node-card.in-both { background: #d5f0da; border-color: #2d9142; }
.dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%; margin: 0 3px 0 10px; vertical-align: middle; }
.read-dot { background: #457b9d; }
.write-dot { background: #e63946; }
.both-dot { background: #2d9142; }
.legend { font-size: .8rem; color: #555; margin: .2rem 0 .6rem; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.warn { color: #b06000; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Quorum system demo: 5 nodes, majority quorum threshold = 3.
// A read quorum and a write quorum are valid if each has >= THRESHOLD nodes.
// They satisfy the quorum intersection property iff their intersection is non-empty.
// By the pigeonhole principle, two majority quorums over n nodes always intersect
// when each has size > n/2 (here 3 > 5/2 = 2.5). We show this visually.
const N = 5;
const THRESHOLD = 3;
const NAMES = ['A', 'B', 'C', 'D', 'E'];
// State: 'none' | 'read' | 'write' | 'both'
const state = Array(N).fill('none');
function toggleNode(i, role) {
const cur = state[i];
if (role === 'r') {
if (cur === 'none') state[i] = 'read';
else if (cur === 'read') state[i] = 'none';
else if (cur === 'write') state[i] = 'both';
else if (cur === 'both') state[i] = 'write';
} else {
if (cur === 'none') state[i] = 'write';
else if (cur === 'write') state[i] = 'none';
else if (cur === 'read') state[i] = 'both';
else if (cur === 'both') state[i] = 'read';
}
render();
setStatus('Select nodes for each quorum.', '');
}
function render() {
const container = document.getElementById('nodes-container');
container.innerHTML = '';
state.forEach((s, i) => {
const card = document.createElement('div');
card.className = 'node-card' +
(s === 'read' ? ' in-read' :
s === 'write' ? ' in-write' :
s === 'both' ? ' in-both' : '');
const label = document.createElement('div');
label.className = 'node-label';
label.textContent = 'N' + NAMES[i];
const btns = document.createElement('div');
btns.className = 'node-btns';
const rBtn = document.createElement('button');
rBtn.className = 'node-btn' + (s === 'read' || s === 'both' ? ' active-r' : '');
rBtn.textContent = 'R';
rBtn.title = 'Toggle into read quorum';
rBtn.addEventListener('click', () => toggleNode(i, 'r'));
const wBtn = document.createElement('button');
wBtn.className = 'node-btn' + (s === 'write' || s === 'both' ? ' active-w' : '');
wBtn.textContent = 'W';
wBtn.title = 'Toggle into write quorum';
wBtn.addEventListener('click', () => toggleNode(i, 'w'));
btns.appendChild(rBtn);
btns.appendChild(wBtn);
card.appendChild(label);
card.appendChild(btns);
container.appendChild(card);
});
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + (cls || '');
}
function checkOverlap() {
const read = state.map((s, i) => (s === 'read' || s === 'both') ? i : -1).filter(x => x >= 0);
const write = state.map((s, i) => (s === 'write' || s === 'both') ? i : -1).filter(x => x >= 0);
const overlap = read.filter(i => write.includes(i));
if (read.length === 0 && write.length === 0) {
setStatus('Select at least one node for each quorum.', 'warn'); return;
}
if (read.length === 0) { setStatus('Read quorum is empty — select some R nodes.', 'warn'); return; }
if (write.length === 0) { setStatus('Write quorum is empty — select some W nodes.', 'warn'); return; }
const rOk = read.length >= THRESHOLD;
const wOk = write.length >= THRESHOLD;
if (!rOk && !wOk) {
setStatus(fmt('Neither quorum reaches the threshold of %THRESHOLD% — both too small.', { THRESHOLD: THRESHOLD }), 'bad');
} else if (!rOk) {
setStatus(fmt('Read quorum has only %length% node(s) — needs %THRESHOLD%.', { length: read.length, THRESHOLD: THRESHOLD }), 'bad');
} else if (!wOk) {
setStatus(fmt('Write quorum has only %length% node(s) — needs %THRESHOLD%.', { length: write.length, THRESHOLD: THRESHOLD }), 'bad');
} else if (overlap.length > 0) {
const names = overlap.map(i => 'N' + NAMES[i]).join(', ');
setStatus(fmt('✓ Valid! Both quorums meet threshold. Overlap at: %names%.', { names: names }), 'ok');
console.log('Read:', read.map(i=>'N'+NAMES[i]).join(','),
'| Write:', write.map(i=>'N'+NAMES[i]).join(','),
'| Overlap:', names);
} else {
setStatus('✗ No overlap — this cannot happen with two majority quorums (pigeonhole)!', 'bad');
}
}
function pickRandom(arr, k) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a.slice(0, k);
}
function randomQuorums() {
state.fill('none');
const indices = Array.from({length: N}, (_, i) => i);
const rSize = THRESHOLD + Math.floor(Math.random() * (N - THRESHOLD + 1));
const wSize = THRESHOLD + Math.floor(Math.random() * (N - THRESHOLD + 1));
const rNodes = pickRandom(indices, rSize);
const wNodes = pickRandom(indices, wSize);
rNodes.forEach(i => { state[i] = state[i] === 'write' ? 'both' : 'read'; });
wNodes.forEach(i => { state[i] = state[i] === 'read' ? 'both' : 'write'; });
render();
checkOverlap();
}
document.getElementById('check').addEventListener('click', checkOverlap);
document.getElementById('random').addEventListener('click', randomQuorums);
document.getElementById('reset').addEventListener('click', () => {
state.fill('none');
render();
setStatus('Select nodes for each quorum.', '');
});
render();
<p class="hint">
A cluster of 5 nodes. Any <b>3 or more</b> nodes form a valid quorum.
Toggle nodes into the <b>Read</b> or <b>Write</b> quorum (or neither), then
press <b>Check Overlap</b> to verify the intersection property.
</p>
<div id="nodes-container"></div>
<div class="legend">
<span class="dot read-dot"></span> Read quorum
<span class="dot write-dot"></span> Write quorum
<span class="dot both-dot"></span> Both (overlap)
</div>
<div class="status" id="status">Select nodes for each quorum.</div>
<div class="btns">
<button id="check" type="button">Check Overlap</button>
<button id="random" type="button">Random Quorums</button>
<button id="reset" type="button" class="ghost">Clear</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 .8rem; line-height: 1.45; }
#nodes-container {
display: flex; gap: 12px; flex-wrap: wrap;
margin: .6rem 0 .5rem;
}
.node-card {
width: 80px; height: 100px;
border: 2px solid #cdd9e3; border-radius: 12px;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 6px; cursor: default; user-select: none;
background: #f2f5f8; transition: all .15s;
position: relative;
}
.node-label {
font-weight: 700; font-size: 1.1rem; color: #1d3557;
}
.node-btns {
display: flex; gap: 4px;
}
.node-btn {
font-size: 0.65rem; font-weight: 700;
padding: 2px 6px; border-radius: 6px; cursor: pointer;
border: 1px solid #aaa; background: #e8eef3; color: #444;
transition: all .1s;
}
.node-btn.active-r { background: #457b9d; border-color: #1d3557; color: #fff; }
.node-btn.active-w { background: #e63946; border-color: #c92f3c; color: #fff; }
.node-card.in-read { background: #d0e6f5; border-color: #457b9d; }
.node-card.in-write { background: #fde8ea; border-color: #e63946; }
.node-card.in-both { background: #d5f0da; border-color: #2d9142; }
.dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%; margin: 0 3px 0 10px; vertical-align: middle; }
.read-dot { background: #457b9d; }
.write-dot { background: #e63946; }
.both-dot { background: #2d9142; }
.legend { font-size: .8rem; color: #555; margin: .2rem 0 .6rem; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.warn { color: #b06000; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Quorum system demo: 5 nodes, majority quorum threshold = 3.
// A read quorum and a write quorum are valid if each has >= THRESHOLD nodes.
// They satisfy the quorum intersection property iff their intersection is non-empty.
// By the pigeonhole principle, two majority quorums over n nodes always intersect
// when each has size > n/2 (here 3 > 5/2 = 2.5). We show this visually.
const N = 5;
const THRESHOLD = 3;
const NAMES = ['A', 'B', 'C', 'D', 'E'];
// State: 'none' | 'read' | 'write' | 'both'
const state = Array(N).fill('none');
function toggleNode(i, role) {
const cur = state[i];
if (role === 'r') {
if (cur === 'none') state[i] = 'read';
else if (cur === 'read') state[i] = 'none';
else if (cur === 'write') state[i] = 'both';
else if (cur === 'both') state[i] = 'write';
} else {
if (cur === 'none') state[i] = 'write';
else if (cur === 'write') state[i] = 'none';
else if (cur === 'read') state[i] = 'both';
else if (cur === 'both') state[i] = 'read';
}
render();
setStatus('Select nodes for each quorum.', '');
}
function render() {
const container = document.getElementById('nodes-container');
container.innerHTML = '';
state.forEach((s, i) => {
const card = document.createElement('div');
card.className = 'node-card' +
(s === 'read' ? ' in-read' :
s === 'write' ? ' in-write' :
s === 'both' ? ' in-both' : '');
const label = document.createElement('div');
label.className = 'node-label';
label.textContent = 'N' + NAMES[i];
const btns = document.createElement('div');
btns.className = 'node-btns';
const rBtn = document.createElement('button');
rBtn.className = 'node-btn' + (s === 'read' || s === 'both' ? ' active-r' : '');
rBtn.textContent = 'R';
rBtn.title = 'Toggle into read quorum';
rBtn.addEventListener('click', () => toggleNode(i, 'r'));
const wBtn = document.createElement('button');
wBtn.className = 'node-btn' + (s === 'write' || s === 'both' ? ' active-w' : '');
wBtn.textContent = 'W';
wBtn.title = 'Toggle into write quorum';
wBtn.addEventListener('click', () => toggleNode(i, 'w'));
btns.appendChild(rBtn);
btns.appendChild(wBtn);
card.appendChild(label);
card.appendChild(btns);
container.appendChild(card);
});
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + (cls || '');
}
function checkOverlap() {
const read = state.map((s, i) => (s === 'read' || s === 'both') ? i : -1).filter(x => x >= 0);
const write = state.map((s, i) => (s === 'write' || s === 'both') ? i : -1).filter(x => x >= 0);
const overlap = read.filter(i => write.includes(i));
if (read.length === 0 && write.length === 0) {
setStatus('Select at least one node for each quorum.', 'warn'); return;
}
if (read.length === 0) { setStatus('Read quorum is empty — select some R nodes.', 'warn'); return; }
if (write.length === 0) { setStatus('Write quorum is empty — select some W nodes.', 'warn'); return; }
const rOk = read.length >= THRESHOLD;
const wOk = write.length >= THRESHOLD;
if (!rOk && !wOk) {
setStatus(fmt('Neither quorum reaches the threshold of %THRESHOLD% — both too small.', { THRESHOLD: THRESHOLD }), 'bad');
} else if (!rOk) {
setStatus(fmt('Read quorum has only %length% node(s) — needs %THRESHOLD%.', { length: read.length, THRESHOLD: THRESHOLD }), 'bad');
} else if (!wOk) {
setStatus(fmt('Write quorum has only %length% node(s) — needs %THRESHOLD%.', { length: write.length, THRESHOLD: THRESHOLD }), 'bad');
} else if (overlap.length > 0) {
const names = overlap.map(i => 'N' + NAMES[i]).join(', ');
setStatus(fmt('✓ Valid! Both quorums meet threshold. Overlap at: %names%.', { names: names }), 'ok');
console.log('Read:', read.map(i=>'N'+NAMES[i]).join(','),
'| Write:', write.map(i=>'N'+NAMES[i]).join(','),
'| Overlap:', names);
} else {
setStatus('✗ No overlap — this cannot happen with two majority quorums (pigeonhole)!', 'bad');
}
}
function pickRandom(arr, k) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a.slice(0, k);
}
function randomQuorums() {
state.fill('none');
const indices = Array.from({length: N}, (_, i) => i);
const rSize = THRESHOLD + Math.floor(Math.random() * (N - THRESHOLD + 1));
const wSize = THRESHOLD + Math.floor(Math.random() * (N - THRESHOLD + 1));
const rNodes = pickRandom(indices, rSize);
const wNodes = pickRandom(indices, wSize);
rNodes.forEach(i => { state[i] = state[i] === 'write' ? 'both' : 'read'; });
wNodes.forEach(i => { state[i] = state[i] === 'read' ? 'both' : 'write'; });
render();
checkOverlap();
}
document.getElementById('check').addEventListener('click', checkOverlap);
document.getElementById('random').addEventListener('click', randomQuorums);
document.getElementById('reset').addEventListener('click', () => {
state.fill('none');
render();
setStatus('Select nodes for each quorum.', '');
});
render();
Notice: no matter which 3 nodes you pick for the read and which 3 you pick for the write, they always share at least one. With 5 nodes and a threshold of 3 you cannot avoid the overlap — that is the guarantee by design.
Comments
Loading comments...