HTTP request cancellation

- replaces abort() with something more structured.
This commit is contained in:
James Turner
2016-04-14 09:15:57 +01:00
parent 493aab8bab
commit a3f1bb546f
10 changed files with 204 additions and 48 deletions

View File

@@ -155,8 +155,9 @@ namespace canvas
//----------------------------------------------------------------------------
Image::~Image()
{
if( _http_request )
_http_request->abort("image destroyed");
if( _http_request ) {
Canvas::getSystemAdapter()->getHTTPClient()->cancelRequest(_http_request, "image destroyed");
}
}
//----------------------------------------------------------------------------
@@ -618,7 +619,7 @@ namespace canvas
// Abort pending request
if( _http_request )
{
_http_request->abort("setting new image");
Canvas::getSystemAdapter()->getHTTPClient()->cancelRequest(_http_request, "setting new image");
_http_request.reset();
}

View File

@@ -78,7 +78,6 @@ class Client::ClientPrivate
public:
#if defined(ENABLE_CURL)
CURLM* curlMulti;
bool haveActiveRequests;
void createCurlMulti()
{
@@ -94,6 +93,9 @@ public:
}
typedef std::map<Request_ptr, CURL*> RequestCurlMap;
RequestCurlMap requests;
#else
NetChannelPoller poller;
// connections by host (potentially more than one)
@@ -205,6 +207,8 @@ public:
// closing of the connection from the server side when getting the body,
bool canCloseState = (state == STATE_GETTING_BODY);
bool isCancelling = (state == STATE_CANCELLING);
if (canCloseState && activeRequest) {
// check bodyTransferSize matches how much we actually transferred
if (bodyTransferSize > 0) {
@@ -229,7 +233,7 @@ public:
sentRequests.erase(sentRequests.begin());
}
if (activeRequest) {
if (activeRequest && !isCancelling) {
activeRequest->setFailure(500, "server closed connection");
// remove the failed request from sentRequests, so it does
// not get restored
@@ -262,6 +266,39 @@ public:
tryStartNextRequest();
}
void cancelRequest(const Request_ptr& r)
{
RequestList::iterator it = std::find(sentRequests.begin(),
sentRequests.end(), r);
if (it != sentRequests.end()) {
sentRequests.erase(it);
if ((r == activeRequest) || !activeRequest) {
// either the cancelling request is active, or we're in waiting
// for response state - close now
setState(STATE_CANCELLING);
close();
setState(STATE_CLOSED);
activeRequest = NULL;
_contentDecoder.reset();
} else if (activeRequest) {
SG_LOG(SG_IO, SG_INFO, "con:" << _connectionId << " cancelling non-active: " << r->url());
// has been sent but not active, let the active finish and
// then close. Otherwise cancelling request #2 would mess up
// active transfer #1
activeRequest->setCloseAfterComplete();
}
} // of request has been sent
// simpler case, not sent yet just remove from the queue
it = std::find(queuedRequests.begin(), queuedRequests.end(), r);
if (it != queuedRequests.end()) {
queuedRequests.erase(it);
}
}
void beginResponse()
{
assert(!sentRequests.empty());
@@ -528,6 +565,7 @@ private:
STATE_GETTING_CHUNKED_BYTES,
STATE_GETTING_TRAILER,
STATE_SOCKET_ERROR,
STATE_CANCELLING, ///< cancelling an acitve request
STATE_CLOSED ///< connection should be closed now
};
@@ -787,34 +825,43 @@ void Client::update(int waitTimeout)
#if defined(ENABLE_CURL)
int remainingActive, messagesInQueue;
curl_multi_perform(d->curlMulti, &remainingActive);
d->haveActiveRequests = (remainingActive > 0);
CURLMsg* msg;
while ((msg = curl_multi_info_read(d->curlMulti, &messagesInQueue))) {
if (msg->msg == CURLMSG_DONE) {
Request* req;
Request* rawReq = 0;
CURL *e = msg->easy_handle;
curl_easy_getinfo(e, CURLINFO_PRIVATE, &req);
curl_easy_getinfo(e, CURLINFO_PRIVATE, &rawReq);
// ensure request stays valid for the moment
// eg if responseComplete cancels us
Request_ptr req(rawReq);
long responseCode;
curl_easy_getinfo(e, CURLINFO_RESPONSE_CODE, &responseCode);
// remove from the requests map now,
// in case the callbacks perform a cancel. We'll use
// the absence from the request dict in cancel to avoid
// a double remove
ClientPrivate::RequestCurlMap::iterator it = d->requests.find(req);
assert(it != d->requests.end());
assert(it->second == e);
d->requests.erase(it);
if (msg->data.result == 0) {
req->responseComplete();
} else {
fprintf(stderr, "Result: %d - %s\n",
msg->data.result, curl_easy_strerror(msg->data.result));
SG_LOG(SG_IO, SG_WARN, "CURL Result:" << msg->data.result << " " << curl_easy_strerror(msg->data.result));
req->setFailure(msg->data.result, curl_easy_strerror(msg->data.result));
}
curl_multi_remove_handle(d->curlMulti, e);
// balance the reference we take in makeRequest
SGReferenced::put(req);
curl_easy_cleanup(e);
}
else {
SG_LOG(SG_IO, SG_ALERT, "CurlMSG:" << msg->msg);
} else {
// should never happen since CURLMSG_DONE is the only code
// defined!
SG_LOG(SG_IO, SG_ALERT, "unknown CurlMSG:" << msg->msg);
}
} // of curl message processing loop
SGTimeStamp::sleepForMSec(waitTimeout);
@@ -880,11 +927,14 @@ void Client::makeRequest(const Request_ptr& r)
r->_client = this;
#if defined(ENABLE_CURL)
ClientPrivate::RequestCurlMap::iterator rit = d->requests.find(r);
assert(rit == d->requests.end());
CURL* curlRequest = curl_easy_init();
curl_easy_setopt(curlRequest, CURLOPT_URL, r->url().c_str());
// manually increase the ref count of the request
SGReferenced::get(r.get());
d->requests[r] = curlRequest;
curl_easy_setopt(curlRequest, CURLOPT_PRIVATE, r.get());
// disable built-in libCurl progress feedback
curl_easy_setopt(curlRequest, CURLOPT_NOPROGRESS, 1);
@@ -950,9 +1000,9 @@ void Client::makeRequest(const Request_ptr& r)
}
curl_multi_add_handle(d->curlMulti, curlRequest);
d->haveActiveRequests = true;
// FIXME - premature?
// this seems premature, but we don't have a callback from Curl we could
// use to trigger when the requst is actually sent.
r->requestStart();
#else
@@ -1034,6 +1084,33 @@ void Client::makeRequest(const Request_ptr& r)
#endif
}
void Client::cancelRequest(const Request_ptr &r, std::string reason)
{
#if defined(ENABLE_CURL)
ClientPrivate::RequestCurlMap::iterator it = d->requests.find(r);
if(it == d->requests.end()) {
// already being removed, presumably inside ::update()
// nothing more to do
return;
}
CURLMcode err = curl_multi_remove_handle(d->curlMulti, it->second);
assert(err == CURLM_OK);
// clear the request pointer form the curl-easy object
curl_easy_setopt(it->second, CURLOPT_PRIVATE, 0);
curl_easy_cleanup(it->second);
d->requests.erase(it);
#else
ConnectionDict::iterator it = d->connections.begin();
for (; it != d->connections.end(); ++it) {
(it->second)->cancelRequest(r);
}
#endif
r->setFailure(-1, reason);
}
//------------------------------------------------------------------------------
FileRequestRef Client::save( const std::string& url,
const std::string& filename )
@@ -1088,7 +1165,7 @@ void Client::setProxy( const std::string& proxy,
bool Client::hasActiveRequests() const
{
#if defined(ENABLE_CURL)
return d->haveActiveRequests;
return !d->requests.empty();
#else
ConnectionDict::const_iterator it = d->connections.begin();
for (; it != d->connections.end(); ++it) {
@@ -1201,7 +1278,12 @@ size_t Client::requestHeaderCallback(char *rawBuffer, size_t size, size_t nitems
void Client::debugDumpRequests()
{
#if defined(ENABLE_CURL)
SG_LOG(SG_IO, SG_INFO, "== HTTP request dump");
ClientPrivate::RequestCurlMap::iterator it = d->requests.begin();
for (; it != d->requests.end(); ++it) {
SG_LOG(SG_IO, SG_INFO, "\t" << it->first->url());
}
SG_LOG(SG_IO, SG_INFO, "==");
#else
SG_LOG(SG_IO, SG_INFO, "== HTTP connection dump");
ConnectionDict::iterator it = d->connections.begin();

View File

@@ -49,6 +49,8 @@ public:
void makeRequest(const Request_ptr& r);
void cancelRequest(const Request_ptr& r, std::string reason = std::string());
/**
* Download a resource and save it to a file.
*

View File

@@ -59,8 +59,6 @@ namespace HTTP
SG_WARN,
"HTTP::FileRequest: failed to open file '" << _filename << "'"
);
abort("Failed to open file.");
}
}

View File

@@ -365,19 +365,6 @@ void Request::setReadyState(ReadyState state)
_cb_always(this);
}
//------------------------------------------------------------------------------
void Request::abort()
{
abort("Request aborted.");
}
//----------------------------------------------------------------------------
void Request::abort(const std::string& reason)
{
setFailure(-1, reason);
_willClose = true;
}
//------------------------------------------------------------------------------
bool Request::closeAfterComplete() const
{
@@ -385,6 +372,13 @@ bool Request::closeAfterComplete() const
return _willClose || (_responseVersion != HTTP_1_1);
}
//------------------------------------------------------------------------------
void Request::setCloseAfterComplete()
{
_willClose = true;
}
//------------------------------------------------------------------------------
bool Request::serverSupportsPipelining() const
{

View File

@@ -199,16 +199,6 @@ public:
ReadyState readyState() const { return _ready_state; }
/**
* Request aborting this request.
*/
void abort();
/**
* Request aborting this request and specify the reported reaseon for it.
*/
void abort(const std::string& reason);
bool closeAfterComplete() const;
bool isComplete() const;
@@ -246,6 +236,8 @@ private:
void processBodyBytes(const char* s, int n);
void setReadyState(ReadyState state);
void setCloseAfterComplete();
Client* _client; // HTTP client we're active on
std::string _method;

View File

@@ -698,6 +698,74 @@ cout << "testing proxy close" << endl;
COMPARE(tr->responseBytesReceived(), 0);
}
// test cancel
{
cout << "cancel request" << endl;
testServer.resetConnectCount();
cl.clearAllConnections();
cl.setProxy("", 80);
TestRequest* tr = new TestRequest("http://localhost:2000/test1");
HTTP::Request_ptr own(tr);
cl.makeRequest(tr);
TestRequest* tr2 = new TestRequest("http://localhost:2000/testLorem");
HTTP::Request_ptr own2(tr2);
cl.makeRequest(tr2);
TestRequest* tr3 = new TestRequest("http://localhost:2000/test1");
HTTP::Request_ptr own3(tr3);
cl.makeRequest(tr3);
cl.cancelRequest(tr, "my reason 1");
cl.cancelRequest(tr2, "my reason 2");
waitForComplete(&cl, tr3);
COMPARE(tr->responseCode(), -1);
COMPARE(tr2->responseReason(), "my reason 2");
COMPARE(tr3->responseLength(), strlen(BODY1));
COMPARE(tr3->responseBytesReceived(), strlen(BODY1));
COMPARE(tr3->bodyData, string(BODY1));
}
// test cancel
{
cout << "cancel middle request" << endl;
testServer.resetConnectCount();
cl.clearAllConnections();
cl.setProxy("", 80);
TestRequest* tr = new TestRequest("http://localhost:2000/test1");
HTTP::Request_ptr own(tr);
cl.makeRequest(tr);
TestRequest* tr2 = new TestRequest("http://localhost:2000/testLorem");
HTTP::Request_ptr own2(tr2);
cl.makeRequest(tr2);
TestRequest* tr3 = new TestRequest("http://localhost:2000/test1");
HTTP::Request_ptr own3(tr3);
cl.makeRequest(tr3);
cl.cancelRequest(tr2, "middle request");
waitForComplete(&cl, tr3);
COMPARE(tr->responseCode(), 200);
COMPARE(tr->responseLength(), strlen(BODY1));
COMPARE(tr->responseBytesReceived(), strlen(BODY1));
COMPARE(tr->bodyData, string(BODY1));
COMPARE(tr2->responseCode(), -1);
COMPARE(tr3->responseLength(), strlen(BODY1));
COMPARE(tr3->responseBytesReceived(), strlen(BODY1));
COMPARE(tr3->bodyData, string(BODY1));
}
{
cout << "get-during-response-send" << endl;
cl.clearAllConnections();

View File

@@ -409,7 +409,7 @@ size_t Install::downloadedBytes() const
void Install::cancelDownload()
{
if (m_download.valid()) {
m_download->abort("User cancelled download");
m_package->catalog()->root()->cancelHTTPRequest(m_download, "User cancelled download");
}
if (m_revision == 0) {

View File

@@ -226,6 +226,20 @@ void Root::makeHTTPRequest(HTTP::Request *req)
d->httpPendingRequests.push_back(req);
}
void Root::cancelHTTPRequest(HTTP::Request *req, const std::string &reason)
{
if (d->http) {
d->http->cancelRequest(req, reason);
}
std::deque<HTTP::Request_ptr>::iterator it = std::find(d->httpPendingRequests.begin(),
d->httpPendingRequests.end(),
req);
if (it != d->httpPendingRequests.end()) {
d->httpPendingRequests.erase(it);
}
}
Root::Root(const SGPath& aPath, const std::string& aVersion) :
d(new RootPrivate)
{

View File

@@ -81,6 +81,11 @@ public:
* set yet.
*/
void makeHTTPRequest(HTTP::Request* req);
/**
* Cancel an HTTP request.
*/
void cancelHTTPRequest(HTTP::Request* req, const std::string& reason);
/**
* The catalog XML/property version in use. This is used to make incomaptible