From 3a5e858556c68ac938c5d2efd96fa2d0df0a1cdd Mon Sep 17 00:00:00 2001 From: arnel Date: Fri, 7 Aug 2026 22:24:36 +0800 Subject: [PATCH] fix(MongoDb): Treat an already initiated replica set as success The startup callback runs on every start, so a reused container runs the replica set initiate script against a set that is already initiated. mongosh throws MongoServerError (code 23, AlreadyInitialized) rather than returning a result, so the script exits non-zero and the wait strategy retries it until it times out. The default timeout is one hour, so the caller appears to hang. Catch that error and exit zero, which makes the callback idempotent. Adds a reuse test that starts the same replica set container twice. It times out before this change and completes in a few seconds after it. --- src/Testcontainers.MongoDb/MongoDbBuilder.cs | 6 +- .../MongoDbReplicaSetReuseTest.cs | 72 +++++++++++++++++++ tests/Testcontainers.MongoDb.Tests/Usings.cs | 3 + 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReuseTest.cs diff --git a/src/Testcontainers.MongoDb/MongoDbBuilder.cs b/src/Testcontainers.MongoDb/MongoDbBuilder.cs index bbc017b83..736f13c6a 100644 --- a/src/Testcontainers.MongoDb/MongoDbBuilder.cs +++ b/src/Testcontainers.MongoDb/MongoDbBuilder.cs @@ -204,7 +204,11 @@ private static async Task InitiateReplicaSetAsync(MongoDbContainer container, Mo // with custom configurations as needed. var options = new WaitStrategy(); - var scriptContent = $"var r=rs.initiate({{_id:\"{configuration.ReplicaSetName}\",members:[{{_id:0,host:\"127.0.0.1:27017\"}}]}});quit(r.ok===1?0:1);"; + // The startup callback runs on every start, so a container that is restarted or reused + // already holds an initiated replica set. Initiating it again throws instead of returning + // a result, which would otherwise be retried until the wait strategy times out: + // https://github.com/testcontainers/testcontainers-dotnet/issues/1722. + var scriptContent = $"try{{var r=rs.initiate({{_id:\"{configuration.ReplicaSetName}\",members:[{{_id:0,host:\"127.0.0.1:27017\"}}]}});quit(r.ok===1?0:1);}}catch(e){{quit(e.codeName===\"AlreadyInitialized\"?0:1);}}"; var initiate = async () => { diff --git a/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReuseTest.cs b/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReuseTest.cs new file mode 100644 index 000000000..beca79df2 --- /dev/null +++ b/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetReuseTest.cs @@ -0,0 +1,72 @@ +namespace Testcontainers.MongoDb; + +/// +/// Reusing a container runs the startup callback again against a replica set that is already +/// initiated (https://github.com/testcontainers/testcontainers-dotnet/issues/1722). +/// +public sealed class MongoDbReplicaSetReuseTest : IAsyncLifetime +{ + private readonly string _labelKey = Guid.NewGuid().ToString("D"); + + private readonly string _labelValue = Guid.NewGuid().ToString("D"); + + private readonly IList _containers = new List(); + + public ValueTask InitializeAsync() + { + return ValueTask.CompletedTask; + } + + public async ValueTask DisposeAsync() + { + foreach (var container in _containers.Distinct()) + { + await container.DisposeAsync() + .ConfigureAwait(false); + } + + GC.SuppressFinalize(this); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ReusedContainerStartsAgain() + { + // Given + // The default wait strategy timeout is one hour, so bound the wait to keep a regression + // from hanging the test run instead of failing it. + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); + + var container = CreateContainer(); + _containers.Add(container); + + await container.StartAsync(cts.Token) + .ConfigureAwait(true); + + // When + var reusedContainer = CreateContainer(); + _containers.Add(reusedContainer); + + await reusedContainer.StartAsync(cts.Token) + .ConfigureAwait(true); + + // Then + Assert.Equal(container.Id, reusedContainer.Id); + + const string scriptContent = "rs.status().ok;"; + + var execResult = await reusedContainer.ExecScriptAsync(scriptContent, cts.Token) + .ConfigureAwait(true); + + Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); + } + + private MongoDbContainer CreateContainer() + { + return new MongoDbBuilder(TestSession.GetImageFromDockerfile()) + .WithReplicaSet() + .WithLabel(_labelKey, _labelValue) + .WithReuse(true) + .Build(); + } +} diff --git a/tests/Testcontainers.MongoDb.Tests/Usings.cs b/tests/Testcontainers.MongoDb.Tests/Usings.cs index b68e8ba14..2482599f0 100644 --- a/tests/Testcontainers.MongoDb.Tests/Usings.cs +++ b/tests/Testcontainers.MongoDb.Tests/Usings.cs @@ -1,4 +1,7 @@ global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; global using System.Threading.Tasks; global using DotNet.Testcontainers.Commons; global using DotNet.Testcontainers.Configurations;