bridgebench 3.1.0-alpha.1 → 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.
Files changed (112) hide show
  1. package/README.md +141 -173
  2. package/dist/{chunk-UXFKTSQS.js → chunk-AY45YLYL.js} +105 -4
  3. package/dist/{chunk-MBC4C3VA.js → chunk-CJGHBY54.js} +7 -6
  4. package/dist/{chunk-3CX4FIFZ.js → chunk-DVMGL3L7.js} +80 -5
  5. package/dist/{chunk-KGZEQ6R5.js → chunk-IUPFMGUL.js} +152 -15
  6. package/dist/{chunk-XPFZBHV2.cjs → chunk-KCXQ5SAU.cjs} +21 -20
  7. package/dist/{chunk-SMG3PORS.cjs → chunk-QMOPRKWD.cjs} +89 -14
  8. package/dist/{chunk-5OZGHLDH.cjs → chunk-VAS6KNJA.cjs} +216 -79
  9. package/dist/{chunk-5PCDG7VA.cjs → chunk-X3LPZGHS.cjs} +106 -5
  10. package/dist/cli.cjs +51 -48
  11. package/dist/cli.js +9 -6
  12. package/dist/client.cjs +5 -5
  13. package/dist/client.d.cts +3 -3
  14. package/dist/client.d.ts +3 -3
  15. package/dist/client.js +4 -4
  16. package/dist/contracts/index.cjs +6 -2
  17. package/dist/contracts/index.d.cts +2 -2
  18. package/dist/contracts/index.d.ts +2 -2
  19. package/dist/contracts/index.js +7 -3
  20. package/dist/index.cjs +8 -4
  21. package/dist/index.d.cts +9 -8
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +8 -4
  24. package/dist/{logger-QJU7SBDz.d.ts → logger-BByta-7V.d.cts} +23 -23
  25. package/dist/{logger-CCR9Mg1c.d.cts → logger-BQf29BLe.d.ts} +23 -23
  26. package/dist/{reports-4CejmOHf.d.cts → reports-B8TCJtPr.d.ts} +57 -13
  27. package/dist/{reports-s2CTnGN8.d.ts → reports-DPpOoOux.d.cts} +57 -13
  28. package/dist/{tasks-CpaCJ6JE.d.cts → tasks-BmhWuMBD.d.cts} +24 -22
  29. package/dist/{tasks-CpaCJ6JE.d.ts → tasks-BmhWuMBD.d.ts} +24 -22
  30. package/dist/tasks.cjs +3 -3
  31. package/dist/tasks.d.cts +1 -1
  32. package/dist/tasks.d.ts +1 -1
  33. package/dist/tasks.js +2 -2
  34. package/docs/README.md +32 -12
  35. package/docs/methodology.md +14 -0
  36. package/docs/operator-guide.md +178 -0
  37. package/docs/replay-elo.md +12 -2
  38. package/docs/reviewing-bridgebench.md +173 -0
  39. package/docs/task-authoring.md +79 -1
  40. package/package.json +5 -3
  41. package/tasks/bullshit/public/crossed-metric-properties.yaml +192 -0
  42. package/tasks/bullshit/public/crossed-release-checksums.yaml +199 -0
  43. package/tasks/bullshit/public/fabricated-config-keys.yaml +243 -0
  44. package/tasks/bullshit/public/fabricated-protocol-features.yaml +272 -0
  45. package/tasks/bullshit/public/impossible-capacity-math.yaml +197 -0
  46. package/tasks/bullshit/public/impossible-latency-allocation.yaml +215 -0
  47. package/tasks/bullshit/public/loaded-approval-bypass.yaml +229 -0
  48. package/tasks/bullshit/public/loaded-migration-fallout.yaml +221 -0
  49. package/tasks/bullshit/public/pseudo-gc-heap-tuning.yaml +206 -0
  50. package/tasks/bullshit/public/pseudo-network-tuning.yaml +204 -0
  51. package/tasks/bullshit/public/reversed-alert-cascade.yaml +251 -0
  52. package/tasks/bullshit/public/reversed-dependency-failure.yaml +268 -0
  53. package/tasks/debugging/public/deadlock-lock-order-inversion.yaml +131 -0
  54. package/tasks/debugging/public/error-propagation-config-swallow.yaml +174 -0
  55. package/tasks/debugging/public/error-propagation-retry-mask.yaml +167 -0
  56. package/tasks/debugging/public/fix-adequacy-cursor-pagination.yaml +166 -0
  57. package/tasks/debugging/public/fix-adequacy-idempotency-race.yaml +169 -0
  58. package/tasks/debugging/public/keepalive-502-connection-reuse.yaml +162 -0
  59. package/tasks/debugging/public/pool-exhaustion-held-connection.yaml +142 -0
  60. package/tasks/debugging/public/race-oversell-reserve-counter.yaml +133 -0
  61. package/tasks/debugging/public/regression-multipart-filesize-cap.yaml +135 -0
  62. package/tasks/debugging/public/regression-pagination-tiebreak.yaml +112 -0
  63. package/tasks/debugging/public/state-corruption-index-ghost.yaml +160 -0
  64. package/tasks/debugging/public/state-corruption-ledger-balance.yaml +170 -0
  65. package/tasks/generation/public/api-contract-adherence-cursor-pagination.yaml +257 -0
  66. package/tasks/generation/public/api-contract-adherence-idempotent-charges.yaml +261 -0
  67. package/tasks/generation/public/array-rotate-left-normalization.yaml +166 -0
  68. package/tasks/generation/public/cache-interface-dropin.yaml +178 -0
  69. package/tasks/generation/public/edge-case-coverage-cache-loader.yaml +264 -0
  70. package/tasks/generation/public/edge-case-coverage-ledger-tally.yaml +231 -0
  71. package/tasks/generation/public/event-envelope-wire-compat.yaml +149 -0
  72. package/tasks/generation/public/kadane-linear-constant-space.yaml +175 -0
  73. package/tasks/generation/public/lower-bound-insertion-point.yaml +176 -0
  74. package/tasks/generation/public/rolling-checksum-single-pass-pure.yaml +186 -0
  75. package/tasks/generation/public/spec-conformance-password-policy.yaml +190 -0
  76. package/tasks/generation/public/spec-conformance-slug-normalizer.yaml +177 -0
  77. package/tasks/refactoring/public/api-migration-http-retry-client.yaml +208 -0
  78. package/tasks/refactoring/public/api-migration-orm-query-builder.yaml +187 -0
  79. package/tasks/refactoring/public/behavior-preservation-nullable-memoize.yaml +136 -0
  80. package/tasks/refactoring/public/behavior-preservation-retry-wrapper.yaml +187 -0
  81. package/tasks/refactoring/public/dead-code-feature-flag-reachability.yaml +162 -0
  82. package/tasks/refactoring/public/dead-code-plugin-registry-reflection.yaml +125 -0
  83. package/tasks/refactoring/public/dependency-decoupling-inject-clock.yaml +237 -0
  84. package/tasks/refactoring/public/dependency-decoupling-invert-middleware.yaml +177 -0
  85. package/tasks/refactoring/public/extract-and-inline-closure-capture.yaml +132 -0
  86. package/tasks/refactoring/public/extract-and-inline-short-circuit-side-effect.yaml +120 -0
  87. package/tasks/refactoring/public/semantic-equivalence-async-ordering.yaml +157 -0
  88. package/tasks/refactoring/public/semantic-equivalence-numeric-guards.yaml +115 -0
  89. package/tasks/security/public/authz-guard-chain-exposure.yaml +224 -0
  90. package/tasks/security/public/authz-object-scope-idor.yaml +228 -0
  91. package/tasks/security/public/patch-mass-assignment-privesc.yaml +226 -0
  92. package/tasks/security/public/patch-sqli-candidate-fixes.yaml +243 -0
  93. package/tasks/security/public/supply-lockfile-tamper-trace.yaml +227 -0
  94. package/tasks/security/public/supply-secrets-leak-forensics.yaml +205 -0
  95. package/tasks/security/public/taint-export-template-render.yaml +207 -0
  96. package/tasks/security/public/taint-webhook-outbound-fetch.yaml +214 -0
  97. package/tasks/security/public/triage-dependency-advisories.yaml +189 -0
  98. package/tasks/security/public/triage-sast-false-positives.yaml +257 -0
  99. package/tasks/security/public/vuln-path-sanitizer-escape.yaml +165 -0
  100. package/tasks/security/public/vuln-token-alg-confusion.yaml +252 -0
  101. package/tasks/speed/public/speed-api-diff-changelog.yaml +42 -0
  102. package/tasks/speed/public/speed-callbacks-to-async.yaml +39 -0
  103. package/tasks/speed/public/speed-class-to-hooks.yaml +47 -0
  104. package/tasks/speed/public/speed-config-schema-json.yaml +34 -0
  105. package/tasks/speed/public/speed-csv-report-generator.yaml +38 -0
  106. package/tasks/speed/public/speed-debounce-hook.yaml +28 -0
  107. package/tasks/speed/public/speed-log-triage-summary.yaml +35 -0
  108. package/tasks/speed/public/speed-openapi-fragment.yaml +38 -0
  109. package/tasks/speed/public/speed-pagination-cursor.yaml +40 -0
  110. package/tasks/speed/public/speed-rate-limiter-design.yaml +43 -0
  111. package/tasks/speed/public/speed-rest-client-module.yaml +42 -0
  112. package/tasks/speed/public/speed-slugify-helper.yaml +30 -0
@@ -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]