From 6baeb832187c357f5f23d18710a8da3d09eaa3dc Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:18:18 +0200 Subject: [PATCH 1/2] feat: Add wait strategy options Signed-off-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- docs/api/wait_strategies.md | 9 + .../CouchbaseBuilder.cs | 4 +- .../Containers/ContainerConfiguration.cs | 6 +- .../Containers/IContainerConfiguration.cs | 2 +- .../WaitStrategies/IWaitForContainerOS.cs | 47 +++-- .../WaitStrategies/IWaitStrategy.cs | 33 ++++ .../WaitStrategies/WaitForContainerOS.cs | 50 +++-- .../WaitStrategies/WaitForContainerUnix.cs | 20 +- .../WaitStrategies/WaitForContainerWindows.cs | 20 +- .../WaitStrategies/WaitStrategy.cs | 171 ++++++++++++++++-- .../Containers/DockerContainer.cs | 81 ++++++--- 11 files changed, 355 insertions(+), 88 deletions(-) create mode 100644 src/Testcontainers/Configurations/WaitStrategies/IWaitStrategy.cs diff --git a/docs/api/wait_strategies.md b/docs/api/wait_strategies.md index 6b38ba2e3..5c293e8d5 100644 --- a/docs/api/wait_strategies.md +++ b/docs/api/wait_strategies.md @@ -11,6 +11,15 @@ _ = Wait.ForUnixContainer() .AddCustomWaitStrategy(new MyCustomWaitStrategy()); ``` + + ## 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. diff --git a/src/Testcontainers.Couchbase/CouchbaseBuilder.cs b/src/Testcontainers.Couchbase/CouchbaseBuilder.cs index b41a63b99..4d6e98105 100644 --- a/src/Testcontainers.Couchbase/CouchbaseBuilder.cs +++ b/src/Testcontainers.Couchbase/CouchbaseBuilder.cs @@ -183,7 +183,7 @@ private CouchbaseBuilder WithBucket(params CouchbaseBucket[] bucket) /// Cancellation token. 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())) @@ -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); } diff --git a/src/Testcontainers/Configurations/Containers/ContainerConfiguration.cs b/src/Testcontainers/Configurations/Containers/ContainerConfiguration.cs index 6fc4da4d7..b2a26a0d2 100644 --- a/src/Testcontainers/Configurations/Containers/ContainerConfiguration.cs +++ b/src/Testcontainers/Configurations/Containers/ContainerConfiguration.cs @@ -60,7 +60,7 @@ public ContainerConfiguration( IEnumerable networkAliases = null, IEnumerable extraHosts = null, IOutputConsumer outputConsumer = null, - IEnumerable waitStrategies = null, + IEnumerable waitStrategies = null, Func startupCallback = null, bool? autoRemove = null, bool? privileged = null) @@ -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>(oldValue.WaitStrategies, newValue.WaitStrategies); + WaitStrategies = BuildConfiguration.Combine>(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); @@ -212,7 +212,7 @@ public ContainerConfiguration(IContainerConfiguration oldValue, IContainerConfig /// [JsonIgnore] - public IEnumerable WaitStrategies { get; } + public IEnumerable WaitStrategies { get; } /// [JsonIgnore] diff --git a/src/Testcontainers/Configurations/Containers/IContainerConfiguration.cs b/src/Testcontainers/Configurations/Containers/IContainerConfiguration.cs index 1093e8480..8966f48d3 100644 --- a/src/Testcontainers/Configurations/Containers/IContainerConfiguration.cs +++ b/src/Testcontainers/Configurations/Containers/IContainerConfiguration.cs @@ -119,7 +119,7 @@ public interface IContainerConfiguration : IResourceConfiguration /// Gets the wait strategies. /// - IEnumerable WaitStrategies { get; } + IEnumerable WaitStrategies { get; } /// /// Gets the startup callback. diff --git a/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs b/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs index f4e3d2df5..341a3976f 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs @@ -14,98 +14,121 @@ public interface IWaitForContainerOS /// /// Adds a custom wait strategy to the wait strategies collection. /// - /// The wait strategy until the container is ready. + /// The wait strategy until the container is ready. + /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . /// Already contains as default wait strategy. [PublicAPI] - IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitStrategy); + IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, Action waitStrategyModifier = null); /// /// Waits until the command is completed successfully. /// /// The command to be executed. /// A configured instance of . + /// + /// Does not invoke the operating system command shell. + /// Normal shell processing does not happen. Expects the exit code to be 0. + /// + [PublicAPI] + IWaitForContainerOS UntilCommandIsCompleted(params string[] command); + + /// + /// Waits until the command is completed successfully. + /// + /// The command to be executed. + /// The wait strategy modifier to cancel the readiness check. + /// A configured instance of . /// Invokes the operating system command shell. Expects the exit code to be 0. [PublicAPI] - IWaitForContainerOS UntilCommandIsCompleted(string command); + IWaitForContainerOS UntilCommandIsCompleted(string command, Action waitStrategyModifier = null); /// /// Waits until the command is completed successfully. /// /// The command to be executed. + /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . /// /// Does not invoke the operating system command shell. /// Normal shell processing does not happen. Expects the exit code to be 0. /// [PublicAPI] - IWaitForContainerOS UntilCommandIsCompleted(params string[] command); + IWaitForContainerOS UntilCommandIsCompleted(IEnumerable command, Action waitStrategyModifier = null); /// /// Waits until the port is available. /// /// The port to be checked. + /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . [PublicAPI] - IWaitForContainerOS UntilPortIsAvailable(int port); + IWaitForContainerOS UntilPortIsAvailable(ushort port, Action waitStrategyModifier = null); /// /// Waits until the file exists. /// /// The file path to be checked. /// The file system to be checked. + /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . [PublicAPI] - IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host); + IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host, Action waitStrategyModifier = null); /// /// Waits until the message is logged. /// /// The regular expression that matches the log message. + /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . [PublicAPI] - IWaitForContainerOS UntilMessageIsLogged(string pattern); + IWaitForContainerOS UntilMessageIsLogged(string pattern, Action waitStrategyModifier = null); /// /// Waits until the message is logged. /// /// The regular expression that matches the log message. + /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . [PublicAPI] - IWaitForContainerOS UntilMessageIsLogged(Regex pattern); + IWaitForContainerOS UntilMessageIsLogged(Regex pattern, Action waitStrategyModifier = null); /// /// Waits until the operation is completed successfully. /// /// The operation to be executed. /// The number of attempts before an exception is thrown. + /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . /// Thrown when number of failed operations exceeded . [PublicAPI] - IWaitForContainerOS UntilOperationIsSucceeded(Func operation, int maxCallCount); + [Obsolete("Use one of the other wait strategies in combination with the `Action` argument, and set the number of retries.")] + IWaitForContainerOS UntilOperationIsSucceeded(Func operation, int maxCallCount, Action waitStrategyModifier = null); /// /// Waits until the http request is completed successfully. /// /// The http request to be executed. + /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . [PublicAPI] - IWaitForContainerOS UntilHttpRequestIsSucceeded(Func request); + IWaitForContainerOS UntilHttpRequestIsSucceeded(Func request, Action waitStrategyModifier = null); /// /// Waits until the container is healthy. /// /// The number of attempts before an exception is thrown. + /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . /// Thrown when number of failed operations exceeded . [PublicAPI] - IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3); + IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3, Action waitStrategyModifier = null); /// /// Returns a collection with all configured wait strategies. /// /// Returns a list with all configured wait strategies. [PublicAPI] - IEnumerable Build(); + IEnumerable Build(); } } diff --git a/src/Testcontainers/Configurations/WaitStrategies/IWaitStrategy.cs b/src/Testcontainers/Configurations/WaitStrategies/IWaitStrategy.cs new file mode 100644 index 000000000..d245412a9 --- /dev/null +++ b/src/Testcontainers/Configurations/WaitStrategies/IWaitStrategy.cs @@ -0,0 +1,33 @@ +namespace DotNet.Testcontainers.Configurations +{ + using System; + using JetBrains.Annotations; + + /// + /// Represents a wait strategy configuration. + /// + [PublicAPI] + public interface IWaitStrategy + { + /// + /// Sets the number of retries for the wait strategy. + /// + /// The number of retries. + /// The updated instance of the wait strategy. + IWaitStrategy WithRetries(ushort retries); + + /// + /// Sets the interval between retries for the wait strategy. + /// + /// The interval between retries. + /// The updated instance of the wait strategy. + IWaitStrategy WithInterval(TimeSpan interval); + + /// + /// Sets the timeout for the wait strategy. + /// + /// The timeout duration. + /// The updated instance of the wait strategy. + IWaitStrategy WithTimeout(TimeSpan timeout); + } +} diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs index 669c5e062..94b48e183 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs @@ -7,77 +7,87 @@ namespace DotNet.Testcontainers.Configurations /// internal abstract class WaitForContainerOS : IWaitForContainerOS { - private readonly ICollection _waitStrategies = new List(); + private readonly ICollection _waitStrategies = new List(); /// /// Initializes a new instance of the class. /// protected WaitForContainerOS() { - _waitStrategies.Add(new UntilContainerIsRunning()); + _waitStrategies.Add(new WaitStrategy(new UntilContainerIsRunning())); } /// - public abstract IWaitForContainerOS UntilCommandIsCompleted(string command); + public abstract IWaitForContainerOS UntilCommandIsCompleted(params string[] command); /// - public abstract IWaitForContainerOS UntilCommandIsCompleted(params string[] command); + public abstract IWaitForContainerOS UntilCommandIsCompleted(string command, Action waitStrategyModifier = null); + + /// + public abstract IWaitForContainerOS UntilCommandIsCompleted(IEnumerable command, Action waitStrategyModifier = null); /// - public abstract IWaitForContainerOS UntilPortIsAvailable(int port); + public abstract IWaitForContainerOS UntilPortIsAvailable(ushort port, Action waitStrategyModifier = null); /// - public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitStrategy) + public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, Action waitStrategyModifier = null) { + var waitStrategy = new WaitStrategy(waitUntil); + + if (waitStrategyModifier != null) + { + waitStrategyModifier(waitStrategy); + } + _waitStrategies.Add(waitStrategy); return this; } /// - public virtual IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host) + public virtual IWaitForContainerOS UntilFileExists(string filePath, FileSystem fileSystem = FileSystem.Host, Action 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); } } /// - public IWaitForContainerOS UntilMessageIsLogged(string pattern) + public IWaitForContainerOS UntilMessageIsLogged(string pattern, Action waitStrategyModifier = null) { - return AddCustomWaitStrategy(new UntilMessageIsLogged(pattern)); + return AddCustomWaitStrategy(new UntilMessageIsLogged(pattern), waitStrategyModifier); } /// - public IWaitForContainerOS UntilMessageIsLogged(Regex pattern) + public IWaitForContainerOS UntilMessageIsLogged(Regex pattern, Action waitStrategyModifier = null) { - return AddCustomWaitStrategy(new UntilMessageIsLogged(pattern)); + return AddCustomWaitStrategy(new UntilMessageIsLogged(pattern), waitStrategyModifier); } /// - public virtual IWaitForContainerOS UntilOperationIsSucceeded(Func operation, int maxCallCount) + public virtual IWaitForContainerOS UntilOperationIsSucceeded(Func operation, int maxCallCount, Action waitStrategyModifier = null) { - return AddCustomWaitStrategy(new UntilOperationIsSucceeded(operation, maxCallCount)); + return AddCustomWaitStrategy(new UntilOperationIsSucceeded(operation, maxCallCount), waitStrategyModifier); } /// - public virtual IWaitForContainerOS UntilHttpRequestIsSucceeded(Func request) + public virtual IWaitForContainerOS UntilHttpRequestIsSucceeded(Func request, Action waitStrategyModifier = null) { - return AddCustomWaitStrategy(request.Invoke(new HttpWaitStrategy())); + return AddCustomWaitStrategy(request.Invoke(new HttpWaitStrategy()), waitStrategyModifier); } /// - public virtual IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3) + public virtual IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 3, Action waitStrategyModifier = null) { - return AddCustomWaitStrategy(new UntilContainerIsHealthy(failingStreak)); + return AddCustomWaitStrategy(new UntilContainerIsHealthy(failingStreak), waitStrategyModifier); } /// - public IEnumerable Build() + public IEnumerable Build() { return _waitStrategies; } diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerUnix.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerUnix.cs index fb1272984..e4c9f4933 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerUnix.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerUnix.cs @@ -1,24 +1,34 @@ namespace DotNet.Testcontainers.Configurations { + using System; + using System.Collections.Generic; + using System.Linq; + /// internal sealed class WaitForContainerUnix : WaitForContainerOS { /// - public override IWaitForContainerOS UntilCommandIsCompleted(string command) + public override IWaitForContainerOS UntilCommandIsCompleted(params string[] command) { return AddCustomWaitStrategy(new UntilUnixCommandIsCompleted(command)); } /// - public override IWaitForContainerOS UntilCommandIsCompleted(params string[] command) + public override IWaitForContainerOS UntilCommandIsCompleted(string command, Action waitStrategyModifier = null) { - return AddCustomWaitStrategy(new UntilUnixCommandIsCompleted(command)); + return AddCustomWaitStrategy(new UntilUnixCommandIsCompleted(command), waitStrategyModifier); + } + + /// + public override IWaitForContainerOS UntilCommandIsCompleted(IEnumerable command, Action waitStrategyModifier = null) + { + return AddCustomWaitStrategy(new UntilUnixCommandIsCompleted(command.ToArray()), waitStrategyModifier); } /// - public override IWaitForContainerOS UntilPortIsAvailable(int port) + public override IWaitForContainerOS UntilPortIsAvailable(ushort port, Action waitStrategyModifier = null) { - return AddCustomWaitStrategy(new UntilUnixPortIsAvailable(port)); + return AddCustomWaitStrategy(new UntilUnixPortIsAvailable(port), waitStrategyModifier); } } } diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerWindows.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerWindows.cs index d4e6f0719..7e2205904 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerWindows.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerWindows.cs @@ -1,24 +1,34 @@ namespace DotNet.Testcontainers.Configurations { + using System; + using System.Collections.Generic; + using System.Linq; + /// internal sealed class WaitForContainerWindows : WaitForContainerOS { /// - public override IWaitForContainerOS UntilCommandIsCompleted(string command) + public override IWaitForContainerOS UntilCommandIsCompleted(params string[] command) { return AddCustomWaitStrategy(new UntilWindowsCommandIsCompleted(command)); } /// - public override IWaitForContainerOS UntilCommandIsCompleted(params string[] command) + public override IWaitForContainerOS UntilCommandIsCompleted(string command, Action waitStrategyModifier = null) { - return AddCustomWaitStrategy(new UntilWindowsCommandIsCompleted(command)); + return AddCustomWaitStrategy(new UntilWindowsCommandIsCompleted(command), waitStrategyModifier); + } + + /// + public override IWaitForContainerOS UntilCommandIsCompleted(IEnumerable command, Action waitStrategyModifier = null) + { + return AddCustomWaitStrategy(new UntilWindowsCommandIsCompleted(command.ToArray()), waitStrategyModifier); } /// - public override IWaitForContainerOS UntilPortIsAvailable(int port) + public override IWaitForContainerOS UntilPortIsAvailable(ushort port, Action waitStrategyModifier = null) { - return AddCustomWaitStrategy(new UntilWindowsPortIsAvailable(port)); + return AddCustomWaitStrategy(new UntilWindowsPortIsAvailable(port), waitStrategyModifier); } } } diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs index 137576867..75466647d 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs @@ -3,22 +3,124 @@ namespace DotNet.Testcontainers.Configurations using System; using System.Threading; using System.Threading.Tasks; + using DotNet.Testcontainers.Containers; using JetBrains.Annotations; - public static class WaitStrategy + /// + [PublicAPI] + public class WaitStrategy : IWaitStrategy { + private const string MaximumRetryExceededException = "The maximum number of retries has been exceeded."; + + private IWaitWhile _waitWhile; + + private IWaitUntil _waitUntil; + + /// + /// Initializes a new instance of the class. + /// + public WaitStrategy() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The wait while condition to be used in the strategy. + public WaitStrategy(IWaitWhile waitWhile) + { + _ = WithStrategy(waitWhile); + } + + /// + /// Initializes a new instance of the class. + /// + /// The wait until condition to be used in the strategy. + public WaitStrategy(IWaitUntil waitUntil) + { + _ = WithStrategy(waitUntil); + } + + /// + /// Gets the number of retries. + /// + public int Retries { get; private set; } + = -1; + + /// + /// Gets the interval between retries. + /// + public TimeSpan Interval { get; private set; } + = TimeSpan.FromSeconds(1); + + /// + /// Gets the timeout. + /// + public TimeSpan Timeout { get; private set; } + = TimeSpan.FromHours(1); + + /// + public IWaitStrategy WithRetries(ushort retries) + { + Retries = retries; + return this; + } + + /// + public IWaitStrategy WithInterval(TimeSpan interval) + { + Interval = interval; + return this; + } + + /// + public IWaitStrategy WithTimeout(TimeSpan timeout) + { + Timeout = timeout; + return this; + } + + /// + /// Executes the wait strategy while the container satisfies the condition. + /// + /// The container to check the condition for. + /// Cancellation token. + /// A task representing the asynchronous operation, returning false if the container satisfies the condition; otherwise, true. + public virtual Task WhileAsync(IContainer container, CancellationToken ct = default) + { + return _waitWhile.WhileAsync(container); + } + + /// + /// Executes the wait strategy until the container satisfies the condition. + /// + /// The container to check the condition for. + /// Cancellation token. + /// A task representing the asynchronous operation, returning true if the container satisfies the condition; otherwise, false. + public virtual Task UntilAsync(IContainer container, CancellationToken ct = default) + { + return _waitUntil.UntilAsync(container); + } + /// - /// Blocks while condition is true or timeout occurs. + /// Waits asynchronously until the specified condition returns false or until a timeout occurs. /// - /// Function to block execution. - /// The frequency in milliseconds to check the condition. - /// Timeout in milliseconds. - /// Propagates notification that operations should be canceled. - /// Thrown as soon as the timeout expires. + /// + /// Zero or a negative value for will run the readiness check infinitely until it becomes false. + /// + /// A function that represents the asynchronous condition to wait for. + /// The time interval between consecutive evaluations of the condition. + /// The maximum duration to wait for the condition to become false. + /// The number of retries to run for the condition to become false. + /// The optional cancellation token to cancel the waiting operation. + /// Thrown when the timeout expires. + /// Thrown when the number of retries is exceeded. /// A task that represents the asynchronous block operation. [PublicAPI] - public static async Task WaitWhileAsync(Func> wait, TimeSpan frequency, TimeSpan timeout, CancellationToken ct = default) + public static async Task WaitWhileAsync(Func> wait, TimeSpan interval, TimeSpan timeout, int retries = -1, CancellationToken ct = default) { + ushort actualRetries = 0; + async Task WhileAsync() { while (!ct.IsCancellationRequested) @@ -31,7 +133,10 @@ async Task WhileAsync() break; } - await Task.Delay(frequency, ct) + _ = Guard.Argument(retries, nameof(retries)) + .ThrowIf(_ => retries > 0 && ++actualRetries > retries, _ => throw new ArgumentException(MaximumRetryExceededException)); + + await Task.Delay(interval, ct) .ConfigureAwait(false); } } @@ -54,17 +159,24 @@ await waitTask } /// - /// Blocks until condition is true or timeout occurs. + /// Waits asynchronously until the specified condition returns true or until a timeout occurs. /// - /// Function to block execution. - /// The frequency in milliseconds to check the condition. - /// The timeout in milliseconds. - /// Propagates notification that operations should be canceled. - /// Thrown as soon as the timeout expires. + /// + /// Zero or a negative value for will run the readiness check infinitely until it becomes true. + /// + /// A function that represents the asynchronous condition to wait for. + /// The time interval between consecutive evaluations of the condition. + /// The maximum duration to wait for the condition to become true. + /// The number of retries to run for the condition to become true. + /// The optional cancellation token to cancel the waiting operation. + /// Thrown when the timeout expires. + /// Thrown when the number of retries is exceeded. /// A task that represents the asynchronous block operation. [PublicAPI] - public static async Task WaitUntilAsync(Func> wait, TimeSpan frequency, TimeSpan timeout, CancellationToken ct = default) + public static async Task WaitUntilAsync(Func> wait, TimeSpan interval, TimeSpan timeout, int retries = -1, CancellationToken ct = default) { + ushort actualRetries = 0; + async Task UntilAsync() { while (!ct.IsCancellationRequested) @@ -77,7 +189,10 @@ async Task UntilAsync() break; } - await Task.Delay(frequency, ct) + _ = Guard.Argument(retries, nameof(retries)) + .ThrowIf(_ => retries > 0 && ++actualRetries > retries, _ => throw new ArgumentException(MaximumRetryExceededException)); + + await Task.Delay(interval, ct) .ConfigureAwait(false); } } @@ -98,5 +213,27 @@ await Task.Delay(frequency, ct) await waitTask .ConfigureAwait(false); } + + /// + /// Sets the wait while condition. + /// + /// The wait while condition to be used in the strategy. + /// The updated instance of the wait strategy. + private WaitStrategy WithStrategy(IWaitWhile waitWhile) + { + _waitWhile = waitWhile; + return this; + } + + /// + /// Sets the wait until condition. + /// + /// The wait until condition to be used in the strategy. + /// The updated instance of the wait strategy. + private WaitStrategy WithStrategy(IWaitUntil waitUntil) + { + _waitUntil = waitUntil; + return this; + } } } diff --git a/src/Testcontainers/Containers/DockerContainer.cs b/src/Testcontainers/Containers/DockerContainer.cs index 1343fce58..d18658eec 100644 --- a/src/Testcontainers/Containers/DockerContainer.cs +++ b/src/Testcontainers/Containers/DockerContainer.cs @@ -455,23 +455,7 @@ protected virtual async Task UnsafeStartAsync(CancellationToken ct = default) { ThrowIfLockNotAcquired(); - async Task CheckPortBindingsAsync() - { - _container = await _client.Container.ByIdAsync(_container.ID, ct) - .ConfigureAwait(false); - - var boundPorts = _container.NetworkSettings.Ports.Values.Where(portBindings => portBindings != null).SelectMany(portBinding => portBinding).Count(portBinding => !string.IsNullOrEmpty(portBinding.HostPort)); - return _configuration.PortBindings == null || /* IPv4 or IPv6 */ _configuration.PortBindings.Count == boundPorts || /* IPv4 and IPv6 */ 2 * _configuration.PortBindings.Count == boundPorts; - } - - async Task CheckWaitStrategyAsync(IWaitUntil wait) - { - _container = await _client.Container.ByIdAsync(_container.ID, ct) - .ConfigureAwait(false); - - return await wait.UntilAsync(this) - .ConfigureAwait(false); - } + WaitStrategy portBindingsMapped = new WaitUntilPortBindingsMapped(this); await _client.AttachAsync(_container.ID, _configuration.OutputConsumer, ct) .ConfigureAwait(false); @@ -479,7 +463,7 @@ await _client.AttachAsync(_container.ID, _configuration.OutputConsumer, ct) await _client.StartAsync(_container.ID, ct) .ConfigureAwait(false); - await WaitStrategy.WaitUntilAsync(CheckPortBindingsAsync, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(15), ct) + _ = await CheckReadinessAsync(new [] { portBindingsMapped }, ct) .ConfigureAwait(false); Starting?.Invoke(this, EventArgs.Empty); @@ -489,11 +473,8 @@ await _configuration.StartupCallback(this, ct) Logger.StartReadinessCheck(_container.ID); - foreach (var waitStrategy in _configuration.WaitStrategies) - { - await WaitStrategy.WaitUntilAsync(() => CheckWaitStrategyAsync(waitStrategy), TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan, ct) - .ConfigureAwait(false); - } + _ = await CheckReadinessAsync(_configuration.WaitStrategies, ct) + .ConfigureAwait(false); Logger.CompleteReadinessCheck(_container.ID); @@ -535,5 +516,59 @@ protected override bool Exists() { return _container != null && ContainerHasBeenCreatedStates.HasFlag(State); } + + /// + /// Updates the internal container field and checks whether the wait strategy indicates readiness or not. + /// + /// The wait strategy to execute. + /// Cancellation token. + /// A task representing the asynchronous operation, returning true if the wait strategy indicates readiness; otherwise, false. + private async Task CheckReadinessAsync(WaitStrategy waitStrategy, CancellationToken ct = default) + { + _container = await _client.Container.ByIdAsync(_container.ID, ct) + .ConfigureAwait(false); + + return await waitStrategy.UntilAsync(this, ct) + .ConfigureAwait(false); + } + + /// + /// Updates the internal container field and checks whether the wait strategy indicates readiness or not. + /// + /// + /// To cancel the readiness check, each wait strategy can be configured using the + /// members, utilizing the wait strategy modifier. + /// + /// The wait strategies to execute. + /// Cancellation token. + /// A task representing the asynchronous operation, returning true if the wait strategies indicate readiness; otherwise, false. + private async Task CheckReadinessAsync(IEnumerable waitStrategies, CancellationToken ct = default) + { + foreach (var waitStrategy in waitStrategies) + { + await WaitStrategy.WaitUntilAsync(() => CheckReadinessAsync(waitStrategy, ct), waitStrategy.Interval, waitStrategy.Timeout, waitStrategy.Retries, ct) + .ConfigureAwait(false); + } + + return true; + } + + private sealed class WaitUntilPortBindingsMapped : WaitStrategy + { + private readonly DockerContainer _parent; + + public WaitUntilPortBindingsMapped(DockerContainer parent) + { + _parent = parent; + _ = WithInterval(TimeSpan.FromSeconds(1)); + _ = WithTimeout(TimeSpan.FromSeconds(15)); + } + + public override Task UntilAsync(IContainer _, CancellationToken ct = default) + { + var boundPorts = _parent._container.NetworkSettings.Ports.Values.Where(portBindings => portBindings != null).SelectMany(portBinding => portBinding).Count(portBinding => !string.IsNullOrEmpty(portBinding.HostPort)); + return Task.FromResult(_parent._configuration.PortBindings == null || /* IPv4 or IPv6 */ _parent._configuration.PortBindings.Count == boundPorts || /* IPv4 and IPv6 */ 2 * _parent._configuration.PortBindings.Count == boundPorts); + } + } } } From acac3070fbc3a452e574d767235ec1d07535ea0d Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 3 May 2024 15:57:59 +0200 Subject: [PATCH 2/2] chore: Change signature port arg ushort to int --- .../Configurations/WaitStrategies/IWaitForContainerOS.cs | 2 +- .../Configurations/WaitStrategies/WaitForContainerOS.cs | 2 +- .../Configurations/WaitStrategies/WaitForContainerUnix.cs | 2 +- .../Configurations/WaitStrategies/WaitForContainerWindows.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs b/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs index 341a3976f..615ccbb17 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs @@ -63,7 +63,7 @@ public interface IWaitForContainerOS /// The wait strategy modifier to cancel the readiness check. /// A configured instance of . [PublicAPI] - IWaitForContainerOS UntilPortIsAvailable(ushort port, Action waitStrategyModifier = null); + IWaitForContainerOS UntilPortIsAvailable(int port, Action waitStrategyModifier = null); /// /// Waits until the file exists. diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs index 94b48e183..4a6646c47 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs @@ -27,7 +27,7 @@ protected WaitForContainerOS() public abstract IWaitForContainerOS UntilCommandIsCompleted(IEnumerable command, Action waitStrategyModifier = null); /// - public abstract IWaitForContainerOS UntilPortIsAvailable(ushort port, Action waitStrategyModifier = null); + public abstract IWaitForContainerOS UntilPortIsAvailable(int port, Action waitStrategyModifier = null); /// public virtual IWaitForContainerOS AddCustomWaitStrategy(IWaitUntil waitUntil, Action waitStrategyModifier = null) diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerUnix.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerUnix.cs index e4c9f4933..e1e132a51 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerUnix.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerUnix.cs @@ -26,7 +26,7 @@ public override IWaitForContainerOS UntilCommandIsCompleted(IEnumerable } /// - public override IWaitForContainerOS UntilPortIsAvailable(ushort port, Action waitStrategyModifier = null) + public override IWaitForContainerOS UntilPortIsAvailable(int port, Action waitStrategyModifier = null) { return AddCustomWaitStrategy(new UntilUnixPortIsAvailable(port), waitStrategyModifier); } diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerWindows.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerWindows.cs index 7e2205904..756063f11 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerWindows.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerWindows.cs @@ -26,7 +26,7 @@ public override IWaitForContainerOS UntilCommandIsCompleted(IEnumerable } /// - public override IWaitForContainerOS UntilPortIsAvailable(ushort port, Action waitStrategyModifier = null) + public override IWaitForContainerOS UntilPortIsAvailable(int port, Action waitStrategyModifier = null) { return AddCustomWaitStrategy(new UntilWindowsPortIsAvailable(port), waitStrategyModifier); }