Fix ReadBuffer cancellation assert in row-format verbose diagnostics#109708
Conversation
When a row-format parse (e.g. TSV/CSV over HTTP) fails because an underlying read throws mid-parse, ReadBuffer::next() self-cancels the buffer in its catch handler (sets canceled = true and rethrows). The error surfaces as a parse error, so IRowInputFormat::read() calls getDiagnosticInfo() to enrich the message. That path called eof() on the input buffer, and eof() calls next(), which trips chassert(!isCanceled()) and aborts in assertion-enabled builds. Guard getDiagnosticAndRawDataImpl() with in->isCanceled() before eof(), mirroring the caller-side guard added for TCPHandler in ClickHouse#100666. A canceled buffer cannot yield further diagnostics, and re-reading it is exactly what the assertion forbids. This is a distinct caller of the same assertion family (fuzz issues ClickHouse#84186, ClickHouse#98866, ClickHouse#99258, ClickHouse#100581) not covered by the TCPHandler fix (ClickHouse#100666) or the S3 backup upload-retry fix (ClickHouse#108573). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-3:20260707-191500 |
|
cc @al13n321 @Avogar — could you review? This guards |
|
Workflow [PR], commit [ea76890] Summary: ✅
AI ReviewSummaryThis PR moves the canceled-buffer guard into PR Metadata
Final Verdict |
With allow_errors_num / allow_errors_ratio, a parse error is caught inside IRowInputFormat::read()'s row loop and syncAfterError() is called to skip the bad row before the diagnostics guard is reached. For TSV/CSV/template/JSON row formats syncAfterError() starts with skipTo*NextLineOrEOF / ignore, whose first eof() calls next() on the same self-canceled buffer, tripping the same chassert(!isCanceled()). Rethrow when the buffer is already canceled instead of skipping. Extends the regression gtest to cover the allow_errors path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch — confirmed and fixed in fdad22b. You're right: with Fix: rethrow the original parse error when Extended the gtest with Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-2:20260708-075500 |
CI finish ledger — fdad22bCI fully finished (Finish Workflow pass; 0 checks queued/in_progress; >20-min ingestion buffer). No test-level FAIL/ERROR in CIDB on this head. No PR-caused failure (this PR only guards a canceled ReadBuffer in the row-format verbose-diagnostics + syncAfterError path).
Session id: cron:our-pr-ci-monitor:20260708-123000 |
There was a problem hiding this comment.
Let's move this check together with isParseError. So we will go to the logic of skipping bad lines only when it's a parse error that wasn't thrown in the buffer itself
There was a problem hiding this comment.
Done in ea76890. Folded the canceled-buffer check into the isParseError gate at the top of the inner catch: `if (!isParseError(e.code()) || getReadBuffer().isCanceled()) throw;`. Now we only enter the skip-bad-lines / syncAfterError logic for a genuine parse error that was not thrown from the buffer itself. Removed the standalone check that was before syncAfterError().
There was a problem hiding this comment.
We should not reach this code if buffer is cancelled. Let's check from where we call getDiagnosticInfo and check if there is a check like isParseError before that.
There was a problem hiding this comment.
Done in ea76890. Traced the caller: getDiagnosticInfo() is invoked from exactly one place, the outer catch in IRowInputFormat::read(), which already gates on isParseError. Added the canceled-buffer check to that same gate (`if (!isParseError(e.code()) || getReadBuffer().isCanceled()) throw;`) so we never reach getDiagnosticInfo() with a canceled buffer, and reverted the check inside getDiagnosticAndRawDataImpl() back to the original `if (in->eof())`.
Per review: gate both the skip-bad-lines path (syncAfterError) and the verbose-diagnostics path on (isParseError && !buffer canceled) in IRowInputFormat::read(), instead of a separate check before syncAfterError and a check inside getDiagnosticAndRawDataImpl. A self-canceled buffer (a throwing read that tripped ReadBuffer::next()'s catch handler) is not a genuine parse error to skip, so we rethrow before ever reading from it again. Reverts the internal guard in RowInputFormatWithDiagnosticInfo.cpp since that code is no longer reached with a canceled buffer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Restructured the canceled-buffer guards per review (ea76890): the checks are now folded into the Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-2:20260708-131300 |
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 39/39 (100.00%) · Uncovered code |
CI finish ledger — ea76890All gating checks passed (Finish Workflow + Mergeable Check green, 0 failed checks). CH Inc sync still running (private, exempt).
Session id: cron:our-pr-ci-monitor:20260708-180000 |
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixed a possible
Logical error: 'ReadBuffer is canceled. Can't read from it.'when a row-based input format (e.g.TSV/CSV) failed to parse input and the underlying read had already been canceled (for example a malformed HTTP chunk or a truncated body). Building the verbose parse diagnostics no longer reads from a canceled buffer.Description
When a row-format parse fails because an underlying read throws mid-parse,
ReadBuffer::next()self-cancels the buffer in itscatch (...) { cancel(); throw; }handler (setscanceled = trueand rethrows). The failure surfaces as a parse error, soIRowInputFormat::read()callsgetDiagnosticInfo()to enrich the message.RowInputFormatWithDiagnosticInfo::getDiagnosticAndRawDataImpl()then calledin->eof(), andeof()callsnext(), which tripschassert(!isCanceled())(src/IO/ReadBuffer.cpp:107) and aborts in assertion-enabled builds.The fix guards the diagnostics entry with
in->isCanceled()beforeeof(), mirroring the caller-side guard added forTCPHandlerin #100666. A canceled buffer cannot yield further diagnostics, and re-reading it is exactly what the assertion forbids.This is a distinct caller of the same assertion family (fuzz issues #84186, #98866, #99258, #100581) not covered by the
TCPHandlerfix (#100666) or the S3 backup upload-retry fix (#108573).A gtest regression (
RowInputFormatDiagnostics.CanceledBufferDoesNotAbort) drives aTabSeparatedRowInputFormatover a buffer that self-cancels on a parse-classified error mid-row; it aborts without the fix and passes with it. This behavior is not observable from SQL because the abort is achassertin assertion-enabled builds and the exact HTTP framing that reaches the format-level read is timing-sensitive.Version info
26.7.1.773(included in26.7and later)