Visitar URL original
Add a throwaway primary/standby cluster for replication tests by Brar · Pull Request #6690 · npgsql/npgsql · GitHub
Skip to content

Add a throwaway primary/standby cluster for replication tests - #6690

Open
Brar wants to merge 1 commit into
npgsql:mainfrom
Brar:repl/standby-harness
Open

Brar wants to merge 1 commit into
npgsql:mainfrom
Brar:repl/standby-harness

Conversation

@Brar

@Brar Brar commented Oct 25, 2026

Copy link
Copy Markdown
Member

PgTestCluster creates PostgreSQL clusters in temporary directories with the binaries that NPGSQL_TEST_PG_BIN points to (initdb, pg_ctl, pg_basebackup): a primary that allows physical and logical replication, streaming standbys of it, promotion and shutdown in any mode. This covers tests that can't run against the shared test server, like timeline switches and server shutdowns. Tests using it are ignored when the variable isn't set, so CI is unaffected.

The clusters listen on 127.0.0.1 only (no Unix sockets, whose default directory isn't writable for unprivileged users on Debian-based builds), pick a free port right before each start, retry when another process took it, and limit every step to well below CI's 30 seconds per test.

PgTestCluster creates PostgreSQL clusters in temporary directories with
the binaries that NPGSQL_TEST_PG_BIN points to (initdb, pg_ctl,
pg_basebackup): a primary that allows physical and logical replication,
streaming standbys of it, promotion and shutdown in any mode. This
covers tests that can't run against the shared test server, like
timeline switches and server shutdowns. Tests using it are ignored when
the variable isn't set, so CI is unaffected.

The clusters listen on 127.0.0.1 only (no Unix sockets, whose default
directory isn't writable for unprivileged users on Debian-based
builds), pick a free port right before each start, retry when another
process took it, and limit every step to well below CI's 30 seconds per
test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@roji roji left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommendation

The direction is justified, but fix the primary restart behavior and strengthen the smoke test before merging. Keep this as a small test helper rather than a general cluster-management framework.

Problem and expected approach

The existing TimelineHistory test is explicit because a freshly initialized shared server has no timeline history. Promotion and shutdown tests genuinely need an isolated server. I would expect one internal C# helper, roughly a couple hundred test-only lines, using PostgreSQL's own tools, with primary-to-standby replay, promotion, and cleanup coverage. No shipped API or public documentation changes are needed; documenting the opt-in binary directory is sufficient.

Fit with that scope

The helper largely matches that approach: temporary data directories, native tools, non-pooled connections, bounded process execution, and opt-in execution. Its basic use is straightforward:

await using var primary = await PgTestCluster.StartPrimary();
await using var standby = await primary.StartStandby();
await standby.Promote();

Gaps

The inline comments cover a reproduced correctness issue: Stop()/Start() changes the primary port without updating existing standbys, so replication does not resume. The smoke test also never checks replay; a disconnected standby can still report recovery and promote successfully.

This PR supplies infrastructure, not actual Npgsql replication coverage yet. A small follow-up should use it for the existing timeline test. My direct probe of IdentifySystem() followed by TimelineHistory(identity.Timeline) on the promoted standby threw InvalidCastException (String to Byte[]) at ReplicationConnection.cs:341. That cast already exists in the base commit: it demonstrates the coverage gap, not a product defect introduced by this PR, and should be fixed separately.

Manual validation

I compiled a separate console client against the unmodified helper and this checkout's Npgsql, using PostgreSQL 18.4. I created a primary and standby, inserted a row on the primary and observed it on the standby, reproduced the restart failure, promoted the standby to timeline 2, exercised the timeline API, restarted after immediate shutdown, and verified disposal removed the temporary cluster root. The added NUnit smoke test also passed when enabled and was skipped when the environment variable was absent. Local SDK constraints required an installed .NET 11 preview SDK; no repository files were changed.

Posted by copilot

{
// We choose the port right before starting so that nothing else grabs it in between. pg_basebackup writes
// primary_conninfo to postgresql.auto.conf, so the port goes there too; the last setting wins.
Port = GetFreePort();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep the primary endpoint stable across restarts

pg_basebackup --write-recovery-conf records the primary's current port in the standby's primary_conninfo, but every Start() chooses another port. With a running standby, await primary.Stop(); await primary.Start(); therefore leaves it trying to reconnect to the old port indefinitely.

I reproduced this with PostgreSQL 18.4: the primary moved from port 54249 to 54262, the standby still targeted 54249, pg_stat_wal_receiver was empty, and a second inserted row never reached the standby. Preserve the selected port across restarts rather than adding reconnection plumbing. If restarting is not intended to be supported, make Start() private instead of exposing this broken path.

Posted by copilot

await using var standby = await primary.StartStandby();

Assert.That(await IsInRecovery(primary), Is.False);
Assert.That(await IsInRecovery(standby), Is.True);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please verify a primary write reaches the standby before promoting it. pg_is_in_recovery() only establishes recovery mode, not a working streaming connection: a disconnected standby can pass all the current assertions and still promote. One insert on the primary and a bounded wait for that row on the standby would exercise the main contract of StartStandby() without expanding the test much.

Posted by copilot

@roji roji left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See agentic review comments, approving without deeper review as this is a test infra only change.

@roji
roji requested a balanced review from Copilot October 27, 2026 07:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Restarting an upstream can change its port without updating existing standbys, preventing replication from resuming.

1 open issue
What changed in this PR

Adds an opt-in PostgreSQL cluster helper for replication tests that require isolated servers rather than the shared test database.

Changes:

  • Creates temporary primaries and streaming standbys with startup, promotion, shutdown, and cleanup support.
  • Adds process timeouts, loopback-only connections, and port-conflict retries.
  • Adds a standby promotion smoke test, skipped when NPGSQL_TEST_PG_BIN is unset.
File Description
test/​Npgsql.Tests/​Support/​PgTestCluster.cs Implements temporary PostgreSQL cluster lifecycle management.
test/​Npgsql.Tests/​Replication/​PgTestClusterTests.cs Tests primary and standby recovery states, promotion, and shutdown.

Review effort: Balanced

Comment on lines +122 to +123
Port = GetFreePort();
await File.AppendAllTextAsync(Path.Combine(DataDirectory, "postgresql.auto.conf"), $"\nport = {Port}\n");

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants