diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e58183bf..ec04bb33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ### Fixed - 525 Read ServerURL, Username and Secret field from CredsStore response to pull private Docker images +- 595 Implement `TestcontainersContainer.DisposeAsync` thread safe ## [2.1.0] diff --git a/src/Testcontainers/Containers/TestcontainersContainer.cs b/src/Testcontainers/Containers/TestcontainersContainer.cs index 8fdcb4ca8..c166ba951 100644 --- a/src/Testcontainers/Containers/TestcontainersContainer.cs +++ b/src/Testcontainers/Containers/TestcontainersContainer.cs @@ -18,7 +18,7 @@ namespace DotNet.Testcontainers.Containers /// public class TestcontainersContainer : ITestcontainersContainer { - private static readonly TestcontainersState[] ContainerHasBeenCreatedStates = { TestcontainersState.Created, TestcontainersState.Running, TestcontainersState.Exited }; + private const TestcontainersState ContainerHasBeenCreatedStates = TestcontainersState.Created | TestcontainersState.Running | TestcontainersState.Exited; private static readonly string[] DockerDesktopGateways = { "host.docker.internal", "gateway.docker.internal" }; @@ -28,6 +28,8 @@ public class TestcontainersContainer : ITestcontainersContainer private readonly ITestcontainersConfiguration configuration; + private int disposed; + [NotNull] private ContainerInspectResponse container = new ContainerInspectResponse(); @@ -228,10 +230,10 @@ public Task ExecAsync(IList command, CancellationToken ct = } /// - /// Removes the Testcontainer. + /// Removes the Testcontainers. /// /// Cancellation token. - /// A task that represents the asynchronous clean up operation of a Testcontainer. + /// A task that represents the asynchronous clean up operation of a Testcontainers. public async Task CleanUpAsync(CancellationToken ct = default) { await this.semaphoreSlim.WaitAsync(ct) @@ -251,31 +253,46 @@ await this.semaphoreSlim.WaitAsync(ct) /// public async ValueTask DisposeAsync() { - if (!ContainerHasBeenCreatedStates.Contains(this.State)) + await this.DisposeAsyncCore() + .ConfigureAwait(false); + + GC.SuppressFinalize(this); + } + + /// + /// Releases any resources associated with the instance of . + /// + /// Value task that completes when any resources associated with the instance have been released. + protected virtual async ValueTask DisposeAsyncCore() + { + if (1.Equals(Interlocked.CompareExchange(ref this.disposed, 1, 0))) { return; } - // If someone calls `DisposeAsync`, we can immediately remove the container. We don't need to wait for the Resource Reaper. - if (!Guid.Empty.Equals(this.configuration.SessionId)) + if (!ContainerHasBeenCreatedStates.HasFlag(this.State)) { - await this.CleanUpAsync() + return; + } + + // If someone calls `DisposeAsync`, we can immediately remove the container. We do not need to wait for the Resource Reaper. + if (Guid.Empty.Equals(this.configuration.SessionId)) + { + await this.StopAsync() .ConfigureAwait(false); } else { - await this.StopAsync() + await this.CleanUpAsync() .ConfigureAwait(false); } this.semaphoreSlim.Dispose(); - - GC.SuppressFinalize(this); } private async Task Create(CancellationToken ct = default) { - if (ContainerHasBeenCreatedStates.Contains(this.State)) + if (ContainerHasBeenCreatedStates.HasFlag(this.State)) { return this.container; } @@ -340,20 +357,23 @@ private async Task CleanUp(string id, CancellationToke { await this.client.RemoveAsync(id, ct) .ConfigureAwait(false); + return new ContainerInspectResponse(); } private void ThrowIfContainerHasNotBeenCreated() { - if (!ContainerHasBeenCreatedStates.Contains(this.State)) + if (ContainerHasBeenCreatedStates.HasFlag(this.State)) { - throw new InvalidOperationException("Testcontainer has not been created."); + return; } + + throw new InvalidOperationException("Testcontainers has not been created."); } private string GetContainerGateway() { - if (!this.client.IsRunningInsideDocker || !ContainerHasBeenCreatedStates.Contains(this.State)) + if (!this.client.IsRunningInsideDocker || !ContainerHasBeenCreatedStates.HasFlag(this.State)) { return "localhost"; } diff --git a/src/Testcontainers/Containers/TestcontainersState.cs b/src/Testcontainers/Containers/TestcontainersState.cs index 017614bc8..a10862fed 100644 --- a/src/Testcontainers/Containers/TestcontainersState.cs +++ b/src/Testcontainers/Containers/TestcontainersState.cs @@ -1,53 +1,55 @@ namespace DotNet.Testcontainers.Containers { + using System; using JetBrains.Annotations; /// /// Docker container states. /// [PublicAPI] + [Flags] public enum TestcontainersState { /// - /// Docker container was not created. + /// Docker container has not been created. /// [PublicAPI] - Undefined, + Undefined = 0x1, /// /// Docker container is created. /// [PublicAPI] - Created, + Created = 0x2, /// /// Docker container is restarting. /// [PublicAPI] - Restarting, + Restarting = 0x4, /// /// Docker container is running. /// [PublicAPI] - Running, + Running = 0x8, /// /// Docker container is paused. /// [PublicAPI] - Paused, + Paused = 0x10, /// /// Docker container is exited. /// [PublicAPI] - Exited, + Exited = 0x20, /// /// Docker container is dead. /// [PublicAPI] - Dead, + Dead = 0x40, } }