TCP Q3/Q4: an idle read deadline, and telling the server when a result is abandoned - #591
TCP Q3/Q4: an idle read deadline, and telling the server when a result is abandoned#591alex-clickhouse wants to merge 1 commit 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
1535b9f to
88971a0
Compare
88971a0 to
15b1dc1
Compare
15b1dc1 to
34d7c6c
Compare
4bd8906 to
78dfbbf
Compare
e8a2481 to
f276df8
Compare
f276df8 to
b0157a4
Compare
3eb6336 to
d15f8ac
Compare
d15f8ac to
26e60d4
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 26e60d4. Configure here.
fa47d22 to
f920c00
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>
f920c00 to
31bbbca
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 throwsTimeoutExceptionand 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 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
IdleReadDeadline.csandReadBuffer.cs100%;ClickHouseTcpConnection.cs94.9%. Independent coverage review found no important gaps.Performance
BenchmarkDotNet 0.15.8, .NET 9.0.10, Release, 10 measured iterations. Compares the PR's previous implementation with this fix over synchronous MemoryStream transport reads, using a cancellable caller token and 256 reads per operation. Values below are per transport read.
Errors are BenchmarkDotNet's 99.9% confidence intervals. The intervals overlap; these timings do not establish a regression. Allocations are unchanged (reported as 1 B/read after amortizing operation setup). This isolates buffer/deadline overhead and is not a network throughput benchmark. The temporary benchmark is not committed.
No per-PR changelog fragment, following the approved policy for this unreleased experimental TCP stack.