Files
MapLibreParticleLab/platform/darwin/http_request_nsurl.mm
John Firebaugh 481fa0e69f Restructure *Request and Context
The previous implementation, based on thread-local storage, did not
ensure that the context was destructed before the FileSource run loop.
This resulted in implementations attempting to uv_close handles for a
loop that had already been destroyed.

This change also fixes #1262.
2015-04-28 19:26:35 -04:00

406 lines
14 KiB
Plaintext

#include <mbgl/storage/http_context.hpp>
#include <mbgl/storage/resource.hpp>
#include <mbgl/storage/response.hpp>
#include <mbgl/util/std.hpp>
#include <mbgl/util/time.hpp>
#include <mbgl/util/parsedate.h>
#import <Foundation/Foundation.h>
#include <map>
#include <cassert>
namespace mbgl {
enum class ResponseStatus : uint8_t {
// This error probably won't be resolved by retrying anytime soon. We are giving up.
PermanentError,
// This error might be resolved by waiting some time (e.g. server issues).
// We are going to do an exponential back-off and will try again in a few seconds.
TemporaryError,
// This error was caused by a temporary error and it is likely that it will be resolved
// immediately. We are going to try again right away. This is like the TemporaryError, except
// that we will not perform exponential back-off.
SingularError,
// This error might be resolved once the network reachability status changes.
// We are going to watch the network status for changes and will retry as soon as the
// operating system notifies us of a network status change.
ConnectionError,
// The request was canceled mid-way.
Canceled,
// The request returned data successfully. We retrieved and decoded the data successfully.
Successful,
// The request confirmed that the data wasn't changed. We already have the data.
NotModified,
};
// -------------------------------------------------------------------------------------------------
class HTTPNSURLContext;
class HTTPRequest : public RequestBase {
public:
HTTPRequest(HTTPNSURLContext*,
const Resource&,
Callback,
uv_loop_t*,
std::shared_ptr<const Response>);
~HTTPRequest();
void cancel() override;
void retry() override;
private:
void cancelTimer();
void start();
void handleResult(NSData *data, NSURLResponse *res, NSError *error);
void handleResponse();
void retry(uint64_t timeout);
static void restart(uv_timer_t *timer, int);
HTTPNSURLContext *context = nullptr;
bool cancelled = false;
NSURLSessionDataTask *task = nullptr;
std::unique_ptr<Response> response;
const std::shared_ptr<const Response> existingResponse;
ResponseStatus status = ResponseStatus::PermanentError;
uv_async_t *async = nullptr;
int attempts = 0;
uv_timer_t *timer = nullptr;
enum : bool { PreemptImmediately, ExponentialBackoff } strategy = PreemptImmediately;
static const int maxAttempts = 4;
};
// -------------------------------------------------------------------------------------------------
class HTTPNSURLContext : public HTTPContext {
public:
HTTPNSURLContext(uv_loop_t *loop);
~HTTPNSURLContext();
RequestBase* createRequest(const Resource&,
RequestBase::Callback,
uv_loop_t*,
std::shared_ptr<const Response>) override;
NSURLSession *session = nil;
NSString *userAgent = nil;
};
HTTPNSURLContext::HTTPNSURLContext(uv_loop_t *loop_) : HTTPContext(loop_) {
@autoreleasepool {
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForResource = 30;
sessionConfig.HTTPMaximumConnectionsPerHost = 8;
sessionConfig.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
sessionConfig.URLCache = nil;
session = [NSURLSession sessionWithConfiguration:sessionConfig];
[session retain];
// Write user agent string
userAgent = @"MapboxGL";
}
}
HTTPNSURLContext::~HTTPNSURLContext() {
[session release];
session = nullptr;
[userAgent release];
userAgent = nullptr;
}
RequestBase* HTTPNSURLContext::createRequest(const Resource& resource,
RequestBase::Callback callback,
uv_loop_t* loop,
std::shared_ptr<const Response> response) {
return new HTTPRequest(this, resource, callback, loop, response);
}
// -------------------------------------------------------------------------------------------------
HTTPRequest::HTTPRequest(HTTPNSURLContext* context_, const Resource& resource_, Callback callback_, uv_loop_t *loop, std::shared_ptr<const Response> existingResponse_)
: RequestBase(resource_, callback_),
context(context_),
existingResponse(existingResponse_),
async(new uv_async_t) {
context->addRequest(this);
async->data = this;
uv_async_init(loop, async, [](uv_async_t *as, int) {
auto impl = reinterpret_cast<HTTPRequest *>(as->data);
impl->handleResponse();
});
start();
}
HTTPRequest::~HTTPRequest() {
assert(!task);
assert(async);
// Stop the backoff timer to avoid re-triggering this request.
cancelTimer();
uv::close(async);
context->removeRequest(this);
}
void HTTPRequest::start() {
assert(!task);
attempts++;
@autoreleasepool {
NSMutableString *url = [[NSMutableString alloc] initWithString:@(resource.url.c_str())];
if ([[NSUserDefaults standardUserDefaults] objectForKey:@"mapbox_metrics_disabled"] == nil) {
if ([url rangeOfString:@"?"].location == NSNotFound) {
[url appendString:@"?"];
} else {
[url appendString:@"&"];
}
[url appendString:@"events=true"];
}
NSMutableURLRequest *req = [[NSMutableURLRequest alloc]
initWithURL:[NSURL URLWithString:url]];
if (existingResponse) {
if (!existingResponse->etag.empty()) {
[req addValue:@(existingResponse->etag.c_str()) forHTTPHeaderField:@"If-None-Match"];
} else if (existingResponse->modified) {
const std::string time = util::rfc1123(existingResponse->modified);
[req addValue:@(time.c_str()) forHTTPHeaderField:@"If-Modified-Since"];
}
}
[req addValue:context->userAgent forHTTPHeaderField:@"User-Agent"];
task = [context->session dataTaskWithRequest:req
completionHandler:^(NSData *data, NSURLResponse *res,
NSError *error) { handleResult(data, res, error); }];
[req release];
[task retain];
[task resume];
}
}
void HTTPRequest::handleResponse() {
if (task) {
[task release];
task = nullptr;
}
if (!cancelled) {
if (status == ResponseStatus::TemporaryError && attempts < maxAttempts) {
strategy = ExponentialBackoff;
return retry((1 << (attempts - 1)) * 1000);
} else if (status == ResponseStatus::ConnectionError && attempts < maxAttempts) {
// By default, we will retry every 30 seconds (network change notification will
// preempt the timeout).
strategy = PreemptImmediately;
return retry(30000);
}
// Actually return the response.
if (status == ResponseStatus::NotModified) {
notify(std::move(response), FileCache::Hint::Refresh);
} else {
notify(std::move(response), FileCache::Hint::Full);
}
}
delete this;
}
void HTTPRequest::cancel() {
context->removeRequest(this);
cancelled = true;
// Stop the backoff timer to avoid re-triggering this request.
cancelTimer();
if (task) {
[task cancel];
[task release];
task = nullptr;
} else {
delete this;
}
}
void HTTPRequest::cancelTimer() {
if (timer) {
uv_timer_stop(timer);
uv::close(timer);
timer = nullptr;
}
}
int64_t parseCacheControl(const char *value) {
if (value) {
unsigned long long seconds = 0;
// TODO: cache-control may contain other information as well:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
if (std::sscanf(value, "max-age=%llu", &seconds) == 1) {
return std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count() +
seconds;
}
}
return 0;
}
void HTTPRequest::handleResult(NSData *data, NSURLResponse *res, NSError *error) {
if (error) {
if ([error code] == NSURLErrorCancelled) {
status = ResponseStatus::Canceled;
} else {
// TODO: Use different codes for host not found, timeout, invalid URL etc.
// These can be categorized in temporary and permanent errors.
response = util::make_unique<Response>();
response->status = Response::Error;
response->message = [[error localizedDescription] UTF8String];
switch ([error code]) {
case NSURLErrorBadServerResponse: // 5xx errors
status = ResponseStatus::TemporaryError;
break;
case NSURLErrorTimedOut:
case NSURLErrorUserCancelledAuthentication:
status = ResponseStatus::SingularError; // retry immediately
break;
case NSURLErrorNetworkConnectionLost:
case NSURLErrorCannotFindHost:
case NSURLErrorCannotConnectToHost:
case NSURLErrorDNSLookupFailed:
case NSURLErrorNotConnectedToInternet:
case NSURLErrorInternationalRoamingOff:
case NSURLErrorCallIsActive:
case NSURLErrorDataNotAllowed:
status = ResponseStatus::ConnectionError;
break;
default:
status = ResponseStatus::PermanentError;
}
}
} else if ([res isKindOfClass:[NSHTTPURLResponse class]]) {
const long responseCode = [(NSHTTPURLResponse *)res statusCode];
response = util::make_unique<Response>();
response->data = {(const char *)[data bytes], [data length]};
NSDictionary *headers = [(NSHTTPURLResponse *)res allHeaderFields];
NSString *cache_control = [headers objectForKey:@"Cache-Control"];
if (cache_control) {
response->expires = parseCacheControl([cache_control UTF8String]);
}
NSString *expires = [headers objectForKey:@"Expires"];
if (expires) {
response->expires = parse_date([expires UTF8String]);
}
NSString *last_modified = [headers objectForKey:@"Last-Modified"];
if (last_modified) {
response->modified = parse_date([last_modified UTF8String]);
}
NSString *etag = [headers objectForKey:@"ETag"];
if (etag) {
response->etag = [etag UTF8String];
}
if (responseCode == 304) {
if (existingResponse) {
// We're going to copy over the existing response's data.
response->status = existingResponse->status;
response->message = existingResponse->message;
response->modified = existingResponse->modified;
response->etag = existingResponse->etag;
response->data = existingResponse->data;
status = ResponseStatus::NotModified;
} else {
// This is an unsolicited 304 response and should only happen on malfunctioning
// HTTP servers. It likely doesn't include any data, but we don't have much options.
response->status = Response::Successful;
status = ResponseStatus::Successful;
}
} else if (responseCode == 200) {
response->status = Response::Successful;
status = ResponseStatus::Successful;
} else if (responseCode >= 500 && responseCode < 600) {
// Server errors may be temporary, so back off exponentially.
response->status = Response::Error;
response->message = "HTTP status code " + std::to_string(responseCode);
status = ResponseStatus::TemporaryError;
} else {
// We don't know how to handle any other errors, so declare them as permanently failing.
response->status = Response::Error;
response->message = "HTTP status code " + std::to_string(responseCode);
status = ResponseStatus::PermanentError;
}
} else {
// This should never happen.
status = ResponseStatus::PermanentError;
response = util::make_unique<Response>();
response->status = Response::Error;
response->message = "response class is not NSHTTPURLResponse";
}
uv_async_send(async);
}
void HTTPRequest::retry(uint64_t timeout) {
response.reset();
assert(!timer);
timer = new uv_timer_t;
timer->data = this;
uv_timer_init(async->loop, timer);
uv_timer_start(timer, restart, timeout, 0);
}
void HTTPRequest::retry() {
// All batons get notified when the network status changed, but some of them
// might not actually wait for the network to become available again.
if (timer && strategy == PreemptImmediately) {
// Triggers the timer upon the next event loop iteration.
uv_timer_stop(timer);
uv_timer_start(timer, restart, 0, 0);
}
}
void HTTPRequest::restart(uv_timer_t *timer, int) {
// Restart the request.
auto impl = reinterpret_cast<HTTPRequest *>(timer->data);
// Get rid of the timer.
impl->timer = nullptr;
uv::close(timer);
impl->start();
}
std::unique_ptr<HTTPContext> HTTPContext::createContext(uv_loop_t* loop) {
return util::make_unique<HTTPNSURLContext>(loop);
}
}