Skip to content

feat: support cancellation tokens in Curl REST client and payloads - #16381

Draft
ajayky-os wants to merge 1 commit into
googleapis:mainfrom
ajayky-os:pr1/curl-cancellation-transport
Draft

feat: support cancellation tokens in Curl REST client and payloads#16381
ajayky-os wants to merge 1 commit into
googleapis:mainfrom
ajayky-os:pr1/curl-cancellation-transport

Conversation

@ajayky-os

Copy link
Copy Markdown
Contributor

Summary

This PR introduces cancellation support to the internal REST transport layer and libcurl client implementation using cooperative cancellation tokens (std::shared_ptr<std::atomic<bool>>).

Problem & Motivation

When performing speculative operations (such as hedging) or handling user-initiated request cancellations, active HTTP streaming transfers continue consuming network bandwidth and socket connections until
the transfer finishes or times out. Libcurl provides transfer progress callbacks (CURLOPT_XFERINFOFUNCTION / CURLOPT_PROGRESSFUNCTION) that allow transfers to be aborted mid-flight by returning a non-
zero exit code (CURLE_ABORTED_BY_CALLBACK).

Changes

  • RestContext: Added optional cancellation_token field and getter/setter (set_cancellation_token() / cancellation_token()).
  • CurlImpl: Registered CURLOPT_XFERINFOFUNCTION (with fallback to CURLOPT_PROGRESSFUNCTION on older libcurl versions) that polls the cancellation token during transfers and aborts early if
    flagged.
  • CurlHttpPayload & CurlRestClient: Propagated cancellation tokens from the request context to the active HTTP payload reader.
  • TracingHttpPayload & Mocks: Updated decorators and testing mocks to forward cancellation tokens across wrappers.

Verification & Testing

bazel test //google/cloud:internal_curl_impl_test \
  //google/cloud:internal_curl_rest_client_test \
  //google/cloud:internal_rest_context_test \
  //google/cloud:internal_tracing_http_payload_test

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces cancellation support for REST/HTTP operations using libcurl by adding a cancellation token to RestContext and propagating it to CurlImpl. This allows in-progress transfers to be aborted via curl_multi_wakeup and transfer info callbacks. The review feedback highlights critical thread-safety issues regarding the std::shared_ptr<std::atomic<bool>> cancellation token. Specifically, concurrent accesses and modifications to the std::shared_ptr itself in Read(), SetCancellationToken(), and Cancel() are not synchronized and can result in data races, so they should be protected by the multi_mu_ mutex.

Comment on lines +565 to +567
if (cancellation_token_) {
context.set_cancellation_token(cancellation_token_);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since Read() can be called on a different thread (e.g., the user thread reading the payload) than the thread modifying the cancellation token, copying cancellation_token_ here can race with SetCancellationToken(). We should protect this access with multi_mu_.

  std::shared_ptr<std::atomic<bool>> token;
  {
    std::lock_guard<std::mutex> lk(multi_mu_);
    token = cancellation_token_;
  }
  if (token) {
    context.set_cancellation_token(std::move(token));
  }

Comment on lines +571 to +580
void CurlImpl::SetCancellationToken(
std::shared_ptr<std::atomic<bool>> token) {
if (token) {
if (cancellation_token_->load(std::memory_order_relaxed)) {
token->store(true, std::memory_order_relaxed);
}
cancellation_token_ = std::move(token);
cancellable_ = true;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a potential data race on the cancellation_token_ std::shared_ptr member variable. While the underlying std::atomic<bool> is thread-safe, std::shared_ptr itself is not thread-safe for concurrent read and write operations.

Since Cancel() can be called concurrently from another thread, and SetCancellationToken() writes to cancellation_token_ (by moving a new std::shared_ptr into it), this can lead to undefined behavior. We should protect accesses and modifications of the cancellation_token_ std::shared_ptr using multi_mu_.

void CurlImpl::SetCancellationToken(
    std::shared_ptr<std::atomic<bool>> token) {
  if (token) {
    std::lock_guard<std::mutex> lk(multi_mu_);
    if (cancellation_token_ &&
        cancellation_token_->load(std::memory_order_relaxed)) {
      token->store(true, std::memory_order_relaxed);
    }
    cancellation_token_ = std::move(token);
    cancellable_ = true;
  }
}

Comment on lines +582 to +593
void CurlImpl::Cancel() {
cancellation_token_->store(true, std::memory_order_relaxed);
#if CURL_AT_LEAST_VERSION(7, 68, 0)
// The lock keeps `multi_` alive and owned by this request while the wakeup
// is delivered: without it the transfer thread could concurrently return
// the handle to the pool, where another request may already be using it.
std::lock_guard<std::mutex> lk(multi_mu_);
if (multi_) {
(void)curl_multi_wakeup(multi_.get());
}
#endif
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To avoid data races on the cancellation_token_ std::shared_ptr when Cancel() is called concurrently from another thread, we should access cancellation_token_ while holding the multi_mu_ lock.

void CurlImpl::Cancel() {
  std::lock_guard<std::mutex> lk(multi_mu_);
  if (cancellation_token_) {
    cancellation_token_->store(true, std::memory_order_relaxed);
  }
#if CURL_AT_LEAST_VERSION(7, 68, 0)
  // The lock keeps `multi_` alive and owned by this request while the wakeup
  // is delivered: without it the transfer thread could concurrently return
  // the handle to the pool, where another request may already be using it.
  if (multi_) {
    (void)curl_multi_wakeup(multi_.get());
  }
#endif
}

@ajayky-os
ajayky-os force-pushed the pr1/curl-cancellation-transport branch from 3516423 to 2808537 Compare August 25, 2026 11:49
@scotthart

Copy link
Copy Markdown
Member

/gcbrun

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.85366% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.28%. Comparing base (2151410) to head (2808537).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
google/cloud/internal/curl_impl.cc 55.76% 23 Missing ⚠️
google/cloud/internal/curl_http_payload.cc 0.00% 3 Missing ⚠️
google/cloud/internal/curl_rest_client.cc 50.00% 1 Missing ⚠️
google/cloud/internal/http_payload.h 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #16381   +/-   ##
=======================================
  Coverage   92.28%   92.28%           
=======================================
  Files        2244     2244           
  Lines      211631   211708   +77     
=======================================
+ Hits       195294   195373   +79     
+ Misses      16337    16335    -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

2 participants