Skip to content

TeardownAsync and Host.DisposeAsync race #18

@kvpt

Description

@kvpt

Hi,

I also encounter the issue mentioned previously in #16 when using InitAndRunAsync method.

I confirm that under certain conditions there can be a race between TeardownAsync and DisposeAsync on the Host, the parallel stacks bellow illustrate this fact.
Capture d'écran 2024-04-03 231550

The InitAndRunAsync method call WaitForShutdownAsync to ensure that the Teardown happen after the shutdown.
But there is no guarantee that the host has not been disposed between the WaitForShutdownAsync ending and the call to TeardownAsync method.

The StopAsync method of the Host call the StopAsync method of each registered HostedService and the application Lifecycle.
This ensure that each HostedService and Lifecycle has stopped correctly when StopAsync call end.
But as this library doesn't use HostedService mecanism it doesn't prevent the call to dispose before the Teardown has executed.

For me it doesn't happen in conventional environment but 95% of the time when I run my integration tests (that use Xunit and Alba) in debug mode.

I tried to reproduce the case in the unit tests of the solution.
I not have found a proper way to trigger the problem without adding a hard coded delay to ensure that the dispose always win the race against WaitForShutdownAsync.

To reproduce the issue :

Adding a delay before the teardown inside the InitAndRunAsync method.

...

finally
{
    cts.CancelAfter(teardownTimeout);

    await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);

    await host.TeardownAsync(cts.Token).WaitAsync(cts.Token).ConfigureAwait(false);
}

Run this test:

 [Fact]
 public async Task Teardown_Run_Before_Host_Shutdown()
 {
     var initializer = A.Fake<IAsyncTeardown>();

     var host = CreateHost(services =>
     {
         services.AddAsyncInitializer(initializer);
     });

     IHostApplicationLifetime lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();

     TaskCompletionSource appStartedTcs = new TaskCompletionSource();

     lifetime.ApplicationStarted.Register(appStartedTcs.SetResult);

     _ = host.InitAndRunAsync();

     await appStartedTcs.Task;

     await host.StopAsync();

     host.Dispose();

     await Task.Delay(TimeSpan.FromSeconds(2));

     A.CallTo(() => initializer.InitializeAsync(A<CancellationToken>._)).MustHaveHappenedOnceExactly();
     A.CallTo(() => initializer.TeardownAsync(A<CancellationToken>._)).MustHaveHappenedOnceExactly();

     Assert.Throws<ObjectDisposedException>(host.Services.CreateScope);
 }

Implementing a HostedService to call TeardownAsync would likely solve the issue but I'm not sure if it a practical solution here because it will prevent the ability to call TeardownAsync when wanted.

I also wonder if it is possible to prevent calling the teardown method when after the init is done we notice that there is no AsyncInitializer that implement IAsyncTeardown.
In my case and in #16 also, we don't implement IAsyncTeardown at all.

For now my workaround is to fallback to the old way as I don't use teardown.

await host.InitAsync();
await host.RunAsync();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions