Below is a live circuit breaker wrapping a flaky service. Hit Call service to make requests. The service fails randomly — when failures exceed the threshold the breaker trips open and short-circuits future calls immediately. After the timeout it enters half-open and lets one probe through. If that probe succeeds, the breaker closes; if it fails, it opens again.
<div class="panel">
<div class="state-row">
<span class="label">Breaker state:</span>
<span id="state-badge" class="badge closed">CLOSED</span>
</div>
<div class="counters">
<div class="counter-box">
<div class="counter-value" id="c-success">0</div>
<div class="counter-label">Successes</div>
</div>
<div class="counter-box">
<div class="counter-value" id="c-failure">0</div>
<div class="counter-label">Failures</div>
</div>
<div class="counter-box">
<div class="counter-value" id="c-rejected">0</div>
<div class="counter-label">Rejected</div>
</div>
</div>
<div class="log-box" id="log"></div>
<div class="config-row">
<label>Failure rate: <b id="rate-val">60</b>%
<input id="rate" type="range" min="0" max="100" value="60"></label>
<label>Threshold: <b id="thr-val">3</b> failures
<input id="thr" type="range" min="1" max="8" value="3"></label>
<label>Timeout: <b id="to-val">4</b>s
<input id="to" type="range" min="1" max="10" value="4"></label>
</div>
<div class="btns">
<button id="btn-call">Call service</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.panel { max-width: 420px; margin: 0 auto; }
.state-row { display: flex; align-items: center; gap: .6rem; margin-bottom: .8rem; }
.label { font-weight: 600; }
.badge { padding: .25rem .7rem; border-radius: 20px; font: 700 .8rem ui-monospace, monospace; letter-spacing: .05em; }
.badge.closed { background: #d1fae5; color: #065f46; }
.badge.open { background: #fee2e2; color: #991b1b; }
.badge.half { background: #fef3c7; color: #92400e; }
.counters { display: flex; gap: .5rem; margin-bottom: .7rem; }
.counter-box { flex: 1; background: #f3f4f6; border-radius: 8px; padding: .5rem; text-align: center; }
.counter-value { font: 700 1.4rem ui-monospace, monospace; }
.counter-label { font-size: .72rem; color: #6b7280; margin-top: .15rem; }
.log-box { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px;
height: 140px; overflow-y: auto; padding: .5rem .7rem;
font: .8rem ui-monospace, monospace; margin-bottom: .7rem; }
.log-box .entry { border-bottom: 1px solid #e9ecef; padding: .15rem 0; display: flex; gap: .5rem; }
.log-box .ts { color: #94a3b8; flex-shrink: 0; }
.ok { color: #059669; }
.fail { color: #dc2626; }
.rej { color: #b45309; }
.probe { color: #7c3aed; }
.config-row { display: flex; flex-direction: column; gap: .4rem; font-size: .85rem; margin-bottom: .7rem; }
.config-row label { display: flex; align-items: center; gap: .5rem; }
.config-row input[type=range] { flex: 1; }
.btns { display: flex; gap: .5rem; }
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] ?? ''));
// Circuit Breaker state machine
// States: CLOSED -> OPEN -> HALF_OPEN -> CLOSED (or OPEN again)
const CLOSED = 'CLOSED', OPEN = 'OPEN', HALF = 'HALF_OPEN';
let state = CLOSED;
let failCount = 0; // consecutive failures in CLOSED window
let openUntil = 0; // timestamp when OPEN expires
let counts = { success: 0, failure: 0, rejected: 0 };
function getThreshold() { return +document.getElementById('thr').value; }
function getTimeout() { return +document.getElementById('to').value * 1000; }
function getRate() { return +document.getElementById('rate').value / 100; }
function updateBadge() {
const b = document.getElementById('state-badge');
b.textContent = state === HALF ? 'HALF-OPEN' : state;
b.className = 'badge ' + (state === CLOSED ? 'closed' : state === OPEN ? 'open' : 'half');
}
function updateCounters() {
document.getElementById('c-success').textContent = counts.success;
document.getElementById('c-failure').textContent = counts.failure;
document.getElementById('c-rejected').textContent = counts.rejected;
}
function log(cls, msg) {
const box = document.getElementById('log');
const now = new Date();
const ts = now.toLocaleTimeString([], { hour12: false });
const e = document.createElement('div');
e.className = 'entry';
e.innerHTML = `<span class="ts">${ts}</span><span class="${cls}">${msg}</span>`;
box.prepend(e);
}
// Simulate flaky upstream call: resolves true (success) or false (failure)
function callUpstream() {
return Math.random() >= getRate(); // probability (1 - rate) of success
}
function transition(newState) {
state = newState;
if (newState === OPEN) {
openUntil = Date.now() + getTimeout();
}
updateBadge();
}
function call() {
const now = Date.now();
if (state === OPEN) {
if (now < openUntil) {
// Still open — reject immediately
counts.rejected++;
updateCounters();
log('rej', '⊘ REJECTED — breaker is OPEN (fast fail)');
return;
} else {
// Timeout elapsed — move to half-open for one probe
transition(HALF);
log('probe', '⟳ Timeout elapsed → HALF-OPEN, sending probe…');
}
}
// CLOSED or HALF-OPEN: attempt the real call
const ok = callUpstream();
if (state === HALF) {
if (ok) {
counts.success++;
transition(CLOSED);
failCount = 0;
log('ok', '✓ Probe SUCCESS → breaker CLOSED');
} else {
counts.failure++;
transition(OPEN);
log('fail', '✗ Probe FAILED → breaker back to OPEN');
}
} else {
// CLOSED
if (ok) {
counts.success++;
failCount = 0;
log('ok', '✓ Call succeeded');
} else {
counts.failure++;
failCount++;
log('fail', fmt('✗ Call failed (%failCount%/%getThreshold% failures)', { failCount: failCount, getThreshold: getThreshold() }));
if (failCount >= getThreshold()) {
transition(OPEN);
log('fail', fmt('⚡ ⚡ Threshold reached → breaker OPEN for %v%s', { v: getTimeout()/1000 }));
failCount = 0;
}
}
}
updateCounters();
}
function reset() {
state = CLOSED; failCount = 0; openUntil = 0;
counts = { success: 0, failure: 0, rejected: 0 };
document.getElementById('log').innerHTML = '';
updateBadge(); updateCounters();
log('ok', 'Breaker reset — state: CLOSED');
}
// Slider sync
['rate','thr','to'].forEach(id => {
const el = document.getElementById(id);
const out = document.getElementById(id === 'rate' ? 'rate-val' : id === 'thr' ? 'thr-val' : 'to-val');
el.addEventListener('input', () => { out.textContent = el.value; });
});
document.getElementById('btn-call').addEventListener('click', call);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initial state log
log('ok', 'Breaker initialized — state: CLOSED');
updateBadge(); updateCounters();
// Verify (node-safe assertions — run in browser sandbox, not node)
// Core logic assertion: at threshold=3 with forced failures the breaker must open.
(function selfTest() {
let s = CLOSED, fc = 0, thr = 3;
function forceCall(success) {
if (s === OPEN) return 'REJECTED';
if (success) { fc = 0; return 'OK'; }
fc++; if (fc >= thr) { s = OPEN; fc = 0; } return 'FAIL';
}
forceCall(false); forceCall(false); forceCall(false);
console.assert(s === OPEN, 'SELF-TEST FAILED: breaker should be OPEN after 3 failures');
console.log('circuit-breaker self-test: state after 3 failures =', s, '— expected OPEN');
})();
<div class="panel">
<div class="state-row">
<span class="label">Breaker state:</span>
<span id="state-badge" class="badge closed">CLOSED</span>
</div>
<div class="counters">
<div class="counter-box">
<div class="counter-value" id="c-success">0</div>
<div class="counter-label">Successes</div>
</div>
<div class="counter-box">
<div class="counter-value" id="c-failure">0</div>
<div class="counter-label">Failures</div>
</div>
<div class="counter-box">
<div class="counter-value" id="c-rejected">0</div>
<div class="counter-label">Rejected</div>
</div>
</div>
<div class="log-box" id="log"></div>
<div class="config-row">
<label>Failure rate: <b id="rate-val">60</b>%
<input id="rate" type="range" min="0" max="100" value="60"></label>
<label>Threshold: <b id="thr-val">3</b> failures
<input id="thr" type="range" min="1" max="8" value="3"></label>
<label>Timeout: <b id="to-val">4</b>s
<input id="to" type="range" min="1" max="10" value="4"></label>
</div>
<div class="btns">
<button id="btn-call">Call service</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.panel { max-width: 420px; margin: 0 auto; }
.state-row { display: flex; align-items: center; gap: .6rem; margin-bottom: .8rem; }
.label { font-weight: 600; }
.badge { padding: .25rem .7rem; border-radius: 20px; font: 700 .8rem ui-monospace, monospace; letter-spacing: .05em; }
.badge.closed { background: #d1fae5; color: #065f46; }
.badge.open { background: #fee2e2; color: #991b1b; }
.badge.half { background: #fef3c7; color: #92400e; }
.counters { display: flex; gap: .5rem; margin-bottom: .7rem; }
.counter-box { flex: 1; background: #f3f4f6; border-radius: 8px; padding: .5rem; text-align: center; }
.counter-value { font: 700 1.4rem ui-monospace, monospace; }
.counter-label { font-size: .72rem; color: #6b7280; margin-top: .15rem; }
.log-box { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px;
height: 140px; overflow-y: auto; padding: .5rem .7rem;
font: .8rem ui-monospace, monospace; margin-bottom: .7rem; }
.log-box .entry { border-bottom: 1px solid #e9ecef; padding: .15rem 0; display: flex; gap: .5rem; }
.log-box .ts { color: #94a3b8; flex-shrink: 0; }
.ok { color: #059669; }
.fail { color: #dc2626; }
.rej { color: #b45309; }
.probe { color: #7c3aed; }
.config-row { display: flex; flex-direction: column; gap: .4rem; font-size: .85rem; margin-bottom: .7rem; }
.config-row label { display: flex; align-items: center; gap: .5rem; }
.config-row input[type=range] { flex: 1; }
.btns { display: flex; gap: .5rem; }
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] ?? ''));
// Circuit Breaker state machine
// States: CLOSED -> OPEN -> HALF_OPEN -> CLOSED (or OPEN again)
const CLOSED = 'CLOSED', OPEN = 'OPEN', HALF = 'HALF_OPEN';
let state = CLOSED;
let failCount = 0; // consecutive failures in CLOSED window
let openUntil = 0; // timestamp when OPEN expires
let counts = { success: 0, failure: 0, rejected: 0 };
function getThreshold() { return +document.getElementById('thr').value; }
function getTimeout() { return +document.getElementById('to').value * 1000; }
function getRate() { return +document.getElementById('rate').value / 100; }
function updateBadge() {
const b = document.getElementById('state-badge');
b.textContent = state === HALF ? 'HALF-OPEN' : state;
b.className = 'badge ' + (state === CLOSED ? 'closed' : state === OPEN ? 'open' : 'half');
}
function updateCounters() {
document.getElementById('c-success').textContent = counts.success;
document.getElementById('c-failure').textContent = counts.failure;
document.getElementById('c-rejected').textContent = counts.rejected;
}
function log(cls, msg) {
const box = document.getElementById('log');
const now = new Date();
const ts = now.toLocaleTimeString([], { hour12: false });
const e = document.createElement('div');
e.className = 'entry';
e.innerHTML = `<span class="ts">${ts}</span><span class="${cls}">${msg}</span>`;
box.prepend(e);
}
// Simulate flaky upstream call: resolves true (success) or false (failure)
function callUpstream() {
return Math.random() >= getRate(); // probability (1 - rate) of success
}
function transition(newState) {
state = newState;
if (newState === OPEN) {
openUntil = Date.now() + getTimeout();
}
updateBadge();
}
function call() {
const now = Date.now();
if (state === OPEN) {
if (now < openUntil) {
// Still open — reject immediately
counts.rejected++;
updateCounters();
log('rej', '⊘ REJECTED — breaker is OPEN (fast fail)');
return;
} else {
// Timeout elapsed — move to half-open for one probe
transition(HALF);
log('probe', '⟳ Timeout elapsed → HALF-OPEN, sending probe…');
}
}
// CLOSED or HALF-OPEN: attempt the real call
const ok = callUpstream();
if (state === HALF) {
if (ok) {
counts.success++;
transition(CLOSED);
failCount = 0;
log('ok', '✓ Probe SUCCESS → breaker CLOSED');
} else {
counts.failure++;
transition(OPEN);
log('fail', '✗ Probe FAILED → breaker back to OPEN');
}
} else {
// CLOSED
if (ok) {
counts.success++;
failCount = 0;
log('ok', '✓ Call succeeded');
} else {
counts.failure++;
failCount++;
log('fail', fmt('✗ Call failed (%failCount%/%getThreshold% failures)', { failCount: failCount, getThreshold: getThreshold() }));
if (failCount >= getThreshold()) {
transition(OPEN);
log('fail', fmt('⚡ ⚡ Threshold reached → breaker OPEN for %v%s', { v: getTimeout()/1000 }));
failCount = 0;
}
}
}
updateCounters();
}
function reset() {
state = CLOSED; failCount = 0; openUntil = 0;
counts = { success: 0, failure: 0, rejected: 0 };
document.getElementById('log').innerHTML = '';
updateBadge(); updateCounters();
log('ok', 'Breaker reset — state: CLOSED');
}
// Slider sync
['rate','thr','to'].forEach(id => {
const el = document.getElementById(id);
const out = document.getElementById(id === 'rate' ? 'rate-val' : id === 'thr' ? 'thr-val' : 'to-val');
el.addEventListener('input', () => { out.textContent = el.value; });
});
document.getElementById('btn-call').addEventListener('click', call);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initial state log
log('ok', 'Breaker initialized — state: CLOSED');
updateBadge(); updateCounters();
// Verify (node-safe assertions — run in browser sandbox, not node)
// Core logic assertion: at threshold=3 with forced failures the breaker must open.
(function selfTest() {
let s = CLOSED, fc = 0, thr = 3;
function forceCall(success) {
if (s === OPEN) return 'REJECTED';
if (success) { fc = 0; return 'OK'; }
fc++; if (fc >= thr) { s = OPEN; fc = 0; } return 'FAIL';
}
forceCall(false); forceCall(false); forceCall(false);
console.assert(s === OPEN, 'SELF-TEST FAILED: breaker should be OPEN after 3 failures');
console.log('circuit-breaker self-test: state after 3 failures =', s, '— expected OPEN');
})();
Notice the asymmetry: while the breaker is open, calls return in microseconds with a predictable error — callers are not blocked waiting for a dependency that cannot respond. That instant rejection is exactly what prevents thread exhaustion and cascading failures upstream.
Comments
Loading comments...