| // Copyright 2014 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/sync/driver/model_association_manager.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <functional> |
| #include <utility> |
| |
| #include "base/barrier_closure.h" |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/stl_util.h" |
| #include "base/trace_event/trace_event.h" |
| #include "components/sync/base/model_type.h" |
| #include "components/sync/base/sync_stop_metadata_fate.h" |
| |
| namespace syncer { |
| |
| namespace { |
| |
| static const ModelType kStartOrder[] = { |
| NIGORI, // Listed for completeness. |
| DEVICE_INFO, |
| PROXY_TABS, // Listed for completeness. |
| |
| // Kick off the association of the non-UI types first so they can associate |
| // in parallel with the UI types. |
| PASSWORDS, AUTOFILL, AUTOFILL_PROFILE, AUTOFILL_WALLET_DATA, |
| AUTOFILL_WALLET_METADATA, AUTOFILL_WALLET_OFFER, EXTENSION_SETTINGS, |
| APP_SETTINGS, TYPED_URLS, HISTORY_DELETE_DIRECTIVES, |
| |
| // Chrome OS settings affect the initial desktop appearance before the |
| // browser window opens, so start them before browser data types. |
| OS_PRIORITY_PREFERENCES, OS_PREFERENCES, |
| |
| // UI thread data types. |
| BOOKMARKS, PREFERENCES, PRIORITY_PREFERENCES, EXTENSIONS, APPS, APP_LIST, |
| ARC_PACKAGE, READING_LIST, THEMES, SEARCH_ENGINES, SESSIONS, DICTIONARY, |
| DEPRECATED_FAVICON_IMAGES, DEPRECATED_FAVICON_TRACKING, PRINTERS, |
| USER_CONSENTS, USER_EVENTS, SHARING_MESSAGE, SUPERVISED_USER_SETTINGS, |
| SUPERVISED_USER_ALLOWLISTS, SEND_TAB_TO_SELF, SECURITY_EVENTS, WEB_APPS, |
| WIFI_CONFIGURATIONS}; |
| |
| static_assert(base::size(kStartOrder) == |
| ModelType::NUM_ENTRIES - FIRST_REAL_MODEL_TYPE, |
| "When adding a new type, update kStartOrder."); |
| |
| // The amount of time we wait for association to finish. If some types haven't |
| // finished association by the time, DataTypeManager is notified of the |
| // unfinished types. |
| const int64_t kAssociationTimeOutInSeconds = 600; |
| |
| } // namespace |
| |
| ModelAssociationManager::ModelAssociationManager( |
| const DataTypeController::TypeMap* controllers, |
| ModelAssociationManagerDelegate* processor) |
| : state_(IDLE), |
| controllers_(controllers), |
| delegate_(processor), |
| configure_status_(DataTypeManager::UNKNOWN), |
| notified_about_ready_for_configure_(false) {} |
| |
| ModelAssociationManager::~ModelAssociationManager() = default; |
| |
| void ModelAssociationManager::Initialize(ModelTypeSet desired_types, |
| ModelTypeSet preferred_types, |
| const ConfigureContext& context) { |
| // state_ can be INITIALIZED if types are reconfigured when |
| // data is being downloaded, so StartAssociationAsync() is never called for |
| // the first configuration. |
| DCHECK_NE(ASSOCIATING, state_); |
| |
| // |desired_types| must be a subset of |preferred_types|. |
| DCHECK(preferred_types.HasAll(desired_types)); |
| |
| bool sync_mode_changed = configure_context_.sync_mode != context.sync_mode; |
| |
| configure_context_ = context; |
| |
| // Only keep types that have controllers. |
| desired_types_.Clear(); |
| for (ModelType type : desired_types) { |
| auto dtc_iter = controllers_->find(type); |
| if (dtc_iter != controllers_->end()) { |
| DataTypeController* dtc = dtc_iter->second.get(); |
| // Controllers in a FAILED state should have been filtered out by the |
| // DataTypeManager. |
| DCHECK_NE(dtc->state(), DataTypeController::FAILED); |
| desired_types_.Put(type); |
| } |
| } |
| |
| DVLOG(1) << "ModelAssociationManager: Initializing for " |
| << ModelTypeSetToString(desired_types_); |
| |
| state_ = INITIALIZED; |
| notified_about_ready_for_configure_ = false; |
| |
| DVLOG(1) << "ModelAssociationManager: Stopping disabled types."; |
| std::map<DataTypeController*, ShutdownReason> types_to_stop; |
| for (const auto& type_and_dtc : *controllers_) { |
| DataTypeController* dtc = type_and_dtc.second.get(); |
| // We generally stop all data types which are not desired. When the storage |
| // option changes, we need to restart all data types so that they can |
| // re-wire to the correct storage. |
| bool should_stop = !desired_types_.Has(dtc->type()) || sync_mode_changed; |
| // If the datatype is already STOPPING, we also wait for it to stop, to make |
| // sure it's ready to start again (if appropriate). |
| if ((should_stop && dtc->state() != DataTypeController::NOT_RUNNING) || |
| dtc->state() == DataTypeController::STOPPING) { |
| // Note: STOP_SYNC means we'll keep the Sync data around; DISABLE_SYNC |
| // means we'll clear it. |
| ShutdownReason reason = |
| preferred_types.Has(dtc->type()) ? STOP_SYNC : DISABLE_SYNC; |
| // If we're switching to transport-only mode, don't clear any old data. |
| // The reason is that if a user temporarily disables Sync, we don't want |
| // to wipe (and later redownload) all their data, just because Sync |
| // restarted in transport-only mode. |
| if (sync_mode_changed && |
| configure_context_.sync_mode == SyncMode::kTransportOnly) { |
| reason = STOP_SYNC; |
| } |
| types_to_stop[dtc] = reason; |
| } |
| } |
| |
| // Run LoadEnabledTypes() only after all relevant types are stopped. |
| // TODO(mastiz): Add test coverage to this waiting logic, including the |
| // case where the datatype is STOPPING when this function is called. |
| base::RepeatingClosure barrier_closure = base::BarrierClosure( |
| types_to_stop.size(), |
| base::BindOnce(&ModelAssociationManager::LoadEnabledTypes, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| for (const auto& dtc_and_reason : types_to_stop) { |
| DataTypeController* dtc = dtc_and_reason.first; |
| const ShutdownReason reason = dtc_and_reason.second; |
| DVLOG(1) << "ModelAssociationManager: stop " << dtc->name() << " due to " |
| << ShutdownReasonToString(reason); |
| StopDatatypeImpl(SyncError(), reason, dtc, barrier_closure); |
| } |
| } |
| |
| void ModelAssociationManager::StopDatatype(ModelType type, |
| ShutdownReason shutdown_reason, |
| SyncError error) { |
| DCHECK(error.IsSet()); |
| desired_types_.Remove(type); |
| |
| DataTypeController* dtc = controllers_->find(type)->second.get(); |
| if (dtc->state() != DataTypeController::NOT_RUNNING && |
| dtc->state() != DataTypeController::STOPPING) { |
| StopDatatypeImpl(error, shutdown_reason, dtc, base::DoNothing()); |
| } |
| |
| // Removing a desired type may mean all models are now loaded. |
| NotifyDelegateIfReadyForConfigure(); |
| } |
| |
| void ModelAssociationManager::StopDatatypeImpl( |
| const SyncError& error, |
| ShutdownReason shutdown_reason, |
| DataTypeController* dtc, |
| DataTypeController::StopCallback callback) { |
| loaded_types_.Remove(dtc->type()); |
| associated_types_.Remove(dtc->type()); |
| associating_types_.Remove(dtc->type()); |
| |
| DCHECK(error.IsSet() || (dtc->state() != DataTypeController::NOT_RUNNING)); |
| |
| delegate_->OnSingleDataTypeWillStop(dtc->type(), error); |
| |
| // Note: Depending on |shutdown_reason|, USS types might clear their metadata |
| // in response to Stop(). |
| dtc->Stop(shutdown_reason, std::move(callback)); |
| } |
| |
| void ModelAssociationManager::LoadEnabledTypes() { |
| // Load in kStartOrder. |
| for (ModelType type : kStartOrder) { |
| if (!desired_types_.Has(type)) |
| continue; |
| |
| auto dtc_iter = controllers_->find(type); |
| DCHECK(dtc_iter != controllers_->end()); |
| DataTypeController* dtc = dtc_iter->second.get(); |
| DCHECK_NE(DataTypeController::STOPPING, dtc->state()); |
| if (dtc->state() == DataTypeController::NOT_RUNNING) { |
| DCHECK(!loaded_types_.Has(dtc->type())); |
| DCHECK(!associated_types_.Has(dtc->type())); |
| dtc->LoadModels( |
| configure_context_, |
| base::BindRepeating(&ModelAssociationManager::ModelLoadCallback, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| // It's possible that all models are already loaded. |
| NotifyDelegateIfReadyForConfigure(); |
| } |
| |
| void ModelAssociationManager::StartAssociationAsync( |
| const ModelTypeSet& types_to_associate) { |
| DCHECK_EQ(INITIALIZED, state_); |
| DVLOG(1) << "Starting association for " |
| << ModelTypeSetToString(types_to_associate); |
| state_ = ASSOCIATING; |
| |
| requested_types_ = types_to_associate; |
| |
| associating_types_ = types_to_associate; |
| associating_types_.RetainAll(desired_types_); |
| associating_types_.RemoveAll(associated_types_); |
| |
| // Assume success. |
| configure_status_ = DataTypeManager::OK; |
| |
| // Done if no types to associate. |
| if (associating_types_.Empty()) { |
| ModelAssociationDone(INITIALIZED); |
| return; |
| } |
| |
| timer_.Start(FROM_HERE, |
| base::TimeDelta::FromSeconds(kAssociationTimeOutInSeconds), |
| base::BindOnce(&ModelAssociationManager::ModelAssociationDone, |
| weak_ptr_factory_.GetWeakPtr(), INITIALIZED)); |
| |
| // Associate types that are already loaded in specified order. |
| for (ModelType type : kStartOrder) { |
| if (!associating_types_.Has(type) || !loaded_types_.Has(type)) |
| continue; |
| |
| DataTypeController* dtc = controllers_->find(type)->second.get(); |
| TRACE_EVENT_ASYNC_BEGIN1("sync", "ModelAssociation", dtc, "DataType", |
| ModelTypeToString(type)); |
| |
| MarkDataTypeAssociationDone(type); |
| } |
| } |
| |
| void ModelAssociationManager::Stop(ShutdownReason shutdown_reason) { |
| // Ignore callbacks from controllers. |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| |
| // Stop started data types. |
| for (const auto& type_and_dtc : *controllers_) { |
| DataTypeController* dtc = type_and_dtc.second.get(); |
| if (dtc->state() != DataTypeController::NOT_RUNNING && |
| dtc->state() != DataTypeController::STOPPING) { |
| // We don't really wait until all datatypes have been fully stopped, which |
| // is only required (and in fact waited for) when Initialize() is called. |
| StopDatatypeImpl(SyncError(), shutdown_reason, dtc, base::DoNothing()); |
| DVLOG(1) << "ModelAssociationManager: Stopped " << dtc->name(); |
| } |
| } |
| |
| desired_types_.Clear(); |
| loaded_types_.Clear(); |
| associated_types_.Clear(); |
| |
| if (state_ == ASSOCIATING) { |
| if (configure_status_ == DataTypeManager::OK) |
| configure_status_ = DataTypeManager::ABORTED; |
| DVLOG(1) << "ModelAssociationManager: Calling OnModelAssociationDone"; |
| ModelAssociationDone(IDLE); |
| } else { |
| DCHECK(associating_types_.Empty()); |
| DCHECK(requested_types_.Empty()); |
| state_ = IDLE; |
| } |
| } |
| |
| void ModelAssociationManager::ModelLoadCallback(ModelType type, |
| const SyncError& error) { |
| DVLOG(1) << "ModelAssociationManager: ModelLoadCallback for " |
| << ModelTypeToString(type); |
| |
| if (error.IsSet()) { |
| DVLOG(1) << "ModelAssociationManager: Type encountered an error."; |
| desired_types_.Remove(type); |
| DataTypeController* dtc = controllers_->find(type)->second.get(); |
| StopDatatypeImpl(error, STOP_SYNC, dtc, base::DoNothing()); |
| NotifyDelegateIfReadyForConfigure(); |
| return; |
| } |
| |
| // This happens when slow loading type is disabled by new configuration or |
| // the model came unready during loading. |
| if (!desired_types_.Has(type)) |
| return; |
| |
| DCHECK(!loaded_types_.Has(type)); |
| loaded_types_.Put(type); |
| NotifyDelegateIfReadyForConfigure(); |
| if (associating_types_.Has(type)) { |
| DataTypeController* dtc = controllers_->find(type)->second.get(); |
| // If initial sync was done for this datatype then |
| // NotifyDelegateIfReadyForConfigure possibly already triggered model |
| // association and StartAssociating was already called for this type. To |
| // ensure StartAssociating is called only once only make a call if state is |
| // MODEL_LOADED. |
| // TODO(pavely): Add test for this scenario in DataTypeManagerImpl |
| // unittests. |
| // TODO(crbug.com/647505): The above sounds quite broken (will |
| // MarkDataTypeAssociationDone never get called in that case?!), and also |
| // outdated (StartAssociating doesn't exist anymore). Can we just move the |
| // NotifyDelegateIfReadyForConfigure call below? |
| if (dtc->state() == DataTypeController::MODEL_LOADED) { |
| MarkDataTypeAssociationDone(type); |
| } |
| } |
| } |
| |
| void ModelAssociationManager::MarkDataTypeAssociationDone(ModelType type) { |
| DCHECK(desired_types_.Has(type)); |
| DCHECK(associating_types_.Has(type)); |
| DCHECK(loaded_types_.Has(type)); |
| DCHECK(!associated_types_.Has(type)); |
| |
| associated_types_.Put(type); |
| |
| // TODO(crbug.com/647505): Should we check this *before* adding the type to |
| // |associated_types_|? |
| if (state_ != ASSOCIATING) |
| return; |
| |
| TRACE_EVENT_ASYNC_END1("sync", "ModelAssociation", |
| controllers_->find(type)->second.get(), "DataType", |
| ModelTypeToString(type)); |
| |
| // Track the merge results if we succeeded or an association failure |
| // occurred. |
| if (ProtocolTypes().Has(type)) |
| delegate_->OnSingleDataTypeAssociationDone(type); |
| |
| associating_types_.Remove(type); |
| |
| if (associating_types_.Empty()) |
| ModelAssociationDone(INITIALIZED); |
| } |
| |
| void ModelAssociationManager::ModelAssociationDone(State new_state) { |
| DCHECK_NE(IDLE, state_); |
| |
| if (state_ == INITIALIZED) { |
| // No associations are currently happening. Just reset the state. |
| state_ = new_state; |
| return; |
| } |
| |
| DVLOG(1) << "Model association complete for " |
| << ModelTypeSetToString(requested_types_); |
| |
| timer_.Stop(); |
| |
| // Treat any unfinished types as having errors. |
| desired_types_.RemoveAll(associating_types_); |
| for (const auto& type_and_dtc : *controllers_) { |
| DataTypeController* dtc = type_and_dtc.second.get(); |
| if (associating_types_.Has(dtc->type()) && |
| dtc->state() != DataTypeController::NOT_RUNNING && |
| dtc->state() != DataTypeController::STOPPING) { |
| base::UmaHistogramEnumeration("Sync.ConfigureFailed", |
| ModelTypeHistogramValue(dtc->type())); |
| StopDatatypeImpl(SyncError(FROM_HERE, SyncError::DATATYPE_ERROR, |
| "Association timed out.", dtc->type()), |
| STOP_SYNC, dtc, base::DoNothing()); |
| } |
| } |
| |
| DataTypeManager::ConfigureResult result(configure_status_, requested_types_); |
| |
| // Need to reset state before invoking delegate in order to avoid re-entrancy |
| // issues (delegate may trigger a reconfiguration). |
| associating_types_.Clear(); |
| requested_types_.Clear(); |
| state_ = new_state; |
| |
| delegate_->OnModelAssociationDone(result); |
| } |
| |
| base::OneShotTimer* ModelAssociationManager::GetTimerForTesting() { |
| return &timer_; |
| } |
| |
| void ModelAssociationManager::NotifyDelegateIfReadyForConfigure() { |
| if (notified_about_ready_for_configure_) |
| return; |
| |
| for (ModelType type : desired_types_) { |
| if (!loaded_types_.Has(type)) { |
| // At least one type is not ready. |
| return; |
| } |
| } |
| |
| notified_about_ready_for_configure_ = true; |
| delegate_->OnAllDataTypesReadyForConfigure(); |
| } |
| |
| } // namespace syncer |