Skip to content

Commit a6730fb

Browse files
committed
docs: align the full-stack demo with the breaker-before-timeout order
1 parent aae2cee commit a6730fb

2 files changed

Lines changed: 26 additions & 26 deletions

File tree

‎docs/demos/engine.js‎

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -793,11 +793,10 @@ window.HttpwareDemo = (function () {
793793
if (L.pend[i].land <= tick) {
794794
const p = L.pend[i]; L.pend.splice(i, 1);
795795
let retried = false;
796-
// AsyncTimeout bounds the WHOLE operation (outermost: timeout ->
797-
// circuitBreaker -> bulkhead -> retry -> terminal). An entry clamped
798-
// to its deadline (see clampToDeadline) lands as a TimeoutError
799-
// failure here, regardless of what the untimed outcome would have
800-
// been, and is never retried.
796+
// AsyncTimeout bounds the WHOLE operation (circuitBreaker -> timeout ->
797+
// bulkhead -> retry -> terminal). An entry clamped to its deadline (see
798+
// clampToDeadline) lands as a TimeoutError failure here, regardless of
799+
// what the untimed outcome would have been, and is never retried.
801800
if (L === B && p.timedOut) {
802801
timedOutCount++;
803802
// Retry decision happens at landing time: a failed lane-B attempt gets
@@ -811,7 +810,7 @@ window.HttpwareDemo = (function () {
811810
const backoffSec = rnd() * Math.min(retryCfg.maxDelay, retryCfg.baseDelay * Math.pow(2, p.attempt));
812811
const f2 = scenario.fault(now + backoffSec, rnd);
813812
const landTicks = Math.max(1, Math.round((backoffSec + f2.ms) * 1000 / TICK));
814-
// Composition order is timeout -> circuitBreaker -> bulkhead -> retry ->
813+
// Composition order is circuitBreaker -> timeout -> bulkhead -> retry ->
815814
// terminal: the bulkhead sits OUTSIDE retry, so one slot is acquired
816815
// once and held for the WHOLE operation (all attempts), released only
817816
// when the operation finally lands. Carry `bh` forward on every retry
@@ -833,14 +832,12 @@ window.HttpwareDemo = (function () {
833832
// so a null/absent bulkhead or a rejected (never-pushed) request is a no-op.
834833
// Breaker records ONCE per fully-exhausted retry sequence (not per attempt):
835834
// brk.res is called here, only on the terminal (non-retried) landing. A
836-
// timed-out entry is an outer-timeout cancellation: AsyncTimeout is
837-
// OUTERMOST, so the inner breaker's `except BaseException` clause does see
838-
// the CancelledError (it releases the in-flight probe slot) but does not
839-
// count it as a success/failure outcome — only the outer TimeoutError
840-
// surfaces to the caller.
835+
// timed-out entry counts as a failure: the breaker sits OUTSIDE
836+
// AsyncTimeout, so the TimeoutError raised on expiry passes through it
837+
// as a counted failure (a CancelledError would not be counted).
841838
if (!retried) {
842839
L.if--;
843-
if (L === B && p.timedOut) { L.bad++; }
840+
if (L === B && p.timedOut) { if (brk) brk.res(false, now); L.bad++; }
844841
else { if (L === B && brk) brk.res(p.ok, now); if (p.ok) L.ok++; else L.bad++; }
845842
if (L === B && bulk && p.bh) bulk.release();
846843
}
@@ -864,8 +861,9 @@ window.HttpwareDemo = (function () {
864861
B.rej++; spawnDot(dotsB, els.trackB, 'rej');
865862
} else {
866863
if (budget) budget.deposit(now);
867-
// Total deadline set once at the first attempt (Infinity when no
868-
// AsyncTimeout is configured, so the clamp below is always a no-op).
864+
// Total deadline set once at the first attempt, after the breaker admitted
865+
// the request, so an open circuit rejects before any deadline starts
866+
// (Infinity when no AsyncTimeout is configured: the clamp is a no-op).
869867
const deadline = tmoCfg ? now + tmoCfg.timeout : Infinity;
870868
const { land, timedOut } = clampToDeadline(tick, landTicks, deadline);
871869
B.if++; B.pend.push({ land, ok: f.ok, attempt: 0, bh: !!bulk, start: now, deadline, timedOut });

‎docs/demos/full-stack.md‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Full stack: combining the patterns
22

33
Real clients combine these patterns, in the recommended order
4-
`AsyncTimeout -> AsyncCircuitBreaker -> AsyncBulkhead -> AsyncRetry -> terminal`.
4+
`AsyncCircuitBreaker -> AsyncTimeout -> AsyncBulkhead -> AsyncRetry -> terminal`.
55
Here both clients go through an incident in three phases, and each layer handles a
66
different phase.
77

@@ -13,30 +13,32 @@ document.addEventListener('DOMContentLoaded', function () {
1313
scenarios: [
1414
{ id: 'incident', label: 'Multi-phase incident', dur: 16.0,
1515
fault: (now, rnd) => {
16-
if (now >= 2.0 && now < 5.0) return { ok: true, ms: 4.0, label: 'latency spike' };
17-
if (now >= 5.0 && now < 8.0) return rnd() < 0.6
16+
if (now >= 2.0 && now < 5.0) return rnd() < 0.6
1817
? { ok: false, ms: 0.6, label: 'brownout' } : { ok: true, ms: 0.6 };
18+
if (now >= 5.0 && now < 8.0) return { ok: true, ms: 4.0, label: 'latency spike' };
1919
if (now >= 8.0 && now < 12.0) return { ok: false, ms: 0.3, label: 'hard down' };
2020
return { ok: true, ms: 0.05 };
2121
},
22-
chainB: { timeout: { timeout: 2.0 },
23-
circuitBreaker: { failureThreshold: 5, resetTimeout: 2.0, successThreshold: 1 },
22+
chainB: { circuitBreaker: { failureThreshold: 5, resetTimeout: 2.0, successThreshold: 1 },
23+
timeout: { timeout: 2.0 },
2424
bulkhead: { maxConcurrent: 10, acquireTimeout: 0 },
2525
retry: { maxAttempts: 3, baseDelay: 0.1, maxDelay: 5.0 },
2626
budget: { ttl: 10.0, minRetriesPerSec: 10.0, percentCanRetry: 0.2 } } },
2727
],
2828
macroStrip: true,
2929
stageLabel: (now) => now < 2 ? 'healthy'
30-
: now < 5 ? 'phase 1: latency spike'
31-
: now < 8 ? 'phase 2: brownout'
30+
: now < 5 ? 'phase 1: brownout'
31+
: now < 8 ? 'phase 2: latency spike'
3232
: now < 12 ? 'phase 3: hard down' : 'recovered',
3333
buildStops: () => [
34-
{ when: (s) => s.now >= 2.4, spot: ['poolB', 'elapsedB'], title: 'Phase 1: latency spike',
35-
body: 'The bulkhead caps concurrency so the slow phase cannot exhaust the client, and the timeout ends each operation after 2s.' },
36-
{ when: (s) => s.now >= 5.4, spot: ['ifB'], title: 'Phase 2: brownout',
34+
{ when: (s) => s.now >= 2.4, spot: ['ifB'], title: 'Phase 1: brownout',
3735
body: 'Retry recovers many of the transient errors, and the budget stops it from adding much load.' },
38-
{ when: (s) => s.mw.cb && s.mw.cb.state === 'OPEN', spot: ['brkB'], title: 'Phase 3: hard down',
39-
body: 'Repeated failures open the breaker. It sits before the retry layer, so it rejects whole retry sequences and counts one outcome per sequence, not per attempt.' },
36+
{ when: (s) => s.now >= 5.4, spot: ['poolB', 'elapsedB'], title: 'Phase 2: latency spike',
37+
body: 'The bulkhead caps concurrency so the slow upstream cannot exhaust the client, and the timeout ends each operation after 2s.' },
38+
{ when: (s) => s.mw.cb && s.mw.cb.state === 'OPEN', spot: ['brkB'], title: 'Timeouts open the breaker',
39+
body: 'The breaker sits before the timeout, so each expired deadline counts as a failure. After five in a row the circuit opens, and new calls fail at once instead of waiting 2s on the slow upstream.' },
40+
{ when: (s) => s.now >= 8.5, spot: ['brkB'], title: 'Phase 3: hard down',
41+
body: 'The circuit stays open through the outage, because each half-open probe fails and reopens it. It sits before the retry layer, so it rejects whole retry sequences and counts one outcome per sequence, not per attempt.' },
4042
{ when: (s) => s.now >= 13.5, spot: ['ifA', 'latA', 'ifB', 'latB'], title: 'Full stack vs none',
4143
body: 'The plain client fails through every phase. In the httpware client, each layer handles the phase it is built for.' },
4244
],

0 commit comments

Comments
 (0)