Skip to content

Commit a1be2ce

Browse files
authored
Merge pull request #358 from H-D-OWL/cancel-requests
Added request cancellation functionality to the HTTP client.
2 parents e79ac64 + f883d0a commit a1be2ce

3 files changed

Lines changed: 106 additions & 5 deletions

File tree

include/tgbot/net/HttpClient.h

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <string>
88
#include <vector>
99
#include <cstdint>
10+
#include <atomic>
1011

1112
namespace TgBot {
1213

@@ -31,22 +32,64 @@ class TGBOT_API HttpClient {
3132
std::int32_t _timeout = 25;
3233

3334
/**
34-
* @brief Get the maximum number of makeRequest() retries before giving up and throwing an exception.
35-
*/
35+
* @brief Get the maximum number of makeRequest() retries before giving up and throwing an exception.
36+
*/
3637
virtual int getRequestMaxRetries() const {
3738
return requestMaxRetries;
3839
}
3940

4041
/**
41-
* @brief Get the makeRequest() backoff duration between retries, in seconds.
42-
*/
42+
* @brief Get the makeRequest() backoff duration between retries, in seconds.
43+
*/
4344
virtual int getRequestBackoff() const {
4445
return requestBackoff;
4546
}
4647

48+
/**
49+
* @brief Cancels the requests.
50+
*
51+
* @param eternal Optional. If true, permanently disables the HTTP client, canceling all current and future requests.
52+
* If false, cancel the currently running requests.
53+
*/
54+
virtual void cancel(const bool eternal = false) const {
55+
if (eternal) {
56+
_isEternalCancel.store(true);
57+
}
58+
else {
59+
_cancelEpoch.fetch_add(1);
60+
}
61+
}
62+
63+
/**
64+
* @brief Checks if the HTTP client is permanently disabled.
65+
*/
66+
virtual bool isEternalCancelled() const {
67+
return _isEternalCancel.load();
68+
}
69+
70+
/**
71+
* @brief Get the exception message that occurs when the request is aborted.
72+
*/
73+
virtual const std::string& getCancelExceptionText() const {
74+
return cancelExceptionText;
75+
}
76+
77+
protected:
78+
79+
/**
80+
* @brief Flag indicating whether the HTTP client is permanently disabled.
81+
*/
82+
mutable std::atomic<bool> _isEternalCancel{ false };
83+
84+
/**
85+
* @brief Counter used to invalidate current requests.
86+
*/
87+
mutable std::atomic<uint64_t> _cancelEpoch{ 0 };
88+
4789
private:
4890
int requestMaxRetries = 3;
4991
int requestBackoff = 1;
92+
const std::string cancelExceptionText = "request cancelled";
5093
};
5194

5295
}

src/Api.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <chrono>
44
#include <thread>
5+
#include <string_view>
56

67
namespace TgBot {
78

@@ -2863,11 +2864,31 @@ boost::property_tree::ptree Api::sendRequest(const std::string& method, const st
28632864
throw TgException(message, static_cast<TgException::ErrorCode>(errorCode));
28642865
}
28652866
} catch (...) {
2867+
bool isCancelException = false;
2868+
2869+
try {
2870+
throw;
2871+
}
2872+
catch (const std::exception& e) {
2873+
const std::string_view sv{e.what()};
2874+
2875+
if(sv.compare(_httpClient.getCancelExceptionText()) == 0) {
2876+
isCancelException = true;
2877+
}
2878+
}
2879+
catch (...) {
2880+
}
2881+
28662882
int max_retries = _httpClient.getRequestMaxRetries();
2867-
if ((max_retries >= 0) && (retries == max_retries)) {
2883+
if (isCancelException || _httpClient.isEternalCancelled() || ((max_retries >= 0) && (retries == max_retries))) {
28682884
throw;
28692885
} else {
28702886
std::this_thread::sleep_for(std::chrono::seconds(requestRetryBackoff));
2887+
2888+
if (_httpClient.isEternalCancelled()) {
2889+
throw;
2890+
}
2891+
28712892
retries++;
28722893
continue;
28732894
}

src/net/CurlHttpClient.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,24 @@
44

55
#include <cstddef>
66
#include <string>
7+
#include <atomic>
8+
#include <cstdint>
9+
#include <cstring>
710

811
namespace TgBot {
912

13+
namespace {
14+
15+
struct RequestCancelState {
16+
RequestCancelState(const std::atomic<bool>* const isEternalCancel, const std::atomic<uint64_t>* const globalCancelEpoch, const uint64_t currentCancelEpoch)
17+
: isEternalCancel(isEternalCancel), globalCancelEpoch(globalCancelEpoch), currentCancelEpoch(currentCancelEpoch) {}
18+
19+
const std::atomic<bool>* const isEternalCancel = nullptr;
20+
const std::atomic<uint64_t>* const globalCancelEpoch = nullptr;
21+
const uint64_t currentCancelEpoch = 0;
22+
};
23+
}
24+
1025
CurlHttpClient::CurlHttpClient() : _httpParser() {
1126
}
1227

@@ -35,6 +50,16 @@ static CURL* getCurlHandle(const CurlHttpClient *c_) {
3550
return it->second;
3651
}
3752

53+
static int curlProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
54+
{
55+
const RequestCancelState* const state = static_cast<const RequestCancelState* const>(clientp);
56+
if (state && ((state->isEternalCancel && state->isEternalCancel->load()) || (state->globalCancelEpoch && state->currentCancelEpoch < state->globalCancelEpoch->load()))) {
57+
return 1;
58+
}
59+
60+
return 0;
61+
}
62+
3863
static std::size_t curlWriteString(char* ptr, std::size_t size, std::size_t nmemb, void* userdata) {
3964
static_cast<std::string*>(userdata)->append(ptr, size * nmemb);
4065
return size * nmemb;
@@ -48,6 +73,12 @@ std::string CurlHttpClient::makeRequest(const Url& url, const std::vector<HttpRe
4873
curl_easy_setopt(curl, CURLOPT_TIMEOUT, _timeout);
4974
curl_easy_setopt(curl, CURLOPT_PROXY, _proxyUrl);
5075

76+
const RequestCancelState state{ &_isEternalCancel, &_cancelEpoch, _cancelEpoch.load() };
77+
78+
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
79+
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, curlProgressCallback);
80+
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &state);
81+
5182
std::string u = url.protocol + "://" + url.host + url.path;
5283
if (args.empty()) {
5384
u += "?" + url.query;
@@ -81,6 +112,12 @@ std::string CurlHttpClient::makeRequest(const Url& url, const std::vector<HttpRe
81112
auto res = curl_easy_perform(curl);
82113
curl_mime_free(mime);
83114

115+
if (res == CURLcode::CURLE_ABORTED_BY_CALLBACK && ((state.isEternalCancel && state.isEternalCancel->load()) || (state.globalCancelEpoch && state.currentCancelEpoch < state.globalCancelEpoch->load()))) {
116+
const size_t slashPos = url.path.rfind('/');
117+
118+
throw std::runtime_error(slashPos == std::string::npos ? getCancelExceptionText() : getCancelExceptionText() + ": " + url.path.substr(slashPos + 1));
119+
}
120+
84121
// If the request did not complete correctly, show the error
85122
// information. If no detailed error information was written to errbuf
86123
// show the more generic information from curl_easy_strerror instead.

0 commit comments

Comments
 (0)