| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/background_fetch/background_fetch_scheduler.h" |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/guid.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "content/browser/background_fetch/background_fetch_data_manager.h" |
| #include "content/browser/background_fetch/background_fetch_delegate_proxy.h" |
| #include "content/browser/background_fetch/background_fetch_job_controller.h" |
| #include "content/browser/background_fetch/background_fetch_registration_notifier.h" |
| #include "content/browser/background_fetch/background_fetch_registration_service_impl.h" |
| #include "content/browser/devtools/devtools_background_services_context_impl.h" |
| #include "content/browser/service_worker/service_worker_context_core_observer.h" |
| #include "content/browser/service_worker/service_worker_context_wrapper.h" |
| #include "content/public/common/content_features.h" |
| #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom.h" |
| #include "third_party/blink/public/mojom/fetch/fetch_api_response.mojom.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // The maximum number of active registrations that can be processed |
| // concurrently. The active registrations are from distinct origins. |
| constexpr char kMaxActiveRegistrations[] = "max_active_registrations"; |
| constexpr int kMaxActiveRegistrationsDefaultValue = 2; |
| |
| // The maximum number of downloads the Download Service can process at the same |
| // time. |
| // TODO(crbug.com/919864): Figure out how to keep this in sync with the |
| // Download Service value. |
| constexpr char kMaxRunningDownloads[] = "max_running_downloads"; |
| constexpr int kMaxRunningDownloadsDefaultValue = 2; |
| |
| } // namespace |
| |
| using blink::mojom::BackgroundFetchError; |
| using blink::mojom::BackgroundFetchFailureReason; |
| |
| // The major stages/events a Background Fetch instance goes through via the |
| // BackgroundFetchScheduler. |
| enum class BackgroundFetchScheduler::Event { |
| // The Background Fetch was successfully registered. |
| kFetchRegistered, |
| // The Background Fetch registration was loaded on start-up. |
| kFetchResumedOnStartup, |
| // The scheduler marked the registration as active. |
| kFetchScheduled, |
| // A request within the registration is being fetched. |
| kRequestStarted, |
| // A request within the registration had been fetched. |
| kRequestCompleted, |
| }; |
| |
| BackgroundFetchScheduler::BackgroundFetchScheduler( |
| BackgroundFetchContext* background_fetch_context, |
| BackgroundFetchDataManager* data_manager, |
| BackgroundFetchRegistrationNotifier* registration_notifier, |
| BackgroundFetchDelegateProxy* delegate_proxy, |
| DevToolsBackgroundServicesContextImpl* devtools_context, |
| scoped_refptr<ServiceWorkerContextWrapper> service_worker_context) |
| : data_manager_(data_manager), |
| registration_notifier_(registration_notifier), |
| delegate_proxy_(delegate_proxy), |
| devtools_context_(devtools_context), |
| event_dispatcher_(background_fetch_context, |
| std::move(service_worker_context), |
| devtools_context) { |
| DCHECK(delegate_proxy_); |
| DCHECK(devtools_context_); |
| delegate_proxy_->SetClickEventDispatcher( |
| base::BindRepeating(&BackgroundFetchScheduler::DispatchClickEvent, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| max_active_registrations_ = base::GetFieldTrialParamByFeatureAsInt( |
| features::kBackgroundFetch, kMaxActiveRegistrations, |
| kMaxActiveRegistrationsDefaultValue); |
| max_running_downloads_ = base::GetFieldTrialParamByFeatureAsInt( |
| features::kBackgroundFetch, kMaxRunningDownloads, |
| kMaxRunningDownloadsDefaultValue); |
| } |
| |
| BackgroundFetchScheduler::~BackgroundFetchScheduler() = default; |
| |
| bool BackgroundFetchScheduler::ScheduleDownload() { |
| DCHECK_LT(num_running_downloads_, max_running_downloads_); |
| |
| // 1. Try to activate a registration from a different origin. |
| if (num_active_registrations_ < max_active_registrations_ && |
| !controller_ids_.empty()) { |
| // Try to find a pending registration with a different origin. |
| for (const auto& controller_id : controller_ids_) { |
| // Make sure the origin is not already active. |
| bool is_new_origin = true; |
| for (auto* controller : active_controllers_) { |
| if (controller->registration_id().origin().IsSameOriginWith( |
| controller_id.origin())) { |
| is_new_origin = false; |
| break; |
| } |
| } |
| |
| if (is_new_origin) { |
| // Start new registration, and move to the front of the queue. |
| auto* controller = job_controllers_[controller_id.unique_id()].get(); |
| active_controllers_.push_front(controller); |
| ++num_active_registrations_; |
| |
| LogBackgroundFetchEventForDevTools(Event::kFetchScheduled, |
| controller_id, |
| /* request_info= */ nullptr); |
| |
| base::Erase(controller_ids_, controller_id); |
| break; |
| } |
| } |
| } |
| |
| // 2. Try to start a request within the LRU registration. |
| for (auto it = active_controllers_.begin(); it != active_controllers_.end(); |
| ++it) { |
| auto* controller = *it; |
| if (!controller->HasMoreRequests()) |
| continue; |
| |
| // Activate a request within |controller| and move it to the end of the |
| // queue. |
| ++num_running_downloads_; |
| controller->PopNextRequest( |
| base::BindOnce(&BackgroundFetchScheduler::DidStartRequest, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&BackgroundFetchScheduler::DidCompleteRequest, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| active_controllers_.erase(it); |
| active_controllers_.push_back(controller); |
| return true; |
| } |
| return false; |
| } |
| |
| void BackgroundFetchScheduler::Abort( |
| const BackgroundFetchRegistrationId& registration_id, |
| BackgroundFetchFailureReason failure_reason, |
| blink::mojom::BackgroundFetchRegistrationService::AbortCallback callback) { |
| DCHECK_EQ(failure_reason, |
| BackgroundFetchFailureReason::CANCELLED_BY_DEVELOPER); |
| |
| base::Erase(controller_ids_, registration_id); |
| |
| auto it = job_controllers_.find(registration_id.unique_id()); |
| if (it == job_controllers_.end()) { |
| std::move(callback).Run(BackgroundFetchError::INVALID_ID); |
| return; |
| } |
| |
| it->second->Abort(failure_reason, std::move(callback)); |
| } |
| |
| void BackgroundFetchScheduler::DidStartRequest( |
| const BackgroundFetchRegistrationId& registration_id, |
| const BackgroundFetchRequestInfo* request_info) { |
| LogBackgroundFetchEventForDevTools(Event::kRequestStarted, registration_id, |
| request_info); |
| } |
| |
| void BackgroundFetchScheduler::DidCompleteRequest( |
| const BackgroundFetchRegistrationId& registration_id, |
| scoped_refptr<BackgroundFetchRequestInfo> request_info) { |
| LogBackgroundFetchEventForDevTools(Event::kRequestCompleted, registration_id, |
| request_info.get()); |
| |
| auto* controller = GetActiveController(registration_id); |
| if (controller) |
| controller->MarkRequestAsComplete(std::move(request_info)); |
| |
| --num_running_downloads_; |
| if (num_running_downloads_ < max_running_downloads_) |
| ScheduleDownload(); |
| } |
| |
| void BackgroundFetchScheduler::FinishJob( |
| const BackgroundFetchRegistrationId& registration_id, |
| BackgroundFetchFailureReason failure_reason, |
| base::OnceCallback<void(BackgroundFetchError)> callback) { |
| auto* active_controller = GetActiveController(registration_id); |
| if (active_controller) { |
| base::EraseIf(active_controllers_, [®istration_id](auto* controller) { |
| return controller->registration_id() == registration_id; |
| }); |
| } |
| |
| data_manager_->MarkRegistrationForDeletion( |
| registration_id, |
| /* check_for_failure= */ failure_reason == |
| BackgroundFetchFailureReason::NONE, |
| base::BindOnce(&BackgroundFetchScheduler::DidMarkForDeletion, |
| weak_ptr_factory_.GetWeakPtr(), registration_id, |
| /* job_started= */ active_controller != nullptr, |
| std::move(callback))); |
| |
| auto it = job_controllers_.find(registration_id.unique_id()); |
| if (it != job_controllers_.end()) { |
| completed_fetches_[it->first] = {registration_id, |
| it->second->NewRegistrationData()}; |
| |
| // Reset scheduler params. |
| num_running_downloads_ -= it->second->pending_downloads(); |
| --num_active_registrations_; |
| |
| // Destroying the controller will stop all in progress tasks. |
| job_controllers_.erase(it); |
| } |
| |
| if (num_running_downloads_ < max_running_downloads_) |
| ScheduleDownload(); |
| } |
| |
| void BackgroundFetchScheduler::DidMarkForDeletion( |
| const BackgroundFetchRegistrationId& registration_id, |
| bool job_started, |
| base::OnceCallback<void(BackgroundFetchError)> callback, |
| BackgroundFetchError error, |
| BackgroundFetchFailureReason failure_reason) { |
| DCHECK(callback); |
| std::move(callback).Run(error); |
| |
| // It's normal to get INVALID_ID errors here - it means the registration was |
| // already inactive (marked for deletion). This happens when an abort (from |
| // developer or from user) races with the download completing/failing, or even |
| // when two aborts race. |
| if (error != BackgroundFetchError::NONE) |
| return; |
| |
| auto it = completed_fetches_.find(registration_id.unique_id()); |
| DCHECK(it != completed_fetches_.end()); |
| |
| blink::mojom::BackgroundFetchRegistrationDataPtr& registration_data = |
| it->second.second; |
| // Include any other failure reasons the marking for deletion may have found. |
| if (registration_data->failure_reason == BackgroundFetchFailureReason::NONE) |
| registration_data->failure_reason = failure_reason; |
| |
| registration_data->result = |
| registration_data->failure_reason == BackgroundFetchFailureReason::NONE |
| ? blink::mojom::BackgroundFetchResult::SUCCESS |
| : blink::mojom::BackgroundFetchResult::FAILURE; |
| |
| registration_notifier_->Notify(registration_id.unique_id(), |
| *registration_data); |
| |
| event_dispatcher_.DispatchBackgroundFetchCompletionEvent( |
| registration_id, registration_data.Clone(), |
| base::BindOnce(&BackgroundFetchScheduler::CleanupRegistration, |
| weak_ptr_factory_.GetWeakPtr(), registration_id)); |
| |
| if (!job_started || |
| registration_data->failure_reason == |
| BackgroundFetchFailureReason::CANCELLED_FROM_UI || |
| registration_data->failure_reason == |
| BackgroundFetchFailureReason::CANCELLED_BY_DEVELOPER) { |
| // No need to keep the controller around since there won't be dispatch |
| // events. |
| completed_fetches_.erase(it); |
| } |
| } |
| |
| void BackgroundFetchScheduler::CleanupRegistration( |
| const BackgroundFetchRegistrationId& registration_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| // Indicate to the renderer that the records for this fetch are no longer |
| // available. |
| registration_notifier_->NotifyRecordsUnavailable(registration_id.unique_id()); |
| |
| // Delete the data associated with this fetch. Cache storage will keep the |
| // downloaded data around so long as there are references to it, and delete |
| // it once there is none. We don't need to do that accounting. |
| data_manager_->DeleteRegistration(registration_id, base::DoNothing()); |
| |
| // Notify other systems that this registration is complete. |
| delegate_proxy_->MarkJobComplete(registration_id.unique_id()); |
| } |
| |
| void BackgroundFetchScheduler::DispatchClickEvent( |
| const std::string& unique_id) { |
| // Case 1: The active fetch received a click event. |
| auto* active_controller = GetActiveController(unique_id); |
| |
| if (active_controller) { |
| event_dispatcher_.DispatchBackgroundFetchClickEvent( |
| active_controller->registration_id(), |
| active_controller->NewRegistrationData(), base::DoNothing()); |
| return; |
| } |
| |
| // Case 2: A completed fetch received a click event. |
| auto it = completed_fetches_.find(unique_id); |
| if (it == completed_fetches_.end()) |
| return; |
| |
| event_dispatcher_.DispatchBackgroundFetchClickEvent( |
| it->second.first, std::move(it->second.second), base::DoNothing()); |
| completed_fetches_.erase(unique_id); |
| } |
| |
| std::unique_ptr<BackgroundFetchJobController> |
| BackgroundFetchScheduler::CreateInitializedController( |
| const BackgroundFetchRegistrationId& registration_id, |
| const blink::mojom::BackgroundFetchRegistrationData& registration_data, |
| blink::mojom::BackgroundFetchOptionsPtr options, |
| const SkBitmap& icon, |
| int num_completed_requests, |
| int num_requests, |
| std::vector<scoped_refptr<BackgroundFetchRequestInfo>> |
| active_fetch_requests, |
| bool start_paused) { |
| // TODO(rayankans): Only create a controller when the fetch starts. |
| auto controller = std::make_unique<BackgroundFetchJobController>( |
| data_manager_, delegate_proxy_, registration_id, std::move(options), icon, |
| registration_data.downloaded, registration_data.uploaded, |
| registration_data.upload_total, |
| // Safe because JobControllers are destroyed before RegistrationNotifier. |
| base::BindRepeating(&BackgroundFetchRegistrationNotifier::Notify, |
| base::Unretained(registration_notifier_)), |
| base::BindOnce(&BackgroundFetchScheduler::FinishJob, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| controller->InitializeRequestStatus(num_completed_requests, num_requests, |
| std::move(active_fetch_requests), |
| start_paused); |
| |
| return controller; |
| } |
| |
| void BackgroundFetchScheduler::OnRegistrationCreated( |
| const BackgroundFetchRegistrationId& registration_id, |
| const blink::mojom::BackgroundFetchRegistrationData& registration_data, |
| blink::mojom::BackgroundFetchOptionsPtr options, |
| const SkBitmap& icon, |
| int num_requests, |
| bool start_paused) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| |
| LogBackgroundFetchEventForDevTools( |
| Event::kFetchRegistered, registration_id, |
| /* request_info= */ nullptr, |
| {{"Total Requests", base::NumberToString(num_requests)}, |
| {"Start Paused", start_paused ? "Yes" : "No"}}); |
| |
| registration_notifier_->NoteTotalRequests(registration_id.unique_id(), |
| num_requests); |
| |
| auto controller = CreateInitializedController( |
| registration_id, registration_data, std::move(options), icon, |
| /* completed_requests= */ 0, num_requests, |
| /* active_fetch_requests= */ {}, start_paused); |
| |
| DCHECK_EQ(job_controllers_.count(registration_id.unique_id()), 0u); |
| job_controllers_[registration_id.unique_id()] = std::move(controller); |
| controller_ids_.push_back(registration_id); |
| |
| // Schedule as much as possible. |
| while (num_running_downloads_ < max_running_downloads_) { |
| if (!ScheduleDownload()) |
| return; |
| } |
| } |
| |
| void BackgroundFetchScheduler::OnRegistrationLoadedAtStartup( |
| const BackgroundFetchRegistrationId& registration_id, |
| const blink::mojom::BackgroundFetchRegistrationData& registration_data, |
| blink::mojom::BackgroundFetchOptionsPtr options, |
| const SkBitmap& icon, |
| int num_completed_requests, |
| int num_requests, |
| std::vector<scoped_refptr<BackgroundFetchRequestInfo>> |
| active_fetch_requests) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| |
| LogBackgroundFetchEventForDevTools( |
| Event::kFetchResumedOnStartup, registration_id, |
| /* request_info= */ nullptr, |
| {{"Completed Requests", base::NumberToString(num_completed_requests)}, |
| {"Active Requests", |
| base::NumberToString(active_fetch_requests.size())}}); |
| |
| auto controller = CreateInitializedController( |
| registration_id, registration_data, std::move(options), icon, |
| num_completed_requests, num_requests, active_fetch_requests, |
| /* start_paused= */ false); |
| |
| auto* controller_ptr = controller.get(); |
| active_controllers_.push_back(controller_ptr); |
| job_controllers_[registration_id.unique_id()] = std::move(controller); |
| |
| ++num_active_registrations_; |
| num_running_downloads_ += active_fetch_requests.size(); |
| |
| if (active_fetch_requests.empty()) { |
| DCHECK_LT(num_completed_requests, num_requests); |
| // Start processing the next request. |
| ++num_running_downloads_; |
| controller_ptr->PopNextRequest( |
| base::BindOnce(&BackgroundFetchScheduler::DidStartRequest, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&BackgroundFetchScheduler::DidCompleteRequest, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| for (auto& request_info : active_fetch_requests) { |
| controller_ptr->StartRequest( |
| std::move(request_info), |
| base::BindOnce(&BackgroundFetchScheduler::DidCompleteRequest, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void BackgroundFetchScheduler::OnRequestCompleted( |
| const std::string& unique_id, |
| blink::mojom::FetchAPIRequestPtr request, |
| blink::mojom::FetchAPIResponsePtr response) { |
| registration_notifier_->NotifyRequestCompleted(unique_id, std::move(request), |
| std::move(response)); |
| } |
| |
| void BackgroundFetchScheduler::AbortFetches( |
| int64_t service_worker_registration_id) { |
| // Abandon all active associated with this service worker. |
| // BackgroundFetchJobController::Abort() will eventually lead to deletion of |
| // the controller from job_controllers, so the IDs need to be copied over. |
| std::vector<BackgroundFetchJobController*> to_abort; |
| for (const auto& controller : job_controllers_) { |
| if (service_worker_registration_id != |
| blink::mojom::kInvalidServiceWorkerRegistrationId && |
| service_worker_registration_id != |
| controller.second->registration_id() |
| .service_worker_registration_id()) { |
| continue; |
| } |
| to_abort.push_back(controller.second.get()); |
| } |
| |
| for (auto* controller : to_abort) { |
| // Erase it from |controller_ids_| first to avoid rescheduling. |
| base::Erase(controller_ids_, controller->registration_id()); |
| controller->Abort(BackgroundFetchFailureReason::SERVICE_WORKER_UNAVAILABLE, |
| base::DoNothing()); |
| } |
| } |
| |
| void BackgroundFetchScheduler::OnRegistrationQueried( |
| const BackgroundFetchRegistrationId& registration_id, |
| blink::mojom::BackgroundFetchRegistrationData* registration_data) { |
| DCHECK(registration_data); |
| |
| auto* controller = GetActiveController(registration_id.unique_id()); |
| if (!controller) |
| return; |
| |
| // The data manager only has the number of bytes from completed downloads, so |
| // augment this with the number of downloaded/uploaded bytes from in-progress |
| // jobs. |
| registration_data->downloaded += controller->GetInProgressDownloadedBytes(); |
| registration_data->uploaded += controller->GetInProgressUploadedBytes(); |
| } |
| |
| void BackgroundFetchScheduler::OnServiceWorkerDatabaseCorrupted( |
| int64_t service_worker_registration_id) { |
| AbortFetches(service_worker_registration_id); |
| } |
| |
| void BackgroundFetchScheduler::OnRegistrationDeleted(int64_t registration_id, |
| const GURL& pattern) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| AbortFetches(registration_id); |
| } |
| |
| void BackgroundFetchScheduler::OnStorageWiped() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| AbortFetches(blink::mojom::kInvalidServiceWorkerRegistrationId); |
| } |
| |
| BackgroundFetchJobController* BackgroundFetchScheduler::GetActiveController( |
| const BackgroundFetchRegistrationId& registration_id) { |
| for (auto* controller : active_controllers_) { |
| if (controller->registration_id() == registration_id) |
| return controller; |
| } |
| return nullptr; |
| } |
| |
| BackgroundFetchJobController* BackgroundFetchScheduler::GetActiveController( |
| const std::string& unique_id) { |
| // |unique_id| is used for all BackgroundFetchRegistrationId comparisons, so |
| // this creates a |unique_id| wrapper with default values for other |
| // parameters. |
| BackgroundFetchRegistrationId registration_id( |
| /* service_worker_registration_id= */ 0, /* origin= */ url::Origin(), |
| /* developer_id= */ "", unique_id); |
| return GetActiveController(registration_id); |
| } |
| |
| void BackgroundFetchScheduler::LogBackgroundFetchEventForDevTools( |
| Event event, |
| const BackgroundFetchRegistrationId& registration_id, |
| const BackgroundFetchRequestInfo* request_info, |
| std::map<std::string, std::string> metadata) { |
| if (!devtools_context_->IsRecording( |
| DevToolsBackgroundService::kBackgroundFetch)) { |
| return; |
| } |
| |
| std::string event_name; |
| |
| // Fill with the appropriate event description in |event_name|, and append |
| // any additional data to |metadata|. |
| switch (event) { |
| case Event::kFetchRegistered: |
| event_name = "Background Fetch registered"; |
| break; |
| case Event::kFetchResumedOnStartup: |
| event_name = "Background Fetch resuming after browser restart"; |
| break; |
| case Event::kFetchScheduled: |
| event_name = "Background Fetch started"; |
| break; |
| case Event::kRequestStarted: |
| event_name = "Request processing started"; |
| DCHECK(request_info); |
| break; |
| case Event::kRequestCompleted: |
| event_name = "Request processing completed"; |
| DCHECK(request_info); |
| metadata["Response Status"] = |
| base::NumberToString(request_info->GetResponseCode()); |
| metadata["Response Size (bytes)"] = |
| base::NumberToString(request_info->GetResponseSize()); |
| break; |
| } |
| |
| DCHECK(!event_name.empty()); |
| |
| // Include common request metadata. |
| if (request_info) { |
| metadata["URL"] = request_info->fetch_request()->url.spec(); |
| metadata["Request Index"] = |
| base::NumberToString(request_info->request_index()); |
| if (request_info->request_body_size()) |
| metadata["Upload Size (bytes)"] = |
| base::NumberToString(request_info->request_body_size()); |
| } |
| |
| devtools_context_->LogBackgroundServiceEventOnIO( |
| registration_id.service_worker_registration_id(), |
| registration_id.origin(), DevToolsBackgroundService::kBackgroundFetch, |
| std::move(event_name), |
| /* instance_id= */ registration_id.developer_id(), metadata); |
| } |
| |
| } // namespace content |