Skip to content

Commit 1747f0a

Browse files
committed
feat: Implement cancellation token support for teardown and removal operations across resources
1 parent ec1a6af commit 1747f0a

8 files changed

Lines changed: 51 additions & 42 deletions

File tree

FluentDocker.Testing.Xunit/XunitComposeFixtureBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ namespace FluentDocker.Testing.Xunit
2323
/// public class AppFixture : XunitComposeFixtureBase
2424
/// {
2525
/// protected override void ConfigureCompose(IComposeBuilder b)
26-
/// => b.FromFile("docker-compose.yml");
26+
/// => b.WithComposeFile("docker-compose.yml");
2727
/// }
2828
///
2929
/// public class AppTests : IClassFixture<AppFixture>

FluentDocker/Testing/Core/ComposeResource.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,26 +81,26 @@ protected override async Task ProvisionAsync(CancellationToken cancellationToken
8181
}
8282

8383
/// <inheritdoc />
84-
protected override async Task TeardownAsync()
84+
protected override async Task TeardownAsync(CancellationToken cancellationToken)
8585
{
8686
if (Service == null)
8787
return;
8888

89-
await Service.StopAsync();
90-
await Service.RemoveAsync(force: false);
89+
await Service.StopAsync(cancellationToken);
90+
await Service.RemoveAsync(force: false, cancellationToken);
9191
Service = null;
9292
}
9393

9494
/// <inheritdoc />
95-
protected override async Task ForceRemoveAsync()
95+
protected override async Task ForceRemoveAsync(CancellationToken cancellationToken)
9696
{
9797
var s = Service;
9898
Service = null;
9999
if (s == null)
100100
return;
101101

102102
try
103-
{ await s.RemoveAsync(force: true); }
103+
{ await s.RemoveAsync(force: true, cancellationToken); }
104104
catch { /* best effort */ }
105105
}
106106

FluentDocker/Testing/Core/ContainerResource.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,26 +89,26 @@ protected override async Task ProvisionAsync(CancellationToken cancellationToken
8989
}
9090

9191
/// <inheritdoc />
92-
protected override async Task TeardownAsync()
92+
protected override async Task TeardownAsync(CancellationToken cancellationToken)
9393
{
9494
if (Container == null)
9595
return;
9696

97-
await Container.StopAsync();
98-
await Container.RemoveAsync(force: false);
97+
await Container.StopAsync(cancellationToken);
98+
await Container.RemoveAsync(force: false, cancellationToken);
9999
Container = null;
100100
}
101101

102102
/// <inheritdoc />
103-
protected override async Task ForceRemoveAsync()
103+
protected override async Task ForceRemoveAsync(CancellationToken cancellationToken)
104104
{
105105
var c = Container;
106106
Container = null;
107107
if (c == null)
108108
return;
109109

110110
try
111-
{ await c.RemoveAsync(force: true); }
111+
{ await c.RemoveAsync(force: true, cancellationToken); }
112112
catch { /* best effort */ }
113113
}
114114

FluentDocker/Testing/Core/DockerResourceOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,11 @@ public class DockerResourceOptions
3131
/// Maximum log lines to capture on failure.
3232
/// </summary>
3333
public int MaxDiagnosticLogLines { get; set; } = 200;
34+
35+
/// <summary>
36+
/// Timeout for teardown (stop + remove) during disposal.
37+
/// Prevents hung cleanup from blocking CI pipelines indefinitely.
38+
/// </summary>
39+
public TimeSpan TeardownTimeout { get; set; } = TimeSpan.FromSeconds(60);
3440
}
3541
}

FluentDocker/Testing/Core/PodmanKubernetesResource.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,36 +99,35 @@ protected override async Task ProvisionAsync(CancellationToken cancellationToken
9999
$"Podman kube play failed for '{_config.YamlPath}': {result.Error}");
100100
}
101101

102-
PlayResult = result.Data;
102+
PlayResult = result.Data
103+
?? throw new FluentDockerException(
104+
$"Podman kube play for '{_config.YamlPath}' returned Success " +
105+
"but no result payload.");
103106
ResourceName = _config.YamlPath;
104107
}
105108

106109
/// <inheritdoc />
107-
protected override async Task TeardownAsync()
110+
protected override async Task TeardownAsync(CancellationToken cancellationToken)
108111
{
109-
if (PlayResult == null)
110-
return;
111-
112112
var driver = Kernel.SysCtl<IPodmanKubernetesDriver>(DriverId);
113113
var context = new DriverContext(DriverId);
114-
var result = await driver.DownAsync(context, _config.YamlPath);
114+
var result = await driver.DownAsync(
115+
context, _config.YamlPath, cancellationToken);
115116
if (!result.Success)
116117
throw new FluentDockerException(
117118
$"Failed to tear down Podman kube for '{_config.YamlPath}': {result.Error}");
118119
PlayResult = null;
119120
}
120121

121122
/// <inheritdoc />
122-
protected override async Task ForceRemoveAsync()
123+
protected override async Task ForceRemoveAsync(CancellationToken cancellationToken)
123124
{
124-
if (PlayResult == null)
125-
return;
126-
127125
try
128126
{
129127
var driver = Kernel.SysCtl<IPodmanKubernetesDriver>(DriverId);
130128
var context = new DriverContext(DriverId);
131-
await driver.DownAsync(context, _config.YamlPath);
129+
await driver.DownAsync(
130+
context, _config.YamlPath, cancellationToken);
132131
}
133132
catch
134133
{

FluentDocker/Testing/Core/ResourceBase.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,19 @@ public async ValueTask DisposeAsync()
142142
// Hooks should not prevent cleanup
143143
}
144144

145+
using var cts = new CancellationTokenSource(Options.TeardownTimeout);
145146
Exception teardownFailure = null;
146147

147148
try
148149
{
149-
await TeardownAsync();
150+
await TeardownAsync(cts.Token);
150151
}
151152
catch (Exception ex)
152153
{
153154
if (Options.ForceRemoveOnDispose)
154155
{
155156
try
156-
{ await ForceRemoveAsync(); }
157+
{ await ForceRemoveAsync(cts.Token); }
157158
catch { /* best effort */ }
158159
}
159160
else
@@ -194,12 +195,16 @@ public async ValueTask DisposeAsync()
194195
/// <summary>
195196
/// Gracefully stops and removes the resource.
196197
/// </summary>
197-
protected abstract Task TeardownAsync();
198+
/// <param name="cancellationToken">Cancellation token bound to
199+
/// <see cref="DockerResourceOptions.TeardownTimeout"/>.</param>
200+
protected abstract Task TeardownAsync(CancellationToken cancellationToken);
198201

199202
/// <summary>
200203
/// Force-removes the resource when graceful teardown fails.
201204
/// </summary>
202-
protected abstract Task ForceRemoveAsync();
205+
/// <param name="cancellationToken">Cancellation token bound to
206+
/// <see cref="DockerResourceOptions.TeardownTimeout"/>.</param>
207+
protected abstract Task ForceRemoveAsync(CancellationToken cancellationToken);
203208

204209
/// <summary>
205210
/// Collects diagnostic information when initialization fails.

FluentDocker/Testing/Core/SwarmStackResource.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,36 +102,35 @@ protected override async Task ProvisionAsync(CancellationToken cancellationToken
102102
$"Stack deploy failed for '{_config.StackName}': {result.Error}");
103103
}
104104

105-
DeployResult = result.Data;
105+
DeployResult = result.Data
106+
?? throw new FluentDockerException(
107+
$"Stack deploy for '{_config.StackName}' returned Success " +
108+
"but no result payload.");
106109
ResourceName = _config.StackName;
107110
}
108111

109112
/// <inheritdoc />
110-
protected override async Task TeardownAsync()
113+
protected override async Task TeardownAsync(CancellationToken cancellationToken)
111114
{
112-
if (DeployResult == null)
113-
return;
114-
115115
var driver = Kernel.SysCtl<IStackDriver>(DriverId);
116116
var context = new DriverContext(DriverId);
117-
var result = await driver.RemoveAsync(context, new[] { _config.StackName });
117+
var result = await driver.RemoveAsync(
118+
context, new[] { _config.StackName }, cancellationToken);
118119
if (!result.Success)
119120
throw new FluentDockerException(
120121
$"Failed to remove stack '{_config.StackName}': {result.Error}");
121122
DeployResult = null;
122123
}
123124

124125
/// <inheritdoc />
125-
protected override async Task ForceRemoveAsync()
126+
protected override async Task ForceRemoveAsync(CancellationToken cancellationToken)
126127
{
127-
if (DeployResult == null)
128-
return;
129-
130128
try
131129
{
132130
var driver = Kernel.SysCtl<IStackDriver>(DriverId);
133131
var context = new DriverContext(DriverId);
134-
await driver.RemoveAsync(context, new[] { _config.StackName });
132+
await driver.RemoveAsync(
133+
context, new[] { _config.StackName }, cancellationToken);
135134
}
136135
catch
137136
{

FluentDocker/Testing/Core/TopologyResource.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,19 @@ protected override async Task ProvisionAsync(CancellationToken cancellationToken
8686
}
8787

8888
/// <inheritdoc />
89-
protected override async Task TeardownAsync()
89+
protected override async Task TeardownAsync(CancellationToken cancellationToken)
9090
{
9191
var failures = new List<Exception>();
9292

9393
// Tear down in reverse order for dependency safety.
9494
for (var i = _services.Count - 1; i >= 0; i--)
9595
{
9696
try
97-
{ await _services[i].StopAsync(); }
97+
{ await _services[i].StopAsync(cancellationToken); }
9898
catch { /* stop failure must not prevent removal */ }
9999

100100
try
101-
{ await _services[i].RemoveAsync(force: false); }
101+
{ await _services[i].RemoveAsync(force: false, cancellationToken); }
102102
catch (Exception ex) { failures.Add(ex); }
103103
}
104104

@@ -111,12 +111,12 @@ protected override async Task TeardownAsync()
111111
}
112112

113113
/// <inheritdoc />
114-
protected override async Task ForceRemoveAsync()
114+
protected override async Task ForceRemoveAsync(CancellationToken cancellationToken)
115115
{
116116
for (var i = _services.Count - 1; i >= 0; i--)
117117
{
118118
try
119-
{ await _services[i].RemoveAsync(force: true); }
119+
{ await _services[i].RemoveAsync(force: true, cancellationToken); }
120120
catch { /* best effort */ }
121121
}
122122

0 commit comments

Comments
 (0)