Visitar URL original
Add copy-both and replication support to the test server mock by Brar · Pull Request #6689 · npgsql/npgsql · GitHub
Skip to content

Add copy-both and replication support to the test server mock - #6689

Open
Brar wants to merge 1 commit into
npgsql:mainfrom
Brar:repl/mock-copyboth
Open

Brar wants to merge 1 commit into
npgsql:mainfrom
Brar:repl/mock-copyboth

Conversation

@Brar

@Brar Brar commented Oct 25, 2026

Copy link
Copy Markdown
Member

PgServerMock can now play a walsender: CopyBothResponse, CopyData, XLogData and keepalive messages, CopyDone, CopyOutResponse and NoticeResponse writers, NULL DataRow columns, a configurable server_version, the parsed startup parameters (e.g. "replication") and Expect helpers for CopyDone, Terminate and standby status updates.

This also fixes length bugs that were hidden because the client parses these messages by content (CopyInResponse, ParameterDescription and ErrorResponse sent wrong lengths), and ExpectMessage skipped synchronously without ensuring the message was fully received.

ReplicationMock opens replication connections against the mock with the feedback timers disabled, and ReplicationMockTests smoke-tests startup, IDENTIFY_SYSTEM, physical streaming, keepalive replies and cancellation, with timeouts on both the client and the mock side.

PgServerMock can now play a walsender: CopyBothResponse, CopyData,
XLogData and keepalive messages, CopyDone, CopyOutResponse and
NoticeResponse writers, NULL DataRow columns, a configurable
server_version, the parsed startup parameters (e.g. "replication") and
Expect helpers for CopyDone, Terminate and standby status updates.

This also fixes length bugs that were hidden because the client parses
these messages by content (CopyInResponse, ParameterDescription and
ErrorResponse sent wrong lengths), and ExpectMessage skipped
synchronously without ensuring the message was fully received.

ReplicationMock opens replication connections against the mock with the
feedback timers disabled, and ReplicationMockTests smoke-tests startup,
IDENTIFY_SYSTEM, physical streaming, keepalive replies and cancellation,
with timeouts on both the client and the mock side.

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

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

Non-ASCII error and notice fields use inconsistent encodings for lengths and payloads, which can stall client reads.

1 open issue
What changed in this PR

Extends the test server mock to emulate PostgreSQL replication, allowing replication behavior to be tested without a live server.

Changes:

  • Adds copy-both, replication message writers, and standby status expectations.
  • Exposes startup parameters and configurable server versions, supports NULL columns, and corrects message lengths and asynchronous skipping.
  • Adds replication connection helpers and smoke tests for startup, streaming, keepalive replies, and cancellation.
File Description
test/​Npgsql.Tests/​Support/​ReplicationMock.cs Adds replication connection and response helpers.
test/​Npgsql.Tests/​Support/​PgServerMock.cs Adds replication protocol support and fixes message framing.
test/​Npgsql.Tests/​Support/​PgPostmasterMock.cs Makes the advertised server version configurable.
test/​Npgsql.Tests/​Replication/​ReplicationMockTests.cs Tests replication startup, streaming, feedback, and cancellation.

Review effort: Balanced

Comment on lines +545 to +547
1 + Encoding.GetByteCount(code) + 1 +
1 + Encoding.GetByteCount(severity) + 1 +
1 + Encoding.GetByteCount(message) + 1 +

@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 core direction is justified. I found no correctness blocker in the exercised paths. I would keep the replication mock and smoke tests, trim unused future-facing helpers, and preserve the important transport fixes with focused regression coverage.

Problem and expected approach

There is a real testability gap: the existing physical replication fixture is explicitly disabled for flakiness, and its streaming tests depend on cluster-wide WAL activity. The shared replication fixture also requires a specially configured live server. A mock cannot replace those integration tests, but it can make client protocol sequencing deterministic.

Independently of this implementation, I would extend the existing wire mock with CopyBothResponse, WAL and keepalive writers, feedback parsing, and a small connection helper that disables automatic feedback. A few hundred lines of C# test-only code is reasonable; no production API or user-documentation change is needed. The essential scenarios are startup/IDENTIFY_SYSTEM, a streamed payload, requested feedback, and cancellation followed by connection reuse.

Alignment

The PR follows that design without adding a separate mocking framework. Its physical smoke test drives the real replication API while scripting the peer. For example, after starting a client MoveNextAsync:

await server.ExpectSimpleQuery("START_REPLICATION PHYSICAL 0/1000000");
await server.WriteCopyBothResponse()
    .WriteXLogData(new(0x1000000), new(0x1000003), "abc"u8)
    .FlushAsync();

The NULL DataRow support is relevant to physical IDENTIFY_SYSTEM. The adjacent length corrections are also legitimate: ParameterDescription previously counted the message-type byte, CopyInResponse declared 5 bytes while emitting 9, and ErrorResponse omitted three string terminators from its declared length. The asynchronous skip fixes consumption of fragmented frontend bodies rather than merely changing style.

Gaps and scope

The new tests do not exercise the changed ExpectMessage skip or the large-payload flushing writer; the inline comments suggest narrow regression cases. Also, CopyOutResponse, raw CopyData/CopyDone writers, and server-version configurability have no consuming tests in this PR. I would defer that unused surface until a concrete scenario needs it rather than treating this as a general protocol-mock expansion. These are coverage/scope recommendations, not demonstrated runtime defects. The smoke tests establish the foundation; they do not yet cover timeline switches, server shutdown, or logical decoding streams.

Manual validation

I built a separate console program against the exact PR mock sources and drove real logical and physical replication connections, independently of NUnit. It verified a 24,593-byte WAL payload, high-bit LSNs, an exact server timestamp, notice handling, all three feedback LSNs, cancellation routing, connection reuse, a NULL database name, and Terminate. A separate raw TCP peer checked the corrected frame lengths, COPY/NULL encodings, a WAL header near the buffer boundary, and a 20 KB frontend body delivered separately from its header, including alignment of subsequent messages. All passed. The two new smoke tests and eight selected existing mock tests also passed. Validation used an extracted source copy and the installed .NET 11 preview SDK because the pinned 10.0.402 SDK was unavailable; no reviewed source was changed.

Posted by copilot

$"Expected message of type '{(char)expectedCode}' but got '{(char)actualCode}'");
var len = _readBuffer.ReadInt32();
_readBuffer.Skip(len - 4);
await _readBuffer.Skip(async: true, len - 4);

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.

Could we add a focused regression case that sends a frontend header first, then delivers a body larger than the read buffer, and verifies the next message stays aligned? This fixes a real transport issue, but the new replication tests call ExpectSimpleQuery/ExpectStandbyStatusUpdate rather than this path. I manually exercised a fragmented 20 KB body and the new implementation worked; keeping that case in the suite would protect the fix.

Posted by copilot

ReadOnlyMemory<byte> data,
DateTime? serverClock = null)
{
if (_writeBuffer.WriteSpaceLeft < XLogDataHeaderLength)

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.

This flushing writer has no caller in the added tests: the only WAL payload is three bytes and uses WriteXLogData. If we keep this helper now, could we add one case with a payload larger than the 8192-byte buffer and less than XLogDataHeaderLength space remaining before the header? I manually checked both boundaries successfully, but neither branch currently has regression coverage. Otherwise I would defer this helper until its first consuming test.

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.

Please see agentic review below, am otherwise approving as this is test infra only.

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