postgresqldocker.net-coredocker-composemarten

57P01: terminating connection due to administrator command when running multiple tests in sequence


I have a Postgres DB running in a Docker environment which is set up using FluentDocker in a Xunit test.

Xunit is configured to run tests serial.

When running a single test, everything works fine.

However, when adding another test (literally just copying the first test) and running both tests in one call using dotnet test, the second test always fails with 57P01: terminating connection due to administrator command.

I tried to close the DB connection, stop the containers after each test run etc. but the error remains and it always happens at the same line of code.

This is the code of the test:

[Fact]
public async Task ShouldProjectUserRegistration()
{
  var file = Path.Combine(
    Directory.GetCurrentDirectory(),
    (TemplateString)"Resources/docker-compose.yml"
  );

  var service = new Builder()
    .UseContainer()
    .UseCompose()
    .FromFile(file)
    .RemoveOrphans()
    .ForceRecreate()
    .WaitForPort(
      "database",
      "5432/tcp",
      30000 /*30s*/
    )
    .Build();
  var container = service.Start();

  var PgTestConnectionString =
    "PORT = 5432; HOST = localhost; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'marten'; PASSWORD = '123456'; USER ID = 'marten'";
  using var store = DocumentStore.For(
    options =>
    {
      options.Connection(PgTestConnectionString);
      options.AutoCreateSchemaObjects = AutoCreate.All;
      options.Projections.SelfAggregate<User>(ProjectionLifecycle.Inline);
    }
  );

  var id = Guid.NewGuid();
  var username = "jane.doe@acme.inc";
  var userRegistered = new UserRegistered(
    id,
    username
  );

  await using var session = store.OpenSession();
  session.Events.StartStream(
    id,
    userRegistered
  );

  await session.SaveChangesAsync();


  var user = session.Load<User>(id);
  await session.Connection?.CloseAsync();
  service.Stop();
  Assert.Equal(
    username,
    user?.Username
  );

This is the docker-compose.yml:

version: "3"

services:
  database:
    image: library/postgres:14
    environment:
      POSTGRES_USER: 'marten'
      POSTGRES_PASSWORD: '123456'
      POSTGRES_DB: 'marten'
    ports:
      - "5432:5432"

The exception:

Npgsql.PostgresException
57P01: terminating connection due to administrator command
   at Npgsql.Internal.NpgsqlConnector.<ReadMessage>g__ReadMessageLong|213_0(NpgsqlConnector connector, Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
   at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior, Boolean async, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior, Boolean async, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
   at Weasel.Core.CommandBuilderBase`6.ExecuteReaderAsync(TConnection conn, CancellationToken cancellation, TTransaction tx)
   at Weasel.Core.SchemaMigration.Determine(DbConnection conn, ISchemaObject[] schemaObjects)
   at Weasel.Core.Migrations.DatabaseBase`1.executeMigration(ISchemaObject[] schemaObjects, CancellationToken token)
   at Weasel.Core.Migrations.DatabaseBase`1.executeMigration(ISchemaObject[] schemaObjects, CancellationToken token)
   at Weasel.Core.Migrations.DatabaseBase`1.generateOrUpdateFeature(Type featureType, IFeatureSchema feature, CancellationToken token)
   at Weasel.Core.Migrations.DatabaseBase`1.ensureStorageExists(IList`1 types, Type featureType, CancellationToken token)
   at Weasel.Core.Migrations.DatabaseBase`1.ensureStorageExists(IList`1 types, Type featureType, CancellationToken token)
   at Weasel.Core.Migrations.DatabaseBase`1.ensureStorageExists(IList`1 types, Type featureType, CancellationToken token)
   at Marten.Events.EventGraph.ProcessEventsAsync(DocumentSessionBase session, CancellationToken token)
   at Marten.Internal.Sessions.DocumentSessionBase.SaveChangesAsync(CancellationToken token)
   at Marten.Internal.Sessions.DocumentSessionBase.SaveChangesAsync(CancellationToken token)
   at MartenFluentDockerNpsql57P01Repro.Tests.UserProjectionTests.ShouldProjectUserRegistrationSecond() in /Users/alexzeitler/src/MartenFluentDockerNpsql57P01Repro/MartenFluentDockerNpsql57P01Repro.Tests/UserProjectionTests.cs:line 120
   at MartenFluentDockerNpsql57P01Repro.Tests.UserProjectionTests.ShouldProjectUserRegistrationSecond() in /Users/alexzeitler/src/MartenFluentDockerNpsql57P01Repro/MartenFluentDockerNpsql57P01Repro.Tests/UserProjectionTests.cs:line 126
   at Xunit.Sdk.TestInvoker`1.<>c__DisplayClass48_1.<<InvokeTestMethodAsync>b__1>d.MoveNext() in C:\Dev\xunit\xunit\src\xunit.execution\Sdk\Frameworks\Runners\TestInvoker.cs:line 264
--- End of stack trace from previous location ---
   at Xunit.Sdk.ExecutionTimer.AggregateAsync(Func`1 asyncAction) in C:\Dev\xunit\xunit\src\xunit.execution\Sdk\Frameworks\ExecutionTimer.cs:line 48
   at Xunit.Sdk.ExceptionAggregator.RunAsync(Func`1 code) in C:\Dev\xunit\xunit\src\xunit.core\Sdk\ExceptionAggregator.cs:line 90

I created a repro which can be found on GitHub.


Solution

  • You have options in your connection string that generate a connection pool: POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100;

    After you generate this pool and run your test logic, you are only closing the current connection in the pool when you call CloseAsync.

    Because the pool has a minimum size of 1, the pool is going to immediately add a new connection after you close the current connection.

    It looks like shutting down your database is then triggering the error on the new connection that is generated because the connection. So to solve the problem and destroy all connection related resources, you need to use DisposeAsync.

    await session.Connection?.DisposeAsync();
    

    You should likely also set POOLING = False since your test appears to only require a single connection.

    The session appears to be properly closing/disposing the connection via using since the connection is created/managed by Marten. The issue here is that this will NOT properly clean up the connection pool that was generated and will simply return the connection to the pool where it will remain idle. Thus, as long as PgTestConnectionString is the same in ShouldProjectUserRegistration and ShouldProjectUserRegistrationSecond, the pool will try to provide the same connection you used in ShouldProjectUserRegistration. The problem is that, while the connection sits idle in the pool, you terminate the database that it is connected to. Consequently, when you fetch this connection from the pool and try to perform an operation on it, you get the error message you described.

    This situation can be prevented in a few different ways: