Add per-request cancellation hook to HTTPFSCurlClient#324
Open
rustyconover wants to merge 1 commit into
Open
Conversation
Adds an optional `should_cancel` std::function<bool()> on HTTPFSParams.
When set, it is wired into libcurl as a CURLOPT_XFERINFOFUNCTION
progress callback that returns non-zero to abort the in-flight transfer
with CURLE_ABORTED_BY_CALLBACK.
Motivation: extensions doing slow HTTP work from inside a query (e.g.
remote-function execution) currently have no way to honor a user
Ctrl-C — libcurl is run with CURLOPT_NOSIGNAL=1 (correctly, for
thread-safety) so SIGINT does not unblock the syscall, and there is no
in-band cancel primitive between the extension and libcurl. This patch
provides one, modeled on libcurl's own blessed pattern for cooperative
cancellation.
Behavior:
- Default (no hook): zero behavior change. Empty std::function makes
the callback return 0 cheaply on every progress tick.
- Hook returning true: libcurl aborts the transfer, the resulting
HTTPResponse has status set to 499 ("Client Closed Request",
matching nginx's convention) and request_error =
"Request cancelled by callback".
- Throwing hook: treated as cancel (libcurl callbacks cannot propagate
C++ exceptions across the C boundary).
Implementation notes:
- The hook is stored as a member on HTTPFSCurlClient so its address
remains stable across cached-client reuse (Initialize() is called
again with new params on each request).
- Field is appended to HTTPFSParams to preserve the existing layout
expected by duckdb-wasm.
- WASM client is unchanged; cancellation there is a separate concern
(emscripten_fetch / sync XHR have different abort primitives).
Follow-up: a small duckdb-core PR could add `Cancelled_499 = 499` to
the HTTPStatusCode enum so callers don't need to cast through the
integer value.
35a8bbd to
d752482
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional
should_cancelstd::function<bool()>field toHTTPFSParams. When set, it is wired into libcurl as aCURLOPT_XFERINFOFUNCTIONprogress callback that returns non-zero to abort the in-flight transfer withCURLE_ABORTED_BY_CALLBACK.Motivation
Extensions doing slow HTTP work from inside a query (remote-function execution, long S3 fetches, etc.) currently have no way to honor a user Ctrl-C while a request is in flight. libcurl is run with
CURLOPT_NOSIGNAL=1(correctly, for thread-safety in DuckDB's multi-threaded executor), so SIGINT does not unblock the underlyingpoll()/recv()syscall. There is no in-band cancel primitive between the extension and libcurl, so a stuck endpoint hangs the query untilCURLOPT_TIMEOUTfires (or forever, if no timeout is set).I hit this concretely while debugging a hung VGI extension query: the main thread was blocked in
curl_easy_perform → multi_wait → poll, completely unresponsive to Ctrl-C. There's no clean fix possible at the extension layer alone — extensions can't add curl options to clients owned by httpfs. The interrupt primitive has to live in httpfs.This patch is modeled on libcurl's own blessed pattern for cooperative cancellation (
XFERINFOFUNCTIONreturning non-zero), which is the standard way to abort an in-flight transfer from another context without removingCURLOPT_NOSIGNAL.Behavior
std::functionmakes the callback return0cheaply on every progress tick.true: libcurl aborts the transfer; the resultingHTTPResponsehasstatusset to499("Client Closed Request", matching nginx's convention) andrequest_error = \"Request cancelled by callback\".Caller pattern
Implementation notes
HTTPFSCurlClientso its address remains stable across cached-client reuse (Initialize()is called again with new params on each request, but&cancel_fndoesn't move).Initialize(), even when the hook is empty — the empty-std::functioncheck inside the callback is a single branch and cheaper than conditionallysetopt-ing per request.HTTPFSParamsto preserve the existing layout expected by duckdb-wasm.httpfs_client_wasm.cpp) is unchanged; cancellation there is a separate concern (emscripten_fetch / sync XHR have different abort primitives).Latency to cancel
Bounded by libcurl's progress-callback cadence — typically every few hundred ms during active I/O, less often during long DNS resolves or TLS handshakes. In practice, well under one second for the common cases. Setting a reasonable
CURLOPT_CONNECTTIMEOUTcomplements this for the connect-phase edge case.Follow-up
A small duckdb-core PR could add
Cancelled_499 = 499to theHTTPStatusCodeenum so callers don't need to cast through the integer value:Once that lands, the cast in
TransformResponseCurlcan be replaced with the named constant.Test plan
trueafter 100ms aborts an in-flight POST and surfaces status 499🤖 Generated with Claude Code