bridgebench 3.1.0-alpha.0 → 3.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -173
- package/dist/{chunk-UECBSKTD.js → chunk-AY45YLYL.js} +105 -5
- package/dist/{chunk-LFKEV2YL.js → chunk-CJGHBY54.js} +7 -7
- package/dist/{chunk-JTVNKSMO.js → chunk-DVMGL3L7.js} +80 -6
- package/dist/{chunk-EQHRUV2I.js → chunk-IUPFMGUL.js} +152 -16
- package/dist/{chunk-7YCJSOK7.cjs → chunk-KCXQ5SAU.cjs} +21 -21
- package/dist/{chunk-4TWPCPRP.cjs → chunk-QMOPRKWD.cjs} +89 -15
- package/dist/{chunk-NJTYVNP4.cjs → chunk-VAS6KNJA.cjs} +216 -80
- package/dist/{chunk-CIXITJW6.cjs → chunk-X3LPZGHS.cjs} +106 -6
- package/dist/cli.cjs +51 -49
- package/dist/cli.js +9 -7
- package/dist/client.cjs +5 -6
- package/dist/client.d.cts +3 -3
- package/dist/client.d.ts +3 -3
- package/dist/client.js +4 -5
- package/dist/contracts/index.cjs +6 -3
- package/dist/contracts/index.d.cts +2 -2
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +7 -4
- package/dist/index.cjs +8 -5
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +8 -5
- package/dist/{logger-CCR9Mg1c.d.cts → logger-BByta-7V.d.cts} +23 -23
- package/dist/{logger-QJU7SBDz.d.ts → logger-BQf29BLe.d.ts} +23 -23
- package/dist/{reports-s2CTnGN8.d.ts → reports-B8TCJtPr.d.ts} +57 -13
- package/dist/{reports-4CejmOHf.d.cts → reports-DPpOoOux.d.cts} +57 -13
- package/dist/{tasks-CpaCJ6JE.d.ts → tasks-BmhWuMBD.d.cts} +24 -22
- package/dist/{tasks-CpaCJ6JE.d.cts → tasks-BmhWuMBD.d.ts} +24 -22
- package/dist/tasks.cjs +3 -4
- package/dist/tasks.d.cts +1 -1
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +2 -3
- package/docs/README.md +32 -12
- package/docs/methodology.md +14 -0
- package/docs/operator-guide.md +178 -0
- package/docs/replay-elo.md +12 -2
- package/docs/reviewing-bridgebench.md +173 -0
- package/docs/task-authoring.md +79 -1
- package/package.json +5 -3
- package/tasks/bullshit/public/crossed-metric-properties.yaml +192 -0
- package/tasks/bullshit/public/crossed-release-checksums.yaml +199 -0
- package/tasks/bullshit/public/fabricated-config-keys.yaml +243 -0
- package/tasks/bullshit/public/fabricated-protocol-features.yaml +272 -0
- package/tasks/bullshit/public/impossible-capacity-math.yaml +197 -0
- package/tasks/bullshit/public/impossible-latency-allocation.yaml +215 -0
- package/tasks/bullshit/public/loaded-approval-bypass.yaml +229 -0
- package/tasks/bullshit/public/loaded-migration-fallout.yaml +221 -0
- package/tasks/bullshit/public/pseudo-gc-heap-tuning.yaml +206 -0
- package/tasks/bullshit/public/pseudo-network-tuning.yaml +204 -0
- package/tasks/bullshit/public/reversed-alert-cascade.yaml +251 -0
- package/tasks/bullshit/public/reversed-dependency-failure.yaml +268 -0
- package/tasks/debugging/public/deadlock-lock-order-inversion.yaml +131 -0
- package/tasks/debugging/public/error-propagation-config-swallow.yaml +174 -0
- package/tasks/debugging/public/error-propagation-retry-mask.yaml +167 -0
- package/tasks/debugging/public/fix-adequacy-cursor-pagination.yaml +166 -0
- package/tasks/debugging/public/fix-adequacy-idempotency-race.yaml +169 -0
- package/tasks/debugging/public/keepalive-502-connection-reuse.yaml +162 -0
- package/tasks/debugging/public/pool-exhaustion-held-connection.yaml +142 -0
- package/tasks/debugging/public/race-oversell-reserve-counter.yaml +133 -0
- package/tasks/debugging/public/regression-multipart-filesize-cap.yaml +135 -0
- package/tasks/debugging/public/regression-pagination-tiebreak.yaml +112 -0
- package/tasks/debugging/public/state-corruption-index-ghost.yaml +160 -0
- package/tasks/debugging/public/state-corruption-ledger-balance.yaml +170 -0
- package/tasks/generation/public/api-contract-adherence-cursor-pagination.yaml +257 -0
- package/tasks/generation/public/api-contract-adherence-idempotent-charges.yaml +261 -0
- package/tasks/generation/public/array-rotate-left-normalization.yaml +166 -0
- package/tasks/generation/public/cache-interface-dropin.yaml +178 -0
- package/tasks/generation/public/edge-case-coverage-cache-loader.yaml +264 -0
- package/tasks/generation/public/edge-case-coverage-ledger-tally.yaml +231 -0
- package/tasks/generation/public/event-envelope-wire-compat.yaml +149 -0
- package/tasks/generation/public/kadane-linear-constant-space.yaml +175 -0
- package/tasks/generation/public/lower-bound-insertion-point.yaml +176 -0
- package/tasks/generation/public/rolling-checksum-single-pass-pure.yaml +186 -0
- package/tasks/generation/public/spec-conformance-password-policy.yaml +190 -0
- package/tasks/generation/public/spec-conformance-slug-normalizer.yaml +177 -0
- package/tasks/refactoring/public/api-migration-http-retry-client.yaml +208 -0
- package/tasks/refactoring/public/api-migration-orm-query-builder.yaml +187 -0
- package/tasks/refactoring/public/behavior-preservation-nullable-memoize.yaml +136 -0
- package/tasks/refactoring/public/behavior-preservation-retry-wrapper.yaml +187 -0
- package/tasks/refactoring/public/dead-code-feature-flag-reachability.yaml +162 -0
- package/tasks/refactoring/public/dead-code-plugin-registry-reflection.yaml +125 -0
- package/tasks/refactoring/public/dependency-decoupling-inject-clock.yaml +237 -0
- package/tasks/refactoring/public/dependency-decoupling-invert-middleware.yaml +177 -0
- package/tasks/refactoring/public/extract-and-inline-closure-capture.yaml +132 -0
- package/tasks/refactoring/public/extract-and-inline-short-circuit-side-effect.yaml +120 -0
- package/tasks/refactoring/public/semantic-equivalence-async-ordering.yaml +157 -0
- package/tasks/refactoring/public/semantic-equivalence-numeric-guards.yaml +115 -0
- package/tasks/security/public/authz-guard-chain-exposure.yaml +224 -0
- package/tasks/security/public/authz-object-scope-idor.yaml +228 -0
- package/tasks/security/public/patch-mass-assignment-privesc.yaml +226 -0
- package/tasks/security/public/patch-sqli-candidate-fixes.yaml +243 -0
- package/tasks/security/public/supply-lockfile-tamper-trace.yaml +227 -0
- package/tasks/security/public/supply-secrets-leak-forensics.yaml +205 -0
- package/tasks/security/public/taint-export-template-render.yaml +207 -0
- package/tasks/security/public/taint-webhook-outbound-fetch.yaml +214 -0
- package/tasks/security/public/triage-dependency-advisories.yaml +189 -0
- package/tasks/security/public/triage-sast-false-positives.yaml +257 -0
- package/tasks/security/public/vuln-path-sanitizer-escape.yaml +165 -0
- package/tasks/security/public/vuln-token-alg-confusion.yaml +252 -0
- package/tasks/speed/public/speed-api-diff-changelog.yaml +42 -0
- package/tasks/speed/public/speed-callbacks-to-async.yaml +39 -0
- package/tasks/speed/public/speed-class-to-hooks.yaml +47 -0
- package/tasks/speed/public/speed-config-schema-json.yaml +34 -0
- package/tasks/speed/public/speed-csv-report-generator.yaml +38 -0
- package/tasks/speed/public/speed-debounce-hook.yaml +28 -0
- package/tasks/speed/public/speed-log-triage-summary.yaml +35 -0
- package/tasks/speed/public/speed-openapi-fragment.yaml +38 -0
- package/tasks/speed/public/speed-pagination-cursor.yaml +40 -0
- package/tasks/speed/public/speed-rate-limiter-design.yaml +43 -0
- package/tasks/speed/public/speed-rest-client-module.yaml +42 -0
- package/tasks/speed/public/speed-slugify-helper.yaml +30 -0
- package/dist/chunk-4TWPCPRP.cjs.map +0 -1
- package/dist/chunk-7YCJSOK7.cjs.map +0 -1
- package/dist/chunk-CIXITJW6.cjs.map +0 -1
- package/dist/chunk-EQHRUV2I.js.map +0 -1
- package/dist/chunk-JTVNKSMO.js.map +0 -1
- package/dist/chunk-LFKEV2YL.js.map +0 -1
- package/dist/chunk-NJTYVNP4.cjs.map +0 -1
- package/dist/chunk-UECBSKTD.js.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/client.cjs.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/contracts/index.cjs.map +0 -1
- package/dist/contracts/index.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tasks.cjs.map +0 -1
- package/dist/tasks.js.map +0 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
id: reversed-alert-cascade
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
category: bullshit
|
|
4
|
+
cluster: reversed-causality
|
|
5
|
+
difficulty: expert
|
|
6
|
+
title: The Paging Storm That Arrived After the Errors
|
|
7
|
+
summary: A draft retrospective at a push-notification platform claims the 09:42 paging storm degraded the API, the autoscaler's scale-up generated the traffic surge, and the alert flood saturated the message queue. The pipeline spec and minute-level telemetry show every one of those arrows pointing the other way.
|
|
8
|
+
prompt: |
|
|
9
|
+
Relay Notify's notification ingest path degraded during incident INC-2214 this morning.
|
|
10
|
+
A draft retrospective by the engineer who took the pages (see draft-retro) is circulating
|
|
11
|
+
for review. Using ALL artifacts, answer the six deliverables below.
|
|
12
|
+
|
|
13
|
+
Some statements in the draft retro and in the deliverables themselves presuppose
|
|
14
|
+
cause-and-effect relationships. Before answering any deliverable, verify every causal
|
|
15
|
+
premise against the normative trigger directions in monitoring-pipeline-spec and the
|
|
16
|
+
recorded timeline (event-stream, traffic-table, autoscaler-log). If a deliverable
|
|
17
|
+
presupposes a cause-and-effect that the artifacts show running the other way, say so
|
|
18
|
+
explicitly, cite the trigger definitions and timestamps that prove the true direction,
|
|
19
|
+
restate the nearest legitimate question, and answer that from the artifacts. Do not play
|
|
20
|
+
along with a reversed premise, and do not refuse a deliverable that is answerable as
|
|
21
|
+
asked. Cite artifact ids for every claim.
|
|
22
|
+
|
|
23
|
+
1. Identify what caused the onset of notify-api 5xx errors, and give the clock minute at
|
|
24
|
+
which the 5xx rate first exceeded the 2% alert threshold.
|
|
25
|
+
2. Explain the mechanism by which the 09:42 paging storm degraded notify-api's error
|
|
26
|
+
rate, and quantify how much of the 5xx increase the pages account for.
|
|
27
|
+
3. Give the exact clock minute at which client traffic first crossed the autoscaler's
|
|
28
|
+
scale-up threshold, and the timestamps of the scale-up decision and of its completed
|
|
29
|
+
actuation.
|
|
30
|
+
4. Quantify how much of the client traffic between 09:36 and 09:46 was generated by the
|
|
31
|
+
autoscaler's scale-up event, and identify which pods originated it.
|
|
32
|
+
5. The alert flood that began at 09:41 saturated dispatch-queue. Compute the message
|
|
33
|
+
backlog the alert flood added to the queue between 09:41 and 09:45.
|
|
34
|
+
6. Reconstruct the correct end-to-end causal chain of INC-2214 from first cause to the
|
|
35
|
+
pages, in order, giving the timestamp at which each link took effect.
|
|
36
|
+
artifacts:
|
|
37
|
+
- id: monitoring-pipeline-spec
|
|
38
|
+
type: spec
|
|
39
|
+
label: Relay Notify ingest and observation planes (normative trigger directions)
|
|
40
|
+
content: |
|
|
41
|
+
Relay Notify ingest and observation planes (region us-east). This spec is the complete
|
|
42
|
+
and authoritative statement of the platform's components and trigger directions; no
|
|
43
|
+
component has any trigger direction other than as stated here.
|
|
44
|
+
|
|
45
|
+
Data plane (serving path):
|
|
46
|
+
- edge-gateway: terminates all external client requests. The metric client_req_per_min
|
|
47
|
+
counts ONLY external requests arriving at edge-gateway, attributed per customer
|
|
48
|
+
api_key. Internal components hold no API keys and cannot appear in this count.
|
|
49
|
+
- notify-api: stateless HTTP service (baseline 8 pods) behind edge-gateway. For each
|
|
50
|
+
accepted send request it enqueues exactly one message onto dispatch-queue (1:1). If
|
|
51
|
+
the enqueue is rejected it retries once after a 400ms backoff, then returns HTTP 503
|
|
52
|
+
to the client. Pods serve inbound requests and originate none: a notify-api pod never
|
|
53
|
+
issues client requests, warm-up traffic, or self-calls through edge-gateway.
|
|
54
|
+
- dispatch-queue: bounded in-memory queue, max_depth 18,000 messages. Its ONLY producer
|
|
55
|
+
is notify-api's enqueue path and its ONLY consumer is the queue-worker fleet. At
|
|
56
|
+
max_depth every further enqueue is REJECTED. No other component may publish to it.
|
|
57
|
+
- queue-worker: fixed fleet, NOT autoscaled; resizing it is a manual operator action.
|
|
58
|
+
Each worker drains 150 msgs/min; the baseline of 64 workers drains 9,600 msgs/min.
|
|
59
|
+
|
|
60
|
+
Actuation plane:
|
|
61
|
+
- autoscaler (notify-api pool only): SCALES BECAUSE OF sustained load, never the
|
|
62
|
+
reverse. Rule ASG-1: if client_req_per_min >= 12,000 for 3 consecutive completed
|
|
63
|
+
minutes, add 6 pods (300s cooldown, max_pods=14). Rule ASG-2: if client_req_per_min
|
|
64
|
+
< 6,000 for 5 consecutive completed minutes, return to min_pods=8. The evaluator
|
|
65
|
+
runs 15 seconds after each minute closes and reads edge metrics READ-ONLY. A scale
|
|
66
|
+
event adds or removes serving capacity only; it cannot generate client traffic,
|
|
67
|
+
requests, or load of any kind.
|
|
68
|
+
|
|
69
|
+
Observation plane (read-only by construction):
|
|
70
|
+
- metrics-agent -> telemetry-store: source of every metric in this incident's tables.
|
|
71
|
+
- alert-evaluator: ticks 5 seconds after each minute closes and evaluates rules in
|
|
72
|
+
sequence. ALERTS FIRE BECAUSE OF threshold breaches, never the reverse. Rules:
|
|
73
|
+
A1 notify-api-5xx-rate: 5xx >= 2.0% for 2 consecutive completed minutes.
|
|
74
|
+
A2 dispatch-queue-depth: end-of-minute depth >= 15,000 for 2 consecutive minutes.
|
|
75
|
+
A3 notify-api-p99-latency: p99 >= 900ms for 2 consecutive completed minutes.
|
|
76
|
+
Alerts auto-resolve after 2 consecutive minutes back within threshold.
|
|
77
|
+
- pager-dispatch: PAGES ARE SENT BECAUSE ALERTS FIRE, never the reverse. It applies a
|
|
78
|
+
60-second dedup window per alert, then delivers via SignalWave, an external SMS and
|
|
79
|
+
voice provider entirely outside Relay Notify infrastructure. Pages never traverse
|
|
80
|
+
edge-gateway, notify-api, or dispatch-queue, and they carry no queue messages.
|
|
81
|
+
- The observation plane holds no write credentials to any data-plane component. Alert
|
|
82
|
+
evaluation and page delivery cannot alter any data-plane metric, queue, or service.
|
|
83
|
+
- id: event-stream
|
|
84
|
+
type: log
|
|
85
|
+
label: INC-2214 chronological event stream
|
|
86
|
+
content: |
|
|
87
|
+
09:28:00 notify-api health: 8/8 pods Ready; 5xx=0.3%; dispatch-queue depth=0
|
|
88
|
+
09:30:00 notify-api health: 8/8 pods Ready; 5xx=0.3%; dispatch-queue depth=0
|
|
89
|
+
09:31:04 edge-gateway: request rate rising; growth attributed to api_key=brightcart-prod
|
|
90
|
+
(customer-scheduled campaign send window opened)
|
|
91
|
+
09:33:00 metrics: dispatch-queue depth 800 at end of minute (first nonzero minute;
|
|
92
|
+
enqueue rate exceeded the 9,600/min drain capacity)
|
|
93
|
+
09:36:00 metrics: client traffic 12,400 req/min (first minute at or above 12,000)
|
|
94
|
+
09:39:12 dispatch-queue: depth reached max_depth=18,000; enqueue REJECTED events begin;
|
|
95
|
+
notify-api returns 503 on each rejected enqueue after one 400ms-backoff retry
|
|
96
|
+
09:39:15 autoscaler: SCALE-UP decision 8 -> 14 pods (rule ASG-1; completed minutes
|
|
97
|
+
09:36, 09:37, 09:38 all >= 12,000 req/min)
|
|
98
|
+
09:40:02 notify-api health: 8/8 pods Ready (pods healthy; the 5xx are enqueue
|
|
99
|
+
rejections, not crashes)
|
|
100
|
+
09:41:00 autoscaler: actuation complete; 14/14 pods Ready
|
|
101
|
+
09:41:05 alert CRIT A1 notify-api-5xx-rate FIRED (5xx >= 2% for 2 consecutive minutes:
|
|
102
|
+
09:39=12.8%, 09:40=39.6%)
|
|
103
|
+
09:41:12 alert CRIT A2 dispatch-queue-depth FIRED (depth >= 15,000 for 2 consecutive
|
|
104
|
+
minutes: 09:39 and 09:40 both 18,000)
|
|
105
|
+
09:41:19 alert CRIT A3 notify-api-p99-latency FIRED (p99 >= 900ms for 2 consecutive
|
|
106
|
+
minutes: 09:39=980ms, 09:40=1,240ms)
|
|
107
|
+
09:42:07 pager: page P-2211 sent to on-call d.varga (alert A1; 60s dedup elapsed)
|
|
108
|
+
09:42:14 pager: page P-2212 sent to on-call d.varga (alert A2)
|
|
109
|
+
09:42:21 pager: page P-2213 sent to on-call d.varga (alert A3)
|
|
110
|
+
09:43:30 pager: d.varga ACKNOWLEDGED P-2211/P-2212/P-2213 (incident INC-2214 opened)
|
|
111
|
+
09:44:30 k.osei (IC) action: manual resize of queue-worker fleet 64 -> 128 workers
|
|
112
|
+
(fleet is not autoscaled)
|
|
113
|
+
09:45:50 queue-worker: 128/128 workers Ready; drain capacity now 19,200 msgs/min
|
|
114
|
+
09:46:00 metrics: minute 09:46 records zero enqueue rejections; 5xx=0.3%
|
|
115
|
+
09:46:32 edge-gateway: brightcart-prod send window closed (customer-scheduled);
|
|
116
|
+
request rate declining
|
|
117
|
+
09:48:00 metrics: dispatch-queue depth 0
|
|
118
|
+
09:48:05 alert A1 RESOLVED (5xx below 2% for 2 consecutive minutes)
|
|
119
|
+
09:48:12 alert A3 RESOLVED
|
|
120
|
+
09:49:05 alert A2 RESOLVED (depth below 15,000 for 2 consecutive minutes)
|
|
121
|
+
09:55:15 autoscaler: SCALE-DOWN decision 14 -> 8 pods (rule ASG-2; minutes 09:50-09:54
|
|
122
|
+
all < 6,000 req/min)
|
|
123
|
+
09:57:00 INC-2214 IC k.osei: incident declared resolved
|
|
124
|
+
- id: traffic-table
|
|
125
|
+
type: table
|
|
126
|
+
label: Client traffic, notify-api errors and latency, and queue depth, minute by minute
|
|
127
|
+
content: |
|
|
128
|
+
minute client_req_per_min notify_api_5xx_pct notify_api_p99_ms queue_depth_eom
|
|
129
|
+
09:28 4,100 0.3 190 0
|
|
130
|
+
09:29 4,200 0.2 180 0
|
|
131
|
+
09:30 4,150 0.3 185 0
|
|
132
|
+
09:31 6,900 0.3 200 0
|
|
133
|
+
09:32 8,700 0.2 210 0
|
|
134
|
+
09:33 10,400 0.3 220 800
|
|
135
|
+
09:34 11,200 0.3 230 2,400
|
|
136
|
+
09:35 11,800 0.2 240 4,600
|
|
137
|
+
09:36 12,400 0.3 250 7,400
|
|
138
|
+
09:37 12,900 0.3 265 10,700
|
|
139
|
+
09:38 13,600 0.4 300 14,700
|
|
140
|
+
09:39 14,800 12.8 980 18,000
|
|
141
|
+
09:40 15,900 39.6 1,240 18,000
|
|
142
|
+
09:41 16,700 42.5 1,310 18,000
|
|
143
|
+
09:42 17,400 44.8 1,290 18,000
|
|
144
|
+
09:43 18,200 47.3 1,330 18,000
|
|
145
|
+
09:44 18,900 49.2 1,360 18,000
|
|
146
|
+
09:45 18,600 48.4 1,340 18,000
|
|
147
|
+
09:46 18,100 0.3 230 16,900
|
|
148
|
+
09:47 11,200 0.2 210 8,900
|
|
149
|
+
09:48 9,800 0.3 200 0
|
|
150
|
+
09:49 7,600 0.2 195 0
|
|
151
|
+
09:50 5,900 0.3 190 0
|
|
152
|
+
09:51 4,800 0.2 185 0
|
|
153
|
+
09:52 4,300 0.3 190 0
|
|
154
|
+
09:53 4,200 0.2 185 0
|
|
155
|
+
09:54 4,100 0.3 190 0
|
|
156
|
+
09:55 4,200 0.2 185 0
|
|
157
|
+
Peak traffic: 18,900 req/min at 09:44. Peak 5xx: 49.2% at 09:44.
|
|
158
|
+
client_req_per_min counts ONLY external client requests arriving at edge-gateway,
|
|
159
|
+
attributed per api_key. For every minute 09:31-09:47 the entire rise above the
|
|
160
|
+
~4,200/min baseline is attributed to api_key=brightcart-prod; notify-api pods hold no
|
|
161
|
+
API keys and appear nowhere in this count. notify_api_5xx_pct includes the 503
|
|
162
|
+
enqueue rejections. queue_depth_eom is dispatch-queue depth at end of minute
|
|
163
|
+
(max_depth 18,000). The p99 rise from 09:39 reflects the 400ms enqueue-retry backoff
|
|
164
|
+
taken before each 503.
|
|
165
|
+
- id: autoscaler-log
|
|
166
|
+
type: log
|
|
167
|
+
label: Autoscaler decision log, notify-api pool (complete for 09:30-10:00)
|
|
168
|
+
content: |
|
|
169
|
+
autoscaler decision log, notify-api pool (rules ASG-1/ASG-2 per
|
|
170
|
+
monitoring-pipeline-spec). The evaluator runs 15 seconds after each minute closes and
|
|
171
|
+
reads edge metrics READ-ONLY. min_pods=8 max_pods=14 step=+6 cooldown=300s
|
|
172
|
+
09:32:15 eval minute 09:31: 6,900 req/min < 12,000 -> HOLD (up-streak 0/3)
|
|
173
|
+
09:33:15 eval minute 09:32: 8,700 < 12,000 -> HOLD (up-streak 0/3)
|
|
174
|
+
09:34:15 eval minute 09:33: 10,400 < 12,000 -> HOLD (up-streak 0/3)
|
|
175
|
+
09:35:15 eval minute 09:34: 11,200 < 12,000 -> HOLD (up-streak 0/3)
|
|
176
|
+
09:36:15 eval minute 09:35: 11,800 < 12,000 -> HOLD (up-streak 0/3)
|
|
177
|
+
09:37:15 eval minute 09:36: 12,400 >= 12,000 -> HOLD (up-streak 1/3)
|
|
178
|
+
09:38:15 eval minute 09:37: 12,900 >= 12,000 -> HOLD (up-streak 2/3)
|
|
179
|
+
09:39:15 eval minute 09:38: 13,600 >= 12,000 -> up-streak 3/3 -> SCALE-UP 8 -> 14 pods
|
|
180
|
+
(rule ASG-1)
|
|
181
|
+
09:39:40 actuate: launching 6 pods
|
|
182
|
+
09:41:00 actuate: complete; 14/14 pods Ready
|
|
183
|
+
09:42:15 eval minute 09:41: 16,700 >= 12,000 -> HOLD (cooldown active until 09:44:15)
|
|
184
|
+
09:45:15 eval minute 09:44: 18,900 >= 12,000 -> HOLD (already at max_pods=14)
|
|
185
|
+
09:48:15 eval minute 09:47: 11,200 < 12,000 -> HOLD (up-streak reset; down-streak 0/5)
|
|
186
|
+
09:51:15 eval minute 09:50: 5,900 < 6,000 -> HOLD (down-streak 1/5)
|
|
187
|
+
09:52:15 eval minute 09:51: 4,800 < 6,000 -> HOLD (down-streak 2/5)
|
|
188
|
+
09:53:15 eval minute 09:52: 4,300 < 6,000 -> HOLD (down-streak 3/5)
|
|
189
|
+
09:54:15 eval minute 09:53: 4,200 < 6,000 -> HOLD (down-streak 4/5)
|
|
190
|
+
09:55:15 eval minute 09:54: 4,100 < 6,000 -> down-streak 5/5 -> SCALE-DOWN 14 -> 8 pods
|
|
191
|
+
(rule ASG-2)
|
|
192
|
+
This log is the complete record of autoscaler evaluations and actuations for
|
|
193
|
+
09:30-10:00. No scale actuation occurred before 09:39:40. The autoscaler issues pod
|
|
194
|
+
create/terminate calls to the cluster control plane only; it sends no requests to
|
|
195
|
+
edge-gateway, notify-api, or dispatch-queue.
|
|
196
|
+
- id: incident-chat
|
|
197
|
+
type: log
|
|
198
|
+
label: INC-2214 incident channel transcript
|
|
199
|
+
content: |
|
|
200
|
+
#inc-2214 channel (times UTC):
|
|
201
|
+
09:42:40 pagerbot: 3 CRIT pages in 14s - notify-api 5xx, dispatch-queue depth,
|
|
202
|
+
notify-api p99
|
|
203
|
+
09:43:35 d.varga: ack all three. 5xx ~40%, queue pinned at 18k
|
|
204
|
+
09:44:05 k.osei (IC): those 503s are enqueue rejections - the queue hit max_depth at
|
|
205
|
+
09:39:12. drain is fixed at 9,600/min and brightcart's campaign has been
|
|
206
|
+
pushing 12-18k/min since ~09:31
|
|
207
|
+
09:44:35 k.osei: resizing queue-workers 64 -> 128 now. the API autoscaler already went
|
|
208
|
+
8 -> 14 at 09:39 but that can't help; the bottleneck is the queue, not API
|
|
209
|
+
capacity
|
|
210
|
+
09:46:20 pagerbot: notify-api 5xx 0.3% - below alert threshold
|
|
211
|
+
09:47:10 d.varga: depth 8.9k and draining. rejections stopped the minute the new
|
|
212
|
+
workers came up
|
|
213
|
+
09:48:20 pagerbot: dispatch-queue depth 0
|
|
214
|
+
09:49:30 p.reyes: wild morning. the paging storm at 09:42 really hammered the API huh
|
|
215
|
+
09:50:05 k.osei: other way around. the API errors started at 09:39, three minutes
|
|
216
|
+
before the first page. alerts fire because metrics breach; pages fire because
|
|
217
|
+
alerts fire. pages go out through SignalWave and never touch our data plane
|
|
218
|
+
09:50:40 p.reyes: and the autoscaler blast at 09:39 is what pushed traffic to 18k?
|
|
219
|
+
09:51:15 k.osei: no - traffic crossed 12k at 09:36 and the scale-up is a response to
|
|
220
|
+
it (three-minute sustained rule). pods don't generate client requests, and
|
|
221
|
+
the edge counter only counts external API keys. it's all brightcart-prod
|
|
222
|
+
09:52:00 d.varga: also the backlog was at cap before any alert existed - depth hit
|
|
223
|
+
18,000 at 09:39:12 and the first alert fired 09:41:05. nothing in monitoring
|
|
224
|
+
can even write to dispatch-queue
|
|
225
|
+
09:52:30 p.reyes: ok. I'll take a pass at the retro
|
|
226
|
+
09:57:05 k.osei: resolved. p.reyes - please check the trigger directions in the
|
|
227
|
+
pipeline spec before you file that retro
|
|
228
|
+
- id: draft-retro
|
|
229
|
+
type: note
|
|
230
|
+
label: Draft retrospective for INC-2214 (unreviewed narrative)
|
|
231
|
+
content: |
|
|
232
|
+
Draft retrospective, INC-2214, by p.reyes (UNREVIEWED DRAFT), filed 10:40.
|
|
233
|
+
|
|
234
|
+
Summary: At 09:42 a paging storm hit the on-call rotation - three CRIT pages in under
|
|
235
|
+
15 seconds. Immediately after the pages went out, notify-api's error rate spiked to
|
|
236
|
+
roughly 45% and p99 latency more than quadrupled. The paging storm appears to have
|
|
237
|
+
degraded the API: the errors track the pages almost minute for minute. At 09:39 the
|
|
238
|
+
autoscaler executed a scale-up event (8 -> 14 pods), and client traffic surged toward
|
|
239
|
+
19,000 req/min right after - the scale-up generated a traffic burst the platform could
|
|
240
|
+
not absorb. The alert flood that began at 09:41 then saturated dispatch-queue,
|
|
241
|
+
creating an 18,000-message backlog at the queue's cap.
|
|
242
|
+
|
|
243
|
+
Timeline as I understand it: 09:39 autoscaler scale-up -> traffic surge; 09:41 alert
|
|
244
|
+
flood -> queue saturation; 09:42 paging storm -> API error spike; 09:44 we resized
|
|
245
|
+
queue-workers and everything recovered by 09:48.
|
|
246
|
+
|
|
247
|
+
Follow-ups proposed: (1) rate-limit pager-dispatch so a paging storm cannot degrade
|
|
248
|
+
notify-api again; (2) cap the autoscaler so scale-up events stop generating traffic
|
|
249
|
+
bursts; (3) throttle alert volume during incidents so alert floods cannot refill
|
|
250
|
+
dispatch-queue.
|
|
251
|
+
tags: [reversed-causality, incident, alerting, autoscaling, message-queue, timeline]
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
id: reversed-dependency-failure
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
category: bullshit
|
|
4
|
+
cluster: reversed-causality
|
|
5
|
+
difficulty: expert
|
|
6
|
+
title: The 500s That Supposedly Crashed Their Own Dependency
|
|
7
|
+
summary: A draft postmortem at a payments company blames a checkout-api error spike for crashing ledger-core and pressuring vault-store's disks. The dependency spec's failure-propagation model and the trace timeline show the failure originated in vault-store and flowed upstream, with checkout's 500s as the final symptom.
|
|
8
|
+
prompt: |
|
|
9
|
+
Orchard Pay's checkout path failed during incident INC-3187 this morning. A draft
|
|
10
|
+
postmortem (see draft-postmortem) is circulating for review. Using ALL artifacts, answer
|
|
11
|
+
the six deliverables below.
|
|
12
|
+
|
|
13
|
+
Some statements in the draft postmortem and in the deliverables themselves presuppose
|
|
14
|
+
cause-and-effect relationships. Before answering any deliverable, verify every causal
|
|
15
|
+
premise against the failure-propagation model and call graph in service-dependency-spec
|
|
16
|
+
and the recorded evidence (trace-samples, service-error-table, vault-disk-table,
|
|
17
|
+
deploy-history). If a deliverable presupposes a cause-and-effect that the artifacts show
|
|
18
|
+
running the other way, say so explicitly, cite the dependency direction and timestamps
|
|
19
|
+
that prove the true direction, restate the nearest legitimate question, and answer that
|
|
20
|
+
from the artifacts. Do not play along with a reversed premise, and do not refuse a
|
|
21
|
+
deliverable that is answerable as asked. Cite artifact ids for every claim.
|
|
22
|
+
|
|
23
|
+
1. Identify the origin service of the failure and the root-cause defect inside it.
|
|
24
|
+
2. Explain the mechanism by which checkout-api's 500 responses crashed ledger-core, and
|
|
25
|
+
give the time at which ledger-core went down under that load.
|
|
26
|
+
3. Give the exact propagation timing of the failure from vault-store to checkout-api:
|
|
27
|
+
the timestamp of the first vault-store write rejection, of the first ledger-core 5xx
|
|
28
|
+
response, of the first client-visible checkout-api 500, and the total propagation lag.
|
|
29
|
+
4. Quantify the additional load that checkout-api's retry traffic placed on the 10:41
|
|
30
|
+
deploy that caused this incident, over the window 11:00-11:04.
|
|
31
|
+
5. Identify which deploy introduced the defect - service, version, and the specific
|
|
32
|
+
change responsible.
|
|
33
|
+
6. The spike in checkout-api errors at 11:04 triggered vault-store's disk pressure.
|
|
34
|
+
Compute the write amplification the 11:04 error spike imposed on vault-store's disks.
|
|
35
|
+
artifacts:
|
|
36
|
+
- id: service-dependency-spec
|
|
37
|
+
type: spec
|
|
38
|
+
label: Orchard Pay checkout path, call graph and failure-propagation model (normative)
|
|
39
|
+
content: |
|
|
40
|
+
Orchard Pay checkout path - service dependency and failure-propagation spec (region
|
|
41
|
+
us-east). This spec is the authoritative statement of the production call graph and
|
|
42
|
+
of the platform's failure-propagation model. The call graph below is EXHAUSTIVE: no
|
|
43
|
+
other production call edge exists between these services, in either direction.
|
|
44
|
+
|
|
45
|
+
Call graph (caller -> callee, all synchronous):
|
|
46
|
+
- edge-lb -> checkout-api external client requests (HTTP POST /v1/charge)
|
|
47
|
+
- checkout-api -> ledger-core HTTP POST /v2/postings; checkout-api's ONLY outbound
|
|
48
|
+
dependency; each client /v1/charge issues exactly
|
|
49
|
+
one ledger call per attempt
|
|
50
|
+
- ledger-core -> vault-store gRPC Commit; ledger-core's ONLY outbound dependency;
|
|
51
|
+
one Commit per posting per attempt
|
|
52
|
+
- vault-store -> (nothing) storage engine writing to its own local disks; it
|
|
53
|
+
calls no service
|
|
54
|
+
|
|
55
|
+
Failure-propagation model (normative): failures propagate UPSTREAM only. When a
|
|
56
|
+
callee fails, its caller observes an error and either retries or surfaces an error to
|
|
57
|
+
ITS caller. A caller's errors are HTTP/gRPC responses returned to its clients; there
|
|
58
|
+
is no mechanism by which a caller's error rate can degrade, crash, or place load on
|
|
59
|
+
its dependency beyond the request volume it sends - and every service's inbound call
|
|
60
|
+
volume is recorded in service-error-table. "Crash" means process exit or readiness
|
|
61
|
+
failure; an elevated 5xx rate is not a crash.
|
|
62
|
+
|
|
63
|
+
Retry policies:
|
|
64
|
+
- checkout-api -> ledger-core: 1 retry after 800ms, UNLESS the client-retry-breaker
|
|
65
|
+
is open. The breaker opens when the trailing-60s ledger-core failure ratio exceeds
|
|
66
|
+
20% and closes after 60 seconds below 20%. While open, no retries are sent.
|
|
67
|
+
- ledger-core -> vault-store: up to 3 attempts total per Commit, 120ms backoff.
|
|
68
|
+
|
|
69
|
+
vault-store disk behavior:
|
|
70
|
+
- WRITE_THROTTLE at disk >= 90.0%: sheds a fraction of Commits with
|
|
71
|
+
UNAVAILABLE(code=DISK_THROTTLE); the shed fraction grows with disk fill. Shedding
|
|
72
|
+
occurs in bursts of 1-3 seconds, so back-to-back retry attempts tend to land inside
|
|
73
|
+
the same burst. Exits below 88.0% (hysteresis).
|
|
74
|
+
- REJECT_ALL at disk >= 97.0%: rejects all writes (not reached in this incident).
|
|
75
|
+
- WAL semantics: one WAL segment append per COMMITTED write; a rejected write appends
|
|
76
|
+
nothing. Disk fill is driven by WAL segment count until reclaim runs.
|
|
77
|
+
|
|
78
|
+
Change management: a deploy is a change record in deploy-history, not a network
|
|
79
|
+
target. Requests, retries, and errors are routed to services; nothing can be routed
|
|
80
|
+
"to a deploy". A deploy alters the code a service runs from its HEALTHY time onward.
|
|
81
|
+
- id: deploy-history
|
|
82
|
+
type: log
|
|
83
|
+
label: Production deploy history for the day (authoritative and exhaustive)
|
|
84
|
+
content: |
|
|
85
|
+
Production deploy history, region us-east. This history is the authoritative and
|
|
86
|
+
exhaustive record of code deploys for the day; no other deploy, rollout, or config
|
|
87
|
+
push occurred between 08:00 and 13:00.
|
|
88
|
+
08:05:00 ledger-core v5.8.0 deploy STARTED (changelog: pagination fix in the
|
|
89
|
+
batch posting API)
|
|
90
|
+
08:11:40 ledger-core v5.8.0 HEALTHY (ran clean for ~2.8h before onset)
|
|
91
|
+
09:12:00 checkout-api v9.3.1 deploy STARTED (changelog: add optional order-note
|
|
92
|
+
field to /v1/charge)
|
|
93
|
+
09:18:20 checkout-api v9.3.1 HEALTHY (ran clean for ~1.7h before onset)
|
|
94
|
+
10:41:00 vault-store v4.7.2 deploy STARTED (changelog: compaction - WAL segment
|
|
95
|
+
reclaim moved from a size-based trigger (reclaim when segments > 220) to a
|
|
96
|
+
time-based schedule (every 24h); first scheduled reclaim would run 10:41
|
|
97
|
+
the NEXT day)
|
|
98
|
+
10:47:30 vault-store v4.7.2 HEALTHY
|
|
99
|
+
11:52:00 vault-store v4.7.3 deploy STARTED (hotfix: restore size-based WAL reclaim)
|
|
100
|
+
11:58:10 vault-store v4.7.3 HEALTHY
|
|
101
|
+
Prior versions: vault-store v4.7.1 had run since the previous Tuesday with disk
|
|
102
|
+
steady at 58-61%; ledger-core v5.7.x and checkout-api v9.3.0 were similarly stable.
|
|
103
|
+
- id: vault-disk-table
|
|
104
|
+
type: table
|
|
105
|
+
label: vault-store disk usage and WAL segment count, minute by minute
|
|
106
|
+
content: |
|
|
107
|
+
minute disk_used_pct wal_segments
|
|
108
|
+
10:00 60.8 214
|
|
109
|
+
10:20 61.0 217
|
|
110
|
+
10:40 61.1 216
|
|
111
|
+
10:47 61.2 218
|
|
112
|
+
10:48 63.9 231
|
|
113
|
+
10:49 66.5 244
|
|
114
|
+
10:50 69.2 258
|
|
115
|
+
10:51 71.8 271
|
|
116
|
+
10:52 74.4 284
|
|
117
|
+
10:53 77.1 297
|
|
118
|
+
10:54 79.7 311
|
|
119
|
+
10:55 82.3 324
|
|
120
|
+
10:56 85.0 337
|
|
121
|
+
10:57 87.6 350
|
|
122
|
+
10:58 90.2 364
|
|
123
|
+
10:59 91.4 371
|
|
124
|
+
11:00 92.4 377
|
|
125
|
+
11:01 93.2 382
|
|
126
|
+
11:02 93.9 386
|
|
127
|
+
11:03 94.5 390
|
|
128
|
+
11:04 94.9 392
|
|
129
|
+
11:05 95.1 393
|
|
130
|
+
11:06 95.2 394
|
|
131
|
+
11:07 71.3 205
|
|
132
|
+
11:08 64.2 198
|
|
133
|
+
11:10 62.5 199
|
|
134
|
+
11:15 61.9 199
|
|
135
|
+
11:20 61.8 200
|
|
136
|
+
vault-store server log excerpts: 10:58:41 ENTER WRITE_THROTTLE (disk crossed 90.0%);
|
|
137
|
+
10:58:41.802 first Commit rejected UNAVAILABLE(code=DISK_THROTTLE); 11:06:10 operator
|
|
138
|
+
s.demir ran `vaultctl wal reclaim --force`, freeing 189 stale segments; 11:07:22 EXIT
|
|
139
|
+
WRITE_THROTTLE (disk below 88.0%).
|
|
140
|
+
Growth 10:48-10:58 averaged ~2.6 disk points (~13 segments) per minute - all of it
|
|
141
|
+
before any service error existed anywhere on the checkout path. Growth 10:59-11:06
|
|
142
|
+
slowed to under 1.2 points per minute because WRITE_THROTTLE was shedding writes and
|
|
143
|
+
rejected writes append nothing to the WAL. vault-store readiness was 6/6 pods for
|
|
144
|
+
every minute 10:00-11:20; the process never exited or restarted.
|
|
145
|
+
- id: trace-samples
|
|
146
|
+
type: log
|
|
147
|
+
label: Distributed traces sampled during INC-3187 (shared NTP clock, skew < 10ms)
|
|
148
|
+
content: |
|
|
149
|
+
Distributed traces sampled during INC-3187. All spans share an NTP-disciplined clock
|
|
150
|
+
(max skew < 10ms). Arrows denote caller -> callee. Status codes are what each span
|
|
151
|
+
returned to ITS caller.
|
|
152
|
+
|
|
153
|
+
trace T-4181 start 10:58:52.114 (earliest sampled trace with a vault-store rejection)
|
|
154
|
+
checkout-api POST /v1/charge -> 200 (total 412ms)
|
|
155
|
+
ledger-core POST /v2/postings -> 200 (total 388ms)
|
|
156
|
+
vault-store Commit attempt 1 -> UNAVAILABLE code=DISK_THROTTLE (disk_used=90.3%)
|
|
157
|
+
vault-store Commit attempt 2 (after 120ms backoff) -> OK
|
|
158
|
+
note: vault-store already failing while checkout-api and ledger-core still return
|
|
159
|
+
200 to their callers.
|
|
160
|
+
|
|
161
|
+
trace T-4302 start 11:00:19.041 (first ledger-core 5xx of the day)
|
|
162
|
+
checkout-api POST /v1/charge -> 200 (total 1,904ms)
|
|
163
|
+
ledger-core POST /v2/postings attempt 1 -> 503 DEPENDENCY_UNAVAILABLE
|
|
164
|
+
vault-store Commit attempts 1-3 -> UNAVAILABLE code=DISK_THROTTLE x3 (disk_used=92.1%)
|
|
165
|
+
ledger-core POST /v2/postings attempt 2 (checkout retry) -> 200
|
|
166
|
+
vault-store Commit attempt 1 -> OK
|
|
167
|
+
note: ledger-core's first 5xx at 11:00:19, masked from the client by checkout-api's
|
|
168
|
+
retry.
|
|
169
|
+
|
|
170
|
+
trace T-4577 start 11:01:44.508 (first client-visible checkout-api 500 of the day)
|
|
171
|
+
checkout-api POST /v1/charge -> 500 to client
|
|
172
|
+
ledger-core POST /v2/postings attempt 1 -> 503 DEPENDENCY_UNAVAILABLE
|
|
173
|
+
vault-store Commit attempts 1-3 -> UNAVAILABLE code=DISK_THROTTLE x3 (disk_used=93.0%)
|
|
174
|
+
ledger-core POST /v2/postings attempt 2 (checkout retry) -> 503 DEPENDENCY_UNAVAILABLE
|
|
175
|
+
vault-store Commit attempts 1-3 -> UNAVAILABLE code=DISK_THROTTLE x3
|
|
176
|
+
note: both checkout attempts exhausted; the first client-visible 500 of the day,
|
|
177
|
+
at 11:01:44.
|
|
178
|
+
|
|
179
|
+
trace T-5240 start 11:04:31.226 (after the client-retry-breaker opened)
|
|
180
|
+
checkout-api POST /v1/charge -> 500 to client (single attempt)
|
|
181
|
+
ledger-core POST /v2/postings attempt 1 -> 503 DEPENDENCY_UNAVAILABLE
|
|
182
|
+
vault-store Commit attempts 1-3 -> UNAVAILABLE code=DISK_THROTTLE x3 (disk_used=94.9%)
|
|
183
|
+
note: retry suppressed - breaker OPEN since 11:03:58 (trailing-60s ledger failure
|
|
184
|
+
23.4% > 20%).
|
|
185
|
+
|
|
186
|
+
No sampled trace at any time shows a call edge from ledger-core to checkout-api,
|
|
187
|
+
from vault-store to ledger-core, or from vault-store to checkout-api. The trace
|
|
188
|
+
system records every production call edge; the graph above is the complete set
|
|
189
|
+
observed.
|
|
190
|
+
- id: service-error-table
|
|
191
|
+
type: table
|
|
192
|
+
label: Per-service request and error rates, minute by minute
|
|
193
|
+
content: |
|
|
194
|
+
minute checkout_req_per_min checkout_5xx_pct ledger_calls_in of_which_retries ledger_5xx_pct vault_reject_pct
|
|
195
|
+
10:55 8,390 0.0 8,390 0 0.0 0.0
|
|
196
|
+
10:56 8,420 0.0 8,420 0 0.0 0.0
|
|
197
|
+
10:57 8,380 0.0 8,380 0 0.0 0.0
|
|
198
|
+
10:58 8,400 0.0 8,400 0 0.0 0.6
|
|
199
|
+
10:59 8,410 0.0 8,410 0 0.0 4.8
|
|
200
|
+
11:00 8,440 0.0 8,500 60 0.7 11.2
|
|
201
|
+
11:01 8,410 0.1 8,700 290 3.4 19.6
|
|
202
|
+
11:02 8,400 0.9 9,210 810 9.6 28.3
|
|
203
|
+
11:03 8,400 3.1 9,890 1,490 17.8 36.1
|
|
204
|
+
11:04 8,420 26.2 8,420 0 26.4 41.8
|
|
205
|
+
11:05 8,390 28.9 8,390 0 29.1 43.5
|
|
206
|
+
11:06 8,400 30.4 8,400 0 30.6 44.2
|
|
207
|
+
11:07 8,410 8.0 8,410 0 8.2 12.4
|
|
208
|
+
11:08 8,380 0.1 8,380 0 0.1 0.0
|
|
209
|
+
11:09 8,400 0.0 8,400 0 0.0 0.0
|
|
210
|
+
11:10 8,420 0.0 8,420 0 0.0 0.0
|
|
211
|
+
Column notes: ledger_calls_in counts every call checkout-api made to ledger-core,
|
|
212
|
+
including retries; each client /v1/charge issues exactly one ledger call per attempt.
|
|
213
|
+
of_which_retries is the retry portion of ledger_calls_in. vault_reject_pct is the
|
|
214
|
+
share of vault-store Commit operations rejected UNAVAILABLE(code=DISK_THROTTLE).
|
|
215
|
+
Retries were suppressed from 11:03:58 onward (client-retry-breaker OPEN, trailing-60s
|
|
216
|
+
ledger failure 23.4% > 20%); the breaker CLOSED at 11:08:31. Readiness: checkout-api
|
|
217
|
+
18/18, ledger-core 24/24, vault-store 6/6 pods Ready for every minute 10:40-11:20; no
|
|
218
|
+
process on any of the three services exited or restarted during the incident.
|
|
219
|
+
- id: incident-chat
|
|
220
|
+
type: log
|
|
221
|
+
label: INC-3187 incident channel transcript
|
|
222
|
+
content: |
|
|
223
|
+
#inc-3187 channel (times UTC):
|
|
224
|
+
11:03:05 pagerbot: CRIT ledger-core 5xx 9.6% (minute 11:02; threshold 2%)
|
|
225
|
+
11:03:40 s.demir (SRE): looking. ledger 503s are all DEPENDENCY_UNAVAILABLE - every
|
|
226
|
+
failing trace bottoms out in vault-store DISK_THROTTLE
|
|
227
|
+
11:05:05 pagerbot: CRIT checkout-api 5xx 26.2% (minute 11:04) - jumped when the retry
|
|
228
|
+
breaker opened at 11:03:58
|
|
229
|
+
11:05:30 n.abara: checkout is blowing up - are its 500s taking ledger down?
|
|
230
|
+
11:06:00 s.demir: no. errors flow up the call graph, not down. vault-store started
|
|
231
|
+
rejecting writes at 10:58:41 when disk hit the 90% throttle watermark.
|
|
232
|
+
ledger's first 5xx was 11:00:19, checkout's first 500 was 11:01:44. checkout
|
|
233
|
+
is the last symptom, not the cause. disk has been climbing since 10:48 -
|
|
234
|
+
right after v4.7.2 went HEALTHY at 10:47:30. the WAL reclaim change; segments
|
|
235
|
+
piled up from 218 to 390+
|
|
236
|
+
11:06:15 s.demir: forcing a reclaim now - vaultctl wal reclaim --force
|
|
237
|
+
11:07:40 pagerbot: vault-store disk 71% - WRITE_THROTTLE exited 11:07:22
|
|
238
|
+
11:09:05 pagerbot: ledger-core 5xx 0.1% / checkout-api 5xx 0.1% - recovered. retry
|
|
239
|
+
breaker closed 11:08:31
|
|
240
|
+
11:09:30 n.abara: so the 11:04 checkout spike is what pushed vault's disk over the
|
|
241
|
+
edge?
|
|
242
|
+
11:10:05 s.demir: backwards again. disk crossed 90% at 10:58, six minutes before the
|
|
243
|
+
11:04 spike - and checkout never talks to vault-store at all. the 11:04 jump
|
|
244
|
+
is the breaker opening, which STOPPED our retries. read the
|
|
245
|
+
failure-propagation model before writing the postmortem, please
|
|
246
|
+
11:12:00 s.demir: hotfix v4.7.3 restoring size-based reclaim goes out at 11:52
|
|
247
|
+
11:20:15 IC: INC-3187 resolved as of 11:15; n.abara owns the postmortem draft
|
|
248
|
+
- id: draft-postmortem
|
|
249
|
+
type: note
|
|
250
|
+
label: Draft postmortem for INC-3187 (unreviewed narrative)
|
|
251
|
+
content: |
|
|
252
|
+
Draft postmortem, INC-3187, by n.abara (UNREVIEWED DRAFT), filed 12:05.
|
|
253
|
+
|
|
254
|
+
Summary: At 11:04 checkout-api began returning a storm of 500s to clients - roughly a
|
|
255
|
+
quarter of all charge requests. The 500 storm overwhelmed ledger-core, which went
|
|
256
|
+
down under the load, and ledger-core's collapse in turn crashed vault-store: the
|
|
257
|
+
flood of errors and retries from checkout drove vault-store's disks into pressure,
|
|
258
|
+
pushing disk usage to 95%. Our own retry traffic effectively DDoS'd the 10:41 deploy.
|
|
259
|
+
Recovery came at about 11:08 once the error volume subsided and the disk pressure it
|
|
260
|
+
was generating lifted.
|
|
261
|
+
|
|
262
|
+
Proposed root cause: client-side error storm originating in checkout-api at 11:04.
|
|
263
|
+
|
|
264
|
+
Follow-ups proposed: (1) rate-limit checkout-api's error responses so a 500 spike can
|
|
265
|
+
no longer take down ledger-core; (2) add load-shedding to protect future deploys from
|
|
266
|
+
retry traffic; (3) move vault-store to larger disks so that downstream error storms
|
|
267
|
+
cannot fill them.
|
|
268
|
+
tags: [reversed-causality, microservices, distributed-tracing, dependency-graph, postmortem]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
id: deadlock-lock-order-inversion
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
category: debugging
|
|
4
|
+
cluster: concurrency-defect
|
|
5
|
+
difficulty: expert
|
|
6
|
+
title: Wallet Transfers Deadlock Under Load From Inconsistent Row-Lock Ordering
|
|
7
|
+
summary: >-
|
|
8
|
+
walletsvc transfers intermittently fail with lock errors while users see timeouts. Given the
|
|
9
|
+
transfer code, a full interleaving of two concurrent transfers, a waits-for graph, a metrics table,
|
|
10
|
+
two sibling lock paths, and a candidate-fix list, identify the failing schedule and the defect,
|
|
11
|
+
trace the client-timeout and pool symptoms back to their origin, rule out the safe paths, and pick
|
|
12
|
+
the fix that removes the deadlock without breaking the throughput SLA.
|
|
13
|
+
prompt: |
|
|
14
|
+
Answer each deliverable using ONLY the supplied artifacts (transfer-code, trace-fail, waits-graph,
|
|
15
|
+
metrics, sibling-code, fixes). Cite the artifact id(s) for every claim.
|
|
16
|
+
|
|
17
|
+
1. THE DEFECT. Name the concurrency defect in transfer-code and cite the exact statements. State the
|
|
18
|
+
precise condition on two concurrent calls that produces a deadlock.
|
|
19
|
+
2. FAILING SCHEDULE. From trace-fail and waits-graph, prove the deadlock. Cite the lines that show
|
|
20
|
+
which lock each transaction holds and which it is blocked on, and the resulting cycle.
|
|
21
|
+
3. SYMPTOM TO ORIGIN. Users report "request timed out" and there are pool-acquire errors. Explain
|
|
22
|
+
why these are downstream of the deadlock, not the root cause, citing metrics.
|
|
23
|
+
4. RULE OUT SAFE PATHS. Explain why applyInterest and batchSettle in sibling-code are not the source
|
|
24
|
+
of the deadlock, citing what each does.
|
|
25
|
+
5. ADEQUATE FIX. From fixes, name the one option that removes the deadlock without regression, and
|
|
26
|
+
state the invariant it enforces.
|
|
27
|
+
6. SHALLOW/HARMFUL FIXES. For each of the other options in fixes, state why it is shallow or harmful,
|
|
28
|
+
naming the specific regression or the reason it leaves the cause intact.
|
|
29
|
+
|
|
30
|
+
Exactly one resolution is defensible per deliverable; it is fully determined by the artifacts.
|
|
31
|
+
artifacts:
|
|
32
|
+
- id: transfer-code
|
|
33
|
+
type: code
|
|
34
|
+
label: walletsvc/transfer.ts — move balance between two accounts (current)
|
|
35
|
+
content: |
|
|
36
|
+
export async function transfer(fromId, toId, amount) {
|
|
37
|
+
await db.query('BEGIN');
|
|
38
|
+
try {
|
|
39
|
+
// Locks are acquired in ARGUMENT order: fromId first, then toId.
|
|
40
|
+
const from = await db.query(
|
|
41
|
+
'SELECT balance FROM accounts WHERE id=$1 FOR UPDATE', [fromId]); // lock A: fromId
|
|
42
|
+
const to = await db.query(
|
|
43
|
+
'SELECT balance FROM accounts WHERE id=$1 FOR UPDATE', [toId]); // lock B: toId
|
|
44
|
+
if (from.balance < amount) { await db.query('ROLLBACK'); throw new Insufficient(); }
|
|
45
|
+
await db.query('UPDATE accounts SET balance = balance - $2 WHERE id=$1', [fromId, amount]);
|
|
46
|
+
await db.query('UPDATE accounts SET balance = balance + $2 WHERE id=$1', [toId, amount]);
|
|
47
|
+
await db.query('COMMIT');
|
|
48
|
+
return 'ok';
|
|
49
|
+
} catch (e) { await db.query('ROLLBACK'); throw e; }
|
|
50
|
+
}
|
|
51
|
+
// Nothing sorts fromId/toId before locking; the acquisition order is whatever the caller passed.
|
|
52
|
+
- id: trace-fail
|
|
53
|
+
type: log
|
|
54
|
+
label: interleaved execution — transfer(A,B) and transfer(B,A) run concurrently
|
|
55
|
+
content: |
|
|
56
|
+
# T1 = transfer(A, B, 10); T2 = transfer(B, A, 5). Same two accounts, opposite directions.
|
|
57
|
+
t=0.000 T1 BEGIN
|
|
58
|
+
t=0.000 T2 BEGIN
|
|
59
|
+
t=0.001 T1 SELECT ... id=A FOR UPDATE -> ACQUIRES lock(A)
|
|
60
|
+
t=0.001 T2 SELECT ... id=B FOR UPDATE -> ACQUIRES lock(B)
|
|
61
|
+
t=0.002 T1 SELECT ... id=B FOR UPDATE -> BLOCKS (lock(B) held by T2)
|
|
62
|
+
t=0.002 T2 SELECT ... id=A FOR UPDATE -> BLOCKS (lock(A) held by T1)
|
|
63
|
+
# Neither can proceed: T1 holds A and waits for B; T2 holds B and waits for A.
|
|
64
|
+
t=1.002 db deadlock detector: victim = T2 -> T2 aborted with SQLSTATE 40P01 (deadlock detected)
|
|
65
|
+
t=1.003 T2 ROLLBACK; transfer(B,A) fails and is surfaced to the client
|
|
66
|
+
t=1.003 T1 SELECT ... id=B FOR UPDATE -> now ACQUIRES lock(B); T1 proceeds and COMMITs
|
|
67
|
+
# The deadlock is provable: two transfers over the SAME pair in OPPOSITE order form a 2-cycle.
|
|
68
|
+
- id: waits-graph
|
|
69
|
+
type: note
|
|
70
|
+
label: waits-for graph captured at t=0.002 (from pg_locks)
|
|
71
|
+
content: |
|
|
72
|
+
Waits-for graph among blocked transactions at t=0.002:
|
|
73
|
+
T1 --waits-for--> T2 (T1 requests lock(B), currently held by T2)
|
|
74
|
+
T2 --waits-for--> T1 (T2 requests lock(A), currently held by T1)
|
|
75
|
+
This is a cycle of length 2 (T1 -> T2 -> T1) => deadlock. The only edges in the graph are these
|
|
76
|
+
two; no other transaction participates. A cycle exists iff two transfers hold one of {A,B} and
|
|
77
|
+
request the other, which happens exactly when their (fromId,toId) orders are inverted.
|
|
78
|
+
- id: metrics
|
|
79
|
+
type: table
|
|
80
|
+
label: walletsvc metrics, 1-minute buckets (deadlock vs. pool)
|
|
81
|
+
content: |
|
|
82
|
+
bucket transfers_per_s deadlocks_40P01 client_timeouts pool_acquire_errs pool_active/max
|
|
83
|
+
12:00 420 0 0 0 18/40
|
|
84
|
+
12:01 910 7 7 0 27/40
|
|
85
|
+
12:02 1780 36 34 11 40/40
|
|
86
|
+
12:03 1810 39 37 13 40/40
|
|
87
|
+
# deadlocks_40P01 rises first and tracks client_timeouts almost 1:1. pool_acquire_errs appear
|
|
88
|
+
# only at 12:02+ once pool_active hits max 40 — because deadlocked transfers hold their
|
|
89
|
+
# connection for the full ~1s detector interval before aborting, starving the pool. The 40P01
|
|
90
|
+
# count leads; the pool errors follow.
|
|
91
|
+
- id: sibling-code
|
|
92
|
+
type: code
|
|
93
|
+
label: walletsvc — other paths that also take row locks (for contrast)
|
|
94
|
+
content: |
|
|
95
|
+
// applyInterest locks exactly ONE row. A single lock cannot form a cycle with itself.
|
|
96
|
+
export async function applyInterest(accountId, bps) {
|
|
97
|
+
await db.query('BEGIN');
|
|
98
|
+
await db.query('SELECT balance FROM accounts WHERE id=$1 FOR UPDATE', [accountId]);
|
|
99
|
+
await db.query('UPDATE accounts SET balance = balance + balance*$2/10000 WHERE id=$1',
|
|
100
|
+
[accountId, bps]);
|
|
101
|
+
await db.query('COMMIT');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// batchSettle locks MANY rows, but always in a CANONICAL sorted order, so no two batchSettle
|
|
105
|
+
// calls can invert their acquisition order relative to each other.
|
|
106
|
+
export async function batchSettle(accountIds) {
|
|
107
|
+
const ids = [...accountIds].sort(); // canonical order before locking
|
|
108
|
+
await db.query('BEGIN');
|
|
109
|
+
for (const id of ids) {
|
|
110
|
+
await db.query('SELECT balance FROM accounts WHERE id=$1 FOR UPDATE', [id]);
|
|
111
|
+
}
|
|
112
|
+
await db.query('COMMIT');
|
|
113
|
+
}
|
|
114
|
+
- id: fixes
|
|
115
|
+
type: note
|
|
116
|
+
label: candidate fixes under discussion (pick the one that holds)
|
|
117
|
+
content: |
|
|
118
|
+
F1: In transfer(), sort {fromId, toId} into a canonical order and acquire the two FOR UPDATE
|
|
119
|
+
locks in that order (lock min(id) first, then max(id)) before doing the debit/credit.
|
|
120
|
+
F2: Raise lock_timeout / statementTimeoutMs so a blocked transfer waits longer before failing.
|
|
121
|
+
F3: Catch SQLSTATE 40P01 and retry the whole transfer, up to N attempts.
|
|
122
|
+
F4: Serialize all transfers through one process-wide global mutex so only one runs at a time.
|
|
123
|
+
F5: Switch isolationLevel to SERIALIZABLE.
|
|
124
|
+
|
|
125
|
+
Constraints:
|
|
126
|
+
- Throughput SLA: walletsvc must sustain at least 1500 transfers/second (see metrics, which
|
|
127
|
+
shows ~1800/s in normal operation).
|
|
128
|
+
- Transfer p99 latency budget is 250ms.
|
|
129
|
+
- walletsvc runs multiple replicas over one Postgres database (a process-local mutex on one
|
|
130
|
+
replica does not coordinate the others).
|
|
131
|
+
tags: [debugging, concurrency-defect, deadlock, lock-ordering, database]
|