Skip to content

service: report the first CTP connection error, not the last - #38722

Open
antiguru wants to merge 1 commit into
MaterializeInc:mainfrom
antiguru:moritz/cpu-249-ctp-transport-first-error
Open

service: report the first CTP connection error, not the last#38722
antiguru wants to merge 1 commit into
MaterializeInc:mainfrom
antiguru:moritz/cpu-249-ctp-transport-first-error

Conversation

@antiguru

@antiguru antiguru commented Sep 9, 2026

Copy link
Copy Markdown
Member

A CTP connection's send and receive tasks both reported their errors into a single watch channel, which keeps only the latest write. When the send task hit its idle deadline it dropped its write half, the peer observed the resulting shutdown and closed the connection, and the receive task's "unexpected end of file" then overwrote the timeout that had caused it. Both raw errors were logged at debug!, so at the default log level the actual cause appeared nowhere and the controllers surfaced the consequence as replica task failed: unexpected end of file. That reads as the replica having gone away when the controller had in fact stopped writing to it.

Replace the raw watch pair with an ErrorTx/ErrorRx pair over Option<String>. ErrorTx::report fills the slot only while it is empty, so the first error wins and later consequential errors are discarded. ErrorRx still falls back to a generic "connection closed" error when both tasks shut down without reporting anything.

Errors now name the failing direction, as send error: {e} or recv error: {e}. First-wins on its own would have surfaced a bare timed out, which still does not say whether the local endpoint stopped writing or the peer stopped answering, and that ambiguity is what made the reported connection loss hard to attribute. Both task errors move from debug! to warn!, matching the level the compute and storage controllers already use for replica task failed, so a warning-filtered query shows the cause and its consequence together. These paths only run on unexpected breaks: a deliberate teardown drops the connection, which aborts both tasks before either can report.

New unit tests in src/service/src/transport/tests.rs cover the error channel directly: first-wins, first-wins against an error reported after collection, the no-error fallback, and waiting for an error that has not been reported yet. In src/service/tests/transport.rs, test_server_error now waits for the send task to fail as well before asserting again, pinning first-wins end to end, and the expected error strings pick up the new direction prefix.

Closes: CPU-249

Posted by Claude Code.

🤖 Generated with Claude Code

A CTP connection's send and receive tasks both reported their errors into a
single `watch` channel, which keeps only the latest write. When the send task
hit its idle deadline it dropped its write half, the peer observed the resulting
shutdown and closed the connection, and the receive task's "unexpected end of
file" then overwrote the timeout that had caused it. Both raw errors were logged
at `debug!`, so at the default log level the actual cause appeared nowhere and
the controllers surfaced the consequence as `replica task failed: unexpected end
of file`. That reads as the replica having gone away when the controller had in
fact stopped writing to it.

Replace the raw `watch` pair with an `ErrorTx`/`ErrorRx` pair over
`Option<String>`. `ErrorTx::report` fills the slot only while it is empty, so
the first error wins and later consequential errors are discarded. `ErrorRx`
still falls back to a generic "connection closed" error when both tasks shut
down without reporting anything.

Errors now name the failing direction, as `send error: {e}` or
`recv error: {e}`. First-wins on its own would have surfaced a bare `timed out`,
which still does not say whether the local endpoint stopped writing or the peer
stopped answering, and that ambiguity is what made the reported connection loss
hard to attribute. Both task errors move from `debug!` to `warn!`, matching the
level the compute and storage controllers already use for `replica task failed`,
so a warning-filtered query shows the cause and its consequence together. These
paths only run on unexpected breaks: a deliberate teardown drops the connection,
which aborts both tasks before either can report.

New unit tests in `src/service/src/transport/tests.rs` cover the error channel
directly: first-wins, first-wins against an error reported after collection, the
no-error fallback, and waiting for an error that has not been reported yet. In
`src/service/tests/transport.rs`, `test_server_error` now waits for the send task
to fail as well before asserting again, pinning first-wins end to end, and the
expected error strings pick up the new direction prefix.

Closes: CPU-249

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@antiguru
antiguru requested a review from a team as a code owner September 9, 2026 13:38
@antiguru
antiguru requested review from DAlperin and ggevay September 9, 2026 15:18
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.

1 participant