blob: 717d390b554e3dce4235f2701e84506a68371567 [file] [log] [blame]
// 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 "components/download/internal/background_service/controller_impl.h"
#include <inttypes.h>
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/optional.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/memory_allocator_dump.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/memory_usage_estimator.h"
#include "base/trace_event/process_memory_dump.h"
#include "components/download/internal/background_service/client_set.h"
#include "components/download/internal/background_service/config.h"
#include "components/download/internal/background_service/entry.h"
#include "components/download/internal/background_service/entry_utils.h"
#include "components/download/internal/background_service/file_monitor.h"
#include "components/download/internal/background_service/log_sink.h"
#include "components/download/internal/background_service/model.h"
#include "components/download/internal/background_service/scheduler/scheduler.h"
#include "components/download/internal/background_service/stats.h"
#include "components/download/public/background_service/client.h"
#include "components/download/public/background_service/download_metadata.h"
#include "components/download/public/background_service/navigation_monitor.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request_body.h"
namespace download {
namespace {
// Helper function to transit the state of |entry| to |new_state|.
void TransitTo(Entry* entry, Entry::State new_state, Model* model) {
DCHECK(entry);
if (entry->state == new_state)
return;
entry->state = new_state;
model->Update(*entry);
}
// Helper function to post the callback once again before starting a download.
void RunOnDownloadReadyToStart(
GetUploadDataCallback callback,
scoped_refptr<network::ResourceRequestBody> post_body) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), post_body));
}
// Helper function to move from a CompletionType to a Client::FailureReason.
Client::FailureReason FailureReasonFromCompletionType(CompletionType type) {
// SUCCEED does not map to a FailureReason.
DCHECK_NE(CompletionType::SUCCEED, type);
switch (type) {
case CompletionType::FAIL: // Intentional fallthrough.
case CompletionType::OUT_OF_RETRIES: // Intentional fallthrough.
case CompletionType::OUT_OF_RESUMPTIONS:
return Client::FailureReason::NETWORK;
case CompletionType::ABORT:
return Client::FailureReason::ABORTED;
case CompletionType::TIMEOUT:
return Client::FailureReason::TIMEDOUT;
case CompletionType::UPLOAD_TIMEOUT:
return Client::FailureReason::UPLOAD_TIMEDOUT;
case CompletionType::UNKNOWN:
return Client::FailureReason::UNKNOWN;
case CompletionType::CANCEL:
return Client::FailureReason::CANCELLED;
default:
NOTREACHED();
}
return Client::FailureReason::UNKNOWN;
}
// Helper function to determine if more downloads can be activated based on
// configuration.
bool CanActivateMoreDownloads(Configuration* config,
uint32_t active_count,
uint32_t paused_count) {
if (config->max_concurrent_downloads <= paused_count + active_count ||
config->max_running_downloads <= active_count) {
return false;
}
return true;
}
Model::EntryList GetRunnableEntries(const Model::EntryList& list) {
Model::EntryList candidates;
for (Entry* entry : list) {
if (entry->state == Entry::State::AVAILABLE ||
entry->state == Entry::State::ACTIVE) {
candidates.emplace_back(entry);
}
}
return candidates;
}
} // namespace
ControllerImpl::ControllerImpl(
Configuration* config,
LogSink* log_sink,
std::unique_ptr<ClientSet> clients,
std::unique_ptr<DownloadDriver> driver,
std::unique_ptr<Model> model,
std::unique_ptr<DeviceStatusListener> device_status_listener,
NavigationMonitor* navigation_monitor,
std::unique_ptr<Scheduler> scheduler,
std::unique_ptr<TaskScheduler> task_scheduler,
std::unique_ptr<FileMonitor> file_monitor,
const base::FilePath& download_file_dir)
: config_(config),
log_sink_(log_sink),
download_file_dir_(download_file_dir),
clients_(std::move(clients)),
driver_(std::move(driver)),
model_(std::move(model)),
device_status_listener_(std::move(device_status_listener)),
navigation_monitor_(navigation_monitor),
scheduler_(std::move(scheduler)),
task_scheduler_(std::move(task_scheduler)),
file_monitor_(std::move(file_monitor)),
controller_state_(State::CREATED),
weak_ptr_factory_(this) {
DCHECK(config_);
DCHECK(log_sink_);
}
ControllerImpl::~ControllerImpl() {
base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider(
this);
}
void ControllerImpl::Initialize(const base::Closure& callback) {
DCHECK_EQ(controller_state_, State::CREATED);
init_callback_ = callback;
controller_state_ = State::INITIALIZING;
base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider(
this, "DownloadService", base::ThreadTaskRunnerHandle::Get());
TRACE_EVENT_ASYNC_BEGIN0("download_service", "DownloadServiceInitialize",
this);
driver_->Initialize(this);
model_->Initialize(this);
file_monitor_->Initialize(base::Bind(&ControllerImpl::OnFileMonitorReady,
weak_ptr_factory_.GetWeakPtr()));
navigation_monitor_->Configure(config_->navigation_completion_delay,
config_->navigation_timeout_delay);
navigation_monitor_->SetObserver(this);
}
Controller::State ControllerImpl::GetState() {
return controller_state_;
}
void ControllerImpl::StartDownload(const DownloadParams& params) {
DCHECK(controller_state_ == State::READY ||
controller_state_ == State::UNAVAILABLE);
// TODO(dtrainor): Validate all input parameters.
DCHECK_LE(base::Time::Now(), params.scheduling_params.cancel_time);
if (controller_state_ != State::READY) {
HandleStartDownloadResponse(params.client, params.guid,
DownloadParams::StartResult::INTERNAL_ERROR,
params.callback);
return;
}
KillTimedOutDownloads();
if (start_callbacks_.find(params.guid) != start_callbacks_.end() ||
model_->Get(params.guid) != nullptr) {
HandleStartDownloadResponse(params.client, params.guid,
DownloadParams::StartResult::UNEXPECTED_GUID,
params.callback);
return;
}
auto* client = clients_->GetClient(params.client);
if (!client) {
HandleStartDownloadResponse(params.client, params.guid,
DownloadParams::StartResult::UNEXPECTED_CLIENT,
params.callback);
return;
}
uint32_t client_count = util::GetNumberOfLiveEntriesForClient(
params.client, model_->PeekEntries());
if (client_count >= config_->max_scheduled_downloads) {
HandleStartDownloadResponse(params.client, params.guid,
DownloadParams::StartResult::BACKOFF,
params.callback);
return;
}
start_callbacks_[params.guid] = params.callback;
Entry entry(params);
entry.target_file_path = download_file_dir_.AppendASCII(params.guid);
model_->Add(entry);
}
void ControllerImpl::PauseDownload(const std::string& guid) {
DCHECK(controller_state_ == State::READY ||
controller_state_ == State::UNAVAILABLE);
if (controller_state_ != State::READY)
return;
auto* entry = model_->Get(guid);
if (!entry || entry->state == Entry::State::PAUSED ||
entry->state == Entry::State::COMPLETE ||
entry->state == Entry::State::NEW) {
return;
}
TransitTo(entry, Entry::State::PAUSED, model_.get());
UpdateDriverState(entry);
// Pausing a download may yield a concurrent slot to start a new download, and
// may change the scheduling criteria.
ActivateMoreDownloads();
}
void ControllerImpl::ResumeDownload(const std::string& guid) {
DCHECK(controller_state_ == State::READY ||
controller_state_ == State::UNAVAILABLE);
if (controller_state_ != State::READY)
return;
auto* entry = model_->Get(guid);
DCHECK(entry);
if (entry->state != Entry::State::PAUSED)
return;
TransitTo(entry, Entry::State::ACTIVE, model_.get());
UpdateDriverState(entry);
ActivateMoreDownloads();
}
void ControllerImpl::CancelDownload(const std::string& guid) {
DCHECK(controller_state_ == State::READY ||
controller_state_ == State::UNAVAILABLE);
if (controller_state_ != State::READY)
return;
auto* entry = model_->Get(guid);
if (!entry)
return;
if (entry->state == Entry::State::NEW) {
// Check if we're currently trying to add the download.
DCHECK(start_callbacks_.find(entry->guid) != start_callbacks_.end());
HandleStartDownloadResponse(entry->client, guid,
DownloadParams::StartResult::CLIENT_CANCELLED);
return;
}
HandleCompleteDownload(CompletionType::CANCEL, guid);
}
void ControllerImpl::ChangeDownloadCriteria(const std::string& guid,
const SchedulingParams& params) {
DCHECK(controller_state_ == State::READY ||
controller_state_ == State::UNAVAILABLE);
if (controller_state_ != State::READY)
return;
auto* entry = model_->Get(guid);
if (!entry || entry->scheduling_params == params) {
DVLOG(1) << "Try to update the same scheduling parameters.";
return;
}
UpdateDriverState(entry);
// Update the scheduling parameters.
entry->scheduling_params = params;
model_->Update(*entry);
ActivateMoreDownloads();
}
DownloadClient ControllerImpl::GetOwnerOfDownload(const std::string& guid) {
DCHECK(controller_state_ == State::READY ||
controller_state_ == State::UNAVAILABLE);
if (controller_state_ != State::READY)
return DownloadClient::INVALID;
auto* entry = model_->Get(guid);
return entry ? entry->client : DownloadClient::INVALID;
}
void ControllerImpl::OnStartScheduledTask(DownloadTaskType task_type,
TaskFinishedCallback callback) {
task_finished_callbacks_[task_type] = std::move(callback);
switch (controller_state_) {
case State::READY:
if (task_type == DownloadTaskType::DOWNLOAD_TASK) {
ActivateMoreDownloads();
} else if (task_type == DownloadTaskType::CLEANUP_TASK) {
RemoveCleanupEligibleDownloads();
ScheduleCleanupTask();
}
break;
case State::UNAVAILABLE:
HandleTaskFinished(task_type, false,
stats::ScheduledTaskStatus::ABORTED_ON_FAILED_INIT);
break;
case State::CREATED: // Intentional fallthrough.
case State::INITIALIZING: // Intentional fallthrough.
case State::RECOVERING: // Intentional fallthrough.
default:
NOTREACHED();
break;
}
}
bool ControllerImpl::OnStopScheduledTask(DownloadTaskType task_type) {
HandleTaskFinished(task_type, false,
stats::ScheduledTaskStatus::CANCELLED_ON_STOP);
return false;
}
void ControllerImpl::OnCompleteCleanupTask() {
HandleTaskFinished(DownloadTaskType::CLEANUP_TASK, false,
stats::ScheduledTaskStatus::COMPLETED_NORMALLY);
}
void ControllerImpl::RemoveCleanupEligibleDownloads() {
std::vector<Entry*> entries_to_remove;
for (auto* entry : model_->PeekEntries()) {
if (entry->state != Entry::State::COMPLETE)
continue;
bool optional_cleanup =
base::Time::Now() >
(entry->last_cleanup_check_time + config_->file_keep_alive_time);
bool mandatory_cleanup =
base::Time::Now() >
(entry->completion_time + config_->max_file_keep_alive_time);
if (!optional_cleanup && !mandatory_cleanup)
continue;
download::Client* client = clients_->GetClient(entry->client);
DCHECK(client);
bool client_ok =
client->CanServiceRemoveDownloadedFile(entry->guid, mandatory_cleanup);
entry->cleanup_attempt_count++;
if (client_ok || mandatory_cleanup) {
entries_to_remove.push_back(entry);
} else {
entry->last_cleanup_check_time = base::Time::Now();
}
}
file_monitor_->CleanupFilesForCompletedEntries(
entries_to_remove, base::Bind(&ControllerImpl::OnCompleteCleanupTask,
weak_ptr_factory_.GetWeakPtr()));
for (auto* entry : entries_to_remove) {
DCHECK_EQ(Entry::State::COMPLETE, entry->state);
model_->Remove(entry->guid);
}
}
void ControllerImpl::HandleTaskFinished(DownloadTaskType task_type,
bool needs_reschedule,
stats::ScheduledTaskStatus status) {
if (task_finished_callbacks_.count(task_type) == 0)
return;
if (status != stats::ScheduledTaskStatus::CANCELLED_ON_STOP) {
base::ResetAndReturn(&task_finished_callbacks_[task_type])
.Run(needs_reschedule);
}
// TODO(dtrainor): It might be useful to log how many downloads we have
// running when we're asked to stop processing.
stats::LogScheduledTaskStatus(task_type, status);
task_finished_callbacks_.erase(task_type);
switch (task_type) {
case DownloadTaskType::DOWNLOAD_TASK:
scheduler_->Reschedule(GetRunnableEntries(model_->PeekEntries()));
break;
case DownloadTaskType::CLEANUP_TASK:
ScheduleCleanupTask();
break;
}
}
void ControllerImpl::OnDriverReady(bool success) {
DCHECK(!startup_status_.driver_ok.has_value());
startup_status_.driver_ok = success;
AttemptToFinalizeSetup();
}
void ControllerImpl::OnDriverHardRecoverComplete(bool success) {
DCHECK(!startup_status_.driver_ok.has_value());
startup_status_.driver_ok = success;
AttemptToFinalizeSetup();
}
void ControllerImpl::OnDownloadCreated(const DriverEntry& download) {
if (controller_state_ != State::READY)
return;
Entry* entry = model_->Get(download.guid);
if (!entry) {
HandleExternalDownload(download.guid, true);
return;
}
entry->url_chain = download.url_chain;
entry->response_headers = download.response_headers;
entry->did_received_response = true;
model_->Update(*entry);
download::Client* client = clients_->GetClient(entry->client);
DCHECK(client);
using ShouldDownload = download::Client::ShouldDownload;
ShouldDownload should_download = client->OnDownloadStarted(
download.guid, download.url_chain, download.response_headers);
stats::LogStartDownloadResponse(entry->client, should_download);
if (should_download == ShouldDownload::ABORT) {
HandleCompleteDownload(CompletionType::ABORT, entry->guid);
}
}
void ControllerImpl::OnDownloadFailed(const DriverEntry& download,
FailureType failure_type) {
if (controller_state_ != State::READY)
return;
Entry* entry = model_->Get(download.guid);
if (!entry) {
HandleExternalDownload(download.guid, false);
return;
}
if (!download.done && failure_type == FailureType::RECOVERABLE &&
!entry->has_upload_data) {
// Because the network offline signal comes later than actual download
// failure, retry the download after a delay to avoid the retry to fail
// immediately again.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ControllerImpl::UpdateDriverStateWithGuid,
weak_ptr_factory_.GetWeakPtr(), download.guid),
config_->download_retry_delay);
} else {
HandleCompleteDownload(CompletionType::FAIL, download.guid);
}
}
void ControllerImpl::OnDownloadSucceeded(const DriverEntry& download) {
if (controller_state_ != State::READY)
return;
Entry* entry = model_->Get(download.guid);
if (!entry) {
HandleExternalDownload(download.guid, false);
return;
}
HandleCompleteDownload(CompletionType::SUCCEED, download.guid);
}
void ControllerImpl::OnDownloadUpdated(const DriverEntry& download) {
if (controller_state_ != State::READY)
return;
Entry* entry = model_->Get(download.guid);
if (!entry) {
HandleExternalDownload(download.guid, !download.paused);
return;
}
DCHECK_EQ(download.state, DriverEntry::State::IN_PROGRESS);
log_sink_->OnServiceDownloadChanged(entry->guid);
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&ControllerImpl::SendOnDownloadUpdated,
weak_ptr_factory_.GetWeakPtr(), entry->client,
download.guid, entry->bytes_uploaded,
download.bytes_downloaded));
}
bool ControllerImpl::IsTrackingDownload(const std::string& guid) const {
if (controller_state_ != State::READY)
return false;
return !!model_->Get(guid);
}
void ControllerImpl::OnUploadProgress(const std::string& guid,
uint64_t bytes_uploaded) const {
Entry* entry = model_->Get(guid);
DCHECK(entry);
entry->bytes_uploaded = bytes_uploaded;
auto* client = clients_->GetClient(entry->client);
DCHECK(client);
client->OnDownloadUpdated(guid, bytes_uploaded, /* bytes_downloaded= */ 0u);
}
void ControllerImpl::OnFileMonitorReady(bool success) {
DCHECK(!startup_status_.file_monitor_ok.has_value());
startup_status_.file_monitor_ok = success;
AttemptToFinalizeSetup();
}
void ControllerImpl::OnFileMonitorHardRecoverComplete(bool success) {
DCHECK(!startup_status_.file_monitor_ok.has_value());
startup_status_.file_monitor_ok = success;
AttemptToFinalizeSetup();
}
void ControllerImpl::OnModelReady(bool success) {
DCHECK(!startup_status_.model_ok.has_value());
startup_status_.model_ok = success;
AttemptToFinalizeSetup();
}
void ControllerImpl::OnModelHardRecoverComplete(bool success) {
DCHECK(!startup_status_.model_ok.has_value());
startup_status_.model_ok = success;
AttemptToFinalizeSetup();
}
void ControllerImpl::OnItemAdded(bool success,
DownloadClient client,
const std::string& guid) {
// If the StartCallback doesn't exist, we already notified the Client about
// this item. That means something went wrong, so stop here.
if (start_callbacks_.find(guid) == start_callbacks_.end())
return;
if (!success) {
HandleStartDownloadResponse(client, guid,
DownloadParams::StartResult::INTERNAL_ERROR);
return;
}
HandleStartDownloadResponse(client, guid,
DownloadParams::StartResult::ACCEPTED);
Entry* entry = model_->Get(guid);
DCHECK(entry);
DCHECK_EQ(Entry::State::NEW, entry->state);
TransitTo(entry, Entry::State::AVAILABLE, model_.get());
ActivateMoreDownloads();
}
void ControllerImpl::OnItemUpdated(bool success,
DownloadClient client,
const std::string& guid) {
Entry* entry = model_->Get(guid);
DCHECK(entry);
// Now that we're sure that our state is set correctly, it is OK to remove the
// DriverEntry. If we restart we'll see a COMPLETE state and handle it
// accordingly.
if (entry->state == Entry::State::COMPLETE)
driver_->Remove(guid, false);
// TODO(dtrainor): If failed, clean up any download state accordingly.
log_sink_->OnServiceDownloadChanged(guid);
}
void ControllerImpl::OnItemRemoved(bool success,
DownloadClient client,
const std::string& guid) {
// TODO(dtrainor): If failed, clean up any download state accordingly.
}
Controller::State ControllerImpl::GetControllerState() {
return controller_state_;
}
const StartupStatus& ControllerImpl::GetStartupStatus() {
return startup_status_;
}
LogSource::EntryDetailsList ControllerImpl::GetServiceDownloads() {
EntryDetailsList list;
if (controller_state_ != State::READY)
return list;
auto entries = model_->PeekEntries();
for (auto* entry : entries) {
list.push_back(std::make_pair(entry, driver_->Find(entry->guid)));
}
return list;
}
base::Optional<LogSource::EntryDetails> ControllerImpl::GetServiceDownload(
const std::string& guid) {
if (controller_state_ != State::READY)
return base::nullopt;
auto* entry = model_->Get(guid);
auto driver_entry = driver_->Find(guid);
return base::Optional<LogSource::EntryDetails>(
std::make_pair(entry, driver_entry));
}
bool ControllerImpl::OnMemoryDump(const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* pmd) {
auto* dump = pmd->CreateAllocatorDump(
base::StringPrintf("components/download/controller_0x%" PRIXPTR,
reinterpret_cast<uintptr_t>(this)));
size_t memory_cost =
base::trace_event::EstimateMemoryUsage(externally_active_downloads_);
memory_cost += model_->EstimateMemoryUsage();
memory_cost += driver_->EstimateMemoryUsage();
dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
base::trace_event::MemoryAllocatorDump::kUnitsBytes,
static_cast<uint64_t>(memory_cost));
return true;
}
void ControllerImpl::OnDeviceStatusChanged(const DeviceStatus& device_status) {
if (controller_state_ != State::READY)
return;
UpdateDriverStates();
ActivateMoreDownloads();
}
void ControllerImpl::AttemptToFinalizeSetup() {
DCHECK(controller_state_ == State::INITIALIZING ||
controller_state_ == State::RECOVERING);
// Always notify the LogSink no matter what path this function takes.
base::ScopedClosureRunner state_notifier(base::BindOnce(
&LogSink::OnServiceStatusChanged, base::Unretained(log_sink_)));
if (!startup_status_.Complete())
return;
bool in_recovery = controller_state_ == State::RECOVERING;
stats::LogControllerStartupStatus(in_recovery, startup_status_);
if (!startup_status_.Ok()) {
if (in_recovery) {
HandleUnrecoverableSetup();
NotifyServiceOfStartup();
} else {
StartHardRecoveryAttempt();
}
return;
}
device_status_listener_->Start(this);
PollActiveDriverDownloads();
CancelOrphanedRequests();
CleanupUnknownFiles();
RemoveCleanupEligibleDownloads();
ResolveInitialRequestStates();
NotifyClientsOfStartup(in_recovery);
controller_state_ = State::READY;
log_sink_->OnServiceDownloadsAvailable();
UpdateDriverStates();
KillTimedOutDownloads();
NotifyServiceOfStartup();
// Pull the initial straw if active downloads haven't reach maximum.
ActivateMoreDownloads();
}
void ControllerImpl::HandleUnrecoverableSetup() {
controller_state_ = State::UNAVAILABLE;
// If we cannot recover, notify Clients that the service is unavailable.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&ControllerImpl::SendOnServiceUnavailable,
weak_ptr_factory_.GetWeakPtr()));
}
void ControllerImpl::StartHardRecoveryAttempt() {
startup_status_.Reset();
controller_state_ = State::RECOVERING;
driver_->HardRecover();
model_->HardRecover();
file_monitor_->HardRecover(
base::Bind(&ControllerImpl::OnFileMonitorHardRecoverComplete,
weak_ptr_factory_.GetWeakPtr()));
}
void ControllerImpl::PollActiveDriverDownloads() {
DCHECK(controller_state_ == State::INITIALIZING ||
controller_state_ == State::RECOVERING);
std::set<std::string> guids = driver_->GetActiveDownloads();
for (auto guid : guids) {
if (!model_->Get(guid))
externally_active_downloads_.insert(guid);
}
}
void ControllerImpl::CancelOrphanedRequests() {
DCHECK(controller_state_ == State::INITIALIZING ||
controller_state_ == State::RECOVERING);
auto entries = model_->PeekEntries();
std::vector<std::string> guids_to_remove;
std::set<base::FilePath> files_to_remove;
for (auto* entry : entries) {
if (!clients_->GetClient(entry->client)) {
guids_to_remove.push_back(entry->guid);
files_to_remove.insert(entry->target_file_path);
}
}
for (const auto& guid : guids_to_remove) {
driver_->Remove(guid, false);
model_->Remove(guid);
}
file_monitor_->DeleteFiles(files_to_remove,
stats::FileCleanupReason::ORPHANED);
}
void ControllerImpl::CleanupUnknownFiles() {
DCHECK(controller_state_ == State::INITIALIZING ||
controller_state_ == State::RECOVERING);
auto entries = model_->PeekEntries();
std::vector<DriverEntry> driver_entries;
for (auto* entry : entries) {
base::Optional<DriverEntry> driver_entry = driver_->Find(entry->guid);
if (driver_entry.has_value())
driver_entries.push_back(driver_entry.value());
}
file_monitor_->DeleteUnknownFiles(entries, driver_entries);
}
void ControllerImpl::ResolveInitialRequestStates() {
DCHECK(controller_state_ == State::INITIALIZING ||
controller_state_ == State::RECOVERING);
auto entries = model_->PeekEntries();
for (auto* entry : entries) {
// Pull the initial Entry::State and DriverEntry::State.
Entry::State state = entry->state;
auto driver_entry = driver_->Find(entry->guid);
base::Optional<DriverEntry::State> driver_state;
if (driver_entry.has_value()) {
DCHECK_NE(DriverEntry::State::UNKNOWN, driver_entry->state);
driver_state = driver_entry->state;
}
// Determine what the new Entry::State should be based on the two original
// states of the two different systems.
Entry::State new_state = state;
switch (state) {
case Entry::State::NEW:
// This means we shut down but may have not ACK'ed the download. That
// is OK, we will still notify the Client about the GUID when we send
// them our initialize method.
new_state = Entry::State::AVAILABLE;
break;
case Entry::State::COMPLETE:
// We're already in our end state. Just stay here.
new_state = Entry::State::COMPLETE;
break;
case Entry::State::AVAILABLE: // Intentional fallthrough.
case Entry::State::ACTIVE: // Intentional fallthrough.
case Entry::State::PAUSED: {
// All three of these states are effectively driven by the DriverEntry
// state.
if (!driver_state.has_value()) {
// If we don't have a DriverEntry::State, just leave the state alone.
new_state = state;
break;
}
// We didn't persist the response headers in time, so just restart the
// the fetch. The driver entry and the temporary file will also be
// deleted.
if (state == Entry::State::ACTIVE && entry->require_response_headers &&
!entry->did_received_response) {
driver_->Remove(entry->guid, true /* remove_file */);
break;
}
// If we have a real DriverEntry::State, we need to determine which of
// those states makes sense for our Entry. Our entry can either be in
// two states now: It's effective 'active' state (ACTIVE or PAUSED) or
// COMPLETE.
bool is_paused = state == Entry::State::PAUSED;
Entry::State active =
is_paused ? Entry::State::PAUSED : Entry::State::ACTIVE;
switch (driver_state.value()) {
case DriverEntry::State::IN_PROGRESS: // Intentional fallthrough.
case DriverEntry::State::INTERRUPTED:
// The DriverEntry isn't done, so we need to set the Entry to the
// 'active' state.
new_state =
entry->has_upload_data ? Entry::State::COMPLETE : active;
break;
case DriverEntry::State::COMPLETE: // Intentional fallthrough.
// TODO(dtrainor, xingliu) Revisit this CANCELLED state to make sure
// all embedders behave properly.
case DriverEntry::State::CANCELLED:
// The DriverEntry is done. We need to set the Entry to the
// COMPLETE state.
new_state = Entry::State::COMPLETE;
break;
default:
NOTREACHED();
break;
}
break;
}
default:
NOTREACHED();
break;
}
// Update the Entry::State to the new correct state.
if (new_state != entry->state) {
stats::LogRecoveryOperation(new_state);
TransitTo(entry, new_state, model_.get());
}
// Given the new correct state, update the DriverEntry to reflect the Entry.
switch (new_state) {
case Entry::State::NEW: // Intentional fallthrough.
case Entry::State::AVAILABLE: // Intentional fallthrough.
// We should not have a DriverEntry here.
if (driver_entry.has_value())
driver_->Remove(entry->guid, false);
break;
case Entry::State::ACTIVE: // Intentional fallthrough.
case Entry::State::PAUSED:
// We're in the correct state. Let UpdateDriverStates() restart us if
// it wants to.
break;
case Entry::State::COMPLETE:
if (state != Entry::State::COMPLETE) {
// We are changing states to COMPLETE. Handle this like a normal
// completed download.
// Treat CANCELLED and INTERRUPTED as failures. We have to assume the
// DriverEntry might not have persisted in time.
CompletionType completion_type =
(!driver_entry.has_value() ||
driver_entry->state == DriverEntry::State::CANCELLED ||
driver_entry->state == DriverEntry::State::INTERRUPTED)
? CompletionType::UNKNOWN
: CompletionType::SUCCEED;
// TODO(shaktisahu) : May be set a completion type for upload.
HandleCompleteDownload(completion_type, entry->guid);
} else {
// We're staying in COMPLETE. Make sure there is no DriverEntry here.
if (driver_entry.has_value())
driver_->Remove(entry->guid, false);
}
break;
case Entry::State::COUNT:
NOTREACHED();
break;
}
}
}
void ControllerImpl::UpdateDriverStates() {
DCHECK(startup_status_.Complete());
for (auto* entry : model_->PeekEntries())
UpdateDriverState(entry);
}
void ControllerImpl::UpdateDriverStateWithGuid(const std::string& guid) {
Entry* entry = model_->Get(guid);
if (entry)
UpdateDriverState(entry);
}
void ControllerImpl::UpdateDriverState(Entry* entry) {
DCHECK_EQ(controller_state_, State::READY);
if ((entry->state != Entry::State::ACTIVE &&
entry->state != Entry::State::PAUSED) ||
pending_uploads_.find(entry->guid) != pending_uploads_.end()) {
return;
}
base::Optional<DriverEntry> driver_entry = driver_->Find(entry->guid);
// Check if the DriverEntry is in a finished state already. If so we need to
// clean up our Entry and finish the download.
if (driver_entry.has_value() && driver_entry->done) {
if (driver_entry->state == DriverEntry::State::COMPLETE) {
HandleCompleteDownload(CompletionType::SUCCEED, entry->guid);
} else {
HandleCompleteDownload(
CompletionType::FAIL /* Make a more detailed failure type? */,
entry->guid);
}
return;
}
bool active = driver_entry.has_value() &&
driver_entry->state == DriverEntry::State::IN_PROGRESS;
auto blockage_status = IsDownloadBlocked(entry);
if (blockage_status.IsBlocked()) {
if (active) {
stats::LogEntryEvent(stats::DownloadEvent::SUSPEND);
stats::LogDownloadPauseReason(blockage_status,
false /*on_upload_data_received*/);
}
if (driver_entry.has_value())
driver_->Pause(entry->guid);
} else {
if (!active) {
// If we aren't active, resuming is going to cost something. Track
// against throttle limits.
if (driver_entry.has_value() && driver_entry->can_resume) {
// This is, as far as we can tell, a free resumption.
entry->resumption_count++;
model_->Update(*entry);
stats::LogEntryResumptionCount(entry->resumption_count);
stats::LogEntryEvent(stats::DownloadEvent::RESUME);
if (entry->resumption_count > config_->max_resumption_count) {
HandleCompleteDownload(CompletionType::OUT_OF_RESUMPTIONS,
entry->guid);
return;
}
} else {
// This is a costly resumption.
entry->attempt_count++;
model_->Update(*entry);
stats::LogEntryRetryCount(entry->attempt_count);
stats::LogEntryEvent(stats::DownloadEvent::RETRY);
if (entry->attempt_count > config_->max_retry_count) {
HandleCompleteDownload(CompletionType::OUT_OF_RETRIES, entry->guid);
return;
}
}
}
if (driver_entry.has_value()) {
// For uploads, we should never call resume unless it is already in
// progress, since we have to re-supply the upload data from client.
DCHECK(!entry->has_upload_data ||
driver_entry->state == DriverEntry::State::IN_PROGRESS);
driver_->Resume(entry->guid);
} else {
stats::LogEntryEvent(stats::DownloadEvent::START);
PrepareToStartDownload(entry);
}
}
log_sink_->OnServiceDownloadChanged(entry->guid);
}
void ControllerImpl::PrepareToStartDownload(Entry* entry) {
pending_uploads_.insert(entry->guid);
auto* client = clients_->GetClient(entry->client);
DCHECK(client);
auto callback = base::BindOnce(&ControllerImpl::OnDownloadReadyToStart,
weak_ptr_factory_.GetWeakPtr(), entry->guid);
// To ensure no re-entrancy, we post the response again after receiving from
// the client
client->GetUploadData(entry->guid, base::BindOnce(&RunOnDownloadReadyToStart,
std::move(callback)));
// Reset the timeout timer in case client doesn't respond.
cancel_uploads_callback_.Reset(base::BindRepeating(
&ControllerImpl::KillTimedOutUploads, weak_ptr_factory_.GetWeakPtr()));
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, cancel_uploads_callback_.callback(),
config_->pending_upload_timeout_delay);
}
void ControllerImpl::OnDownloadReadyToStart(
const std::string& guid,
scoped_refptr<network::ResourceRequestBody> post_body) {
DCHECK(pending_uploads_.find(guid) != pending_uploads_.end());
pending_uploads_.erase(guid);
auto* entry = model_->Get(guid);
if (!entry) {
stats::LogEntryRemovedWhileWaitingForUploadResponse();
return;
}
if (post_body) {
entry->has_upload_data = true;
model_->Update(*entry);
}
stats::LogHasUploadData(entry->client, entry->has_upload_data);
auto blockage_status = IsDownloadBlocked(entry);
if (blockage_status.IsBlocked()) {
stats::LogDownloadPauseReason(blockage_status,
true /*on_upload_data_received*/);
return;
}
DCHECK(!driver_->Find(guid).has_value());
driver_->Start(entry->request_params, entry->guid, entry->target_file_path,
post_body,
net::NetworkTrafficAnnotationTag(entry->traffic_annotation));
}
DownloadBlockageStatus ControllerImpl::IsDownloadBlocked(Entry* entry) {
DownloadBlockageStatus status;
status.blocked_by_criteria =
!device_status_listener_->CurrentDeviceStatus()
.MeetsCondition(entry->scheduling_params,
config_->download_battery_percentage)
.MeetsRequirements();
status.blocked_by_downloads =
!externally_active_downloads_.empty() &&
entry->scheduling_params.priority <= SchedulingParams::Priority::NORMAL;
status.blocked_by_navigation = ShouldBlockDownloadOnNavigation(entry);
status.entry_not_active = entry->state != Entry::State::ACTIVE;
return status;
}
void ControllerImpl::KillTimedOutUploads() {
for (const std::string& guid : std::move(pending_uploads_))
HandleCompleteDownload(CompletionType::UPLOAD_TIMEOUT, guid);
}
void ControllerImpl::NotifyClientsOfStartup(bool state_lost) {
auto categorized = util::MapEntriesToMetadataForClients(
clients_->GetRegisteredClients(), model_->PeekEntries(), driver_.get());
for (auto client_id : clients_->GetRegisteredClients()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&ControllerImpl::SendOnServiceInitialized,
weak_ptr_factory_.GetWeakPtr(), client_id,
state_lost, categorized[client_id]));
}
}
void ControllerImpl::NotifyServiceOfStartup() {
TRACE_EVENT_ASYNC_END0("download_service", "DownloadServiceInitialize", this);
if (init_callback_.is_null())
return;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::ResetAndReturn(&init_callback_));
}
void ControllerImpl::HandleStartDownloadResponse(
DownloadClient client,
const std::string& guid,
DownloadParams::StartResult result) {
auto callback = start_callbacks_[guid];
start_callbacks_.erase(guid);
HandleStartDownloadResponse(client, guid, result, callback);
}
void ControllerImpl::HandleStartDownloadResponse(
DownloadClient client,
const std::string& guid,
DownloadParams::StartResult result,
const DownloadParams::StartCallback& callback) {
stats::LogStartDownloadResult(client, result);
// UNEXPECTED_GUID means the guid was already in use. Don't remove this entry
// from the model because it's there due to another request.
if (result != DownloadParams::StartResult::ACCEPTED &&
result != DownloadParams::StartResult::UNEXPECTED_GUID &&
model_->Get(guid) != nullptr) {
model_->Remove(guid);
}
log_sink_->OnServiceRequestMade(client, guid, result);
if (callback.is_null())
return;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(callback, guid, result));
}
void ControllerImpl::HandleCompleteDownload(CompletionType type,
const std::string& guid) {
Entry* entry = model_->Get(guid);
DCHECK(entry);
if (entry->state == Entry::State::COMPLETE) {
DVLOG(1) << "Download is already completed.";
return;
}
auto driver_entry = driver_->Find(guid);
uint64_t file_size =
driver_entry.has_value() ? driver_entry->bytes_downloaded : 0;
stats::LogDownloadCompletion(type, file_size);
if (type == CompletionType::SUCCEED) {
DCHECK(driver_entry.has_value());
stats::LogFilePathRenamed(driver_entry->current_file_path !=
entry->target_file_path);
entry->target_file_path = driver_entry->current_file_path;
entry->completion_time = driver_entry->completion_time;
entry->bytes_downloaded = driver_entry->bytes_downloaded;
CompletionInfo completion_info(driver_entry->current_file_path,
driver_entry->bytes_downloaded,
entry->url_chain, entry->response_headers);
completion_info.blob_handle = driver_entry->blob_handle;
entry->last_cleanup_check_time = driver_entry->completion_time;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&ControllerImpl::SendOnDownloadSucceeded,
weak_ptr_factory_.GetWeakPtr(), entry->client,
guid, completion_info));
TransitTo(entry, Entry::State::COMPLETE, model_.get());
ScheduleCleanupTask();
} else {
CompletionInfo completion_info;
completion_info.url_chain = entry->url_chain;
completion_info.response_headers = entry->response_headers;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&ControllerImpl::SendOnDownloadFailed,
weak_ptr_factory_.GetWeakPtr(), entry->client, guid,
completion_info, FailureReasonFromCompletionType(type)));
log_sink_->OnServiceDownloadFailed(type, *entry);
// TODO(dtrainor): Handle the case where we crash before the model write
// happens and we have no driver entry.
driver_->Remove(entry->guid, false);
model_->Remove(guid);
}
ActivateMoreDownloads();
}
void ControllerImpl::ScheduleCleanupTask() {
if (task_finished_callbacks_.count(DownloadTaskType::CLEANUP_TASK) != 0)
return;
base::Time earliest_cleanup_start_time = base::Time::Max();
for (const Entry* entry : model_->PeekEntries()) {
if (entry->state != Entry::State::COMPLETE)
continue;
base::Time cleanup_time_for_entry =
std::min(entry->last_cleanup_check_time + config_->file_keep_alive_time,
entry->completion_time + config_->max_file_keep_alive_time);
cleanup_time_for_entry =
std::max(cleanup_time_for_entry, base::Time::Now());
if (cleanup_time_for_entry < earliest_cleanup_start_time) {
earliest_cleanup_start_time = cleanup_time_for_entry;
}
}
if (earliest_cleanup_start_time == base::Time::Max()) {
task_scheduler_->CancelTask(DownloadTaskType::CLEANUP_TASK);
return;
}
base::TimeDelta start_time = earliest_cleanup_start_time - base::Time::Now();
base::TimeDelta end_time = start_time + config_->file_cleanup_window;
DCHECK_LT(std::ceil(start_time.InSecondsF()),
std::ceil(end_time.InSecondsF()))
<< "GCM requires start time to be less than end time";
task_scheduler_->ScheduleTask(DownloadTaskType::CLEANUP_TASK, false, false,
DeviceStatus::kBatteryPercentageAlwaysStart,
std::ceil(start_time.InSecondsF()),
std::ceil(end_time.InSecondsF()));
}
void ControllerImpl::ScheduleKillDownloadTaskIfNecessary() {
base::Time earliest_cancel_time = base::Time::Max();
for (const Entry* entry : model_->PeekEntries()) {
if (entry->state != Entry::State::COMPLETE &&
entry->scheduling_params.cancel_time < earliest_cancel_time) {
earliest_cancel_time = entry->scheduling_params.cancel_time;
}
}
if (earliest_cancel_time == base::Time::Max())
return;
base::TimeDelta time_to_cancel =
earliest_cancel_time > base::Time::Now()
? earliest_cancel_time - base::Time::Now()
: base::TimeDelta();
cancel_downloads_callback_.Reset(base::Bind(
&ControllerImpl::KillTimedOutDownloads, weak_ptr_factory_.GetWeakPtr()));
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, cancel_downloads_callback_.callback(), time_to_cancel);
}
void ControllerImpl::KillTimedOutDownloads() {
for (const Entry* entry : model_->PeekEntries()) {
if (entry->state != Entry::State::COMPLETE &&
entry->scheduling_params.cancel_time <= base::Time::Now()) {
HandleCompleteDownload(CompletionType::TIMEOUT, entry->guid);
}
}
ScheduleKillDownloadTaskIfNecessary();
}
void ControllerImpl::ActivateMoreDownloads() {
if (controller_state_ != State::READY)
return;
if (!device_status_listener_->is_valid_state())
return;
TRACE_EVENT0("download_service", "ActivateMoreDownloads");
// Check all the entries and the configuration to throttle number of
// downloads.
std::map<Entry::State, uint32_t> entries_states;
for (auto* const entry : model_->PeekEntries()) {
entries_states[entry->state]++;
}
uint32_t paused_count = entries_states[Entry::State::PAUSED];
uint32_t active_count = entries_states[Entry::State::ACTIVE];
while (CanActivateMoreDownloads(config_, active_count, paused_count)) {
Entry* next = scheduler_->Next(
model_->PeekEntries(), device_status_listener_->CurrentDeviceStatus());
if (!next)
break;
DCHECK_EQ(Entry::State::AVAILABLE, next->state);
TransitTo(next, Entry::State::ACTIVE, model_.get());
active_count++;
UpdateDriverState(next);
}
Model::EntryList candidates = GetRunnableEntries(model_->PeekEntries());
bool has_actionable_downloads = false;
for (auto* entry : candidates) {
has_actionable_downloads |=
device_status_listener_->CurrentDeviceStatus()
.MeetsCondition(entry->scheduling_params,
config_->download_battery_percentage)
.MeetsRequirements();
if (has_actionable_downloads)
break;
}
// Only schedule a task if we are not currently running one of the same type.
if (task_finished_callbacks_.count(DownloadTaskType::DOWNLOAD_TASK) == 0)
scheduler_->Reschedule(candidates);
if (!has_actionable_downloads) {
HandleTaskFinished(DownloadTaskType::DOWNLOAD_TASK, false,
stats::ScheduledTaskStatus::COMPLETED_NORMALLY);
}
}
void ControllerImpl::OnNavigationEvent() {
if (controller_state_ != State::READY)
return;
UpdateDriverStates();
}
bool ControllerImpl::ShouldBlockDownloadOnNavigation(Entry* entry) {
if (!navigation_monitor_->IsNavigationInProgress())
return false;
bool pausable_priority =
entry->scheduling_params.priority <= SchedulingParams::Priority::NORMAL;
base::Optional<DriverEntry> driver_entry = driver_->Find(entry->guid);
bool new_download = !driver_entry.has_value();
bool resumable_download =
driver_entry.has_value() && driver_entry->can_resume;
return pausable_priority && (new_download || resumable_download);
}
void ControllerImpl::HandleExternalDownload(const std::string& guid,
bool active) {
if (active) {
externally_active_downloads_.insert(guid);
} else {
externally_active_downloads_.erase(guid);
}
UpdateDriverStates();
}
void ControllerImpl::SendOnServiceInitialized(
DownloadClient client_id,
bool state_lost,
const std::vector<DownloadMetaData>& downloads) {
auto* client = clients_->GetClient(client_id);
DCHECK(client);
client->OnServiceInitialized(state_lost, downloads);
}
void ControllerImpl::SendOnServiceUnavailable() {
for (auto client_id : clients_->GetRegisteredClients()) {
clients_->GetClient(client_id)->OnServiceUnavailable();
}
}
void ControllerImpl::SendOnDownloadUpdated(DownloadClient client_id,
const std::string& guid,
uint64_t bytes_uploaded,
uint64_t bytes_downloaded) {
if (!model_->Get(guid))
return;
auto* client = clients_->GetClient(client_id);
DCHECK(client);
client->OnDownloadUpdated(guid, bytes_uploaded, bytes_downloaded);
}
void ControllerImpl::SendOnDownloadSucceeded(
DownloadClient client_id,
const std::string& guid,
const CompletionInfo& completion_info) {
auto* client = clients_->GetClient(client_id);
DCHECK(client);
client->OnDownloadSucceeded(guid, completion_info);
}
void ControllerImpl::SendOnDownloadFailed(
DownloadClient client_id,
const std::string& guid,
const CompletionInfo& completion_info,
download::Client::FailureReason reason) {
auto* client = clients_->GetClient(client_id);
DCHECK(client);
client->OnDownloadFailed(guid, completion_info, reason);
}
} // namespace download