Skip to content

Commit 63422de

Browse files
feat: Add pausing and unpausing container (#1315)
Co-authored-by: Andre Hofmeister <[email protected]>
1 parent 60f50b6 commit 63422de

File tree

9 files changed

+257
-1
lines changed

9 files changed

+257
-1
lines changed

src/Testcontainers/Clients/DockerContainerOperations.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ public Task StopAsync(string id, CancellationToken ct = default)
9191
return DockerClient.Containers.StopContainerAsync(id, new ContainerStopParameters { WaitBeforeKillSeconds = 15 }, ct);
9292
}
9393

94+
public Task PauseAsync(string id, CancellationToken ct = default)
95+
{
96+
Logger.PauseDockerContainer(id);
97+
return DockerClient.Containers.PauseContainerAsync(id, ct);
98+
}
99+
100+
public Task UnpauseAsync(string id, CancellationToken ct = default)
101+
{
102+
Logger.UnpauseDockerContainer(id);
103+
return DockerClient.Containers.UnpauseContainerAsync(id, ct);
104+
}
105+
94106
public Task RemoveAsync(string id, CancellationToken ct = default)
95107
{
96108
Logger.DeleteDockerContainer(id);

src/Testcontainers/Clients/IDockerContainerOperations.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ internal interface IDockerContainerOperations : IHasListOperations<ContainerList
1919

2020
Task StopAsync(string id, CancellationToken ct = default);
2121

22+
Task PauseAsync(string id, CancellationToken ct = default);
23+
24+
Task UnpauseAsync(string id, CancellationToken ct = default);
25+
2226
Task RemoveAsync(string id, CancellationToken ct = default);
2327

2428
Task ExtractArchiveToContainerAsync(string id, string path, TarOutputMemoryStream tarStream, CancellationToken ct = default);

src/Testcontainers/Clients/ITestcontainersClient.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,22 @@ internal interface ITestcontainersClient
7878
/// <returns>Task that completes when the container has been stopped.</returns>
7979
Task StopAsync(string id, CancellationToken ct = default);
8080

81+
/// <summary>
82+
/// Pauses the container.
83+
/// </summary>
84+
/// <param name="id">The container id.</param>
85+
/// <param name="ct">Cancellation token.</param>
86+
/// <returns>Task that completes when the container has been paused.</returns>
87+
Task PauseAsync(string id, CancellationToken ct = default);
88+
89+
/// <summary>
90+
/// Unpauses the container.
91+
/// </summary>
92+
/// <param name="id">The container id.</param>
93+
/// <param name="ct">Cancellation token.</param>
94+
/// <returns>Task that completes when the container has been unpaused.</returns>
95+
Task UnpauseAsync(string id, CancellationToken ct = default);
96+
8197
/// <summary>
8298
/// Removes the container.
8399
/// </summary>

src/Testcontainers/Clients/TestcontainersClient.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ await Container.StopAsync(id, ct)
142142
}
143143
}
144144

145+
/// <inheritdoc />
146+
public async Task PauseAsync(string id, CancellationToken ct = default)
147+
{
148+
if (await Container.ExistsWithIdAsync(id, ct)
149+
.ConfigureAwait(false))
150+
{
151+
await Container.PauseAsync(id, ct)
152+
.ConfigureAwait(false);
153+
}
154+
}
155+
156+
/// <inheritdoc />
157+
public async Task UnpauseAsync(string id, CancellationToken ct = default)
158+
{
159+
if (await Container.ExistsWithIdAsync(id, ct)
160+
.ConfigureAwait(false))
161+
{
162+
await Container.UnpauseAsync(id, ct)
163+
.ConfigureAwait(false);
164+
}
165+
}
166+
145167
/// <inheritdoc />
146168
public async Task RemoveAsync(string id, CancellationToken ct = default)
147169
{

src/Testcontainers/Containers/DockerContainer.cs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace DotNet.Testcontainers.Containers
1919
[PublicAPI]
2020
public class DockerContainer : Resource, IContainer
2121
{
22-
private const TestcontainersStates ContainerHasBeenCreatedStates = TestcontainersStates.Created | TestcontainersStates.Running | TestcontainersStates.Exited;
22+
private const TestcontainersStates ContainerHasBeenCreatedStates = TestcontainersStates.Created | TestcontainersStates.Running | TestcontainersStates.Paused | TestcontainersStates.Exited;
2323

2424
private const TestcontainersHealthStatus ContainerHasHealthCheck = TestcontainersHealthStatus.Starting | TestcontainersHealthStatus.Healthy | TestcontainersHealthStatus.Unhealthy;
2525

@@ -48,6 +48,12 @@ public DockerContainer(IContainerConfiguration configuration)
4848
/// <inheritdoc />
4949
public event EventHandler Stopping;
5050

51+
/// <inheritdoc />
52+
public event EventHandler Pausing;
53+
54+
/// <inheritdoc />
55+
public event EventHandler Unpausing;
56+
5157
/// <inheritdoc />
5258
public event EventHandler Created;
5359

@@ -57,6 +63,12 @@ public DockerContainer(IContainerConfiguration configuration)
5763
/// <inheritdoc />
5864
public event EventHandler Stopped;
5965

66+
/// <inheritdoc />
67+
public event EventHandler Paused;
68+
69+
/// <inheritdoc />
70+
public event EventHandler Unpaused;
71+
6072
/// <inheritdoc />
6173
public DateTime CreatedTime { get; private set; }
6274

@@ -66,6 +78,12 @@ public DockerContainer(IContainerConfiguration configuration)
6678
/// <inheritdoc />
6779
public DateTime StoppedTime { get; private set; }
6880

81+
/// <inheritdoc />
82+
public DateTime PausedTime { get; private set; }
83+
84+
/// <inheritdoc />
85+
public DateTime UnpausedTime { get; private set; }
86+
6987
/// <inheritdoc />
7088
public ILogger Logger
7189
{
@@ -294,6 +312,26 @@ await UnsafeStopAsync(ct)
294312
.ConfigureAwait(false);
295313
}
296314

315+
/// <inheritdoc />
316+
public async Task PauseAsync(CancellationToken ct = default)
317+
{
318+
using var disposable = await AcquireLockAsync(ct)
319+
.ConfigureAwait(false);
320+
321+
await UnsafePauseAsync(ct)
322+
.ConfigureAwait(false);
323+
}
324+
325+
/// <inheritdoc />
326+
public async Task UnpauseAsync(CancellationToken ct = default)
327+
{
328+
using var disposable = await AcquireLockAsync(ct)
329+
.ConfigureAwait(false);
330+
331+
await UnsafeUnpauseAsync(ct)
332+
.ConfigureAwait(false);
333+
}
334+
297335
/// <inheritdoc />
298336
public Task CopyAsync(byte[] fileContent, string filePath, UnixFileModes fileMode = Unix.FileMode644, CancellationToken ct = default)
299337
{
@@ -522,6 +560,64 @@ await _client.StopAsync(_container.ID, ct)
522560
Stopped?.Invoke(this, EventArgs.Empty);
523561
}
524562

563+
/// <summary>
564+
/// Pauses the container.
565+
/// </summary>
566+
/// <remarks>
567+
/// Only the public members <see cref="PauseAsync" /> and <see cref="UnpauseAsync" /> are thread-safe for now.
568+
/// </remarks>
569+
/// <param name="ct">Cancellation token.</param>
570+
/// <returns>Task that completes when the container has been paused.</returns>
571+
protected virtual async Task UnsafePauseAsync(CancellationToken ct = default)
572+
{
573+
ThrowIfLockNotAcquired();
574+
575+
if (!Exists())
576+
{
577+
return;
578+
}
579+
580+
Pausing?.Invoke(this, EventArgs.Empty);
581+
582+
await _client.PauseAsync(_container.ID, ct)
583+
.ConfigureAwait(false);
584+
585+
_container = await _client.Container.ByIdAsync(_container.ID, ct)
586+
.ConfigureAwait(false);
587+
588+
PausedTime = DateTime.UtcNow;
589+
Paused?.Invoke(this, EventArgs.Empty);
590+
}
591+
592+
/// <summary>
593+
/// Unpauses the container.
594+
/// </summary>
595+
/// <remarks>
596+
/// Only the public members <see cref="PauseAsync" /> and <see cref="UnpauseAsync" /> are thread-safe for now.
597+
/// </remarks>
598+
/// <param name="ct">Cancellation token.</param>
599+
/// <returns>Task that completes when the container has been unpaused.</returns>
600+
protected virtual async Task UnsafeUnpauseAsync(CancellationToken ct = default)
601+
{
602+
ThrowIfLockNotAcquired();
603+
604+
if (!Exists())
605+
{
606+
return;
607+
}
608+
609+
Unpausing?.Invoke(this, EventArgs.Empty);
610+
611+
await _client.UnpauseAsync(_container.ID, ct)
612+
.ConfigureAwait(false);
613+
614+
_container = await _client.Container.ByIdAsync(_container.ID, ct)
615+
.ConfigureAwait(false);
616+
617+
UnpausedTime = DateTime.UtcNow;
618+
Unpaused?.Invoke(this, EventArgs.Empty);
619+
}
620+
525621
/// <inheritdoc />
526622
protected override bool Exists()
527623
{

src/Testcontainers/Containers/IContainer.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ public interface IContainer : IAsyncDisposable
3434
[CanBeNull]
3535
event EventHandler Stopping;
3636

37+
/// <summary>
38+
/// Subscribes to the pausing event.
39+
/// </summary>
40+
[CanBeNull]
41+
event EventHandler Pausing;
42+
43+
/// <summary>
44+
/// Subscribes to the unpausing event.
45+
/// </summary>
46+
[CanBeNull]
47+
event EventHandler Unpausing;
48+
3749
/// <summary>
3850
/// Subscribes to the created event.
3951
/// </summary>
@@ -52,6 +64,18 @@ public interface IContainer : IAsyncDisposable
5264
[CanBeNull]
5365
event EventHandler Stopped;
5466

67+
/// <summary>
68+
/// Subscribes to the paused event.
69+
/// </summary>
70+
[CanBeNull]
71+
event EventHandler Paused;
72+
73+
/// <summary>
74+
/// Subscribes to the unpaused event.
75+
/// </summary>
76+
[CanBeNull]
77+
event EventHandler Unpaused;
78+
5579
/// <summary>
5680
/// Gets the created timestamp.
5781
/// </summary>
@@ -67,6 +91,16 @@ public interface IContainer : IAsyncDisposable
6791
/// </summary>
6892
DateTime StoppedTime { get; }
6993

94+
/// <summary>
95+
/// Gets the paused timestamp.
96+
/// </summary>
97+
DateTime PausedTime { get; }
98+
99+
/// <summary>
100+
/// Gets the unpaused timestamp.
101+
/// </summary>
102+
DateTime UnpausedTime { get; }
103+
70104
/// <summary>
71105
/// Gets the logger.
72106
/// </summary>
@@ -187,6 +221,24 @@ public interface IContainer : IAsyncDisposable
187221
/// <exception cref="TaskCanceledException">Thrown when a Testcontainers task gets canceled.</exception>
188222
Task StopAsync(CancellationToken ct = default);
189223

224+
/// <summary>
225+
/// Pauses the container.
226+
/// </summary>
227+
/// <param name="ct">Cancellation token.</param>
228+
/// <returns>Task that completes when the container has been paused.</returns>
229+
/// <exception cref="OperationCanceledException">Thrown when a Docker API call gets canceled.</exception>
230+
/// <exception cref="TaskCanceledException">Thrown when a Testcontainers task gets canceled.</exception>
231+
Task PauseAsync(CancellationToken ct = default);
232+
233+
/// <summary>
234+
/// Unpauses the container.
235+
/// </summary>
236+
/// <param name="ct">Cancellation token.</param>
237+
/// <returns>Task that completes when the container has been unpaused.</returns>
238+
/// <exception cref="OperationCanceledException">Thrown when a Docker API call gets canceled.</exception>
239+
/// <exception cref="TaskCanceledException">Thrown when a Testcontainers task gets canceled.</exception>
240+
Task UnpauseAsync(CancellationToken ct = default);
241+
190242
/// <summary>
191243
/// Copies a test host file to the container.
192244
/// </summary>

src/Testcontainers/Logging.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ private static readonly Action<ILogger, string, Exception> _StartDockerContainer
2323
private static readonly Action<ILogger, string, Exception> _StopDockerContainer
2424
= LoggerMessage.Define<string>(LogLevel.Information, default, "Stop Docker container {Id}");
2525

26+
private static readonly Action<ILogger, string, Exception> _PauseDockerContainer
27+
= LoggerMessage.Define<string>(LogLevel.Information, default, "Pause Docker container {Id}");
28+
29+
private static readonly Action<ILogger, string, Exception> _UnpauseDockerContainer
30+
= LoggerMessage.Define<string>(LogLevel.Information, default, "Unpause Docker container {Id}");
31+
2632
private static readonly Action<ILogger, string, Exception> _DeleteDockerContainer
2733
= LoggerMessage.Define<string>(LogLevel.Information, default, "Delete Docker container {Id}");
2834

@@ -132,6 +138,16 @@ public static void StopDockerContainer(this ILogger logger, string id)
132138
_StopDockerContainer(logger, TruncId(id), null);
133139
}
134140

141+
public static void PauseDockerContainer(this ILogger logger, string id)
142+
{
143+
_PauseDockerContainer(logger, TruncId(id), null);
144+
}
145+
146+
public static void UnpauseDockerContainer(this ILogger logger, string id)
147+
{
148+
_UnpauseDockerContainer(logger, TruncId(id), null);
149+
}
150+
135151
public static void DeleteDockerContainer(this ILogger logger, string id)
136152
{
137153
_DeleteDockerContainer(logger, TruncId(id), null);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace Testcontainers.Tests;
2+
3+
public sealed class PauseUnpauseTest : IAsyncLifetime
4+
{
5+
private readonly IContainer _container = new ContainerBuilder()
6+
.WithImage(CommonImages.Alpine)
7+
.WithCommand(CommonCommands.SleepInfinity)
8+
.Build();
9+
10+
public Task InitializeAsync()
11+
{
12+
return _container.StartAsync();
13+
}
14+
15+
public Task DisposeAsync()
16+
{
17+
return _container.DisposeAsync().AsTask();
18+
}
19+
20+
[Fact]
21+
public async Task PausesAndUnpausesContainerSuccessfully()
22+
{
23+
await _container.PauseAsync()
24+
.ConfigureAwait(true);
25+
Assert.Equal(TestcontainersStates.Paused, _container.State);
26+
27+
await _container.UnpauseAsync()
28+
.ConfigureAwait(true);
29+
Assert.Equal(TestcontainersStates.Running, _container.State);
30+
}
31+
32+
[Fact]
33+
public Task UnpausingRunningContainerThrowsDockerApiException()
34+
{
35+
return Assert.ThrowsAsync<DockerApiException>(() => _container.UnpauseAsync());
36+
}
37+
}

tests/Testcontainers.Platform.Linux.Tests/Usings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
global using System.Text;
99
global using System.Threading;
1010
global using System.Threading.Tasks;
11+
global using Docker.DotNet;
1112
global using Docker.DotNet.Models;
1213
global using DotNet.Testcontainers;
1314
global using DotNet.Testcontainers.Builders;

0 commit comments

Comments
 (0)