Skip to content

Commit

Permalink
fix(#595): Implement TestcontainersContainer.DisposeAsync thread safe
Browse files Browse the repository at this point in the history
  • Loading branch information
HofmeisterAn committed Sep 30, 2022
1 parent 9baa4fd commit fe00966
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
48 changes: 34 additions & 14 deletions src/Testcontainers/Containers/TestcontainersContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace DotNet.Testcontainers.Containers
/// <inheritdoc cref="ITestcontainersContainer" />
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" };

Expand All @@ -28,6 +28,8 @@ public class TestcontainersContainer : ITestcontainersContainer

private readonly ITestcontainersConfiguration configuration;

private int disposed;

[NotNull]
private ContainerInspectResponse container = new ContainerInspectResponse();

Expand Down Expand Up @@ -228,10 +230,10 @@ public Task<ExecResult> ExecAsync(IList<string> command, CancellationToken ct =
}

/// <summary>
/// Removes the Testcontainer.
/// Removes the Testcontainers.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous clean up operation of a Testcontainer.</returns>
/// <returns>A task that represents the asynchronous clean up operation of a Testcontainers.</returns>
public async Task CleanUpAsync(CancellationToken ct = default)
{
await this.semaphoreSlim.WaitAsync(ct)
Expand All @@ -251,31 +253,46 @@ await this.semaphoreSlim.WaitAsync(ct)
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
if (!ContainerHasBeenCreatedStates.Contains(this.State))
await this.DisposeAsyncCore()
.ConfigureAwait(false);

GC.SuppressFinalize(this);
}

/// <summary>
/// Releases any resources associated with the instance of <see cref="TestcontainersContainer" />.
/// </summary>
/// <returns>Value task that completes when any resources associated with the instance have been released.</returns>
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<ContainerInspectResponse> Create(CancellationToken ct = default)
{
if (ContainerHasBeenCreatedStates.Contains(this.State))
if (ContainerHasBeenCreatedStates.HasFlag(this.State))
{
return this.container;
}
Expand Down Expand Up @@ -340,20 +357,23 @@ private async Task<ContainerInspectResponse> 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";
}
Expand Down
18 changes: 10 additions & 8 deletions src/Testcontainers/Containers/TestcontainersState.cs
Original file line number Diff line number Diff line change
@@ -1,53 +1,55 @@
namespace DotNet.Testcontainers.Containers
{
using System;
using JetBrains.Annotations;

/// <summary>
/// Docker container states.
/// </summary>
[PublicAPI]
[Flags]
public enum TestcontainersState
{
/// <summary>
/// Docker container was not created.
/// Docker container has not been created.
/// </summary>
[PublicAPI]
Undefined,
Undefined = 0x1,

/// <summary>
/// Docker container is created.
/// </summary>
[PublicAPI]
Created,
Created = 0x2,

/// <summary>
/// Docker container is restarting.
/// </summary>
[PublicAPI]
Restarting,
Restarting = 0x4,

/// <summary>
/// Docker container is running.
/// </summary>
[PublicAPI]
Running,
Running = 0x8,

/// <summary>
/// Docker container is paused.
/// </summary>
[PublicAPI]
Paused,
Paused = 0x10,

/// <summary>
/// Docker container is exited.
/// </summary>
[PublicAPI]
Exited,
Exited = 0x20,

/// <summary>
/// Docker container is dead.
/// </summary>
[PublicAPI]
Dead,
Dead = 0x40,
}
}

0 comments on commit fe00966

Please sign in to comment.