Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add wait strategy options #1168

Merged
merged 2 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/api/wait_strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ _ = Wait.ForUnixContainer()
.AddCustomWaitStrategy(new MyCustomWaitStrategy());
```

<!-- In some cases, it might be necessary to configure the behavior of a wait strategy further, being able to cancel the readiness check. The API provides a callback that allows setting additional configurations such as `Retries`, `Interval`, and `Timeout`.

```csharp title="Cancel the readiness check after one minute"
_ = Wait.ForUnixContainer()
.UntilMessageIsLogged("Server started", o => o.WithTimeout(TimeSpan.FromMinutes(1)));
```

Besides configuring the wait strategy, cancelling a container start can always be done utilizing a [CancellationToken](create_docker_container.md#canceling-a-container-start). -->

## Wait until an HTTP(S) endpoint is available

You can choose to wait for an HTTP(S) endpoint to return a particular HTTP response status code or to match a predicate. The default configuration tries to access the HTTP endpoint running inside the container. Chose `ForPort(ushort)` or `ForPath(string)` to adjust the endpoint or `UsingTls()` to switch to HTTPS. When using `UsingTls()` port 443 is used as a default. If your container exposes a different HTTPS port, make sure that the correct waiting port is configured accordingly.
Expand Down
4 changes: 2 additions & 2 deletions src/Testcontainers.Couchbase/CouchbaseBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ private CouchbaseBuilder WithBucket(params CouchbaseBucket[] bucket)
/// <param name="ct">Cancellation token.</param>
private async Task ConfigureCouchbaseAsync(IContainer container, CancellationToken ct = default)
{
await WaitStrategy.WaitUntilAsync(() => WaitUntilNodeIsReady.UntilAsync(container), TimeSpan.FromSeconds(2), TimeSpan.FromMinutes(5), ct)
await WaitStrategy.WaitUntilAsync(() => WaitUntilNodeIsReady.UntilAsync(container), TimeSpan.FromSeconds(2), TimeSpan.FromMinutes(5), -1, ct)
.ConfigureAwait(false);

using (var httpClient = new HttpClient(new RetryHandler()))
Expand Down Expand Up @@ -269,7 +269,7 @@ await EnsureSuccessStatusCodeAsync(response)
.Build()
.Last();

await WaitStrategy.WaitUntilAsync(() => waitUntilBucketIsCreated.UntilAsync(container), TimeSpan.FromSeconds(2), TimeSpan.FromMinutes(5), ct)
await WaitStrategy.WaitUntilAsync(() => waitUntilBucketIsCreated.UntilAsync(container), TimeSpan.FromSeconds(2), TimeSpan.FromMinutes(5), -1, ct)
.ConfigureAwait(false);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public ContainerConfiguration(
IEnumerable<string> networkAliases = null,
IEnumerable<string> extraHosts = null,
IOutputConsumer outputConsumer = null,
IEnumerable<IWaitUntil> waitStrategies = null,
IEnumerable<WaitStrategy> waitStrategies = null,
Func<IContainer, CancellationToken, Task> startupCallback = null,
bool? autoRemove = null,
bool? privileged = null)
Expand Down Expand Up @@ -133,7 +133,7 @@ public ContainerConfiguration(IContainerConfiguration oldValue, IContainerConfig
NetworkAliases = BuildConfiguration.Combine(oldValue.NetworkAliases, newValue.NetworkAliases);
ExtraHosts = BuildConfiguration.Combine(oldValue.ExtraHosts, newValue.ExtraHosts);
OutputConsumer = BuildConfiguration.Combine(oldValue.OutputConsumer, newValue.OutputConsumer);
WaitStrategies = BuildConfiguration.Combine<IEnumerable<IWaitUntil>>(oldValue.WaitStrategies, newValue.WaitStrategies);
WaitStrategies = BuildConfiguration.Combine<IEnumerable<WaitStrategy>>(oldValue.WaitStrategies, newValue.WaitStrategies);
StartupCallback = BuildConfiguration.Combine(oldValue.StartupCallback, newValue.StartupCallback);
AutoRemove = (oldValue.AutoRemove.HasValue && oldValue.AutoRemove.Value) || (newValue.AutoRemove.HasValue && newValue.AutoRemove.Value);
Privileged = (oldValue.Privileged.HasValue && oldValue.Privileged.Value) || (newValue.Privileged.HasValue && newValue.Privileged.Value);
Expand Down Expand Up @@ -212,7 +212,7 @@ public ContainerConfiguration(IContainerConfiguration oldValue, IContainerConfig

/// <inheritdoc />
[JsonIgnore]
public IEnumerable<IWaitUntil> WaitStrategies { get; }
public IEnumerable<WaitStrategy> WaitStrategies { get; }

/// <inheritdoc />
[JsonIgnore]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public interface IContainerConfiguration : IResourceConfiguration<CreateContaine
/// <summary>
/// Gets the wait strategies.
/// </summary>
IEnumerable<IWaitUntil> WaitStrategies { get; }
IEnumerable<WaitStrategy> WaitStrategies { get; }

/// <summary>
/// Gets the startup callback.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,98 +14,121 @@ public interface IWaitForContainerOS
/// <summary>
/// Adds a custom wait strategy to the wait strategies collection.
/// </summary>
/// <param name="waitStrategy">The wait strategy until the container is ready.</param>
/// <param name="waitUntil">The wait strategy until the container is ready.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
/// <remarks>Already contains <see cref="UntilContainerIsRunning" /> as default wait strategy.</remarks>
[PublicAPI]
IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitStrategy);
IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the command is completed successfully.
/// </summary>
/// <param name="command">The command to be executed.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
/// <remarks>
/// Does not invoke the operating system command shell.
/// Normal shell processing does not happen. Expects the exit code to be 0.
/// </remarks>
[PublicAPI]
IWaitForContainerOS UntilCommandIsCompleted(params string[] command);

/// <summary>
/// Waits until the command is completed successfully.
/// </summary>
/// <param name="command">The command to be executed.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
/// <remarks>Invokes the operating system command shell. Expects the exit code to be 0.</remarks>
[PublicAPI]
IWaitForContainerOS UntilCommandIsCompleted(string command);
IWaitForContainerOS UntilCommandIsCompleted(string command, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the command is completed successfully.
/// </summary>
/// <param name="command">The command to be executed.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
/// <remarks>
/// Does not invoke the operating system command shell.
/// Normal shell processing does not happen. Expects the exit code to be 0.
/// </remarks>
[PublicAPI]
IWaitForContainerOS UntilCommandIsCompleted(params string[] command);
IWaitForContainerOS UntilCommandIsCompleted(IEnumerable<string> command, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the port is available.
/// </summary>
/// <param name="port">The port to be checked.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
[PublicAPI]
IWaitForContainerOS UntilPortIsAvailable(int port);
IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the file exists.
/// </summary>
/// <param name="filePath">The file path to be checked.</param>
/// <param name="fileSystem">The file system to be checked.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
[PublicAPI]
IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host);
IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the message is logged.
/// </summary>
/// <param name="pattern">The regular expression that matches the log message.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
[PublicAPI]
IWaitForContainerOS UntilMessageIsLogged(string pattern);
IWaitForContainerOS UntilMessageIsLogged(string pattern, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the message is logged.
/// </summary>
/// <param name="pattern">The regular expression that matches the log message.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
[PublicAPI]
IWaitForContainerOS UntilMessageIsLogged(Regex pattern);
IWaitForContainerOS UntilMessageIsLogged(Regex pattern, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the operation is completed successfully.
/// </summary>
/// <param name="operation">The operation to be executed.</param>
/// <param name="maxCallCount">The number of attempts before an exception is thrown.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
/// <exception cref="TimeoutException">Thrown when number of failed operations exceeded <paramref name="maxCallCount" />.</exception>
[PublicAPI]
IWaitForContainerOS UntilOperationIsSucceeded(Func<bool> operation, int maxCallCount);
[Obsolete("Use one of the other wait strategies in combination with the `Action<IWaitStrategy>` argument, and set the number of retries.")]
IWaitForContainerOS UntilOperationIsSucceeded(Func<bool> operation, int maxCallCount, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the http request is completed successfully.
/// </summary>
/// <param name="request">The http request to be executed.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
[PublicAPI]
IWaitForContainerOS UntilHttpRequestIsSucceeded(Func<HttpWaitStrategy, HttpWaitStrategy> request);
IWaitForContainerOS UntilHttpRequestIsSucceeded(Func<HttpWaitStrategy, HttpWaitStrategy> request, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Waits until the container is healthy.
/// </summary>
/// <param name="failingStreak">The number of attempts before an exception is thrown.</param>
/// <param name="waitStrategyModifier">The wait strategy modifier to cancel the readiness check.</param>
/// <returns>A configured instance of <see cref="IWaitForContainerOS" />.</returns>
/// <exception cref="TimeoutException">Thrown when number of failed operations exceeded <paramref name="failingStreak" />.</exception>
[PublicAPI]
IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3);
IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3, Action<IWaitStrategy> waitStrategyModifier = null);

/// <summary>
/// Returns a collection with all configured wait strategies.
/// </summary>
/// <returns>Returns a list with all configured wait strategies.</returns>
[PublicAPI]
IEnumerable<IWaitUntil> Build();
IEnumerable<WaitStrategy> Build();
}
}
33 changes: 33 additions & 0 deletions src/Testcontainers/Configurations/WaitStrategies/IWaitStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace DotNet.Testcontainers.Configurations
{
using System;
using JetBrains.Annotations;

/// <summary>
/// Represents a wait strategy configuration.
/// </summary>
[PublicAPI]
public interface IWaitStrategy
{
/// <summary>
/// Sets the number of retries for the wait strategy.
/// </summary>
/// <param name="retries">The number of retries.</param>
/// <returns>The updated instance of the wait strategy.</returns>
IWaitStrategy WithRetries(ushort retries);

/// <summary>
/// Sets the interval between retries for the wait strategy.
/// </summary>
/// <param name="interval">The interval between retries.</param>
/// <returns>The updated instance of the wait strategy.</returns>
IWaitStrategy WithInterval(TimeSpan interval);

/// <summary>
/// Sets the timeout for the wait strategy.
/// </summary>
/// <param name="timeout">The timeout duration.</param>
/// <returns>The updated instance of the wait strategy.</returns>
IWaitStrategy WithTimeout(TimeSpan timeout);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,77 +7,87 @@ namespace DotNet.Testcontainers.Configurations
/// <inheritdoc cref="IWaitForContainerOS" />
internal abstract class WaitForContainerOS : IWaitForContainerOS
{
private readonly ICollection<IWaitUntil> _waitStrategies = new List<IWaitUntil>();
private readonly ICollection<WaitStrategy> _waitStrategies = new List<WaitStrategy>();

/// <summary>
/// Initializes a new instance of the <see cref="WaitForContainerOS" /> class.
/// </summary>
protected WaitForContainerOS()
{
_waitStrategies.Add(new UntilContainerIsRunning());
_waitStrategies.Add(new WaitStrategy(new UntilContainerIsRunning()));
}

/// <inheritdoc />
public abstract IWaitForContainerOS UntilCommandIsCompleted(string command);
public abstract IWaitForContainerOS UntilCommandIsCompleted(params string[] command);

/// <inheritdoc />
public abstract IWaitForContainerOS UntilCommandIsCompleted(params string[] command);
public abstract IWaitForContainerOS UntilCommandIsCompleted(string command, Action<IWaitStrategy> waitStrategyModifier = null);

/// <inheritdoc />
public abstract IWaitForContainerOS UntilCommandIsCompleted(IEnumerable<string> command, Action<IWaitStrategy> waitStrategyModifier = null);

/// <inheritdoc />
public abstract IWaitForContainerOS UntilPortIsAvailable(int port);
public abstract IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null);

/// <inheritdoc />
public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitStrategy)
public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, Action<IWaitStrategy> waitStrategyModifier = null)
{
var waitStrategy = new WaitStrategy(waitUntil);

if (waitStrategyModifier != null)
{
waitStrategyModifier(waitStrategy);
}

_waitStrategies.Add(waitStrategy);
return this;
}

/// <inheritdoc />
public virtual IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host)
public virtual IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host, Action<IWaitStrategy> waitStrategyModifier = null)
{
switch (fileSystem)
{
case FileSystem.Container:
return AddCustomWaitStrategy(new UntilFileExistsInContainer(filePath));
return AddCustomWaitStrategy(new UntilFileExistsInContainer(filePath), waitStrategyModifier);
case FileSystem.Host:
default:
return AddCustomWaitStrategy(new UntilFileExistsOnHost(filePath));
return AddCustomWaitStrategy(new UntilFileExistsOnHost(filePath), waitStrategyModifier);
}
}

/// <inheritdoc />
public IWaitForContainerOS UntilMessageIsLogged(string pattern)
public IWaitForContainerOS UntilMessageIsLogged(string pattern, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilMessageIsLogged(pattern));
return AddCustomWaitStrategy(new UntilMessageIsLogged(pattern), waitStrategyModifier);
}

/// <inheritdoc />
public IWaitForContainerOS UntilMessageIsLogged(Regex pattern)
public IWaitForContainerOS UntilMessageIsLogged(Regex pattern, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilMessageIsLogged(pattern));
return AddCustomWaitStrategy(new UntilMessageIsLogged(pattern), waitStrategyModifier);
}

/// <inheritdoc />
public virtual IWaitForContainerOS UntilOperationIsSucceeded(Func<bool> operation, int maxCallCount)
public virtual IWaitForContainerOS UntilOperationIsSucceeded(Func<bool> operation, int maxCallCount, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilOperationIsSucceeded(operation, maxCallCount));
return AddCustomWaitStrategy(new UntilOperationIsSucceeded(operation, maxCallCount), waitStrategyModifier);
}

/// <inheritdoc />
public virtual IWaitForContainerOS UntilHttpRequestIsSucceeded(Func<HttpWaitStrategy, HttpWaitStrategy> request)
public virtual IWaitForContainerOS UntilHttpRequestIsSucceeded(Func<HttpWaitStrategy, HttpWaitStrategy> request, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(request.Invoke(new HttpWaitStrategy()));
return AddCustomWaitStrategy(request.Invoke(new HttpWaitStrategy()), waitStrategyModifier);
}

/// <inheritdoc />
public virtual IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3)
public virtual IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilContainerIsHealthy(failingStreak));
return AddCustomWaitStrategy(new UntilContainerIsHealthy(failingStreak), waitStrategyModifier);
}

/// <inheritdoc />
public IEnumerable<IWaitUntil> Build()
public IEnumerable<WaitStrategy> Build()
{
return _waitStrategies;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
namespace DotNet.Testcontainers.Configurations
{
using System;
using System.Collections.Generic;
using System.Linq;

/// <inheritdoc cref="IWaitForContainerOS" />
internal sealed class WaitForContainerUnix : WaitForContainerOS
{
/// <inheritdoc />
public override IWaitForContainerOS UntilCommandIsCompleted(string command)
public override IWaitForContainerOS UntilCommandIsCompleted(params string[] command)
{
return AddCustomWaitStrategy(new UntilUnixCommandIsCompleted(command));
}

/// <inheritdoc />
public override IWaitForContainerOS UntilCommandIsCompleted(params string[] command)
public override IWaitForContainerOS UntilCommandIsCompleted(string command, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilUnixCommandIsCompleted(command));
return AddCustomWaitStrategy(new UntilUnixCommandIsCompleted(command), waitStrategyModifier);
}

/// <inheritdoc />
public override IWaitForContainerOS UntilCommandIsCompleted(IEnumerable<string> command, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilUnixCommandIsCompleted(command.ToArray()), waitStrategyModifier);
}

/// <inheritdoc />
public override IWaitForContainerOS UntilPortIsAvailable(int port)
public override IWaitForContainerOS UntilPortIsAvailable(int port, Action<IWaitStrategy> waitStrategyModifier = null)
{
return AddCustomWaitStrategy(new UntilUnixPortIsAvailable(port));
return AddCustomWaitStrategy(new UntilUnixPortIsAvailable(port), waitStrategyModifier);
}
}
}
Loading
Loading