Fix dangling stream readers, serialization bugs, and refactor abort reducer code#1647
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🧪 E2E Test ResultsNo test result files found. ❌ Some E2E test jobs failed:
Check the workflow run for details. |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
414942e to
d786ebb
Compare
|
Deployment failed with the following error: View Documentation: https://vercel.com/docs/two-factor-authentication |
Two leak paths the prior fix left uncovered: - External signal aborted after serialization: verifies the listener attached by reduceAbortWithListener actually fires and writes the abort packet once the caller aborts later. - Signal nested inside a Request: exposed a real leak. The Request constructor copies the signal to an internal AbortSignal, so the ABORT_READER_CANCEL symbol set by reviveAbortSignal never reached request.signal, and cancelAbortReaders' walker had no Request case so Object.values(request) returned []. Fixed both sides: - Request reviver copies abort-internal symbols via copyAbortInternals - Walker descends into Request.signal explicitly Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Should probably be reviewed by Pranay and then merged into |
Conflict resolution: - packages/core/src/serialization.ts: kept PR's reviveAbortSignal() function (PR #1647's signal-only revival path) alongside the base branch's getCommonRevivers() function from the modular refactor Co-authored-by: Cursor <cursoragent@cursor.com>
|
I'm resolving conflicts and merging your commits into my base PR from CLI @karthikscale3, ty! |
Description
Follow-up to #1301 (AbortController/AbortSignal serialization). Fixes bugs uncovered while exercising the end-to-end path and cleans up duplicated reducer code.
What changed
Bug fixes:
reviveAbortControllerstarts a stream reader that blocks onreader.read()indefinitely if no abort arrives, keeping the serverless function alive until the platform timeout. Introduced anABORT_READER_CANCELsymbol stored on the controller/signal and anAbortControllerthe reader races against. The newcancelAbortReaders(...args, thisVal, closureVars)helper walks step arguments after the step function returns (success or failure) and aborts every reader, lettingPromise.raceresolve cleanly.Requestreducer previously serialized any signal that was aborted or hadABORT_STREAM_NAMEset, which caused plain native signals (e.g. fromfetchtimeouts or userAbortControllers passed vianew Request(url, { signal })) to inherit stream+hook infrastructure they did not need. The reducer now only serializes signals tagged withABORT_STREAM_NAME, so plain native signals are silently stripped during Request serialization.is_systemmigration — Added the0010_add_is_system.sqlDrizzle migration (workflow_hooks.is_system boolean default false), updated the journal, and plumbedisSystemthrough thehook_createdevent insuspension-handler,world-postgres, andworld-localso system hooks (currently only the abort hook) persist correctly and do not collide with user hook tokens.getAbortStreamIdFromTokenhelper — Replaced the fragilequeueItem.token.replace('abrt_', '')+ manualstrm_${id}_system_abortconcat insuspension-handlerwith a shared helper inutil.tsthat validates the token prefix and reusesgetAbortStreamId. Eliminates a latent bug where a token without theabrt_prefix would silently produce a garbage stream name.DurableAgenttimeout path in workflow VM — PR feat: serializable AbortController/AbortSignal #1301 addedAbortControllerto the workflow VM globals, which flipped the existingtypeof AbortController !== 'undefined'guard inDurableAgent.generate/.streamtotrueinside workflows. That caused the timeout block to callsetTimeout, which the VM traps, crashing anyDurableAgentcall from a workflow with atimeoutoption. Added aninWorkflowVmcheck based on the presence ofSymbol.for('WORKFLOW_CONTEXT')onglobalThis(set by the workflow runtime before user code runs). When true, the timeout block is skipped, restoring the pre-feat: serializable AbortController/AbortSignal #1301 behavior (timeouts silently do not fire in workflows; usesleep+abort()for durable time bounds).onabortsetter onWorkflowAbortSignal— The VM-side signal only exposed listener-based abort hooks. Added anonabortgetter/setter that matches the nativeAbortSignalcontract, fires the handler immediately when assigned if the signal is already aborted, and is invoked by_setAbortedalongside the listener list.Refactors:
reduceAbortWithListener()(symbol mint + listener attach + serialize) andreduceAbortBySymbol()(read existing symbols + serialize) helpers from the three near-identicalAbortController/AbortSignalreducer implementations ingetExternalReducers,getWorkflowReducers, andgetStepReducers. IntroducedAbortInternals,AbortSignalLike, andAbortHoldertypes so the shared helpers work for both controllers and standalone signals.setupAbortStreamReader+tagAbortPairhelpers — Extracted the stream-reader wiring and symbol-stamping into focused helpers used by bothreviveAbortControllerand the newreviveAbortSignal.reviveAbortSignal()used bygetStepRevivers.AbortSignalandgetExternalRevivers.AbortSignal. Previously those revived throughreviveAbortController(...).signal, which installed the patchedabort()method even though nothing could reach the controller. The dedicated path skips the patch overhead and, crucially, still storesABORT_READER_CANCELon the returned signal socancelAbortReaderscan clean up standalone-signal readers (fixing a latent leak).Tests:
getWorldmock that delivers an actual abort payload (the default mock closes the stream immediately and would mask the code path).Request.signalis stripped during serialization — the hydrated Request gets a fresh default signal.request.signalwithABORT_STREAM_NAME/ABORT_HOOK_TOKEN(the Request constructor clones the signal, so symbols from the source controller do not transfer automatically).step-handler.test.tsmock to includecancelAbortReaders.