Skip to content

turbo-tasks-backend: add persistence delete/tombstone plumbing for GC#95975

Draft
lukesandberg wants to merge 1 commit into
lukesandberg/gc-01-scope-self-feedingfrom
lukesandberg/gc-02-tombstone-plumbing
Draft

turbo-tasks-backend: add persistence delete/tombstone plumbing for GC#95975
lukesandberg wants to merge 1 commit into
lukesandberg/gc-01-scope-self-feedingfrom
lukesandberg/gc-02-tombstone-plumbing

Conversation

@lukesandberg

@lukesandberg lukesandberg commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

The persistence-layer mechanism to tombstone (delete) tasks from the on-disk cache. A GC-collected task is represented as a SnapshotItem::Delete that rides the same streaming iterator save_snapshot already consumes for puts, so tombstones are applied in the same commit/batch as the snapshot with no side-channel.

  • SnapshotItem becomes an enum: Put { … } (the existing path) and Delete(TaskDeletion).
  • TurboWriteBatch::delete passthrough to turbo_persistence.
  • save_snapshot applies deletes inline in the parallel put phase: TaskMeta/TaskData (SingleValue) are tombstoned by id key; the MultiValue TaskCache bucket is tombstoned wholesale and any live task that xxh3-collides in that bucket is re-inserted — those survivors are resolved by reading the authoritative on-disk bucket at apply time (not carried on TaskDeletion). The bucket-rewrite runs in parallel across distinct hashes.

Why this shape (addressing "do we still need the delete list?")

Note

SnapshotItem::Delete is #[allow(dead_code)] here: this PR lands the persistence-side handling and its tests, but nothing emits a Delete yet. The GC pass that soft-deletes tasks and serializes them to Delete lands in the next PR in the stack. So this PR is a no-op at runtime.

Tests

  • Tombstone across a reopen erases TaskMeta/TaskData/TaskCache.
  • Deleting one entry from a colliding TaskCache bucket re-inserts the survivor (both add-orders), proving delete+put compose in one commit.
  • save_snapshot re-inserts a disk-only colliding survivor when applying a SnapshotItem::Delete.

lukesandberg commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

🟢 1 improvement

Metric Canary PR Change Trend
node_modules Size 517 MB 516 MB 🟢 997 kB (0%) ███▁▁
📊 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 Trend
Cold (Listen) 814ms 813ms ▂▂█▁█
Cold (Ready in log) 792ms 804ms ▁▁█▁█
Cold (First Request) 1.368s 1.367s ▁▁█▁█
Warm (Listen) 814ms 813ms ▂▂█▂█
Warm (Ready in log) 794ms 792ms ▁▁█▁█
Warm (First Request) 1.352s 1.333s ▁▁█▁█
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 810ms 811ms ▁▁▅▄▄
Cold (Ready in log) 771ms 772ms ▁▁▃▃▁
Cold (First Request) 3.356s 3.317s ▁▁▂▂▁
Warm (Listen) 812ms 812ms ▁▁██▁
Warm (Ready in log) 773ms 773ms ▁▂▄▄▂
Warm (First Request) 3.297s 3.318s ▁▂▃▃▂

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 5.437s 5.707s ▁▂█▁█
Cached Build 5.542s 5.771s ▁▁█▁█
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 24.713s 24.807s ▁▂▃▂▁
Cached Build 24.935s 25.062s ▁▁▄▃▁
node_modules Size 517 MB 516 MB 🟢 997 kB (0%) ███▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
01njvfbkgn2hi.js gzip 166 B N/A -
03gmyxr7dukvs.js gzip 10.3 kB N/A -
040t1q17lo593.js gzip 8.77 kB N/A -
0bb0vtp8dwsez.js gzip 450 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0kb7gdprkm5-s.js gzip 153 B N/A -
0l0sr6gau5xjn.js gzip 3.53 kB N/A -
0nxqz3q165m2l.js gzip 220 B N/A -
0oygs1fnokdzg.js gzip 153 B N/A -
0qr-1tnb7_hg5.js gzip 5.72 kB N/A -
0uilzvklme4ea.js gzip 8.76 kB N/A -
0wmscwh7_m_n7.js gzip 156 B N/A -
0z0y83mfx-l-h.js gzip 10 kB N/A -
14ob2gnup7hmq.js gzip 154 B N/A -
15ok8ydf274z0.js gzip 10.6 kB N/A -
16cqqcigykfea.js gzip 13.6 kB N/A -
17h18zxt1915l.js gzip 1.46 kB N/A -
1cnwkx9jenjj4.js gzip 65.6 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1fq1h7dxad4cv.js gzip 9.44 kB N/A -
1fym-adbk_69_.js gzip 13.1 kB N/A -
1gav7nfh4y8ln.js gzip 8.76 kB N/A -
1jk3928phpavq.js gzip 154 B N/A -
1o34vh44ndvtu.js gzip 13.2 kB N/A -
23pnldcl3-q5n.js gzip 155 B N/A -
2qzmpe44xqltw.js gzip 155 B N/A -
2rypqffktcxqh.js gzip 8.69 kB N/A -
2uqsizxtehgqf.js gzip 8.79 kB N/A -
2vzktdqwbijqc.js gzip 8.74 kB N/A -
3_4kzn9ufgy-9.js gzip 45.2 kB N/A -
30sv6wnb24ko7.js gzip 156 B N/A -
3bfy3uzdkvnyq.js gzip 154 B N/A -
3c244akvx_-be.js gzip 8.74 kB N/A -
3dmlemitx3ef3.js gzip 2.29 kB N/A -
3lk7xgw97hbb3.js gzip 7.4 kB N/A -
3o37r02i9tn88.js gzip 71 kB N/A -
3t19gbtd8i-0c.js gzip 159 B N/A -
3u1fm7t_ty27_.js gzip 159 B N/A -
3vtsfutwdus1c.js gzip 151 B N/A -
4237vdj6eyslt.js gzip 8.7 kB N/A -
turbopack-04..ts1w.js gzip 3.8 kB N/A -
turbopack-0n..w_i9.js gzip 3.8 kB N/A -
turbopack-0y..tur0.js gzip 3.8 kB N/A -
turbopack-1l..wghi.js gzip 3.82 kB N/A -
turbopack-1z..he5j.js gzip 3.8 kB N/A -
turbopack-20..89v1.js gzip 3.8 kB N/A -
turbopack-2j..kko7.js gzip 3.79 kB N/A -
turbopack-2p..monf.js gzip 3.8 kB N/A -
turbopack-2s..tv4e.js gzip 3.8 kB N/A -
turbopack-2y..s9qo.js gzip 3.8 kB N/A -
turbopack-30..7wrv.js gzip 3.8 kB N/A -
turbopack-3d..sk8_.js gzip 3.8 kB N/A -
turbopack-3f..tcof.js gzip 3.8 kB N/A -
turbopack-3u..xlau.js gzip 3.8 kB N/A -
0_brf1o8xtfgw.js gzip N/A 156 B -
00tc16j5-eos9.js gzip N/A 1.46 kB -
0gaw1gmpwuwkn.js gzip N/A 8.8 kB -
0lfd6n4hnvfus.js gzip N/A 156 B -
0mwo2e6a43qd6.js gzip N/A 154 B -
0oxxm8ra6x40w.js gzip N/A 155 B -
12zyjjmq4oad1.js gzip N/A 5.72 kB -
1b_5lm2rb0cxg.js gzip N/A 65.6 kB -
1k9cc4-gpa2cx.js gzip N/A 13.1 kB -
1kfiwrcr725kl.js gzip N/A 13.2 kB -
1n59uogejo81s.js gzip N/A 10.3 kB -
1npei3hlexdv3.js gzip N/A 9.99 kB -
1q-0qyccjh4q3.js gzip N/A 158 B -
1ssuiwj_rnpc5.js gzip N/A 8.77 kB -
2-phc9obd3nyd.js gzip N/A 8.77 kB -
20z9kb96_57f4.js gzip N/A 164 B -
24utq57r3l1cw.js gzip N/A 8.7 kB -
29tie9lu2une4.js gzip N/A 71 kB -
2aw0kxkhyra0_.js gzip N/A 153 B -
2eemzo7z71a10.js gzip N/A 154 B -
2fhjq6ek3srmr.js gzip N/A 8.69 kB -
2g0t7upz_v-hf.js gzip N/A 10.6 kB -
2ia9qqy99aie9.js gzip N/A 13.6 kB -
2n5fp99a5xt9s.js gzip N/A 7.4 kB -
2oza28kdqvoay.js gzip N/A 8.74 kB -
2s1ku9sx60j5g.js gzip N/A 155 B -
2smk18o7qzte6.js gzip N/A 154 B -
2zh8-mmhujy8o.js gzip N/A 2.29 kB -
30h_m0irj-q6p.js gzip N/A 221 B -
37ejbfx1u9lpo.js gzip N/A 8.75 kB -
38c63ct4i1cy7.js gzip N/A 449 B -
38pfb-vjc58zl.js gzip N/A 160 B -
3jimpal3lvz5e.js gzip N/A 9.44 kB -
3q86bccmeq9ht.js gzip N/A 45.1 kB -
3su6a4l8jbfth.js gzip N/A 3.53 kB -
3vp18tbs9ie7b.js gzip N/A 156 B -
3wb4pe4z4-aim.js gzip N/A 8.77 kB -
43cuc9gzge2dj.js gzip N/A 151 B -
turbopack-0c..ceu6.js gzip N/A 3.76 kB -
turbopack-0r..o21k.js gzip N/A 3.77 kB -
turbopack-0v..6n4f.js gzip N/A 3.78 kB -
turbopack-12..bfke.js gzip N/A 3.77 kB -
turbopack-1a..ll78.js gzip N/A 3.77 kB -
turbopack-1j..lqtq.js gzip N/A 3.77 kB -
turbopack-1v..tzol.js gzip N/A 3.79 kB -
turbopack-1x..lsi3.js gzip N/A 3.78 kB -
turbopack-2i..bzur.js gzip N/A 3.77 kB -
turbopack-3_..wlef.js gzip N/A 3.77 kB -
turbopack-3-..hdvm.js gzip N/A 3.77 kB -
turbopack-3l..eco6.js gzip N/A 3.77 kB -
turbopack-3m..txtb.js gzip N/A 3.77 kB -
turbopack-3w..c3ex.js gzip N/A 3.78 kB -
Total 448 kB 447 kB ✅ -333 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 777 B 749 B 🟢 28 B (-4%)
Total 777 B 749 B ✅ -28 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 434 B 436 B
Total 434 B 436 B ⚠️ +2 B

📦 Webpack

Client

Main Bundles
Canary PR Change
2486.HASH.js gzip 169 B N/A -
3146-HASH.js gzip 64.2 kB N/A -
39fcf99b-HASH.js gzip 62.8 kB N/A -
8443-HASH.js gzip 4.68 kB N/A -
9431-HASH.js gzip 5.62 kB N/A -
framework-HASH.js gzip 59.8 kB 59.8 kB
main-app-HASH.js gzip 255 B 254 B
main-HASH.js gzip 39.7 kB 40.1 kB 🔴 +452 B (+1%)
webpack-HASH.js gzip 1.68 kB 1.68 kB
6105-HASH.js gzip N/A 5.63 kB -
764.HASH.js gzip N/A 169 B -
8898-HASH.js gzip N/A 63.5 kB -
9597-HASH.js gzip N/A 4.65 kB -
e1ccab69-HASH.js gzip N/A 62.9 kB -
Total 239 kB 239 kB ✅ -204 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 194 B
_error-HASH.js gzip 183 B 182 B
css-HASH.js gzip 335 B 335 B
dynamic-HASH.js gzip 1.8 kB 1.8 kB
edge-ssr-HASH.js gzip 255 B 254 B
head-HASH.js gzip 351 B 349 B
hooks-HASH.js gzip 384 B 384 B
image-HASH.js gzip 580 B 580 B
index-HASH.js gzip 259 B 259 B
link-HASH.js gzip 2.49 kB 2.49 kB
routerDirect..HASH.js gzip 319 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 313 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.96 kB 7.95 kB ✅ -7 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 128 kB 128 kB
page.js gzip 285 kB 285 kB
Total 413 kB 413 kB ✅ -394 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 615 B 614 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 45.6 kB 45.4 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 47.2 kB 47 kB ✅ -280 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 719 B 720 B
Total 719 B 720 B ⚠️ +1 B
Build Cache
Canary PR Change
0.pack gzip 4.83 MB 4.81 MB 🟢 19 kB (0%)
index.pack gzip 118 kB 119 kB
index.pack.old gzip 118 kB 118 kB
Total 5.06 MB 5.04 MB ✅ -18.4 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 365 kB 364 kB
app-page-exp..prod.js gzip 202 kB 201 kB
app-page-tur...dev.js gzip 365 kB 363 kB
app-page-tur..prod.js gzip 201 kB 201 kB
app-page-tur...dev.js gzip 362 kB 360 kB
app-page-tur..prod.js gzip 199 kB 199 kB
app-page.run...dev.js gzip 362 kB 360 kB
app-page.run..prod.js gzip 200 kB 199 kB
app-route-ex...dev.js gzip 81.7 kB 81.5 kB
app-route-ex..prod.js gzip 55.6 kB 55.4 kB
app-route-tu...dev.js gzip 81.8 kB 81.5 kB
app-route-tu..prod.js gzip 55.6 kB 55.4 kB
app-route-tu...dev.js gzip 81.4 kB 81.1 kB
app-route-tu..prod.js gzip 55.4 kB 55.1 kB
app-route.ru...dev.js gzip 81.3 kB 81.1 kB
app-route.ru..prod.js gzip 55.3 kB 55.1 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 45.4 kB 45.2 kB
pages-api-tu..prod.js gzip 34.1 kB 33.9 kB
pages-api.ru...dev.js gzip 45.4 kB 45.2 kB
pages-api.ru..prod.js gzip 34.1 kB 33.9 kB
pages-turbo....dev.js gzip 54.9 kB 54.6 kB
pages-turbo...prod.js gzip 39.7 kB 39.6 kB
pages.runtim...dev.js gzip 54.8 kB 54.6 kB
pages.runtim..prod.js gzip 39.7 kB 39.5 kB
server.runti..prod.js gzip 67.8 kB 67.5 kB
use-cache-pr...dev.js gzip 71.6 kB 71.4 kB
use-cache-pr...dev.js gzip 71.6 kB 71.4 kB
use-cache-pr...dev.js gzip 69.9 kB 69.7 kB
use-cache-pr...dev.js gzip 69.9 kB 69.7 kB
Total 3.5 MB 3.49 MB ✅ -13.3 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
  • pages-api-tu..ntime.dev.js
  • pages-api-tu..time.prod.js
  • pages-api.runtime.dev.js
  • pages-api.ru..time.prod.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

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

use-cache-pr..ntime.dev.js

Diff too large to display

use-cache-pr..ntime.dev.js

Diff too large to display

use-cache-pr..ntime.dev.js

Diff too large to display

use-cache-pr..ntime.dev.js

Diff too large to display

📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/37de3fea7982c44b785545d848b2b33e5ef64d28/next

Commit: 37de3fe

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: 37de3fe

@lukesandberg
lukesandberg force-pushed the lukesandberg/gc-01-scope-self-feeding branch from 8e95270 to ed85eb4 Compare July 20, 2026 22:43
@lukesandberg
lukesandberg force-pushed the lukesandberg/gc-02-tombstone-plumbing branch from b80ac91 to 14080b5 Compare July 20, 2026 22:43
@lukesandberg
lukesandberg force-pushed the lukesandberg/gc-01-scope-self-feeding branch from ed85eb4 to f43d126 Compare July 20, 2026 23:17
@lukesandberg
lukesandberg force-pushed the lukesandberg/gc-02-tombstone-plumbing branch 2 times, most recently from 5dbf6f4 to 73dc675 Compare July 21, 2026 16:08
@lukesandberg

Copy link
Copy Markdown
Contributor Author

Reworked per the 'do we still need the delete list?' question — good catch. Dropped the deletes: Vec<TaskDeletion> parameter (and the TaskDeletion.surviving_task_ids field) that a later PR replaced anyway, and instead landed the final delete mechanism directly: SnapshotItem::Delete on the streaming snapshot iterator + inline application in save_snapshot (survivors resolved from disk). SnapshotItem::Delete is #[allow(dead_code)] since nothing emits it until the collection PR. All 92 backend lib tests pass incl. the 3 tombstone tests.

@lukesandberg
lukesandberg force-pushed the lukesandberg/gc-02-tombstone-plumbing branch from 73dc675 to b5ba401 Compare July 21, 2026 16:29
@lukesandberg
lukesandberg force-pushed the lukesandberg/gc-01-scope-self-feeding branch 2 times, most recently from 040c21d to 6d9359b Compare July 21, 2026 16:41
@lukesandberg
lukesandberg force-pushed the lukesandberg/gc-02-tombstone-plumbing branch 2 times, most recently from 011b444 to d903753 Compare July 21, 2026 16:46
@lukesandberg
lukesandberg force-pushed the lukesandberg/gc-01-scope-self-feeding branch from 6d9359b to d3ffbd3 Compare July 21, 2026 16:46
Stage 0 of the mark-and-sweep GC work: the persistence-layer plumbing to
tombstone unreachable tasks, landed independently of the mark/sweep logic
so its semantics can be verified on their own.

- Add `TurboWriteBatch::delete` passthrough to `turbo_persistence`.
- Add a `TaskDeletion` struct and thread a `deletes: Vec<TaskDeletion>` list
  through `save_snapshot`. Tombstones are applied in the same commit as the
  snapshot, after the parallel put phase and before flush.
- `TaskMeta`/`TaskData` (SingleValue) are tombstoned by task-id key. The
  MultiValue `TaskCache` is tombstoned by whole hash bucket and any colliding
  live survivors are re-put in the same batch (collisions are astronomically
  rare; a composite key was rejected because get_multiple is exact-hash and
  there is no prefix-scan primitive).
- The production snapshot call passes an empty delete list for now; GC wiring
  comes in a later stage.

Tests verify the tombstone path across a reopen, and that deleting one entry
from a colliding TaskCache bucket + re-putting the survivor leaves exactly the
survivor — confirming that a delete and a put for the same key compose
correctly in one commit regardless of insertion order (the collector sorts
tombstones last within a key group).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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