| // Copyright 2020 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/model_impl/client_tag_based_remote_update_handler.h" |
| |
| #include <utility> |
| |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "components/sync/base/time.h" |
| #include "components/sync/model/metadata_change_list.h" |
| #include "components/sync/model/model_type_sync_bridge.h" |
| #include "components/sync/model_impl/processor_entity.h" |
| |
| namespace syncer { |
| |
| namespace { |
| |
| void LogNonReflectionUpdateFreshnessToUma(ModelType type, |
| base::Time remote_modification_time) { |
| const base::TimeDelta latency = base::Time::Now() - remote_modification_time; |
| |
| UMA_HISTOGRAM_CUSTOM_TIMES("Sync.NonReflectionUpdateFreshnessPossiblySkewed2", |
| latency, |
| /*min=*/base::TimeDelta::FromMilliseconds(100), |
| /*max=*/base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| |
| base::UmaHistogramCustomTimes( |
| std::string("Sync.NonReflectionUpdateFreshnessPossiblySkewed2.") + |
| ModelTypeToHistogramSuffix(type), |
| latency, |
| /*min=*/base::TimeDelta::FromMilliseconds(100), |
| /*max=*/base::TimeDelta::FromDays(7), |
| /*bucket_count=*/50); |
| } |
| |
| } // namespace |
| |
| ClientTagBasedRemoteUpdateHandler::ClientTagBasedRemoteUpdateHandler( |
| ModelType type, |
| ModelTypeSyncBridge* bridge, |
| sync_pb::ModelTypeState* model_type_state, |
| std::map<std::string, ClientTagHash>* storage_key_to_tag_hash, |
| std::map<ClientTagHash, std::unique_ptr<ProcessorEntity>>* entities) |
| : type_(type), |
| bridge_(bridge), |
| model_type_state_(model_type_state), |
| storage_key_to_tag_hash_(storage_key_to_tag_hash), |
| entities_(entities) { |
| DCHECK(bridge_); |
| DCHECK(model_type_state_); |
| DCHECK(storage_key_to_tag_hash_); |
| DCHECK(entities_); |
| } |
| |
| base::Optional<ModelError> |
| ClientTagBasedRemoteUpdateHandler::ProcessIncrementalUpdate( |
| const sync_pb::ModelTypeState& model_type_state, |
| UpdateResponseDataList updates) { |
| std::unique_ptr<MetadataChangeList> metadata_changes = |
| bridge_->CreateMetadataChangeList(); |
| EntityChangeList entity_changes; |
| |
| metadata_changes->UpdateModelTypeState(model_type_state); |
| const bool got_new_encryption_requirements = |
| model_type_state_->encryption_key_name() != |
| model_type_state.encryption_key_name(); |
| *model_type_state_ = model_type_state; |
| |
| // If new encryption requirements come from the server, the entities that are |
| // in |updates| will be recorded here so they can be ignored during the |
| // re-encryption phase at the end. |
| std::unordered_set<std::string> already_updated; |
| |
| for (std::unique_ptr<syncer::UpdateResponseData>& update : updates) { |
| DCHECK(update); |
| std::string storage_key_to_clear; |
| ProcessorEntity* entity = ProcessUpdate(std::move(update), &entity_changes, |
| &storage_key_to_clear); |
| |
| if (!entity) { |
| // The update is either of the following: |
| // 1. Tombstone of entity that didn't exist locally. |
| // 2. Reflection, thus should be ignored. |
| // 3. Update without a client tag hash (including permanent nodes, which |
| // have server tags instead). |
| continue; |
| } |
| |
| LogNonReflectionUpdateFreshnessToUma( |
| type_, |
| /*remote_modification_time=*/ |
| ProtoTimeToTime(entity->metadata().modification_time())); |
| |
| if (entity->storage_key().empty()) { |
| // Storage key of this entity is not known yet. Don't update metadata, it |
| // will be done from UpdateStorageKey. |
| |
| // If this is the result of a conflict resolution (where a remote |
| // undeletion was preferred), then need to clear a metadata entry from |
| // the database. |
| if (!storage_key_to_clear.empty()) { |
| metadata_changes->ClearMetadata(storage_key_to_clear); |
| storage_key_to_tag_hash_->erase(storage_key_to_clear); |
| } |
| continue; |
| } |
| |
| DCHECK(storage_key_to_clear.empty()); |
| |
| if (entity->CanClearMetadata()) { |
| metadata_changes->ClearMetadata(entity->storage_key()); |
| storage_key_to_tag_hash_->erase(entity->storage_key()); |
| entities_->erase( |
| ClientTagHash::FromHashed(entity->metadata().client_tag_hash())); |
| } else { |
| metadata_changes->UpdateMetadata(entity->storage_key(), |
| entity->metadata()); |
| } |
| |
| if (got_new_encryption_requirements) { |
| already_updated.insert(entity->storage_key()); |
| } |
| } |
| |
| if (got_new_encryption_requirements) { |
| // TODO(pavely): Currently we recommit all entities. We should instead |
| // recommit only the ones whose encryption key doesn't match the one in |
| // DataTypeState. Work is tracked in http://crbug.com/727874. |
| RecommitAllForEncryption(already_updated, metadata_changes.get()); |
| } |
| |
| // Inform the bridge of the new or updated data. |
| return bridge_->ApplySyncChanges(std::move(metadata_changes), |
| std::move(entity_changes)); |
| } |
| |
| ProcessorEntity* ClientTagBasedRemoteUpdateHandler::ProcessUpdate( |
| std::unique_ptr<UpdateResponseData> update, |
| EntityChangeList* entity_changes, |
| std::string* storage_key_to_clear) { |
| const EntityData& data = *update->entity; |
| const ClientTagHash& client_tag_hash = data.client_tag_hash; |
| |
| // Filter out updates without a client tag hash (including permanent nodes, |
| // which have server tags instead). |
| if (client_tag_hash.value().empty()) { |
| return nullptr; |
| } |
| |
| // Filter out unexpected client tag hashes. |
| if (!data.is_deleted() && bridge_->SupportsGetClientTag() && |
| client_tag_hash != |
| ClientTagHash::FromUnhashed(type_, bridge_->GetClientTag(data))) { |
| DLOG(WARNING) << "Received unexpected client tag hash: " << client_tag_hash |
| << " for " << ModelTypeToString(type_); |
| return nullptr; |
| } |
| |
| ProcessorEntity* entity = GetEntityForTagHash(client_tag_hash); |
| |
| // Handle corner cases first. |
| if (entity == nullptr && data.is_deleted()) { |
| // Local entity doesn't exist and update is tombstone. |
| DLOG(WARNING) << "Received remote delete for a non-existing item." |
| << " client_tag_hash: " << client_tag_hash << " for " |
| << ModelTypeToString(type_); |
| return nullptr; |
| } |
| |
| if (entity) { |
| entity->RecordEntityUpdateLatency(update->response_version, type_); |
| } |
| |
| if (entity && entity->UpdateIsReflection(update->response_version)) { |
| // Seen this update before; just ignore it. |
| return nullptr; |
| } |
| |
| // Cache update encryption key name in case |update| will be moved away into |
| // ResolveConflict(). |
| const std::string update_encryption_key_name = update->encryption_key_name; |
| ConflictResolution resolution_type = ConflictResolution::kTypeSize; |
| if (entity && entity->IsUnsynced()) { |
| // Handle conflict resolution. |
| resolution_type = ResolveConflict(std::move(update), entity, entity_changes, |
| storage_key_to_clear); |
| UMA_HISTOGRAM_ENUMERATION("Sync.ResolveConflict", resolution_type, |
| ConflictResolution::kTypeSize); |
| } else { |
| // Handle simple create/delete/update. |
| base::Optional<EntityChange::ChangeType> change_type; |
| |
| if (entity == nullptr) { |
| entity = CreateEntity(data); |
| change_type = EntityChange::ACTION_ADD; |
| } else if (data.is_deleted()) { |
| DCHECK(!entity->metadata().is_deleted()); |
| change_type = EntityChange::ACTION_DELETE; |
| } else if (!entity->MatchesData(data)) { |
| change_type = EntityChange::ACTION_UPDATE; |
| } |
| entity->RecordAcceptedUpdate(*update); |
| // Inform the bridge about the changes if needed. |
| if (change_type) { |
| switch (change_type.value()) { |
| case EntityChange::ACTION_ADD: |
| entity_changes->push_back(EntityChange::CreateAdd( |
| entity->storage_key(), std::move(update->entity))); |
| break; |
| case EntityChange::ACTION_DELETE: |
| // The entity was deleted; inform the bridge. Note that the local data |
| // can never be deleted at this point because it would have either |
| // been acked (the add case) or pending (the conflict case). |
| entity_changes->push_back( |
| EntityChange::CreateDelete(entity->storage_key())); |
| break; |
| case EntityChange::ACTION_UPDATE: |
| // Specifics have changed, so update the bridge. |
| entity_changes->push_back(EntityChange::CreateUpdate( |
| entity->storage_key(), std::move(update->entity))); |
| break; |
| } |
| } |
| } |
| |
| // If the received entity has out of date encryption, we schedule another |
| // commit to fix it. |
| if (model_type_state_->encryption_key_name() != update_encryption_key_name) { |
| DVLOG(2) << ModelTypeToString(type_) << ": Requesting re-encrypt commit " |
| << update_encryption_key_name << " -> " |
| << model_type_state_->encryption_key_name(); |
| |
| entity->IncrementSequenceNumber(base::Time::Now()); |
| } |
| return entity; |
| } |
| |
| void ClientTagBasedRemoteUpdateHandler::RecommitAllForEncryption( |
| const std::unordered_set<std::string>& already_updated, |
| MetadataChangeList* metadata_changes) { |
| ModelTypeSyncBridge::StorageKeyList entities_needing_data; |
| |
| for (const auto& kv : *entities_) { |
| ProcessorEntity* entity = kv.second.get(); |
| if (entity->storage_key().empty() || |
| (already_updated.find(entity->storage_key()) != |
| already_updated.end())) { |
| // Entities with empty storage key were already processed. ProcessUpdate() |
| // incremented their sequence numbers and cached commit data. Their |
| // metadata will be persisted in UpdateStorageKey(). |
| continue; |
| } |
| entity->IncrementSequenceNumber(base::Time::Now()); |
| if (entity->RequiresCommitData()) { |
| entities_needing_data.push_back(entity->storage_key()); |
| } |
| metadata_changes->UpdateMetadata(entity->storage_key(), entity->metadata()); |
| } |
| } |
| |
| ConflictResolution ClientTagBasedRemoteUpdateHandler::ResolveConflict( |
| std::unique_ptr<UpdateResponseData> update, |
| ProcessorEntity* entity, |
| EntityChangeList* changes, |
| std::string* storage_key_to_clear) { |
| const EntityData& remote_data = *update->entity; |
| |
| ConflictResolution resolution_type = ConflictResolution::kTypeSize; |
| |
| // Determine the type of resolution. |
| if (entity->MatchesData(remote_data)) { |
| // The changes are identical so there isn't a real conflict. |
| resolution_type = ConflictResolution::kChangesMatch; |
| } else if (entity->metadata().is_deleted()) { |
| // Local tombstone vs remote update (non-deletion). Should be undeleted. |
| resolution_type = ConflictResolution::kUseRemote; |
| } else if (entity->MatchesOwnBaseData()) { |
| // If there is no real local change, then the entity must be unsynced due to |
| // a pending local re-encryption request. In this case, the remote data |
| // should win. |
| resolution_type = ConflictResolution::kIgnoreLocalEncryption; |
| } else if (entity->MatchesBaseData(remote_data)) { |
| // The remote data isn't actually changing from the last remote data that |
| // was seen, so it must have been a re-encryption and can be ignored. |
| resolution_type = ConflictResolution::kIgnoreRemoteEncryption; |
| } else { |
| // There's a real data conflict here; let the bridge resolve it. |
| resolution_type = |
| bridge_->ResolveConflict(entity->storage_key(), remote_data); |
| } |
| |
| // Apply the resolution. |
| switch (resolution_type) { |
| case ConflictResolution::kChangesMatch: |
| // Record the update and squash the pending commit. |
| entity->RecordForcedUpdate(*update); |
| break; |
| case ConflictResolution::kUseLocal: |
| case ConflictResolution::kIgnoreRemoteEncryption: |
| // Record that we received the update from the server but leave the |
| // pending commit intact. |
| entity->RecordIgnoredUpdate(*update); |
| break; |
| case ConflictResolution::kUseRemote: |
| case ConflictResolution::kIgnoreLocalEncryption: |
| // Update client data to match server. |
| if (update->entity->is_deleted()) { |
| DCHECK(!entity->metadata().is_deleted()); |
| // Squash the pending commit. |
| entity->RecordForcedUpdate(*update); |
| changes->push_back(EntityChange::CreateDelete(entity->storage_key())); |
| } else if (!entity->metadata().is_deleted()) { |
| // Squash the pending commit. |
| entity->RecordForcedUpdate(*update); |
| changes->push_back(EntityChange::CreateUpdate( |
| entity->storage_key(), std::move(update->entity))); |
| } else { |
| // Remote undeletion. This could imply a new storage key for some |
| // bridges, so we may need to wait until UpdateStorageKey() is called. |
| if (!bridge_->SupportsGetStorageKey()) { |
| *storage_key_to_clear = entity->storage_key(); |
| entity->ClearStorageKey(); |
| } |
| // Squash the pending commit. |
| entity->RecordForcedUpdate(*update); |
| changes->push_back(EntityChange::CreateAdd(entity->storage_key(), |
| std::move(update->entity))); |
| } |
| break; |
| case ConflictResolution::kUseNewDEPRECATED: |
| case ConflictResolution::kTypeSize: |
| NOTREACHED(); |
| break; |
| } |
| |
| return resolution_type; |
| } |
| |
| ProcessorEntity* ClientTagBasedRemoteUpdateHandler::GetEntityForTagHash( |
| const ClientTagHash& tag_hash) { |
| const auto it = entities_->find(tag_hash); |
| return it != entities_->end() ? it->second.get() : nullptr; |
| } |
| |
| ProcessorEntity* ClientTagBasedRemoteUpdateHandler::CreateEntity( |
| const std::string& storage_key, |
| const EntityData& data) { |
| DCHECK(!data.client_tag_hash.value().empty()); |
| DCHECK(entities_->find(data.client_tag_hash) == entities_->end()); |
| DCHECK(!bridge_->SupportsGetStorageKey() || !storage_key.empty()); |
| DCHECK(storage_key.empty() || storage_key_to_tag_hash_->find(storage_key) == |
| storage_key_to_tag_hash_->end()); |
| std::unique_ptr<ProcessorEntity> entity = ProcessorEntity::CreateNew( |
| storage_key, data.client_tag_hash, data.id, data.creation_time); |
| ProcessorEntity* entity_ptr = entity.get(); |
| (*entities_)[data.client_tag_hash] = std::move(entity); |
| if (!storage_key.empty()) |
| (*storage_key_to_tag_hash_)[storage_key] = data.client_tag_hash; |
| return entity_ptr; |
| } |
| |
| ProcessorEntity* ClientTagBasedRemoteUpdateHandler::CreateEntity( |
| const EntityData& data) { |
| if (bridge_->SupportsGetClientTag()) { |
| DCHECK_EQ(data.client_tag_hash, |
| ClientTagHash::FromUnhashed(type_, bridge_->GetClientTag(data))); |
| } |
| std::string storage_key; |
| if (bridge_->SupportsGetStorageKey()) |
| storage_key = bridge_->GetStorageKey(data); |
| return CreateEntity(storage_key, data); |
| } |
| |
| } // namespace syncer |