TCP Q3/Q4: an idle read deadline, and telling the server when a result is abandoned - #591
alex-clickhouse wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds native TCP idle-read timeouts and best-effort server cancellation when responses are abandoned.
Changes:
- Enforces
ReadTimeoutper transport read, with zero disabling it. - Sends
Cancelbefore terminating incomplete queries/inserts. - Adds timeout, cancellation, pooling, and idle-semantics coverage.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
ClickHouse.Driver.Tcp/Protocol/ReadBuffer.cs |
Arms deadlines around transport reads. |
ClickHouse.Driver.Tcp/Protocol/IdleReadDeadline.cs |
Implements reusable idle deadlines. |
ClickHouse.Driver.Tcp/Protocol/ClickHouseTcpConnection.cs |
Integrates deadlines and cancellation packets. |
ClickHouse.Driver.Tcp/Client/IConnectionFactory.cs |
Passes configured read timeout. |
ClickHouse.Driver.Tcp/Client/ClickHouseTcpConnectionStringBuilder.cs |
Documents zero-timeout behavior. |
ClickHouse.Driver.Tcp/Client/ClickHouseTcpClientOptions.cs |
Documents and validates timeout semantics. |
ClickHouse.Driver.Tcp.Tests/Utilities/ScriptedDuplexStream.cs |
Simulates delayed transport reads. |
ClickHouse.Driver.Tcp.Tests/Protocol/ClickHouseTcpConnectionQueryTests.cs |
Tests query timeout and cancellation behavior. |
ClickHouse.Driver.Tcp.Tests/Protocol/ClickHouseTcpConnectionInsertTests.cs |
Tests insert cancellation boundaries. |
ClickHouse.Driver.Tcp.Tests/Integration/ClickHouseTcpCancellationIntegrationTests.cs |
Verifies server-side cancellation and pooling. |
ClickHouse.Driver.Tcp.Tests/Client/ClickHouseTcpClientOptionsTests.cs |
Covers updated timeout validation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
77a0fcf to
eace85e
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
eace85e to
7297455
Compare
7297455 to
0cc1b31
Compare
0cc1b31 to
e4fb2da
Compare
e4fb2da to
3807c4c
Compare
3807c4c to
b93b25e
Compare
b93b25e to
35c3782
Compare
35c3782 to
042edb7
Compare
537a438 to
d4ec682
Compare
981642e to
99f3349
Compare
99f3349 to
957fd12
Compare
e8a2481 to
f276df8
Compare
f276df8 to
b0157a4
Compare
3eb6336 to
d15f8ac
Compare
d15f8ac to
26e60d4
Compare
fa47d22 to
f920c00
Compare
f920c00 to
31bbbca
Compare
31bbbca to
ad0fa60
Compare
…s abandoned ReadTimeout was parsed, stored and read by nothing. It now bounds every read of an operation, armed immediately before each read from the transport and disarmed as soon as that read returns, so it measures silence rather than duration: a result that streams for an hour never trips it, and neither does a consumer that holds a block longer than the deadline. TimeSpan.Zero disables it, as it does for the pool's limits. Giving up on a result now sends the Cancel packet before closing the connection, so the server stops rather than finishing a query nobody reads. That covers cancellation, a read that gave up, and a consumer that breaks out of the enumeration. The insert row phase is excluded: a block is part-written there, so an appended Cancel would be read as more block bytes. Co-Authored-By: Claude <noreply@anthropic.com>
ad0fa60 to
6f7946d
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6f7946d. Configure here.
DialTimeout and ReadTimeout expiry now throw ClickHouseTcpConnectionException with a TimeoutException as the inner exception. Both discard the connection, and both are failures between the client and the server, so they belong to the ClickHouseTcpException hierarchy. PoolTimeout still throws a plain TimeoutException, so a caller can tell pool saturation from a server that did not answer. A dial that times out while SslStream reports the cancellation as an IOException is also reported as a timeout. Use "timeout" instead of "deadline" in code and comments: IdleReadDeadline becomes IdleReadTimeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63cfdfc to
47ffc8b
Compare

Stacked on #590 (
tcp/epic-q1-exceptions). Completes Q3 (read timeouts) and Q4 (cancellation).ReadTimeoutnow limits each transport read after the handshake. It defaults to 300 seconds; zero disables it. Each successful read allows the next read a full timeout, and time spent processing a yielded block does not count. Expiry throwsClickHouseTcpConnectionExceptionwith an innerTimeoutExceptionand discards the connection. Connection establishment remains governed byDialTimeout.Incomplete responses trigger a best-effort Cancel packet before the connection closes, including caller cancellation, read timeout, and early disposal of result enumeration. Cancel uses an independent two-second flush timeout. It is suppressed during insert row writes, where a partial Data packet could make the server interpret Cancel as block data.
Timeout exceptions
DialTimeoutandReadTimeoutexpiry throwClickHouseTcpConnectionExceptionwith an innerTimeoutException. Both are failures between the client and the server, and both discard the connection, socatch (ClickHouseTcpException)covers them.PoolTimeoutstill throws a plainTimeoutException: no connection failed, and the fix is on the caller's side. So a caller can tell pool saturation from a server that did not answer. ATimeoutExceptionsubtype cannot also derive fromDbException, so this has to be decided before the API is released.A dial whose cancellation
SslStreamreports as anIOExceptionis also reported as a timeout.The code and comments use "timeout" instead of "deadline":
IdleReadDeadlineis nowIdleReadTimeout.Timeout completion race
A timer can cancel its token after a transport read succeeds but before the read's continuation disarms the timer. Reusing that cancelled token would make the next read fail immediately.
The transport buffer now obtains a token for each read.
CancellationTokenSource.TryReset()disarms and reuses the source only when no timeout callback can remain; otherwise the source is disposed and replaced. Parsing, decompression adapters, and writes receive the caller's token. Normal reads reuse the source without adding per-read allocations.The new deterministic test holds a successful read's continuation until its timer fires, then verifies the next read succeeds, a later stalled read still times out, and caller cancellation still reaches a replacement source. Restoring the old disarming logic makes the regression test fail on the next read. Existing integration tests exercise cancellation on the server, compression, handshake timeouts, and pooled connection reuse. Comments and XML documentation have also been simplified.
Validation
After the timeout exception change: the TCP unit tests pass on .NET 9 on this branch (1,916) and on the stack tip (2,192). The integration tests were not run locally (no Docker); CI covers them.
Full TCP suite on .NET 9 against ClickHouse 26.7.3.19: 3,531 passed.
Focused protocol and live cancellation tests on .NET 8 and .NET 10: 157 passed on each framework.
Final deterministic regression cases rerun after test hardening: 2 passed.
After restacking, 159 selected protocol and live cancellation tests passed on the stack tip. All 95 descendant commits retained identical stable patch IDs and commit messages.
Coverage:
IdleReadDeadline.csandReadBuffer.cs100%;ClickHouseTcpConnection.cs94.9%. Independent coverage review found no important gaps.Independent review, Codex CLI review, and Claude CLI review completed. No correctness findings remain.