@@ -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
130136std::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
250261void 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+
559602std::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) {
624667Status 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
841925Status 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
9091000std::optional<std::string> CurlOptProxy (Options const & options) {
0 commit comments