#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace mbgl { class OnlineFileRequestImpl : public util::noncopyable { public: using Callback = std::function; OnlineFileRequestImpl(FileRequest*, const Resource&, Callback, OnlineFileSource::Impl&); ~OnlineFileRequestImpl(); void networkIsReachableAgain(OnlineFileSource::Impl&); void schedule(OnlineFileSource::Impl&, optional expires); void completed(OnlineFileSource::Impl&, Response); FileRequest* key; Resource resource; HTTPRequestBase* request = nullptr; util::Timer timer; Callback callback; // Counts the number of times a response was already expired when received. We're using // this to add a delay when making a new request so we don't keep retrying immediately // in case of a server serving expired tiles. uint32_t expiredRequests = 0; // Counts the number of subsequent failed requests. We're using this value for exponential // backoff when retrying requests. uint32_t failedRequests = 0; Response::Error::Reason failedRequestReason = Response::Error::Reason::Success; }; class OnlineFileSource::Impl { public: // Dummy parameter is a workaround for a gcc 4.9 bug. Impl(int) { NetworkStatus::Subscribe(&reachability); } ~Impl() { NetworkStatus::Unsubscribe(&reachability); } void request(FileRequest* key, Resource resource, Callback callback) { allRequests[key] = std::make_unique(key, resource, callback, *this); } void cancel(FileRequest* key) { allRequests.erase(key); if (activeRequests.erase(key)) { activatePendingRequest(); } else { auto it = pendingRequestsMap.find(key); if (it != pendingRequestsMap.end()) { pendingRequestsList.erase(it->second); pendingRequestsMap.erase(it); } } } void activateOrQueueRequest(OnlineFileRequestImpl* impl) { assert(allRequests.find(impl->key) != allRequests.end()); assert(activeRequests.find(impl->key) == activeRequests.end()); assert(!impl->request); if (activeRequests.size() >= HTTPContextBase::maximumConcurrentRequests()) { queueRequest(impl); } else { activateRequest(impl); } } void queueRequest(OnlineFileRequestImpl* impl) { auto it = pendingRequestsList.insert(pendingRequestsList.end(), impl->key); pendingRequestsMap.emplace(impl->key, std::move(it)); } void activateRequest(OnlineFileRequestImpl* impl) { activeRequests.insert(impl->key); impl->request = httpContext->createRequest(impl->resource, [=] (Response response) { impl->request = nullptr; activeRequests.erase(impl->key); activatePendingRequest(); impl->completed(*this, response); }); } void activatePendingRequest() { if (pendingRequestsList.empty()) { return; } FileRequest* key = pendingRequestsList.front(); pendingRequestsList.pop_front(); pendingRequestsMap.erase(key); auto it = allRequests.find(key); assert(it != allRequests.end()); activateRequest(it->second.get()); } private: void networkIsReachableAgain() { for (auto& req : allRequests) { req.second->networkIsReachableAgain(*this); } } /** * The lifetime of a request is: * * 1. Waiting for timeout (revalidation or retry) * 2. Pending (waiting for room in the active set) * 3. Active (open network connection) * 4. Back to #1 * * Requests in any state are in `allRequests`. Requests in the pending state are in * `pendingRequests`. Requests in the active state are in `activeRequests`. */ std::unordered_map> allRequests; std::list pendingRequestsList; std::unordered_map::iterator> pendingRequestsMap; std::unordered_set activeRequests; const std::unique_ptr httpContext { HTTPContextBase::createContext() }; util::AsyncTask reachability { std::bind(&Impl::networkIsReachableAgain, this) }; }; OnlineFileSource::OnlineFileSource() : thread(std::make_unique>( util::ThreadContext{ "OnlineFileSource", util::ThreadType::Unknown, util::ThreadPriority::Low }, 0)) { } OnlineFileSource::~OnlineFileSource() = default; std::unique_ptr OnlineFileSource::request(const Resource& resource, Callback callback) { Resource res = resource; switch (resource.kind) { case Resource::Kind::Unknown: break; case Resource::Kind::Style: res.url = mbgl::util::mapbox::normalizeStyleURL(resource.url, accessToken); break; case Resource::Kind::Source: res.url = util::mapbox::normalizeSourceURL(resource.url, accessToken); break; case Resource::Kind::Glyphs: res.url = util::mapbox::normalizeGlyphsURL(resource.url, accessToken); break; case Resource::Kind::SpriteImage: case Resource::Kind::SpriteJSON: res.url = util::mapbox::normalizeSpriteURL(resource.url, accessToken); break; case Resource::Kind::Tile: res.url = util::mapbox::normalizeTileURL(resource.url, accessToken); break; } class OnlineFileRequest : public FileRequest { public: OnlineFileRequest(Resource resource_, FileSource::Callback callback_, util::Thread& thread_) : thread(thread_), workRequest(thread.invokeWithCallback(&OnlineFileSource::Impl::request, callback_, this, resource_)) { } ~OnlineFileRequest() { thread.invoke(&OnlineFileSource::Impl::cancel, this); } util::Thread& thread; std::unique_ptr workRequest; }; return std::make_unique(res, callback, *thread); } OnlineFileRequestImpl::OnlineFileRequestImpl(FileRequest* key_, const Resource& resource_, Callback callback_, OnlineFileSource::Impl& impl) : key(key_), resource(resource_), callback(std::move(callback_)) { // Force an immediate first request if we don't have an expiration time. if (resource.priorExpires) { schedule(impl, resource.priorExpires); } else { schedule(impl, SystemClock::now()); } } OnlineFileRequestImpl::~OnlineFileRequestImpl() { if (request) { request->cancel(); } } static Duration errorRetryTimeout(Response::Error::Reason failedRequestReason, uint32_t failedRequests) { if (failedRequestReason == Response::Error::Reason::Server) { // Retry after one second three times, then start exponential backoff. return Seconds(failedRequests <= 3 ? 1 : 1 << std::min(failedRequests - 3, 31u)); } else if (failedRequestReason == Response::Error::Reason::Connection) { // Immediate exponential backoff. assert(failedRequests > 0); return Seconds(1 << std::min(failedRequests - 1, 31u)); } else { // No error, or not an error that triggers retries. return Duration::max(); } } static Duration expirationTimeout(optional expires, uint32_t expiredRequests) { if (expiredRequests) { return Seconds(1 << std::min(expiredRequests - 1, 31u)); } else if (expires) { return std::max(SystemDuration::zero(), *expires - SystemClock::now()); } else { return Duration::max(); } } SystemTimePoint interpolateExpiration(const SystemTimePoint& current, optional prior, bool& expired) { auto now = SystemClock::now(); if (current > now) { return current; } if (!bool(prior)) { expired = true; return current; } // Expiring date is going backwards, // fallback to exponential backoff. if (current < *prior) { expired = true; return current; } auto delta = current - *prior; // Server is serving the same expired resource // over and over, fallback to exponential backoff. if (delta == Duration::zero()) { expired = true; return current; } // Assume that either the client or server clock is wrong and // try to interpolate a valid expiration date (from the client POV) // observing a minimum timeout. return now + std::max(delta, util::CLOCK_SKEW_RETRY_TIMEOUT); } void OnlineFileRequestImpl::schedule(OnlineFileSource::Impl& impl, optional expires) { if (request) { // There's already a request in progress; don't start another one. return; } // If we're not being asked for a forced refresh, calculate a timeout that depends on how many // consecutive errors we've encountered, and on the expiration time, if present. Duration timeout = std::min(errorRetryTimeout(failedRequestReason, failedRequests), expirationTimeout(expires, expiredRequests)); if (timeout == Duration::max()) { return; } // Emulate a Connection error when the Offline mode is forced with // a really long timeout. The request will get re-triggered when // the NetworkStatus is set back to Online. if (NetworkStatus::Get() == NetworkStatus::Status::Offline) { failedRequestReason = Response::Error::Reason::Connection; failedRequests = 1; timeout = Duration::max(); } timer.start(timeout, Duration::zero(), [&] { impl.activateOrQueueRequest(this); }); } void OnlineFileRequestImpl::completed(OnlineFileSource::Impl& impl, Response response) { // If we didn't get various caching headers in the response, continue using the // previous values. Otherwise, update the previous values to the new values. if (!response.modified) { response.modified = resource.priorModified; } else { resource.priorModified = response.modified; } bool isExpired = false; if (response.expires) { auto prior = resource.priorExpires; resource.priorExpires = response.expires; response.expires = interpolateExpiration(*response.expires, prior, isExpired); } if (isExpired) { expiredRequests++; } else { expiredRequests = 0; } if (!response.etag) { response.etag = resource.priorEtag; } else { resource.priorEtag = response.etag; } if (response.error) { failedRequests++; failedRequestReason = response.error->reason; } else { failedRequests = 0; failedRequestReason = Response::Error::Reason::Success; } callback(response); schedule(impl, response.expires); } void OnlineFileRequestImpl::networkIsReachableAgain(OnlineFileSource::Impl& impl) { // We need all requests to fail at least once before we are going to start retrying // them, and we only immediately restart request that failed due to connection issues. if (failedRequestReason == Response::Error::Reason::Connection) { schedule(impl, SystemClock::now()); } } } // namespace mbgl