Report cancellation instead of a network error for cancelled S3 requests#108673
Conversation
|
Workflow [PR], commit [ef5f275] Summary: ❌
AI ReviewSummaryThis PR routes failed S3 outcomes through Findings
Final VerdictChanges requested. |
When a query issuing S3 requests is cancelled mid-request (e.g. by KILL QUERY), the retry loop returned the last network error, so it was reported as S3_ERROR instead of cancelled. This affects any cancelled S3 request, including backups and restores. Surface QUERY_WAS_CANCELLED instead. Closes: ClickHouse/clickhouse-private#45444 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8674fec to
61b41ab
Compare
Throwing a fresh QUERY_WAS_CANCELLED when the S3 retry loop stops on a killed query loses the real cause: a query cancelled by max_execution_time should be TIMEOUT_EXCEEDED, and a query cancelled with a stored exception (e.g. a backup/restore coordination failure on another host) should rethrow it. Use QueryStatus::throwIfKilled when a process-list element is available, falling back to a fresh QUERY_WAS_CANCELLED only when there is none. Add a max_execution_time backup test that keeps failing S3 uploads (retryable) until the timeout fires, proving the S3 layer reports TIMEOUT_EXCEEDED rather than QUERY_WAS_CANCELLED. Shorten the slow-answer timeouts in the cancellation tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route HeadObject through headObjectInternal so the wrong-region and redirect retries also report cancellation instead of a stale S3/network outcome. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`throwIfQueryWasCanceled` runs after any failed S3 outcome. It previously called `Context::getProcessListElement`, which throws `LOGICAL_ERROR` when the `Context` still exists but the process-list entry has expired during query start/finish (the same edge that made `Context::isCurrentQueryKilled` use the safe accessor). That could replace the original S3/network error with an unrelated logical exception at a normal query-lifecycle edge. Switch to `getProcessListElementSafe`, which returns a null `QueryStatusPtr` instead of throwing. When the safe element is absent the existing `ThreadStatus::isQueryCanceled` fallback still reports the generic `QUERY_WAS_CANCELLED`, so a real cancellation is preserved while ordinary S3 failures are no longer turned into `LOGICAL_ERROR`. Addresses review feedback on #108673 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This pull request fixes misreporting of cancelled in-flight S3 operations by translating the stale final S3/network failure outcome into a proper query cancellation error (QUERY_WAS_CANCELLED). This improves user-facing error reporting for cancelled S3-backed operations (notably backups/restores).
Changes:
- Add a cancellation check in the S3 client retry/redirect paths to surface query cancellation (and preserve the true cancellation cause via the process-list element when available).
- Wrap
HeadObjectredirect/region handling so failed outcomes also report cancellation correctly. - Add integration regression tests for cancelled async S3 backup/restore, plus a test ensuring timeout cancellations still report
TIMEOUT_EXCEEDED.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
tests/integration/test_backup_restore_s3/test.py |
Adds regression coverage for cancelled async S3 backup/restore and ensures timeout cancellation cause is preserved. |
tests/integration/helpers/s3_mocks/broken_s3.py |
Extends the S3 mock to support slow GET/HEAD responses for reliable in-flight restore cancellation testing. |
src/IO/S3/Client.h |
Declares internal HeadObject helper to centralize retry/redirect handling and enable consistent cancellation surfacing. |
src/IO/S3/Client.cpp |
Implements cancellation surfacing in S3 request loops and HeadObject failure handling, preserving timeout/kill causes via process list. |
Introduce a reusable static method that throws the real cancellation cause of the current query (`TIMEOUT_EXCEEDED`, or an exception stored by `QueryStatus::cancelQuery`) instead of a generic error, falling back to `QUERY_WAS_CANCELLED` when the process-list element is gone. It is backed by a new `ThreadStatus` predicate set alongside `query_is_canceled_predicate`. Review: #108673 (review) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the local `throwIfQueryWasCanceled` helper with the shared `CurrentThread::checkIfNotCancelled`, called after a failed S3 outcome in `headObject` and `doRequestWithRetryNetworkErrors`. Drop the now-unused `ProcessList.h` include and the `QUERY_WAS_CANCELLED` error code, whose logic moved into `CurrentThread`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fallback in CurrentThread::checkIfNotCancelled consulted the same process-list element as the primary check, so a cancellation was lost once the entry expired during query teardown. Record the killed state on the thread group (which outlives the entry) via QueryStatus::markKilled, and let ThreadStatus::isQueryCanceled consult this durable flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| if (result.IsSuccess()) | ||
| return result; | ||
|
|
||
| CurrentThread::checkIfNotCancelled(); |
There was a problem hiding this comment.
CurrentThread::checkIfNotCancelled() only translates failed AWS outcomes returned from doRequest(), but GetObject body reads still bypass it. Once client_ptr->GetObject(req) succeeds, ReadBufferFromS3::nextImpl() / readBigAt() handle stream timeouts and resets in their own retry loop (ReadBufferFromS3::processException()), and that path never checks whether the query was killed before retrying or surfacing the last network error. A KILL QUERY during an in-flight download can therefore still end as S3_ERROR for SELECT FROM s3(...) / RESTORE FROM S3 when the failure happens while consuming the response body rather than while creating the AWS outcome, so the read-path contract of this PR is still incomplete.
There was a problem hiding this comment.
A legit suggestion. The fix belongs in ReadBufferFromS3::processException. I'll take a closer look and file a follow-up PR if needs be, since ReadBufferFromS3 is a different subsystem.
| if count is not None: | ||
| url += f"&count={count}" | ||
|
|
||
| response = self._cluster.exec_in_container( |
There was a problem hiding this comment.
setup_slow_get_answers() bypasses _apply() and invokes curl with no bounded retry or timeout. The rest of MockControl uses _apply() specifically because the mock can be briefly unreachable or overloaded; here the new helper can fail on the first transient refusal or hang until the container's TCP timeout, which makes the new cancellation tests flaky in exactly the failure mode they are trying to exercise. Please route this through _apply(url) as well so the slow_get control path inherits the same capped retry behavior.
There was a problem hiding this comment.
There is no _apply() in broken_s3.py. Every MockControl method – reset, setup_action, setup_fake_puts, and the pre-existing setup_slow_answers this mirrors – issues the same single exec_in_container(["curl", "-s", url], nothrow=True) call.
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 27/32 (84.38%) · Uncovered code |
When a query issuing S3 requests is cancelled mid-request (e.g. by KILL QUERY), the retry loop returned the last network error, so it was reported as S3_ERROR instead of cancelled. This affects any cancelled S3 request, including backups and restores. Surface QUERY_WAS_CANCELLED instead.
Closes: https://github.com/ClickHouse/clickhouse-private/issues/45444
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
A query reading from or writing to S3 that is cancelled (e.g. by
KILL QUERY) while an S3 request is in flight is now reported as cancelled instead of with a misleading network/S3 error. In particular, a cancelled backup or restore to S3 now reportsBACKUP_CANCELLED/RESTORE_CANCELLEDinsystem.backups.Version info
26.7.1.568(included in26.7and later)