-
Notifications
You must be signed in to change notification settings - Fork 156
feat(workflow): Automatically cleanup AsyncLocalStorage on workflow context disposal #1871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(workflow): Automatically cleanup AsyncLocalStorage on workflow context disposal #1871
Conversation
chris-olszewski
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall makes sense to me.
Just a few small questions.
| const wf3 = await startWorkflow(asyncLocalStorageWorkflow); | ||
|
|
||
| await worker.runUntil(Promise.all([wf1.result(), wf2.result(), wf3.result()])); | ||
| t.pass(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any additional assertions that would be valuable here aside from "it doesn't throw"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I played around with the possibility of observing the destruction of ALS by escaping the sandbox to create a FinalizationRegistry, which worked. The problem, though, is that I couldn't get that test to fail with the previous code, so that's not proving anything.
With that in mind, and given the complexity and fragility of that test, I decided not to commit it.
And I really can't think of any other side effect to look for.
| disable(): void { | ||
| super.disable(); | ||
| } | ||
|
|
||
| __temporal_disableForReal(): void { | ||
| super.disable(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If both of these actually disable the ALS, do we need both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was hesitant about whether we want the "normal" disable() function to work properly, or if it should be a noop. I'm now leading for the former, so I'll delete the __temporal one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 on having it function properly
Co-authored-by: Chris Olszewski <[email protected]>
|
Looking into how the lint violation got through CI and why it isn't showing up locally, but fix is in #1875 |
Ah, yeah, I was also looking at that just now. Let's merge your dep upgrade pr first. |
What was changed and why
Track instances of
AsyncLocalStoragecreated inside the workflow sandbox and automatically clean them up on workflow context disposal, after execution of thedisposehook on internal interceptors.AsyncLocalStoragewas difficult due to the necessity of cleaning them up from an internal interceptor'sdisposehook (fixes [Feature Request] Simplify proper usage ofAsyncLocalStoragein Workflow context #1432).The
AsyncLocalStorageinstances created by the SDK to track cancellation and update scopes are now automatically cleaned up at the sandbox vm destruction time.PR 1834 added a
workflowModule.dispose()call during cleanup of the reusable VM workflow, but that resulted in a regression in 1.14.0 where a workflow might end up getting unexpectedly cancelled if evicted from cache while blocked on acondition(fn, timeout)(see investigation details below). Fixes [Bug] Signal causedconditionto fail withCancelledFailureon1.14.0#1866.conditionwith timeout creates aCancellationScopeto perform the race between the sleep and the blocking condition srcCancellationScopeis canceledCancellationScopeis stored in aAsyncLocalStoragewhich falls back to the workflow cancellation scope if not in a context where there is an active store. srcworkflowModule.dispose()call to thedisposemethod for reusable VM workflowAsyncLocalStoragethat stores the cancellation tokens (src). Any subsequentgetStorecalls that happen before a newrunwill returnundefined.conditionscope is created, but before the scope is cancelled, then the workflow ends up get cancelled instead of theconditionscope.CancelledFailure: Workflow cancelled, but coming from the cancel inside of theconditionscope on this lineProperly
disposea workflow execution context on early failure (e.g. if requested workflow type doesn't resolve to an exported workflow function).AsyncLocalStorage) had been allocated during early initialization. We now do ensure thatdisposeis properly called in that scenario.Checklist
AsyncLocalStoragelifecycle can't really be asserted through automated testing. We temporarily addedconsole.logcalls inside our mocked ALS class to confirm thatdispose()is called at the correct moment in various scenarios.AsyncLocalStoragein workflow context.