Skip to content

Commit c4e358e

Browse files
authored
Changing Testcontainers dapr service readiness checks from console log evaluation to health API endpoints (#1705)
* Changed wait readiness strategy from console logs to Health API endpoints for each service * Placement and Scheduler are coming online and testing accurately now * Successfully tested the daprd container coming online and testing via probes * Added 2 minute timeout to container readiness * Adding 10 minute timeout to integration tests * Reverting to use console message for daprd --------- Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
1 parent 9bea1e3 commit c4e358e

File tree

5 files changed

+98
-45
lines changed

5 files changed

+98
-45
lines changed

.github/workflows/sdk_build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ jobs:
208208
name: ${{ matrix.prefix }}-${{ matrix['dapr-runtime-versions'].version }}-${{ matrix.projectName }}
209209
needs: [ compute-integration-matrix ]
210210
runs-on: ${{ matrix.os }}
211+
timeout-minutes: 30
211212
strategy:
212213
fail-fast: false
213214
matrix: ${{ fromJson(needs.compute-integration-matrix.outputs.matrix) }}

src/Dapr.Testcontainers/Containers/Dapr/DaprPlacementContainer.cs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
// ------------------------------------------------------------------------
1313

1414
using System;
15+
using System.Net;
1516
using System.Threading;
1617
using System.Threading.Tasks;
1718
using Dapr.Testcontainers.Common;
1819
using Dapr.Testcontainers.Common.Options;
20+
using Docker.DotNet.Models;
1921
using DotNet.Testcontainers.Builders;
22+
using DotNet.Testcontainers.Configurations;
2023
using DotNet.Testcontainers.Containers;
2124
using DotNet.Testcontainers.Networks;
2225

@@ -44,9 +47,13 @@ public sealed class DaprPlacementContainer : IAsyncStartable
4447
/// </summary>
4548
public int ExternalPort { get; private set; }
4649
/// <summary>
47-
/// THe contains' internal port.
50+
/// The container's internal port.
4851
/// </summary>
4952
public const int InternalPort = 50006;
53+
/// <summary>
54+
/// The container's internal health port.
55+
/// </summary>
56+
private const int HealthPort = 8080;
5057

5158
/// <summary>
5259
/// Initializes a new instance of the <see cref="DaprPlacementContainer"/>.
@@ -59,14 +66,24 @@ public DaprPlacementContainer(DaprRuntimeOptions options, INetwork network, stri
5966
_logAttachment = ContainerLogAttachment.TryCreate(logDirectory, "placement", _containerName);
6067

6168
//Placement service runs via port 50006
62-
var containerBuilder = new ContainerBuilder()
63-
.WithImage(options.PlacementImageTag)
64-
.WithName(_containerName)
69+
var containerBuilder = new ContainerBuilder()
70+
.WithImage(options.PlacementImageTag)
71+
.WithName(_containerName)
6572
.WithNetwork(network)
66-
.WithCommand("./placement", "-port", InternalPort.ToString())
67-
.WithPortBinding(InternalPort, assignRandomHostPort: true)
68-
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("placement server leadership acquired"))
69-
;
73+
.WithCommand("./placement", "-port", InternalPort.ToString())
74+
.WithPortBinding(InternalPort, assignRandomHostPort: true)
75+
.WithPortBinding(HealthPort, assignRandomHostPort: true)
76+
.WithWaitStrategy(Wait.ForUnixContainer()
77+
.UntilHttpRequestIsSucceeded(endpoint =>
78+
endpoint
79+
.ForPort(HealthPort)
80+
.ForPath("/healthz")
81+
.ForStatusCodeMatching(code => (int)code >= 200 && (int)code < 300),
82+
mod =>
83+
mod
84+
.WithTimeout(TimeSpan.FromMinutes(2))
85+
.WithInterval(TimeSpan.FromSeconds(5))
86+
.WithMode(WaitStrategyMode.Running)));
7087

7188
if (_logAttachment is not null)
7289
{

src/Dapr.Testcontainers/Containers/Dapr/DaprSchedulerContainer.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
using System;
1515
using System.Linq;
16+
using System.Net;
17+
using System.Text;
1618
using System.Threading;
1719
using System.Threading.Tasks;
1820
using Dapr.Testcontainers.Common;
@@ -52,6 +54,10 @@ public sealed class DaprSchedulerContainer : IAsyncStartable
5254
/// The container's internal port.
5355
/// </summary>
5456
public const int InternalPort = 51005;
57+
/// <summary>
58+
/// The container's internal health port.
59+
/// </summary>
60+
private const int HealthPort = 8080;
5561

5662
/// <summary>
5763
/// Creates a new instance of a <see cref="DaprSchedulerContainer"/>.
@@ -70,17 +76,27 @@ public DaprSchedulerContainer(DaprRuntimeOptions options, INetwork network, stri
7076
];
7177

7278
_testDirectory = TestDirectoryManager.CreateTestDirectory("scheduler");
73-
79+
7480
var containerBuilder = new ContainerBuilder()
7581
.WithImage(options.SchedulerImageTag)
76-
.WithName(_containerName)
82+
.WithName(_containerName)
7783
.WithNetwork(network)
7884
.WithCommand(cmd.ToArray())
79-
.WithPortBinding(InternalPort, assignRandomHostPort: true)
85+
.WithPortBinding(InternalPort, assignRandomHostPort: true)
86+
.WithPortBinding(HealthPort, assignRandomHostPort: true) // Allows probes to reach healthz
8087
// Mount an anonymous volume to /data to ensure the scheduler has write permissions
8188
.WithBindMount(_testDirectory, containerDataDir, AccessMode.ReadWrite)
82-
.WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("api is ready"))
83-
;
89+
.WithWaitStrategy(Wait.ForUnixContainer()
90+
.UntilHttpRequestIsSucceeded(endpoint =>
91+
endpoint
92+
.ForPort(HealthPort)
93+
.ForPath("/healthz")
94+
.ForStatusCodeMatching(code => (int)code >= 200 && (int)code < 300),
95+
mod =>
96+
mod
97+
.WithTimeout(TimeSpan.FromMinutes(2))
98+
.WithInterval(TimeSpan.FromSeconds(5))
99+
.WithMode(WaitStrategyMode.Running)));
84100

85101
if (_logAttachment is not null)
86102
{

src/Dapr.Testcontainers/Containers/Dapr/DaprdContainer.cs

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
using System;
1515
using System.Collections.Generic;
16+
using System.Net;
1617
using System.Net.Sockets;
1718
using System.Threading;
1819
using System.Threading.Tasks;
@@ -33,9 +34,10 @@ public sealed class DaprdContainer : IAsyncStartable
3334
{
3435
private const int InternalHttpPort = 3500;
3536
private const int InternalGrpcPort = 50001;
37+
private const int InternalHealthPort = 8080;
3638
private readonly IContainer _container;
3739
private readonly ContainerLogAttachment? _logAttachment;
38-
private string _containerName = $"dapr-{Guid.NewGuid():N}";
40+
private readonly string _containerName = $"dapr-{Guid.NewGuid():N}";
3941

4042
/// <summary>
4143
/// The internal network alias/name of the container.
@@ -123,57 +125,74 @@ public DaprdContainer(
123125
cmd.Add("-scheduler-host-address");
124126
cmd.Add("");
125127
}
126-
127-
var containerBuilder = new ContainerBuilder()
128-
.WithImage(options.RuntimeImageTag)
129-
.WithName(_containerName)
128+
129+
var containerBuilder = new ContainerBuilder()
130+
.WithImage(options.RuntimeImageTag)
131+
.WithName(_containerName)
130132
.WithLogger(ConsoleLogger.Instance)
131-
.WithCommand(cmd.ToArray())
133+
.WithCommand(cmd.ToArray())
132134
.WithNetwork(network)
133135
.WithExtraHost(ContainerHostAlias, "host-gateway")
134-
.WithBindMount(componentsHostFolder, componentsPath, AccessMode.ReadOnly)
135-
.WithWaitStrategy(Wait.ForUnixContainer()
136+
.WithBindMount(componentsHostFolder, componentsPath, AccessMode.ReadOnly)
137+
.WithWaitStrategy(Wait.ForUnixContainer()
136138
.UntilMessageIsLogged("Internal gRPC server is running"));
137-
//.UntilMessageIsLogged(@"^dapr initialized. Status: Running. Init Elapsed "))
139+
// .UntilHttpRequestIsSucceeded(endpoint =>
140+
// endpoint
141+
// .ForPort(InternalHttpPort)
142+
// .ForPath("/healthz")
143+
// .ForStatusCodeMatching(code => (int)code >= 200 && (int)code < 300),
144+
// mod =>
145+
// mod
146+
// .WithTimeout(TimeSpan.FromMinutes(2))
147+
// .WithInterval(TimeSpan.FromSeconds(5))
148+
// .WithMode(WaitStrategyMode.Running)));
138149

139150
if (_logAttachment is not null)
140151
{
141152
containerBuilder = containerBuilder.WithOutputConsumer(_logAttachment.OutputConsumer);
142153
}
143154

144-
containerBuilder = daprHttpPort is not null ? containerBuilder.WithPortBinding(containerPort: InternalHttpPort, hostPort: daprHttpPort.Value) : containerBuilder.WithPortBinding(port: InternalHttpPort, assignRandomHostPort: true);
145-
containerBuilder = daprGrpcPort is not null ? containerBuilder.WithPortBinding(containerPort: InternalGrpcPort, hostPort: daprGrpcPort.Value) : containerBuilder.WithPortBinding(port: InternalGrpcPort, assignRandomHostPort: true);
155+
containerBuilder = daprHttpPort is not null ? containerBuilder.WithPortBinding(containerPort: InternalHttpPort, hostPort: daprHttpPort.Value) : containerBuilder.WithPortBinding(port: InternalHttpPort, assignRandomHostPort: true);
156+
containerBuilder = daprGrpcPort is not null ? containerBuilder.WithPortBinding(containerPort: InternalGrpcPort, hostPort: daprGrpcPort.Value) : containerBuilder.WithPortBinding(port: InternalGrpcPort, assignRandomHostPort: true);
146157

147-
_container = containerBuilder.Build();
158+
_container = containerBuilder.Build();
148159
}
149160

150161
/// <inheritdoc />
151162
public async Task StartAsync(CancellationToken cancellationToken = default)
152163
{
153-
await _container.StartAsync(cancellationToken);
164+
try
165+
{
166+
await _container.StartAsync(cancellationToken);
154167

155-
var mappedHttpPort = _container.GetMappedPublicPort(InternalHttpPort);
156-
var mappedGrpcPort = _container.GetMappedPublicPort(InternalGrpcPort);
168+
var mappedHttpPort = _container.GetMappedPublicPort(InternalHttpPort);
169+
var mappedGrpcPort = _container.GetMappedPublicPort(InternalGrpcPort);
157170

158-
if (_requestedHttpPort is not null && mappedHttpPort != _requestedHttpPort.Value)
159-
{
160-
throw new InvalidOperationException(
161-
$"Dapr HTTP port mapping mismatch. Requested {_requestedHttpPort.Value}, but Docker mapped {mappedHttpPort}");
162-
}
171+
if (_requestedHttpPort is not null && mappedHttpPort != _requestedHttpPort.Value)
172+
{
173+
throw new InvalidOperationException(
174+
$"Dapr HTTP port mapping mismatch. Requested {_requestedHttpPort.Value}, but Docker mapped {mappedHttpPort}");
175+
}
163176

164-
if (_requestedGrpcPort is not null && mappedGrpcPort != _requestedGrpcPort.Value)
165-
{
166-
throw new InvalidOperationException(
167-
$"Dapr gRPC port mapping mismatch. Requested {_requestedGrpcPort.Value}, but Docker mapped {mappedGrpcPort}");
168-
}
177+
if (_requestedGrpcPort is not null && mappedGrpcPort != _requestedGrpcPort.Value)
178+
{
179+
throw new InvalidOperationException(
180+
$"Dapr gRPC port mapping mismatch. Requested {_requestedGrpcPort.Value}, but Docker mapped {mappedGrpcPort}");
181+
}
169182

170-
HttpPort = mappedHttpPort;
171-
GrpcPort = mappedGrpcPort;
183+
HttpPort = mappedHttpPort;
184+
GrpcPort = mappedGrpcPort;
172185

173-
// The container log wait strategy can fire before the host port is actually accepting connections
174-
// (especially on Windows). Ensure the ports are reachable from the test process.
175-
await WaitForTcpPortAsync("127.0.0.1", HttpPort, TimeSpan.FromSeconds(30), cancellationToken);
176-
await WaitForTcpPortAsync("127.0.0.1", GrpcPort, TimeSpan.FromSeconds(30), cancellationToken);
186+
// The container log wait strategy can fire before the host port is actually accepting connections
187+
// (especially on Windows). Ensure the ports are reachable from the test process.
188+
await WaitForTcpPortAsync("127.0.0.1", HttpPort, TimeSpan.FromSeconds(30), cancellationToken);
189+
await WaitForTcpPortAsync("127.0.0.1", GrpcPort, TimeSpan.FromSeconds(30), cancellationToken);
190+
}
191+
catch (Exception ex)
192+
{
193+
var msg = ex.Message;
194+
throw;
195+
}
177196
}
178197

179198
private static async Task WaitForTcpPortAsync(

src/Dapr.Testcontainers/Harnesses/BaseHarness.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ DaprSchedulerExternalPort is null || DaprSchedulerAlias is null
229229
await _daprd!.StartAsync(cancellationToken);
230230
_sidecarPortsReady.TrySetResult();
231231
}, cancellationToken);
232-
232+
233233
Task? appTask = null;
234234
if (startApp is not null)
235235
{

0 commit comments

Comments
 (0)