Skip to content

Conversation

@maciejdudko
Copy link
Contributor

What was changed

Changed how resources are disposed inside TemporalWorker. If ExecuteAsync is called, then the Dispose method does nothing and instead the resources are disposed at the end of ExecuteAsync. If it's not called, then resources are disposed with Dispose call as normal.

Why?

Fixes crash that happens if TemporalWorker is disposed too early.

Checklist

  1. Closes [Bug] Segfault/crash occurs when using tasks without await #500

  2. How was this tested:
    Added testcase WorkflowWorkerTests.ExecuteWorkflowAsync_PrematureDispose_WorkflowCompletes

@maciejdudko maciejdudko requested a review from a team as a code owner August 5, 2025 23:09
Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

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

Looks good, nothing blocking (comments are mostly unimportant)

protected virtual void Dispose(bool disposing)
{
if (disposing)
var disposer = Interlocked.Exchange(ref this.disposer, null);
Copy link
Member

Choose a reason for hiding this comment

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

Should we not do this unless disposing is true? I guess it doesn't matter much, but it is a bit confusing to read

// Take ownership of the disposer to make sure the workers aren't disposed before completion of this function
using var disposer = Interlocked.Exchange(ref this.disposer, null);
Copy link
Member

Choose a reason for hiding this comment

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

I believe I have confirmed that even in this situation:

public Task StartWorkerAsync()
{
    using var worker = new TemporalWorker(someOptions);
    // Note this does not take the disposer because code is not run
    // inside of this because it is "async" so it waits for first "await"
    return worker.ExecuteAsync(someToken);
    // worker is disposed here
}

var task = StartWorkerAsync();
await task;

The disposer is properly transfered before the ExecuteAsync call is returned. But yes it will be important for future SDK devs to make sure no yieldable await points are ever added before this line is called. Technically we can help with this by doing this in the outer ExecuteAsync calls and removing the async keyword from them, but I am fine with this too.

return await worker.ExecuteAsync(() => action(worker));
}

private static TemporalWorkerOptions PrepareOptions<TWorkflow>(TemporalWorkerOptions options)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private static TemporalWorkerOptions PrepareOptions<TWorkflow>(TemporalWorkerOptions options)
private static TemporalWorkerOptions PrepareWorkerOptions<TWorkflow>(TemporalWorkerOptions options)

Pedantic, don't have to change

@maciejdudko maciejdudko force-pushed the temporal-worker-dispose-crash branch from da013d2 to bf2f78c Compare August 6, 2025 20:55
@maciejdudko maciejdudko merged commit b55175e into temporalio:main Aug 7, 2025
10 checks passed
@maciejdudko maciejdudko deleted the temporal-worker-dispose-crash branch August 7, 2025 13:34
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.

[Bug] Segfault/crash occurs when using tasks without await

2 participants