') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [world-local] Fix resumeHook racing hook disposal being journaled after hook_disposed by VaguelySerious · Pull Request #2808 · vercel/workflow · GitHub
Skip to content

[world-local] Fix resumeHook racing hook disposal being journaled after hook_disposed - #2808

Merged
VaguelySerious merged 2 commits into
mainfrom
peter/issue-2781
Jul 7, 2026
Merged

[world-local] Fix resumeHook racing hook disposal being journaled after hook_disposed#2808
VaguelySerious merged 2 commits into
mainfrom
peter/issue-2781

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 7, 2026

Copy link
Copy Markdown
Member

Closes#2781. Also addresses the post-merge feedback on #2779 (guarded claim release).

1. hook_received journaled after hook_disposed (#2781)

A hook_received write only validated that the hook entity exists, and was not serialized against the hook_disposed write for the same hook. A resume that passed its existence check before the disposal began could be journaled afterhook_disposed — once that order is in the log, every replay of the owning run diverges at the post-disposal hook_received and the run escalates to CorruptedEventLogError.

  • hook_received and hook_disposed now share the per-hook in-process lock previously taken only by hook_created, making the resume's "validate, then append" atomic with the disposer's "dispose lock → entity delete → append" within a storage instance.
  • The hook_received write re-validates the durable dispose lock from [core] Fix hook token reuse after dispose() (same-run and cross-run) #2779 (at acceptance, and once more immediately before the event append, narrowing the cross-instance window to the single event write), rejecting with HookNotFoundError — the same surface as a resume that arrives after teardown finished. Acceptance now observes the same order replay will see.

2. Guarded token claim release (#2779 follow-up)

Both in-flight releasers (the hook_disposed handler and the terminal-run deleteAllHooksForRun cleanup) deleted the token claim file unconditionally after reading the hook entity. A releaser stalled between those operations could outlive a claimant force-releasing its stale claim, and its deferred delete would then destroy the new claimant's live claim — transiently breaking token uniqueness. Releasers now re-read the claim and delete it only if it still points at their own (runId, hookId); a claim owned by someone else is left for the claimant-side force-release path to reap.

Tests

  • A deterministic mid-teardown acceptance test (dispose lock committed, hook entity still present) that fails without fix 1.
  • A resume-vs-dispose race loop asserting hook_received is never journaled after hook_disposed and that a rejected resume leaves no event behind.
  • Two claim-takeover tests (one per releaser) that rewrite the claim to a foreign (runId, hookId) and assert the release skips it; both fail without fix 2.

🤖 Generated with Claude Code

A hook_received write only validated that the hook entity exists, and was
not serialized against the hook_disposed write for the same hook. A resume
that passed its existence check before the disposal began could therefore
be journaled after hook_disposed — an order that makes every subsequent
replay of the owning run diverge at that event and escalate to
CorruptedEventLogError.
hook_received and hook_disposed now share the per-hook in-process lock
with hook_created, and the hook_received write re-validates the durable
dispose lock (both at acceptance and immediately before the event append),
rejecting with HookNotFoundError exactly like a resume that arrived after
teardown finished.
Closes#2781
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0e77b7d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
workflowPatch
@workflow/web-sharedPatch
@workflow/webPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.048s (+2.4%)1.006s (~)0.959s101.00x
💻 LocalExpress0.051s (+13.9% 🔺)1.007s (~)0.956s101.07x
💻 LocalNext.js (Turbopack)0.053s (~)1.006s (~)0.953s101.12x
🐘 PostgresNitro0.065s (~)1.012s (~)0.947s101.38x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.229s (+21.7% 🔺)1.610s (-20.7% 🟢)1.381s101.00x
▲ VercelExpress0.301s (+33.7% 🔺)2.060s (+0.7%)1.759s101.31x
▲ VercelNext.js (Turbopack)0.492s (+102.2% 🔺)2.065s (-13.2% 🟢)1.573s102.15x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.086s (~)2.006s (~)0.920s101.00x
💻 LocalNitro1.087s (+0.8%)2.007s (~)0.920s101.00x
💻 LocalExpress1.092s (+0.6%)2.007s (~)0.915s101.01x
🐘 PostgresNitro1.101s (~)2.010s (~)0.909s101.01x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.534s (+8.2% 🔺)3.089s (+9.4% 🔺)1.555s101.00x
▲ VercelNitro1.580s (+16.1% 🔺)3.425s (+13.5% 🔺)1.845s101.03x
▲ VercelNext.js (Turbopack)2.112s (-10.0% 🟢)3.862s (+7.6% 🔺)1.751s101.38x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.475s (~)11.022s (~)0.547s31.00x
💻 LocalNext.js (Turbopack)10.499s (~)11.022s (~)0.523s31.00x
🐘 PostgresNitro10.500s (~)11.019s (~)0.519s31.00x
💻 LocalExpress10.502s (~)11.025s (~)0.523s31.00x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.899s (+1.8%)13.762s (+8.4% 🔺)1.862s31.00x
▲ VercelExpress11.936s (+2.2%)13.986s (~)2.050s31.00x
▲ VercelNext.js (Turbopack)12.459s (-3.6%)14.330s (+1.1%)1.871s31.05x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)13.648s (~)14.027s (~)0.379s51.00x
💻 LocalNitro13.678s (~)14.029s (~)0.351s51.00x
💻 LocalExpress13.738s (~)14.030s (~)0.292s51.01x
🐘 PostgresNitro13.792s (+0.7%)14.019s (~)0.227s51.01x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.897s (+1.4%)18.793s (-0.8%)1.896s41.00x
▲ VercelNitro16.939s (+4.7%)18.793s (+7.8% 🔺)1.854s41.00x
▲ VercelNext.js (Turbopack)17.489s (-2.6%)19.473s (-1.5%)1.984s41.04x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.215s (-1.0%)13.018s (~)0.804s71.00x
💻 LocalNext.js (Turbopack)12.397s (+1.3%)13.025s (~)0.628s71.01x
💻 LocalExpress12.434s (+1.7%)13.028s (~)0.594s71.02x
💻 LocalNitro12.522s (+2.4%)13.027s (~)0.505s71.03x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.927s (+9.4% 🔺)21.202s (+10.5% 🔺)2.276s51.00x
▲ VercelNitro19.350s (+13.3% 🔺)21.559s (+14.8% 🔺)2.208s51.02x
▲ VercelNext.js (Turbopack)20.136s (+2.2%)21.846s (+0.8%)1.710s51.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.198s (+1.0%)2.008s (~)0.810s151.00x
💻 LocalNext.js (Turbopack)1.399s (-0.5%)2.007s (~)0.607s151.17x
💻 LocalNitro1.419s (+0.8%)2.007s (~)0.588s151.18x
💻 LocalExpress1.456s (+0.9%)2.008s (~)0.551s151.22x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.002s (-12.5% 🟢)3.804s (+5.6% 🔺)1.802s81.00x
▲ VercelExpress2.214s (-2.6%)3.937s (+0.9%)1.723s81.11x
▲ VercelNext.js (Turbopack)3.442s (+17.2% 🔺)5.213s (+12.3% 🔺)1.770s61.72x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.350s (-3.6%)2.316s (-6.3% 🟢)0.966s131.00x
💻 LocalNext.js (Turbopack)2.382s (-4.3%)3.009s (-3.2%)0.627s101.77x
💻 LocalExpress2.476s (-6.8% 🟢)3.009s (-3.2%)0.534s101.83x
💻 LocalNitro2.505s (+8.0% 🔺)3.009s (~)0.504s101.86x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.251s (-11.0% 🟢)3.841s (+7.9% 🔺)1.590s81.00x
▲ VercelExpress2.509s (-3.7%)4.085s (-0.8%)1.575s81.11x
▲ VercelNext.js (Turbopack)3.940s (+18.3% 🔺)5.388s (+9.8% 🔺)1.448s61.75x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.581s (-2.9%)4.011s (-3.1%)2.430s81.00x
💻 LocalNext.js (Turbopack)3.475s (-26.1% 🟢)4.726s (-14.3% 🟢)1.251s72.20x
💻 LocalNitro4.530s (+5.5% 🔺)5.347s (+9.8% 🔺)0.817s62.87x
💻 LocalExpress4.867s (+10.3% 🔺)5.513s (+6.4% 🔺)0.646s63.08x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.283s (+12.4% 🔺)5.100s (+16.1% 🔺)1.817s61.00x
▲ VercelNitro3.397s (+20.0% 🔺)5.091s (+12.9% 🔺)1.695s61.03x
▲ VercelNext.js (Turbopack)5.924s (+49.9% 🔺)7.534s (+30.5% 🔺)1.610s41.80x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.204s (+0.6%)2.007s (~)0.803s151.00x
💻 LocalNitro1.474s (+4.2%)2.007s (~)0.533s151.22x
💻 LocalExpress1.485s (+2.2%)2.008s (~)0.523s151.23x
💻 LocalNext.js (Turbopack)1.506s (~)2.073s (+3.3%)0.567s151.25x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.964s (+3.4%)3.881s (+16.0% 🔺)1.917s81.00x
▲ VercelExpress2.104s (+8.9% 🔺)3.900s (+6.7% 🔺)1.796s81.07x
▲ VercelNext.js (Turbopack)3.835s (+29.0% 🔺)5.753s (+22.3% 🔺)1.918s61.95x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.315s (~)2.151s (+7.1% 🔺)0.836s141.00x
💻 LocalNitro2.482s (+1.6%)3.009s (~)0.527s101.89x
💻 LocalNext.js (Turbopack)2.552s (-9.2% 🟢)3.109s (-7.0% 🟢)0.557s101.94x
💻 LocalExpress2.559s (+2.8%)3.009s (~)0.450s101.95x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.247s (-27.5% 🟢)3.897s (-14.9% 🟢)1.649s81.00x
▲ VercelNitro2.286s (+4.1%)3.880s (+10.7% 🔺)1.594s81.02x
▲ VercelNext.js (Turbopack)4.096s (+29.8% 🔺)5.808s (+28.0% 🔺)1.712s61.82x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.605s (-1.8%)4.011s (-6.7% 🟢)2.406s81.00x
💻 LocalNext.js (Turbopack)5.152s (-4.7%)5.847s (-2.8%)0.695s63.21x
💻 LocalExpress5.311s (+48.9% 🔺)6.017s (+20.0% 🔺)0.706s53.31x
💻 LocalNitro5.818s (+5.4% 🔺)6.617s (+10.0% 🔺)0.799s53.63x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.841s (+6.7% 🔺)4.765s (+7.4% 🔺)1.924s71.00x
▲ VercelNitro2.846s (+11.7% 🔺)4.503s (+18.3% 🔺)1.658s71.00x
▲ VercelNext.js (Turbopack)4.248s (-5.8% 🟢)6.187s (+0.7%)1.939s51.50x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.573s (-4.8%)1.023s (~)0.450s591.00x
💻 LocalNitro0.597s (+2.1%)1.005s (~)0.408s601.04x
💻 LocalExpress0.600s (-0.9%)1.006s (~)0.406s601.05x
💻 LocalNext.js (Turbopack)0.632s (+5.5% 🔺)1.005s (~)0.373s601.10x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.674s (+2.6%)4.342s (+8.2% 🔺)1.668s151.00x
▲ VercelExpress2.708s (+8.8% 🔺)4.187s (+5.5% 🔺)1.479s151.01x
▲ VercelNext.js (Turbopack)4.472s (+10.3% 🔺)6.433s (+13.7% 🔺)1.961s101.67x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.338s (-4.0%)2.052s (+1.1%)0.714s441.00x
💻 LocalExpress1.518s (+1.7%)2.007s (~)0.489s451.14x
💻 LocalNitro1.540s (+6.4% 🔺)2.029s (+1.2%)0.489s451.15x
💻 LocalNext.js (Turbopack)1.591s (+1.1%)2.006s (~)0.415s451.19x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.115s (+4.7%)7.830s (+6.1% 🔺)1.715s121.00x
▲ VercelExpress6.153s (+5.7% 🔺)7.857s (+5.0% 🔺)1.704s121.01x
▲ VercelNext.js (Turbopack)8.030s (-2.7%)9.778s (+2.0%)1.747s101.31x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.699s (-5.2% 🟢)3.136s (-2.6%)0.437s391.00x
💻 LocalExpress3.294s (-1.2%)4.010s (~)0.717s301.22x
💻 LocalNitro3.337s (+5.0% 🔺)4.043s (+2.5%)0.706s301.24x
💻 LocalNext.js (Turbopack)3.472s (+1.2%)4.042s (+0.8%)0.571s301.29x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.636s (+1.3%)13.602s (+5.2% 🔺)1.965s91.00x
▲ VercelExpress12.147s (+3.9%)14.154s (+2.3%)2.007s91.04x
▲ VercelNext.js (Turbopack)17.210s (~)19.394s (+3.7%)2.183s71.48x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.252s (+14.0% 🔺)1.006s (~)0.754s601.00x
💻 LocalNitro0.541s (+5.6% 🔺)1.005s (~)0.464s602.15x
💻 LocalExpress0.626s (+21.2% 🔺)1.006s (~)0.380s602.48x
💻 LocalNext.js (Turbopack)0.649s (+1.0%)1.039s (+1.7%)0.391s582.57x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.130s (+3.1%)2.788s (+16.1% 🔺)1.658s221.00x
▲ VercelExpress1.136s (+0.9%)2.930s (+11.5% 🔺)1.794s211.01x
▲ VercelNext.js (Turbopack)2.414s (+23.3% 🔺)3.965s (+18.1% 🔺)1.551s162.14x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.354s (+1.2%)1.006s (~)0.652s901.00x
💻 LocalNitro2.451s (-3.0%)3.009s (~)0.559s306.92x
💻 LocalNext.js (Turbopack)2.527s (-8.0% 🟢)3.009s (-4.4%)0.482s307.13x
💻 LocalExpress2.651s (+4.8%)3.076s (+1.1%)0.425s307.48x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.420s (~)3.131s (+2.6%)1.712s291.00x
▲ VercelNitro1.431s (+8.1% 🔺)3.033s (+21.8% 🔺)1.603s301.01x
▲ VercelNext.js (Turbopack)3.245s (+25.3% 🔺)5.133s (+26.4% 🔺)1.888s182.29x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.590s (+2.9%)1.150s (+6.7% 🔺)0.560s1051.00x
💻 LocalNitro5.771s (-1.4%)8.090s (-6.9% 🟢)2.319s159.78x
💻 LocalExpress5.934s (-0.8%)8.895s (+4.8%)2.960s1510.06x
💻 LocalNext.js (Turbopack)6.015s (-6.5% 🟢)8.814s (-4.6%)2.799s1410.20x
🐘 PostgresExpress⚠️missing----
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.866s (+12.0% 🔺)3.747s (+5.9% 🔺)1.881s331.00x
▲ VercelExpress2.023s (+26.0% 🔺)4.059s (+11.6% 🔺)2.036s301.08x
▲ VercelNext.js (Turbopack)4.339s (+3.3%)6.244s (+9.0% 🔺)1.905s202.33x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.144s (~)1.966s (~)0.013s (+24.3% 🔺)2.020s (~)0.876s101.00x
💻 LocalExpress1.159s (~)2.004s (~)0.011s (-5.3% 🟢)2.019s (~)0.860s101.01x
🐘 PostgresNitro1.161s (~)1.997s (~)0.001s (~)2.011s (~)0.849s101.02x
💻 LocalNitro1.174s (+1.6%)2.004s (~)0.013s (+24.8% 🔺)2.020s (~)0.846s101.03x
🐘 PostgresExpress⚠️missing-----
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.091s (+9.7% 🔺)3.755s (+26.8% 🔺)5.370s (+192.0% 🔺)9.677s (+85.4% 🔺)7.586s101.00x
▲ VercelExpress2.166s (+14.6% 🔺)3.730s (+15.2% 🔺)5.318s (+224.7% 🔺)9.710s (+81.6% 🔺)7.544s101.04x
▲ VercelNext.js (Turbopack)3.863s (+21.2% 🔺)4.858s (+38.9% 🔺)4.790s (+228.9% 🔺)10.937s (+69.9% 🔺)7.074s101.85x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.584s (~)2.010s (~)0.013s (+7.6% 🔺)2.027s (~)0.443s301.00x
💻 LocalNitro1.589s (~)2.010s (-1.5%)0.013s (-3.7%)2.027s (-1.6%)0.437s301.00x
💻 LocalNext.js (Turbopack)1.600s (~)1.969s (~)0.012s (-6.0% 🟢)2.025s (~)0.424s301.01x
🐘 PostgresNitro1.612s (-1.1%)2.005s (~)0.005s (-5.8% 🟢)2.027s (~)0.414s301.02x
🐘 PostgresExpress⚠️missing-----
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.645s (-0.8%)7.596s (+15.2% 🔺)0.095s (-59.3% 🟢)8.202s (+10.7% 🔺)2.557s81.00x
▲ VercelExpress5.670s (+4.4%)7.729s (+9.6% 🔺)0.190s (-14.9% 🟢)8.422s (+8.2% 🔺)2.752s81.00x
▲ VercelNext.js (Turbopack)10.213s (+7.4% 🔺)12.132s (+15.1% 🔺)0.273s (+24.7% 🔺)13.147s (+13.8% 🔺)2.934s51.81x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.772s (-8.9% 🟢)1.060s (-5.5% 🟢)0.000s (+89.3% 🔺)1.085s (-5.0%)0.313s561.00x
💻 LocalNext.js (Turbopack)1.258s (-10.1% 🟢)1.976s (~)0.000s (-47.4% 🟢)2.016s (~)0.757s301.63x
💻 LocalExpress1.352s (~)1.981s (+3.2%)0.000s (+3.2%)1.984s (+3.3%)0.633s311.75x
💻 LocalNitro1.357s (+0.6%)1.982s (+1.7%)0.000s (+83.3% 🔺)1.985s (+1.7%)0.628s311.76x
🐘 PostgresExpress⚠️missing-----
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.467s (-15.6% 🟢)5.158s (-1.8%)0.000s (+Infinity% 🔺)5.652s (-1.2%)2.185s111.00x
▲ VercelExpress3.620s (-30.7% 🟢)5.306s (-19.8% 🟢)0.001s (+227.3% 🔺)5.790s (-18.9% 🟢)2.170s111.04x
▲ VercelNext.js (Turbopack)5.452s (+21.2% 🔺)6.463s (+19.6% 🔺)0.000s (NaN%)7.496s (+21.4% 🔺)2.044s91.57x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.646s (-7.6% 🟢)2.252s (-6.9% 🟢)0.000s (NaN%)2.281s (-6.2% 🟢)0.635s271.00x
💻 LocalNitro3.285s (+1.3%)3.903s (~)0.001s (+30.0% 🔺)3.907s (~)0.622s162.00x
💻 LocalNext.js (Turbopack)3.385s (-2.1%)3.925s (-1.6%)0.000s (-61.4% 🟢)3.967s (-1.6%)0.583s162.06x
💻 LocalExpress3.579s (+7.7% 🔺)3.966s (+1.6%)0.001s (+300.0% 🔺)3.971s (+1.6%)0.392s162.17x
🐘 PostgresExpress⚠️missing-----
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.489s (~)6.276s (+10.2% 🔺)0.000s (NaN%)6.772s (+9.9% 🔺)2.283s91.00x
▲ VercelNitro4.561s (+4.9%)6.272s (+19.4% 🔺)0.000s (NaN%)6.726s (+18.7% 🔺)2.165s91.02x
▲ VercelNext.js (Turbopack)8.022s (+9.0% 🔺)9.130s (+17.0% 🔺)0.000s (+Infinity% 🔺)10.233s (+14.4% 🔺)2.211s61.79x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresNitro21/21
▲ VercelNitro13/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local16/21
Next.js (Turbopack)💻 Local19/21
Nitro🐘 Postgres16/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 7, 2026 21:51
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 7, 2026 21:51
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed and verified the fixes hands-on. Approving.

What I checked:

  • Fix 1 mechanism: the per-hook in-process lock now wraps the entirecreateImpl() for all three hook lifecycle events, so the resume's "validate → append" is atomic with the disposer's "dispose lock → entity delete → append" within an instance; the lock key (${runId}-${correlationId}.hook) matches on both sides. The disposer's whole sequence runs inside createImpl() with no nested events.create — no self-deadlock. Cross-instance, the durable dispose-lock re-check at acceptance + immediately before the append narrows the window to the single event write, as documented.
  • Fix 2: the ownership match on (runId, hookId) is safe for normal releases — the claim writer has always persisted both fields — and a skipped release is reaped by the claimant-side force-release loop, so tokens still free up.
  • Counterfactual: reverted events-storage.ts/hooks-storage.ts/helpers.ts to main while keeping the new tests — the mid-teardown acceptance test and both claim-takeover tests fail exactly as claimed, and pass again on the branch.
  • Suites: full @workflow/world-local 438/438 locally; CI fully green (103 checks, incl. the usually-flaky webpack lane).

Non-blocking observations:

  1. The 20-round race-loop test passed even with the fix reverted on my machine — it's a soak, not a reliable regression guard; the deterministic mid-teardown test is the real one.
  2. Corrupt (unparseable) claim files are no longer deleted by any path: the releaser now skips them (correctly — ownership is undeterminable), and the claimant loop's force-delete is only reachable with a parsed claim (readHookTokenClaim → null → continue), so an unreadable claim can block its token indefinitely. Very low likelihood; a "unparseable after N observations → delete" fallback in the claimant loop would close it if ever seen in practice.
  3. Nit: hook_created still builds the claim path inline (~L1631) instead of using the new hookTokenClaimPath() helper — worth unifying so the layout can't drift.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed both parts against the #2779 machinery. Approving.

Part 1 (hook_received vs hook_disposed, #2781): The two-layer approach is right: the per-hook in-process lock makes resume-vs-dispose atomic within a storage instance, and the durable dispose-lock re-validation — at acceptance and immediately before the event append — narrows the cross-instance window to the single event write. The comments are honest that this is a narrowing rather than full elimination; that matches the module's existing convention (on-disk state as source of truth, in-process locks as fast path), and going further would require holding an fs-level mutex across validate+append, which isn't worth it for this world. Rejecting with HookNotFoundError gives resumes that lose the race the same surface as resumes arriving after teardown — correct and unobservable to callers as a new state.

Part 2 (guarded claim release): Good catch — this closes a gap in the #2779 force-release design I didn't flag in my review: a releaser stalled between its entity read and claim delete could destroy the next claimant's live claim. The guarded release direction is right in both failure modes: a claim that's missing/unreadable/foreign is left alone (never delete what you can't verify), and genuinely stale debris still gets reaped by the claimant-side force-release. Both releasers (hook_disposed handler and deleteAllHooksForRun) are covered. The remaining read→unlink window is honestly documented and only reachable via a second takeover within adjacent fs ops — proportionate for this world.

I also checked the hookTokenClaimPath helper against the hook_created claim-write site: both use path.join with identical segments, so there's no absolute/relative path-construction mismatch (a hazard this module has hit before).

Verified locally: full world-local suite passes including the 4 new tests (both releaser-guard takeover tests, the mid-teardown acceptance rejection, and the resume-vs-dispose race loop asserting the journal-order invariant). CI fully green.

One non-blocking nit: the hook_created claim site still builds its constraintPath inline — now that hookTokenClaimPath exists, unifying on the helper would remove the last duplicated construction of that path.

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for c1d29f1 (AI decision).

Both fixes in this commit build directly on the #2779 durable dispose-lock and force-release/claim infrastructure that exists only on main: fix #1 re-validates via isHookDisposalCommitted/hookDisposeLockPath and fix #2 guards against the isHookTokenClaimReleasable force-release path — none of which exist on origin/stable (verified via git grep). The regression tests likewise depend on hookDisposeLockPath and the claim-takeover semantics absent on stable, so the change cannot apply cleanly or meaningfully there.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

c1d29f14ca01e2219c5ccbaa4e9f62f9349dd75e

VaguelySerious added a commit that referenced this pull request Jul 8, 2026
Address #2808 review:
- The claimant loop now force-deletes a claim file that persists but never
parses (after a few observations) so a corrupt/orphan claim can't block its
token forever — the releaser correctly leaves such files alone, so nothing
else reaped them. A live hook's claim is still rebuilt from the event log, so
reaping a corrupt claim can't steal a token from a live hook.
- hook_created now builds the claim path via hookTokenClaimPath() instead of
inline, so the layout can't drift.
- Drop the 20-round resume-vs-disposal soak test: both orderings are valid so
it passed even with the fix reverted (a soak, not a regression guard). The
deterministic mid-teardown test remains the real guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

resumeHook racing hook disposal is journaled after hook_disposed; the owner's replay then diverges into CorruptedEventLogError

3 participants

@VaguelySerious@TooTallNate@karthikscale3