From 7c29b2bf90b0e25f928fd9cedd41c21a56e78803 Mon Sep 17 00:00:00 2001 From: arnel Date: Sun, 9 Aug 2026 09:41:14 +0800 Subject: [PATCH 1/2] fix(MongoDb): Probe the server instead of counting log messages before initiating The readiness check that runs before the replica set is initiated counts how often "Waiting for connections" appears in the log and compares that count for equality. The count only grows and the module does not control what else writes that text, so any additional occurrence pushes it past the expected value and the check can never match. The default wait strategy timeout is one hour, so the container start appears to hang. A stale occurrence can also satisfy the check before mongod is serving, which surfaces as ECONNREFUSED on the next command. Ask the server instead. rs.status() answers only once the final mongod is serving: the temporary mongod the official image forks during first-time initialization is not started with --replSet, so it never reports NotYetInitialized. That keeps the handover guarantee #1656 added for #1636 while removing the dependency on log content. Scoped to the replica set path, which is where #1656 introduced this. The non-replica-set wait strategy still counts log messages and has the same weakness, but changing it affects long-standing behaviour for every MongoDb container and is better decided separately. --- src/Testcontainers.MongoDb/MongoDbBuilder.cs | 28 ++++++++- .../MongoDbReplicaSetReadinessTest.cs | 59 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReadinessTest.cs diff --git a/src/Testcontainers.MongoDb/MongoDbBuilder.cs b/src/Testcontainers.MongoDb/MongoDbBuilder.cs index bbc017b83..6cd185609 100644 --- a/src/Testcontainers.MongoDb/MongoDbBuilder.cs +++ b/src/Testcontainers.MongoDb/MongoDbBuilder.cs @@ -198,7 +198,7 @@ private static async Task InitiateReplicaSetAsync(MongoDbContainer container, Mo return; } - var readiness = new WaitIndicateReadiness(configuration); + var readiness = new WaitReplicationEnabled(); // This is a simple workaround to use the default options, which can be configured // with custom configurations as needed. @@ -227,6 +227,32 @@ await WaitStrategy.WaitUntilAsync(initiate, options.Interval, options.Timeout, o } /// + /// + private sealed class WaitReplicationEnabled : IWaitUntil + { + // rs.status() only answers once the final mongod is serving. The official image forks a + // temporary mongod during first-time initialization to create the root user, and that + // process is not started with --replSet, so it never reports NotYetInitialized. This + // preserves the handover guarantee from #1636 without counting log messages, whose + // number depends on log content the module does not control: #1732. + private const string ScriptContent = "try{rs.status();quit(0);}catch(e){quit(e.codeName===\"NotYetInitialized\"?0:1);}"; + + /// + public Task UntilAsync(IContainer container) + { + return UntilAsync(container as MongoDbContainer); + } + + /// + private static async Task UntilAsync(MongoDbContainer container) + { + var execResult = await container.ExecScriptAsync(ScriptContent) + .ConfigureAwait(false); + + return 0L.Equals(execResult.ExitCode); + } + } + private sealed class WaitIndicateReadiness : IWaitUntil { private static readonly string[] LineEndings = { "\r\n", "\n" }; diff --git a/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReadinessTest.cs b/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReadinessTest.cs new file mode 100644 index 000000000..ef3649a27 --- /dev/null +++ b/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReadinessTest.cs @@ -0,0 +1,59 @@ +using System.Text; +using System.Threading; +using DotNet.Testcontainers.Configurations; + +namespace Testcontainers.MongoDb; + +/// +/// The readiness check that runs before the replica set is initiated counts occurrences of a log +/// message and compares that count for equality. Any other log line carrying the same text pushes +/// the count past the expected value, and it can then never match +/// (https://github.com/testcontainers/testcontainers-dotnet/issues/1732). +/// +public sealed class MongoDbReplicaSetReadinessTest : IAsyncLifetime +{ + private const string ExtraMarkerScriptFilePath = "/docker-entrypoint-initdb.d/00-extra-marker.sh"; + + private readonly MongoDbContainer _mongoDbContainer = new MongoDbBuilder(TestSession.GetImageFromDockerfile()) + .WithReplicaSet() + .WithResourceMapping( + Encoding.Default.GetBytes("#!/bin/bash\necho 'Waiting for connections'\necho 'Waiting for connections'\n"), + ExtraMarkerScriptFilePath, + fileMode: Unix.FileMode755) + .Build(); + + public ValueTask InitializeAsync() + { + return ValueTask.CompletedTask; + } + + public async ValueTask DisposeAsync() + { + await _mongoDbContainer.DisposeAsync() + .ConfigureAwait(false); + + GC.SuppressFinalize(this); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task StartsWhenTheLogContainsAdditionalReadinessMessages() + { + // Given + // The default wait strategy timeout is one hour, so bound the wait to keep a regression + // from stalling the test run instead of failing it. + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); + + // When + await _mongoDbContainer.StartAsync(cts.Token) + .ConfigureAwait(true); + + // Then + const string scriptContent = "rs.status().ok;"; + + var execResult = await _mongoDbContainer.ExecScriptAsync(scriptContent, cts.Token) + .ConfigureAwait(true); + + Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); + } +} From d99311ccaaa34dba2017a36142590af2265f643b Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:13:42 +0200 Subject: [PATCH 2/2] chore: Add WaitUntilScriptBase base class --- src/Testcontainers.MongoDb/MongoDbBuilder.cs | 52 ++++++++----------- .../MongoDbReplicaSetReadinessTest.cs | 47 +++++------------ tests/Testcontainers.MongoDb.Tests/Usings.cs | 1 + 3 files changed, 36 insertions(+), 64 deletions(-) diff --git a/src/Testcontainers.MongoDb/MongoDbBuilder.cs b/src/Testcontainers.MongoDb/MongoDbBuilder.cs index 5dc7ab5bc..b3000d5a6 100644 --- a/src/Testcontainers.MongoDb/MongoDbBuilder.cs +++ b/src/Testcontainers.MongoDb/MongoDbBuilder.cs @@ -130,7 +130,7 @@ public override MongoDbContainer Build() } else { - waitUntil = new WaitInitiateReplicaSet(); + waitUntil = new WaitReplicaSetPrimary(); } // If the user does not provide a custom waiting strategy, append the default MongoDb waiting strategy. @@ -198,7 +198,7 @@ private static async Task InitiateReplicaSetAsync(MongoDbContainer container, Mo return; } - var readiness = new WaitReplicationEnabled(); + var readiness = new WaitReplicaSetEnabled(); // This is a simple workaround to use the default options, which can be configured // with custom configurations as needed. @@ -231,32 +231,6 @@ await WaitStrategy.WaitUntilAsync(initiate, options.Interval, options.Timeout, o } /// - /// - private sealed class WaitReplicationEnabled : IWaitUntil - { - // rs.status() only answers once the final mongod is serving. The official image forks a - // temporary mongod during first-time initialization to create the root user, and that - // process is not started with --replSet, so it never reports NotYetInitialized. This - // preserves the handover guarantee from #1636 without counting log messages, whose - // number depends on log content the module does not control: #1732. - private const string ScriptContent = "try{rs.status();quit(0);}catch(e){quit(e.codeName===\"NotYetInitialized\"?0:1);}"; - - /// - public Task UntilAsync(IContainer container) - { - return UntilAsync(container as MongoDbContainer); - } - - /// - private static async Task UntilAsync(MongoDbContainer container) - { - var execResult = await container.ExecScriptAsync(ScriptContent) - .ConfigureAwait(false); - - return 0L.Equals(execResult.ExitCode); - } - } - private sealed class WaitIndicateReadiness : IWaitUntil { private static readonly string[] LineEndings = { "\r\n", "\n" }; @@ -286,9 +260,9 @@ public async Task UntilAsync(IContainer container) } /// - private sealed class WaitInitiateReplicaSet : IWaitUntil + private abstract class WaitUntilScriptBase : IWaitUntil { - private const string ScriptContent = "var r=db.runCommand({hello:1}).isWritablePrimary;quit(r===true?0:1);"; + protected abstract string ScriptContent { get; } /// public Task UntilAsync(IContainer container) @@ -297,7 +271,7 @@ public Task UntilAsync(IContainer container) } /// - private static async Task UntilAsync(MongoDbContainer container) + private async Task UntilAsync(MongoDbContainer container) { var execResult = await container.ExecScriptAsync(ScriptContent) .ConfigureAwait(false); @@ -305,4 +279,20 @@ private static async Task UntilAsync(MongoDbContainer container) return 0L.Equals(execResult.ExitCode); } } + + /// + private sealed class WaitReplicaSetEnabled : WaitUntilScriptBase + { + // rs.status() only answers once the final mongod is serving. The official image + // forks a temporary mongod during first-time initialization to create the root + // user. That process is not started with --replSet, so it never reports + // NotYetInitialized. + protected override string ScriptContent => "try{rs.status();quit(0);}catch(e){quit(e.codeName===\"NotYetInitialized\"?0:1);}"; + } + + /// + private sealed class WaitReplicaSetPrimary : WaitUntilScriptBase + { + protected override string ScriptContent => "var r=db.runCommand({hello:1}).isWritablePrimary;quit(r===true?0:1);"; + } } \ No newline at end of file diff --git a/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReadinessTest.cs b/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReadinessTest.cs index ef3649a27..6f5db835f 100644 --- a/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReadinessTest.cs +++ b/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReadinessTest.cs @@ -1,59 +1,40 @@ -using System.Text; -using System.Threading; -using DotNet.Testcontainers.Configurations; - namespace Testcontainers.MongoDb; -/// -/// The readiness check that runs before the replica set is initiated counts occurrences of a log -/// message and compares that count for equality. Any other log line carrying the same text pushes -/// the count past the expected value, and it can then never match -/// (https://github.com/testcontainers/testcontainers-dotnet/issues/1732). -/// public sealed class MongoDbReplicaSetReadinessTest : IAsyncLifetime { - private const string ExtraMarkerScriptFilePath = "/docker-entrypoint-initdb.d/00-extra-marker.sh"; + private const string ReadinessMessagesScriptContent = "#!/bin/sh\necho 'Waiting for connections'\necho 'Waiting for connections'\n"; + + private const string ReadinessMessagesScriptFilePath = "/docker-entrypoint-initdb.d/00-readiness-messages.sh"; private readonly MongoDbContainer _mongoDbContainer = new MongoDbBuilder(TestSession.GetImageFromDockerfile()) .WithReplicaSet() - .WithResourceMapping( - Encoding.Default.GetBytes("#!/bin/bash\necho 'Waiting for connections'\necho 'Waiting for connections'\n"), - ExtraMarkerScriptFilePath, - fileMode: Unix.FileMode755) + .WithResourceMapping(Encoding.Default.GetBytes(ReadinessMessagesScriptContent), ReadinessMessagesScriptFilePath, fileMode: Unix.FileMode755) .Build(); - public ValueTask InitializeAsync() + public async ValueTask InitializeAsync() { - return ValueTask.CompletedTask; + await _mongoDbContainer.StartAsync() + .ConfigureAwait(false); } - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { - await _mongoDbContainer.DisposeAsync() - .ConfigureAwait(false); - - GC.SuppressFinalize(this); + return _mongoDbContainer.DisposeAsync(); } [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task StartsWhenTheLogContainsAdditionalReadinessMessages() + public async Task StartsWithAdditionalReadinessMessages() { // Given - // The default wait strategy timeout is one hour, so bound the wait to keep a regression - // from stalling the test run instead of failing it. - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); + const string scriptContent = "rs.status().ok;"; // When - await _mongoDbContainer.StartAsync(cts.Token) + var execResult = await _mongoDbContainer.ExecScriptAsync(scriptContent, TestContext.Current.CancellationToken) .ConfigureAwait(true); // Then - const string scriptContent = "rs.status().ok;"; - - var execResult = await _mongoDbContainer.ExecScriptAsync(scriptContent, cts.Token) - .ConfigureAwait(true); - Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); + Assert.Empty(execResult.Stderr); } -} +} \ No newline at end of file diff --git a/tests/Testcontainers.MongoDb.Tests/Usings.cs b/tests/Testcontainers.MongoDb.Tests/Usings.cs index d9d3fa9ac..26affceb1 100644 --- a/tests/Testcontainers.MongoDb.Tests/Usings.cs +++ b/tests/Testcontainers.MongoDb.Tests/Usings.cs @@ -1,6 +1,7 @@ global using System; global using System.Collections.Generic; global using System.Linq; +global using System.Text; global using System.Threading.Tasks; global using DotNet.Testcontainers.Commons; global using DotNet.Testcontainers.Configurations;