Skip to content

Commit 3516423

Browse files
committed
feat: support cancellation tokens in Curl REST client and payloads
1 parent 30817a8 commit 3516423

11 files changed

Lines changed: 165 additions & 7 deletions

google/cloud/internal/curl_http_payload.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ std::multimap<std::string, std::string> CurlHttpPayload::DebugHeaders() const {
3535
return impl_->headers();
3636
}
3737

38+
void CurlHttpPayload::Cancel() {
39+
if (impl_) impl_->Cancel();
40+
}
41+
3842
StatusOr<std::string> ReadAll(std::unique_ptr<HttpPayload> payload,
3943
std::size_t read_size) {
4044
std::string output_buffer;

google/cloud/internal/curl_http_payload.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class CurlHttpPayload : public HttpPayload {
4545

4646
std::multimap<std::string, std::string> DebugHeaders() const override;
4747

48+
void Cancel() override;
49+
4850
private:
4951
friend class CurlRestResponse;
5052
CurlHttpPayload(std::unique_ptr<CurlImpl> impl, Options options);

google/cloud/internal/curl_impl.cc

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ static int SeekFunction( // NOLINT(misc-use-anonymous-namespace)
125125
: CURL_SEEKFUNC_FAIL;
126126
}
127127

128+
static int TransferInfoFunction( // NOLINT(misc-use-anonymous-namespace)
129+
void* userdata, curl_off_t, curl_off_t, curl_off_t, curl_off_t) {
130+
auto* const request = reinterpret_cast<CurlImpl*>(userdata);
131+
return request->TransferInfoCallback();
132+
}
133+
128134
} // extern "C"
129135

130136
std::size_t SpillBuffer::CopyFrom(absl::Span<char const> src) {
@@ -244,7 +250,12 @@ CurlImpl::~CurlImpl() {
244250
CleanupHandles();
245251

246252
CurlHandle::ReturnToPool(*factory_, std::move(handle_));
247-
factory_->CleanupMultiHandle(std::move(multi_), HandleDisposition::kKeep);
253+
factory_->CleanupMultiHandle(ReleaseMulti(), HandleDisposition::kKeep);
254+
}
255+
256+
CurlMulti CurlImpl::ReleaseMulti() {
257+
std::lock_guard<std::mutex> lk(multi_mu_);
258+
return std::move(multi_);
248259
}
249260

250261
void CurlImpl::SetHeader(HttpHeader header) {
@@ -460,8 +471,6 @@ Status CurlImpl::MakeRequest(HttpMethod method, RestContext& context,
460471
if (!status.ok()) return OnTransferError(context, std::move(status));
461472

462473
if (method == HttpMethod::kGet) {
463-
status = handle_.SetOption(CURLOPT_NOPROGRESS, 1L);
464-
if (!status.ok()) return OnTransferError(context, std::move(status));
465474
if (download_stall_timeout_ != std::chrono::seconds::zero()) {
466475
// NOLINTNEXTLINE(google-runtime-int) - libcurl *requires* long
467476
auto const timeout = static_cast<long>(download_stall_timeout_.count());
@@ -553,9 +562,43 @@ StatusOr<std::size_t> CurlImpl::Read(absl::Span<char> output) {
553562
// This context is discarded. Any interesting information was already
554563
// captured when the request was started.
555564
RestContext context;
565+
if (cancellation_token_) {
566+
context.set_cancellation_token(cancellation_token_);
567+
}
556568
return ReadImpl(context, std::move(output));
557569
}
558570

571+
void CurlImpl::SetCancellationToken(
572+
std::shared_ptr<std::atomic<bool>> token) {
573+
if (token) {
574+
if (cancellation_token_->load(std::memory_order_relaxed)) {
575+
token->store(true, std::memory_order_relaxed);
576+
}
577+
cancellation_token_ = std::move(token);
578+
cancellable_ = true;
579+
}
580+
}
581+
582+
void CurlImpl::Cancel() {
583+
cancellation_token_->store(true, std::memory_order_relaxed);
584+
#if CURL_AT_LEAST_VERSION(7, 68, 0)
585+
// The lock keeps `multi_` alive and owned by this request while the wakeup
586+
// is delivered: without it the transfer thread could concurrently return
587+
// the handle to the pool, where another request may already be using it.
588+
std::lock_guard<std::mutex> lk(multi_mu_);
589+
if (multi_) {
590+
(void)curl_multi_wakeup(multi_.get());
591+
}
592+
#endif
593+
}
594+
595+
int CurlImpl::TransferInfoCallback() {
596+
if (cancellation_token_->load(std::memory_order_relaxed)) {
597+
return 1;
598+
}
599+
return 0;
600+
}
601+
559602
std::size_t CurlImpl::WriteCallback(absl::Span<char> response) {
560603
handle_.FlushDebug(__func__);
561604
TRACE_STATE() << ", begin"
@@ -624,6 +667,17 @@ std::size_t CurlImpl::HeaderCallback(absl::Span<char> response) {
624667
Status CurlImpl::MakeRequestImpl(RestContext& context) {
625668
TRACE_STATE() << ", url_=" << url_;
626669

670+
if (context.cancellation_token() &&
671+
context.cancellation_token() != cancellation_token_) {
672+
SetCancellationToken(context.cancellation_token());
673+
}
674+
675+
if (cancellation_token_->load(std::memory_order_relaxed)) {
676+
return OnTransferError(
677+
context,
678+
internal::CancelledError("Request cancelled", GCP_ERROR_INFO()));
679+
}
680+
627681
Status status;
628682
status = handle_.SetOption(CURLOPT_URL, url_.c_str());
629683
if (!status.ok()) return OnTransferError(context, std::move(status));
@@ -645,6 +699,13 @@ Status CurlImpl::MakeRequestImpl(RestContext& context) {
645699
handle_.SetOptionUnchecked(CURLOPT_HTTP_VERSION,
646700
VersionToCurlCode(http_version_));
647701

702+
status = handle_.SetOption(CURLOPT_NOPROGRESS, 0L);
703+
if (!status.ok()) return OnTransferError(context, std::move(status));
704+
status = handle_.SetOption(CURLOPT_XFERINFOFUNCTION, &TransferInfoFunction);
705+
if (!status.ok()) return OnTransferError(context, std::move(status));
706+
status = handle_.SetOption(CURLOPT_XFERINFODATA, this);
707+
if (!status.ok()) return OnTransferError(context, std::move(status));
708+
648709
auto error = curl_multi_add_handle(multi_.get(), handle_.handle_.get());
649710

650711
// This indicates that we are using the API incorrectly. The application
@@ -669,6 +730,17 @@ StatusOr<std::size_t> CurlImpl::ReadImpl(RestContext& context,
669730
avail_ = output;
670731
TRACE_STATE() << ", begin";
671732

733+
if (context.cancellation_token() &&
734+
context.cancellation_token() != cancellation_token_) {
735+
SetCancellationToken(context.cancellation_token());
736+
}
737+
738+
if (cancellation_token_->load(std::memory_order_relaxed)) {
739+
return OnTransferError(
740+
context,
741+
internal::CancelledError("Request cancelled", GCP_ERROR_INFO()));
742+
}
743+
672744
// Before calling WaitForHandles(), move any data from the spill buffer
673745
// into the output buffer. It is possible that WaitForHandles() will
674746
// never call WriteCallback() (e.g., because PerformWork() closed the
@@ -807,6 +879,15 @@ StatusOr<int> CurlImpl::PerformWork() {
807879
// (see above) tells libcurl that it cannot receive more data.
808880
if (closing_) continue;
809881
if (multi_info_read_result != CURLE_OK) {
882+
// CURLE_ABORTED_BY_CALLBACK maps to `kAborted`, but when the abort
883+
// came from TransferInfoCallback() observing the cancellation token
884+
// the caller asked for the cancellation: report `kCancelled` so it is
885+
// not mistaken for a permanent failure.
886+
if (multi_info_read_result == CURLE_ABORTED_BY_CALLBACK &&
887+
cancellation_token_->load(std::memory_order_relaxed)) {
888+
return internal::CancelledError("Request cancelled",
889+
GCP_ERROR_INFO());
890+
}
810891
return CurlHandle::AsStatus(multi_info_read_result, __func__);
811892
}
812893
if (multi_remove_result != CURLM_OK) {
@@ -822,6 +903,9 @@ Status CurlImpl::PerformWorkUntil(absl::FunctionRef<bool()> predicate) {
822903
TRACE_STATE() << ", begin";
823904
int repeats = 0;
824905
while (!predicate()) {
906+
if (cancellation_token_->load(std::memory_order_relaxed)) {
907+
return internal::CancelledError("Request cancelled", GCP_ERROR_INFO());
908+
}
825909
handle_.FlushDebug(__func__);
826910
TRACE_STATE() << ", repeats=" << repeats;
827911
auto running_handles = PerformWork();
@@ -839,7 +923,14 @@ Status CurlImpl::PerformWorkUntil(absl::FunctionRef<bool()> predicate) {
839923
}
840924

841925
Status CurlImpl::WaitForHandles(int& repeats) {
926+
#if !CURL_AT_LEAST_VERSION(7, 68, 0)
927+
// Without curl_multi_wakeup() a Cancel() from another thread cannot
928+
// interrupt the wait, so poll frequently -- but only when some caller can
929+
// actually cancel this transfer; other transfers keep the long timeout.
930+
int const timeout_ms = cancellable_ ? 50 : 1000;
931+
#else
842932
int const timeout_ms = 1000;
933+
#endif
843934
int numfds = 0;
844935
CURLMcode result;
845936
#if CURL_AT_LEAST_VERSION(7, 66, 0)
@@ -890,7 +981,7 @@ Status CurlImpl::OnTransferError(RestContext& context, Status status) {
890981
// While the handle is suspect, there is probably nothing wrong with the
891982
// CURLM* handle. That just represents a local resource, such as data
892983
// structures for epoll(7) or select(2).
893-
factory_->CleanupMultiHandle(std::move(multi_), HandleDisposition::kKeep);
984+
factory_->CleanupMultiHandle(ReleaseMulti(), HandleDisposition::kKeep);
894985

895986
return status;
896987
}
@@ -903,7 +994,7 @@ void CurlImpl::OnTransferDone() {
903994
// in PerformWork(). Release the handles back to the factory as soon as
904995
// possible, so they can be reused for any other requests.
905996
CurlHandle::ReturnToPool(*factory_, std::move(handle_));
906-
factory_->CleanupMultiHandle(std::move(multi_), HandleDisposition::kKeep);
997+
factory_->CleanupMultiHandle(ReleaseMulti(), HandleDisposition::kKeep);
907998
}
908999

9091000
std::optional<std::string> CurlOptProxy(Options const& options) {

google/cloud/internal/curl_impl.h

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
#include "google/cloud/version.h"
3030
#include "absl/types/span.h"
3131
#include <array>
32+
#include <atomic>
3233
#include <chrono>
3334
#include <cstdint>
3435
#include <map>
3536
#include <memory>
37+
#include <mutex>
3638
#include <optional>
3739
#include <string>
3840
#include <vector>
@@ -80,9 +82,11 @@ class CurlImpl {
8082
~CurlImpl();
8183

8284
CurlImpl(CurlImpl const&) = delete;
83-
CurlImpl(CurlImpl&&) = default;
85+
// Not movable: `multi_mu_` synchronizes Cancel(), which may run on a
86+
// different thread, with the handoff of `multi_` back to the pool.
87+
CurlImpl(CurlImpl&&) = delete;
8488
CurlImpl& operator=(CurlImpl const&) = delete;
85-
CurlImpl& operator=(CurlImpl&&) = default;
89+
CurlImpl& operator=(CurlImpl&&) = delete;
8690

8791
void SetHeader(HttpHeader header);
8892
void SetHeaders(HttpHeaders const& headers);
@@ -105,6 +109,10 @@ class CurlImpl {
105109
bool HasUnreadData() const;
106110
StatusOr<std::size_t> Read(absl::Span<char> output);
107111

112+
void SetCancellationToken(std::shared_ptr<std::atomic<bool>> token);
113+
void Cancel();
114+
int TransferInfoCallback();
115+
108116
// Called from libcurl callbacks for received data.
109117
std::size_t WriteCallback(absl::Span<char> response);
110118
std::size_t HeaderCallback(absl::Span<char> response);
@@ -132,6 +140,10 @@ class CurlImpl {
132140

133141
// Cleanup the CURL handles, leaving them ready for reuse.
134142
void CleanupHandles();
143+
// Take ownership of `multi_` away from this request, synchronizing with any
144+
// concurrent Cancel(). Call this instead of `std::move(multi_)` before
145+
// returning the handle to the pool.
146+
CurlMulti ReleaseMulti();
135147
// Perform at least part of the request.
136148
StatusOr<int> PerformWork();
137149
// Loop on PerformWork until a condition is met.
@@ -146,7 +158,16 @@ class CurlImpl {
146158
std::vector<HttpHeader> pending_request_headers_;
147159
CurlHeaders request_headers_;
148160
CurlHandle handle_;
161+
// Guards the handoff of `multi_` back to the pool against a concurrent
162+
// Cancel(), which calls `curl_multi_wakeup()` on it from another thread.
163+
std::mutex multi_mu_;
149164
CurlMulti multi_;
165+
std::shared_ptr<std::atomic<bool>> cancellation_token_ =
166+
std::make_shared<std::atomic<bool>>(false);
167+
// True once an external cancellation token was installed, i.e. some other
168+
// thread may call Cancel() on this transfer. Only read and written on the
169+
// transfer thread.
170+
bool cancellable_ = false;
150171

151172
bool logging_enabled_;
152173
bool follow_location_;

google/cloud/internal/curl_rest_client.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ StatusOr<std::unique_ptr<CurlImpl>> CurlRestClient::CreateCurlImpl(
133133
auto handle = CurlHandle::MakeFromPool(*handle_factory_);
134134
auto impl = std::make_unique<CurlImpl>(std::move(handle), handle_factory_,
135135
options, pqc_ec_curves_);
136+
if (context.cancellation_token()) {
137+
impl->SetCancellationToken(context.cancellation_token());
138+
}
136139
if (credentials_) {
137140
auto auth_headers = credentials_->AuthenticationHeaders(
138141
std::chrono::system_clock::now(), endpoint_address_);

google/cloud/internal/http_payload.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ class HttpPayload {
4646
virtual std::multimap<std::string, std::string> DebugHeaders() const {
4747
return {};
4848
}
49+
50+
/// Cancels an in-progress or subsequent read.
51+
virtual void Cancel() {}
4952
};
5053

5154
// This function makes one or more HttpPayload::Read calls and writes all the

google/cloud/internal/rest_context.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
#include "google/cloud/internal/http_header.h"
1919
#include "google/cloud/options.h"
2020
#include "google/cloud/version.h"
21+
#include <atomic>
2122
#include <chrono>
23+
#include <memory>
2224
#include <optional>
2325
#include <string>
2426
#include <utility>
@@ -106,10 +108,18 @@ class RestContext {
106108
appconnect_time_ = us;
107109
}
108110

111+
std::shared_ptr<std::atomic<bool>> cancellation_token() const {
112+
return cancellation_token_;
113+
}
114+
void set_cancellation_token(std::shared_ptr<std::atomic<bool>> token) {
115+
cancellation_token_ = std::move(token);
116+
}
117+
109118
private:
110119
friend bool operator==(RestContext const& lhs, RestContext const& rhs);
111120
Options options_;
112121
HttpHeaders headers_;
122+
std::shared_ptr<std::atomic<bool>> cancellation_token_;
113123
std::optional<std::string> local_ip_address_;
114124
std::optional<std::int32_t> local_port_;
115125
std::optional<std::string> primary_ip_address_;

google/cloud/internal/rest_context_test.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ TEST_F(RestContextTest, Equality) {
7878
EXPECT_THAT(lhs, Eq(rhs));
7979
}
8080

81+
TEST_F(RestContextTest, CancellationToken) {
82+
RestContext context;
83+
EXPECT_EQ(context.cancellation_token(), nullptr);
84+
85+
auto token = std::make_shared<std::atomic<bool>>(false);
86+
context.set_cancellation_token(token);
87+
EXPECT_EQ(context.cancellation_token(), token);
88+
}
89+
8190
} // namespace
8291
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
8392
} // namespace rest_internal

google/cloud/internal/tracing_http_payload.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class TracingHttpPayload : public HttpPayload {
3535
bool HasUnreadData() const override;
3636
StatusOr<std::size_t> Read(absl::Span<char> buffer) override;
3737
std::multimap<std::string, std::string> DebugHeaders() const override;
38+
void Cancel() override {
39+
if (impl_) impl_->Cancel();
40+
}
3841

3942
private:
4043
std::unique_ptr<HttpPayload> impl_;

google/cloud/internal/tracing_http_payload_test.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,17 @@ TEST(TracingHttpPayload, Failure) {
124124
SpanHasEvents(MakeReadMatcher(16, 16), MakeReadMatcher(16)))));
125125
}
126126

127+
TEST(TracingHttpPayload, Cancel) {
128+
auto impl = std::make_unique<MockHttpPayload>();
129+
EXPECT_CALL(*impl, Cancel).Times(1);
130+
131+
RestRequest request("https://example.com/ignored");
132+
auto span = MakeSpanHttp(request, "GET");
133+
134+
TracingHttpPayload payload(std::move(impl), std::move(span));
135+
payload.Cancel();
136+
}
137+
127138
} // namespace
128139
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
129140
} // namespace rest_internal

0 commit comments

Comments
 (0)