Skip to content

fix(turbo-tasks): eliminate ABBA deadlock in the compilation event queue - #96939

Draft
marcoshernanz wants to merge 1 commit into
vercel:canaryfrom
marcoshernanz:deepsec/message-queue-deadlock
Draft

fix(turbo-tasks): eliminate ABBA deadlock in the compilation event queue#96939
marcoshernanz wants to merge 1 commit into
vercel:canaryfrom
marcoshernanz:deepsec/message-queue-deadlock

Conversation

@marcoshernanz

Copy link
Copy Markdown
Contributor

What

CompilationEventQueue (turbo-tasks message_queue.rs — the fan-out for compilation/timing/trace/diagnostic events consumed by the napi layer, backgroundLogCompilationEvents / projectCompilationEventsSubscribe) had an ABBA deadlock:

  • send() spawned a task that acquired event_history.lock() and held it for the task's whole life, then acquired DashMap shard write guards (get_mut) and awaited channel sends while holding both.
  • subscribe(None) acquired the Global shard write guard via entry().or_default(), then awaited event_history.lock() and replayed history while still holding the shard guard.

Lock orders: send = history → shard, subscribe = shard → history. A two-worker interleave deadlocks (the parking_lot shard wait is a synchronous, non-yielding block), and every subsequent send then blocks another worker thread — progressively stalling the whole runtime (next dev/next build hangs with no error). Secondarily, one slow/full subscriber channel (capacity 256) stalled all history recording and global delivery.

Fix

Restructure around one serialization point (the history lock), with no lock ever held across an .await:

  • subscribe: [lock] register the sender + replay history with try_send [unlock] — synchronous, because a fresh subscriber channel's capacity (MAX_QUEUE_SIZE = 256) is always ≥ the history's capacity (also 256), so it can never block.
  • send: [lock] append to history + snapshot the subscriber lists [unlock], then deliver guard-free.

Consequences, beyond eliminating the deadlock:

  • Exactly-once at subscribe boundaries: an event is in the replay snapshot xor in delivery snapshots — the lock partitions them.
  • Strict ordering: replays fully enqueue before any later live delivery.
  • No stall: delivery is concurrent across subscribers (join_all), and failed subscribers are removed afterwards under freshly-taken guards, matched by same_channel identity so concurrently-added subscribers are kept.

Tests

  • 50-round stress test (seeded history + subscribe(None) replay + live send, 10s collect deadline) with duplicate detection after the expected count.
  • Dead-subscriber removal test: dropped receiver removed, live kept — proven by a second event only deliverable if the live sender survived.
  • Existing TimingEvent tests pass; cargo check/clippy/fmt green.

Honest caveat: the ABBA deadlock is timing-dependent and my stress runs did not reproduce it on the old code (the lock-order inversion is proven statically from the lock acquisition order, not empirically reproduced).


Note: commits on this branch are currently unsigned; happy to re-sign and force-push if needed before review.

send() held event_history.lock() for the whole task, then acquired
DashMap shard write guards and awaited channel sends, while
subscribe(None) acquired the Global shard write guard and then awaited
event_history.lock() plus replayed history while still holding the
shard guard. Lock orders: send = history -> shard, subscribe = shard ->
history — a two-worker interleave deadlocks (the parking_lot shard wait
is a synchronous non-yielding block), and every subsequent send then
blocks another worker thread, progressively stalling the runtime.

Restructure around one serialization point (the history lock) with no
lock ever held across an await:

- subscribe: [lock] register the sender + replay history with try_send
  [unlock] — synchronous because the fresh channel's capacity
  (MAX_QUEUE_SIZE) is always >= the history's capacity, so it can never
  block.
- send: [lock] append to history + snapshot the subscriber lists
  [unlock], then deliver guard-free.

That yields exactly-once at subscribe boundaries (an event is in the
replay snapshot xor in delivery snapshots) and strict ordering
(replays fully enqueue before any later live delivery), while removing
the deadlock and the global stall a full/slow subscriber channel used
to cause. Delivery is concurrent across subscribers (join_all), and
failed subscribers are removed afterwards under fresh guards, matched
by channel identity so concurrent additions are kept.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: 18e0fa4

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

🟢 2 improvements

Metric Canary PR Change
node_modules Size 553 MB 552 MB 🟢 605 kB (0%)
Webpack Cold (First Request) 3.508s 3.260s 🟢 248ms (-7%)
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change
Cold (Listen) 813ms 812ms
Cold (Ready in log) 790ms 782ms
Cold (First Request) 1.290s 1.292s
Warm (Listen) 812ms 813ms
Warm (Ready in log) 783ms 786ms
Warm (First Request) 1.273s 1.282s
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change
Cold (Listen) 814ms 760ms
Cold (Ready in log) 795ms 759ms
Cold (First Request) 3.508s 3.260s 🟢 248ms (-7%)
Warm (Listen) 811ms 760ms
Warm (Ready in log) 782ms 763ms
Warm (First Request) 3.363s 3.309s

⚡ Production Builds

Metric Canary PR Change
Fresh Build 5.675s 5.665s
Cached Build 2.938s 2.944s
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change
Fresh Build 23.524s 23.756s
Cached Build 23.756s 23.363s
node_modules Size 553 MB 552 MB 🟢 605 kB (0%)
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
06bwjz_-4znb-.js gzip 154 B N/A -
07jdby0ue616s.js gzip 450 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0im0vz0lo815s.js gzip 8.77 kB N/A -
0l54qj3-79a7-.js gzip 162 B N/A -
0qa98wh5-vacg.js gzip 160 B N/A -
1-jrl0bfay5va.js gzip 157 B N/A -
133405p6ewydx.js gzip 10 kB N/A -
17p50_vq5-xc5.js gzip 8.71 kB N/A -
19xzn89g-pvj8.js gzip 9.45 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1ey-k8tr3q-bs.js gzip 10.6 kB N/A -
1p6ukk737nprf.js gzip 156 B N/A -
1q6ufgdgm0nh1.js gzip 157 B N/A -
1tf1phijqlx9j.js gzip 220 B 220 B
1wd1e9uuc4euy.js gzip 8.76 kB N/A -
1x8o4qqktz5ba.js gzip 157 B N/A -
2-w5wamautz2c.js gzip 65.6 kB N/A -
219oe54sobbj5.js gzip 8.81 kB N/A -
22alyybqz5_09.js gzip 3.57 kB N/A -
23_81vni0opf0.js gzip 157 B N/A -
2816ugqjp0cw7.js gzip 156 B N/A -
28p1njha-2ovo.js gzip 13.6 kB N/A -
2b-nvb0ybzif_.js gzip 8.7 kB N/A -
2bdp2_cq5bxqf.js gzip 169 B N/A -
2j5mn0ng9x-8y.js gzip 71.5 kB N/A -
2jrojsoli4din.js gzip 155 B N/A -
2m3bh1binl60-.js gzip 13.2 kB N/A -
2oks0a7s7ajem.js gzip 10.3 kB N/A -
31r35oy32u7ag.js gzip 46.7 kB N/A -
38-q43pzktqhs.js gzip 1.46 kB N/A -
39vr3swgz7fry.js gzip 155 B N/A -
3fw39x1tg924h.js gzip 8.79 kB N/A -
3kfiy2dr7a5m3.js gzip 5.72 kB N/A -
3l2c-qzlafpi-.js gzip 8.78 kB N/A -
3mrwxzed0ylgv.js gzip 13.1 kB N/A -
3rdf0zyd2dgxs.js gzip 8.76 kB N/A -
41jfc8g2x7su6.js gzip 7.54 kB N/A -
41u5s3oe2-erp.js gzip 2.29 kB N/A -
445z7nxr_34_5.js gzip 155 B N/A -
turbopack-07..iac9.js gzip 3.86 kB N/A -
turbopack-0d..3g83.js gzip 3.88 kB N/A -
turbopack-0h..fyuy.js gzip 3.86 kB N/A -
turbopack-0u..33e_.js gzip 3.86 kB N/A -
turbopack-14..19iy.js gzip 3.86 kB N/A -
turbopack-15..1srt.js gzip 3.86 kB N/A -
turbopack-1k..6a7i.js gzip 3.86 kB N/A -
turbopack-22..g_rn.js gzip 3.86 kB N/A -
turbopack-2d..r8zj.js gzip 3.84 kB N/A -
turbopack-2f..kjh-.js gzip 3.86 kB N/A -
turbopack-36..nj8y.js gzip 3.86 kB N/A -
turbopack-3b..xviv.js gzip 3.86 kB N/A -
turbopack-3q..regv.js gzip 3.86 kB N/A -
turbopack-3t..-aad.js gzip 3.86 kB N/A -
0_nm136-ya6-_.js gzip N/A 157 B -
0_sfs0nf236wo.js gzip N/A 3.65 kB -
030a1njeh5b77.js gzip N/A 10 kB -
03w0p83bts959.js gzip N/A 156 B -
0m9cqrcp88jl4.js gzip N/A 156 B -
0ot0qt5np20h7.js gzip N/A 8.78 kB -
1_2jrt6oqh_rm.js gzip N/A 169 B -
12m9rr7_7cexd.js gzip N/A 8.7 kB -
186r3y_rfvr0b.js gzip N/A 8.78 kB -
19v97848yrjp7.js gzip N/A 8.81 kB -
1fod4qxxtwx_f.js gzip N/A 157 B -
1nd_0rwgz2ozk.js gzip N/A 9.45 kB -
1oi9pltr_e10m.js gzip N/A 10.6 kB -
1rd9tzqgzsz2w.js gzip N/A 8.77 kB -
1rq1g_cwv642d.js gzip N/A 10.3 kB -
1rqt72flgtr9y.js gzip N/A 161 B -
1uojw_uabeg93.js gzip N/A 450 B -
1w-7lusgyl81y.js gzip N/A 8.7 kB -
21h56wdn9zddo.js gzip N/A 8.75 kB -
24bh56dxfkn27.js gzip N/A 65.6 kB -
2auhrzpzovkv5.js gzip N/A 46.3 kB -
2lawtsdpb_hpb.js gzip N/A 156 B -
2n1fpb4r6gy_r.js gzip N/A 163 B -
2ovff1533zvno.js gzip N/A 13.2 kB -
2rkr1of02xai3.js gzip N/A 155 B -
2s4z8jy0z7v93.js gzip N/A 1.46 kB -
2tm97fjdke0kt.js gzip N/A 7.54 kB -
2y6gb5bs4mnam.js gzip N/A 8.75 kB -
33d1y1xzwxzcx.js gzip N/A 158 B -
341h71ln1jpl3.js gzip N/A 2.29 kB -
34ttpt_n7605h.js gzip N/A 5.72 kB -
35hr5wjydnzwy.js gzip N/A 158 B -
36ckfq9o-ys-a.js gzip N/A 13.1 kB -
39vjw81_y3le-.js gzip N/A 71.6 kB -
3ld1h33ci3vnl.js gzip N/A 156 B -
3nsokxeud6jww.js gzip N/A 152 B -
3vdh6_u50pu5t.js gzip N/A 13.6 kB -
turbopack-01..g7l3.js gzip N/A 3.86 kB -
turbopack-0q..t7mp.js gzip N/A 3.86 kB -
turbopack-0u..yq5n.js gzip N/A 3.86 kB -
turbopack-14..tltd.js gzip N/A 3.84 kB -
turbopack-17..6yy_.js gzip N/A 3.88 kB -
turbopack-2-..ppur.js gzip N/A 3.86 kB -
turbopack-23..sup7.js gzip N/A 3.86 kB -
turbopack-2c..we8c.js gzip N/A 3.86 kB -
turbopack-2w..gwhn.js gzip N/A 3.86 kB -
turbopack-3_..vv2k.js gzip N/A 3.86 kB -
turbopack-34..2yjm.js gzip N/A 3.86 kB -
turbopack-35..upcz.js gzip N/A 3.86 kB -
turbopack-3r..q8cr.js gzip N/A 3.86 kB -
turbopack-3w..w_f2.js gzip N/A 3.86 kB -
Total 451 kB 451 kB ✅ -321 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 782 B 774 B 🟢 8 B (-1%)
Total 782 B 774 B ✅ -8 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 434 B 433 B
Total 434 B 433 B ✅ -1 B
Build Cache
Canary PR Change
00000001.sst gzip 16.5 MB 14 MB 🟢 2.44 MB (-15%)
00000002.sst gzip 11.2 MB 11.5 MB 🔴 +317 kB (+3%)
00000003.sst gzip 12.1 MB 14.7 MB 🔴 +2.57 MB (+21%)
00000004.sst gzip 15.4 MB 14.9 MB 🟢 494 kB (-3%)
00000005.sst gzip 2.81 MB 10.2 MB 🔴 +7.39 MB (+263%)
00000006.sst gzip 10.2 MB 2.81 MB 🟢 7.39 MB (-72%)
00000007.sst gzip 59 B 59 B
00000008.meta gzip 89 B 89 B
00000009.meta gzip 298 kB 298 kB
00000010.meta gzip 298 kB 298 kB
00000011.meta gzip 298 kB 298 kB
00000012.sst gzip 50.9 kB 51.2 kB
00000013.sst gzip 1.82 MB 1.89 MB 🔴 +73.5 kB (+4%)
00000014.sst gzip 59 B 59 B
00000015.meta gzip 116 B 116 B
00000016.meta gzip 325 kB 324 kB
00000017.meta gzip 405 kB 405 kB
00000018.sst gzip 51.9 kB 51.9 kB
00000019.sst gzip 1.37 MB 1.38 MB 🔴 +10.8 kB (+1%)
00000020.sst gzip 59 B 59 B
00000021.meta gzip 116 B 116 B
00000022.meta gzip 325 kB 324 kB
00000023.meta gzip 371 kB 370 kB
00000024.sst gzip 51.8 kB 51.9 kB
00000025.sst gzip 1.37 MB 1.38 MB 🔴 +10.2 kB (+1%)
00000026.sst gzip 59 B 59 B
00000027.meta gzip 116 B 116 B
00000028.meta gzip 325 kB 324 kB
00000029.meta gzip 371 kB 370 kB
00000030.sst gzip 51 kB 51.3 kB
00000031.sst gzip 1.37 MB 1.38 MB 🔴 +10.4 kB (+1%)
00000032.sst gzip 59 B 59 B
00000033.meta gzip 116 B 116 B
00000034.meta gzip 325 kB 324 kB
00000035.meta gzip 371 kB 370 kB
00000036.sst gzip 51.7 kB 51.8 kB
00000037.sst gzip 1.37 MB 1.38 MB 🔴 +10.5 kB (+1%)
00000038.sst gzip 59 B 59 B
00000039.meta gzip 116 B 116 B
00000040.meta gzip 325 kB 324 kB
00000041.meta gzip 371 kB 370 kB
CURRENT gzip 24 B 24 B
LOG gzip 669 B 663 B
Total 80.1 MB 80.2 MB ⚠️ +61.1 kB

📦 Webpack

Client

Main Bundles
Canary PR Change
3322-HASH.js gzip 65.7 kB N/A -
4191.HASH.js gzip 169 B N/A -
7920-HASH.js gzip 4.67 kB N/A -
9784-HASH.js gzip 5.63 kB N/A -
b1ad9f4c-HASH.js gzip 63.2 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 253 B 253 B
main-HASH.js gzip 40 kB 40 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
3577.HASH.js gzip N/A 168 B -
578-HASH.js gzip N/A 66 kB -
8590-HASH.js gzip N/A 5.61 kB -
9750-HASH.js gzip N/A 4.69 kB -
a8984546-HASH.js gzip N/A 63.2 kB -
Total 241 kB 241 kB ⚠️ +266 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 193 B
_error-HASH.js gzip 181 B 182 B
css-HASH.js gzip 334 B 331 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 255 B 253 B
head-HASH.js gzip 349 B 351 B
hooks-HASH.js gzip 382 B 384 B
image-HASH.js gzip 581 B 582 B
index-HASH.js gzip 260 B 259 B
link-HASH.js gzip 2.48 kB 2.48 kB
routerDirect..HASH.js gzip 317 B 318 B
script-HASH.js gzip 384 B 386 B
withRouter-HASH.js gzip 316 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.95 kB 7.95 kB ⚠️ +2 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 129 kB 128 kB
page.js gzip 294 kB 295 kB
Total 423 kB 423 kB ⚠️ +63 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 618 B 616 B
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 45.5 kB 45.5 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 47.1 kB 47.1 kB ⚠️ +1 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 718 B 718 B
Total 718 B 718 B
Build Cache
Canary PR Change
0.pack gzip 4.8 MB 4.86 MB 🔴 +62.8 kB (+1%)
index.pack gzip 123 kB 122 kB
index.pack.old gzip 122 kB 123 kB
Total 5.05 MB 5.11 MB ⚠️ +62.8 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 375 kB 374 kB
app-page-exp..prod.js gzip 207 kB 207 kB
app-page-tur...dev.js gzip 374 kB 374 kB
app-page-tur..prod.js gzip 207 kB 206 kB
app-page-tur...dev.js gzip 370 kB 370 kB
app-page-tur..prod.js gzip 205 kB 204 kB
app-page.run...dev.js gzip 371 kB 370 kB
app-page.run..prod.js gzip 205 kB 204 kB
app-route-ex...dev.js gzip 83.3 kB 83.3 kB
app-route-ex..prod.js gzip 56.4 kB 56.4 kB
app-route-tu...dev.js gzip 83.3 kB 83.3 kB
app-route-tu..prod.js gzip 56.4 kB 56.4 kB
app-route-tu...dev.js gzip 82.9 kB 82.9 kB
app-route-tu..prod.js gzip 56.2 kB 56.2 kB
app-route.ru...dev.js gzip 82.8 kB 82.8 kB
app-route.ru..prod.js gzip 56.2 kB 56.2 kB
dev-validati...dev.js gzip 133 kB 133 kB
dev-validati...dev.js gzip 133 kB 133 kB
dev-validati...dev.js gzip 131 kB 131 kB
dev-validati...dev.js gzip 131 kB 131 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 46 kB 46 kB
pages-api-tu..prod.js gzip 34.3 kB 34.3 kB
pages-api.ru...dev.js gzip 46 kB 46 kB
pages-api.ru..prod.js gzip 34.3 kB 34.3 kB
pages-turbo....dev.js gzip 54.8 kB 54.8 kB
pages-turbo...prod.js gzip 39.9 kB 39.9 kB
pages.runtim...dev.js gzip 54.8 kB 54.8 kB
pages.runtim..prod.js gzip 39.9 kB 39.9 kB
server.runti..prod.js gzip 67 kB 67 kB
use-cache-pr...dev.js gzip 72.3 kB 72.3 kB
use-cache-pr...dev.js gzip 72.4 kB 72.4 kB
use-cache-pr...dev.js gzip 70.6 kB 70.6 kB
use-cache-pr...dev.js gzip 70.6 kB 70.6 kB
Total 4.1 MB 4.1 MB ✅ -3.35 kB
📝 Changed Files (29 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
  • app-route-ex..ntime.dev.js
  • app-route-ex..time.prod.js
  • app-route-tu..ntime.dev.js
  • app-route-tu..time.prod.js
  • app-route-tu..ntime.dev.js
  • app-route-tu..time.prod.js
  • app-route.runtime.dev.js
  • app-route.ru..time.prod.js
  • dev-validati..ntime.dev.js
  • dev-validati..ntime.dev.js
  • dev-validati..ntime.dev.js
  • dev-validati..ntime.dev.js
  • ... and 9 more
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js
failed to diff
app-route-ex..ntime.dev.js

Diff too large to display

app-route-ex..time.prod.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route-tu..time.prod.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route-tu..time.prod.js

Diff too large to display

app-route.runtime.dev.js

Diff too large to display

app-route.ru..time.prod.js

Diff too large to display

dev-validati..ntime.dev.js

Diff too large to display

dev-validati..ntime.dev.js

Diff too large to display

dev-validati..ntime.dev.js

Diff too large to display

dev-validati..ntime.dev.js

Diff too large to display

pages-api-tu..ntime.dev.js

Diff too large to display

pages-api-tu..time.prod.js

Diff too large to display

pages-api.runtime.dev.js

Diff too large to display

pages-api.ru..time.prod.js

Diff too large to display

pages-turbo...ntime.dev.js

Diff too large to display

pages-turbo...time.prod.js

Diff too large to display

pages.runtime.dev.js

Diff too large to display

pages.runtime.prod.js

Diff too large to display

server.runtime.prod.js

Diff too large to display

📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/18e0fa451affca184f47df7baf59b0ea5acbb8e6/next

Commit: 18e0fa4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant