Repository navigation
Conversation
A backend message bigger than the read buffer (e.g. a notice longer
than 8 KB) makes the connector switch to a bigger buffer.
NpgsqlRawCopyStream, which BeginTextExport also reads through, and
NpgsqlBinaryExporter kept the buffer they found when they were
created. After such a message they read the following data from the
old buffer, which the switch had emptied, i.e. directly from the
socket, bypassing what the connector had already received into the
new buffer. From there they misread the stream, or waited for data
that had already arrived. With a function that raises a long notice
for each row of COPY (SELECT ...) TO STDOUT, the raw stream returned
part of a notice as COPY data and then failed with an unknown message
code, the binary exporter failed with "Invalid COPY binary signature",
and the text export timed out. If an earlier message had already made
the connector switch, the next switch returned that bigger buffer's
array to the ArrayPool, and the reader went on reading socket data into
it, even after the pool had handed it out again.
The readers now take the connector's current buffer whenever they
read a CopyData message, like the data reader does for each row. The
raw stream also takes it when it reads the response to the COPY
command: after such a message before that response, its ReadTimeout
(which the text reader's and writer's Timeout set) went to the old
buffer, so asynchronous reads ignored it. The pgoutput decoder had the
same problem ("Read pgoutput messages from the connector's current
read buffer").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brar
force-pushed
the
core/copy-read-buffer
branch
from
November 1, 2026 13:38
2933ff0 to
fc71698
Compare
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues are blocking the COPY read-buffer refresh changes.
No open issues 🎉
What changed in this PR
Fixes #6702 by making COPY readers refresh their cached read buffer after messages that may cause the connector to replace it. This prevents stale-buffer reads and preserves timeout handling without changing public APIs.
Changes:
- Refresh the raw COPY stream’s buffer after the COPY response and at each CopyData message.
- Refresh the binary exporter’s buffer before reading the header and subsequent rows.
- Add 10 mock test cases covering oversized notices, short-notice controls, and timeout propagation.
| File | Description |
|---|---|
test/Npgsql.Tests/CopyTests.cs |
Adds regression tests for buffer switches during COPY and before COPY responses. |
src/Npgsql/NpgsqlRawCopyStream.cs |
Refreshes the buffer for COPY data reads and timeout handling. |
src/Npgsql/NpgsqlBinaryExporter.cs |
Refreshes the buffer before reading binary headers and rows. |
Review effort: Balanced
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A backend message longer than the read buffer (e.g. a notice over 8 KB) makes the connector switch to a bigger buffer.
NpgsqlRawCopyStream, whichBeginTextExportalso reads through, andNpgsqlBinaryExporterkept the buffer they found when they were created. After such a message they read the following data from the old buffer, which the switch had emptied, i.e. directly from the socket, bypassing what the connector had already received into the new buffer. From there they misread the stream, or waited for data that had already arrived. If an earlier message had already made the connector switch, the next switch returned that bigger buffer's array to the ArrayPool, and the reader went on reading socket data into it, even after the pool had handed it out again (shown with a mock probe).The readers now take the connector's current buffer whenever they read a CopyData message, like the data reader does for each row. The raw stream also takes it when it reads the response to the COPY command: after an oversize message before that response, its
ReadTimeout(which the text reader's and writer'sTimeoutset) went to the old buffer, so asynchronous reads ignored it. #6721 fixes the same pattern in the pgoutput decoder.How a real server gets there
Notices may come between CopyData messages (
protocol.sgml:1341-1345), and PostgreSQL flushes each one at once (elog.c:3695; references to REL_18_STABLE). Sources of a notice longer than the read buffer duringCOPY ... TO STDOUT, each run live:RAISE NOTICEof a large value in a function the COPY query calls;client_min_messages = logwithdebug_print_plan = on, both user-settable, which print an ordinary join's plan of 11 to 13 KB (postgres.c:954): at a row when a function plans an SPI query there, or before CopyOutResponse for the COPY query itself;The binary header goes out inside the first row's CopyData, so a notice raised while the first row is computed lands between CopyOutResponse and the header. These triggers are realistic but uncommon; with the default Command Timeout the failures below take 30 s, and with 0 they never end.
Live on 18.6 with Command Timeout 5 s,
COPY (SELECT f(i) FROM generate_series(1, 3) i) TO STDOUTwithfraising a 20000-character notice for each row: without this change,BeginRawBinaryCopyreturned 29 bytes of a notice's text as COPY data and then failed with "Unknown message code: 80",BeginBinaryExportfailed with "Invalid COPY binary signature at beginning!", andBeginTextExporttimed out and left the connection broken. With it, all three read the same data as with a 10-character notice.The tests' scenarios, live on 18.6 and 14.24 with Command Timeout 5 s:
ReadTimeoutafter a notice before CopyOutResponse (debug_print_plan,ReadTimeout = 500 ms, a 3-s stall at the first row): main timed out after 3.5 s, this change after 0.5 s. Only async reads are affected; sync reads use the socket's timeout. The import's variant, with a long notice before CopyInResponse (e.g. raised by an IMMUTABLE function in the default of a column the COPY doesn't list), behaves the same, but that trigger is contrived.Tests
Three mock tests in
CopyTestswith 10 cases, each oversize case with a 10-character-notice control; they use the smallest read buffer (4096 bytes), so that the mock can write an oversize notice in one piece:Raw_binary_export_with_notice_between_CopyData_messages;Raw_binary_copy_timeout_with_notice_before_copy_response(export and import);Binary_export_with_notice_before_row(before the header, or before a later row).Red first: without the source change, the 5 oversize cases fail in Debug and Release (NpgsqlException "Exception while reading from stream" at the reads on the old buffer, or the timeout assertion), and the 5 controls pass. The read cases fail the same way with main's COPY code. Removing any one of the four lookups fails exactly its own cases.
For the reviewer
NpgsqlRawCopyStreamandNpgsqlBinaryExporter, with no API change. The buffer is looked up per message, not per use.Initdoesn't take the buffer: its only effect would be on the error path that Fix NpgsqlBinaryExporter's dispose after an error or a connection break #6713 fixes.NpgsqlBinaryImporterread only messages, through the connector.Dependencies and merge order
NpgsqlBinaryExporter(theReadHeaderandStartRowread lines) and inCopyTests.cs(new tests at the same place). Merge this PR first; Fix NpgsqlBinaryExporter's dispose after an error or a connection break #6713 is then rebased keeping both changes: its read line followed by this PR's two lines, and both test blocks.Fixes #6702