| // Copyright 2012 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "components/sync/engine/commit.h" | |
| #include <utility> | |
| #include "base/logging.h" | |
| #include "base/metrics/histogram_functions.h" | |
| #include "base/metrics/histogram_macros.h" | |
| #include "base/notreached.h" | |
| #include "base/rand_util.h" | |
| #include "base/trace_event/trace_event.h" | |
| #include "components/sync/engine/active_devices_invalidation_info.h" | |
| #include "components/sync/engine/commit_processor.h" | |
| #include "components/sync/engine/commit_util.h" | |
| #include "components/sync/engine/cycle/sync_cycle.h" | |
| #include "components/sync/engine/events/commit_request_event.h" | |
| #include "components/sync/engine/events/commit_response_event.h" | |
| #include "components/sync/engine/syncer.h" | |
| #include "components/sync/engine/syncer_proto_util.h" | |
| #include "net/http/http_status_code.h" | |
| namespace syncer { | |
| namespace { | |
| // The number of random ASCII bytes we'll add to CommitMessage. We choose 256 | |
| // because it is not too large (to hurt performance and compression ratio), but | |
| // it is not too small to easily be canceled out using statistical analysis. | |
| constexpr size_t kPaddingSize = 256; | |
| std::string RandASCIIString(size_t length) { | |
| std::string result; | |
| const int kMin = static_cast<int>(' '); | |
| const int kMax = static_cast<int>('~'); | |
| result.reserve(length); | |
| for (size_t i = 0; i < length; ++i) { | |
| result.push_back(static_cast<char>(base::RandIntInclusive(kMin, kMax))); | |
| } | |
| return result; | |
| } | |
| SyncCommitError GetSyncCommitError(SyncerError syncer_error) { | |
| switch (syncer_error.type()) { | |
| case SyncerError::Type::kSuccess: | |
| NOTREACHED(); | |
| case SyncerError::Type::kNetworkError: | |
| return SyncCommitError::kNetworkError; | |
| case SyncerError::Type::kHttpError: | |
| if (syncer_error.GetHttpErrorOrDie() == net::HTTP_UNAUTHORIZED) { | |
| return SyncCommitError::kAuthError; | |
| } else { | |
| return SyncCommitError::kServerError; | |
| } | |
| case SyncerError::Type::kProtocolError: | |
| return SyncCommitError::kServerError; | |
| case SyncerError::Type::kProtocolViolationError: | |
| return SyncCommitError::kBadServerResponse; | |
| } | |
| NOTREACHED(); | |
| } | |
| } // namespace | |
| Commit::Commit(ContributionMap contributions, | |
| const sync_pb::ClientToServerMessage& message, | |
| ExtensionsActivity::Records extensions_activity_buffer) | |
| : contributions_(std::move(contributions)), | |
| message_(message), | |
| extensions_activity_buffer_(extensions_activity_buffer) {} | |
| Commit::~Commit() = default; | |
| // static | |
| std::unique_ptr<Commit> Commit::Init( | |
| DataTypeSet enabled_types, | |
| size_t max_entries, | |
| const std::string& account_name, | |
| const std::string& cache_guid, | |
| bool cookie_jar_mismatch, | |
| const ActiveDevicesInvalidationInfo& active_devices_invalidation_info, | |
| CommitProcessor* commit_processor, | |
| ExtensionsActivity* extensions_activity) { | |
| // Gather per-type contributions. | |
| ContributionMap contributions = | |
| commit_processor->GatherCommitContributions(max_entries); | |
| // Give up if no one had anything to commit. | |
| if (contributions.empty()) { | |
| return nullptr; | |
| } | |
| sync_pb::ClientToServerMessage message; | |
| message.set_message_contents(sync_pb::ClientToServerMessage::COMMIT); | |
| message.set_share(account_name); | |
| sync_pb::CommitMessage* commit_message = message.mutable_commit(); | |
| commit_message->set_cache_guid(cache_guid); | |
| // Set padding to mitigate CRIME attack. | |
| commit_message->set_padding(RandASCIIString(kPaddingSize)); | |
| // Set extensions activity if bookmark commits are present. | |
| ExtensionsActivity::Records extensions_activity_buffer; | |
| if (extensions_activity != nullptr) { | |
| ContributionMap::const_iterator it = contributions.find(BOOKMARKS); | |
| if (it != contributions.end() && it->second->GetNumEntries() != 0) { | |
| commit_util::AddExtensionsActivityToMessage( | |
| extensions_activity, &extensions_activity_buffer, commit_message); | |
| } | |
| } | |
| DataTypeSet contributed_data_types; | |
| for (const auto& [type, contribution] : contributions) { | |
| contributed_data_types.Put(type); | |
| } | |
| // Set the client config params. | |
| commit_util::AddClientConfigParamsToMessage( | |
| enabled_types, cookie_jar_mismatch, | |
| active_devices_invalidation_info.IsSingleClientForTypes( | |
| contributed_data_types), | |
| active_devices_invalidation_info | |
| .IsSingleClientWithStandaloneInvalidationsForTypes( | |
| contributed_data_types), | |
| active_devices_invalidation_info | |
| .IsSingleClientWithOldInvalidationsForTypes(contributed_data_types), | |
| active_devices_invalidation_info.all_fcm_registration_tokens(), | |
| active_devices_invalidation_info | |
| .GetFcmRegistrationTokensForInterestedClients(contributed_data_types), | |
| commit_message); | |
| // Finally, serialize all our contributions. | |
| for (const auto& contribution : contributions) { | |
| contribution.second->AddToCommitMessage(&message); | |
| } | |
| // If we made it this far, then we've successfully prepared a commit message. | |
| return std::make_unique<Commit>(std::move(contributions), message, | |
| extensions_activity_buffer); | |
| } | |
| SyncerError Commit::PostAndProcessResponse( | |
| NudgeTracker* nudge_tracker, | |
| SyncCycle* cycle, | |
| StatusController* status, | |
| ExtensionsActivity* extensions_activity) { | |
| DataTypeSet request_types; | |
| for (const auto& [request_type, contribution] : contributions_) { | |
| request_types.Put(request_type); | |
| UMA_HISTOGRAM_ENUMERATION("Sync.PostedDataTypeCommitRequest", | |
| DataTypeHistogramValue(request_type)); | |
| } | |
| if (cycle->context()->debug_info_getter()) { | |
| *message_.mutable_debug_info() = | |
| cycle->context()->debug_info_getter()->GetDebugInfo(); | |
| } | |
| DVLOG(1) << "Sending commit message."; | |
| SyncerProtoUtil::AddRequiredFieldsToClientToServerMessage(cycle, &message_); | |
| CommitRequestEvent request_event(base::Time::Now(), | |
| message_.commit().entries_size(), | |
| request_types, message_); | |
| cycle->SendProtocolEvent(request_event); | |
| sync_pb::ClientToServerResponse response; | |
| const SyncerError post_result = [&]() { | |
| TRACE_EVENT("sync", "PostCommit"); | |
| return SyncerProtoUtil::PostClientToServerMessage(message_, &response, | |
| cycle, nullptr); | |
| }(); | |
| // TODO(rlarocque): Use result that includes errors captured later? | |
| CommitResponseEvent response_event(base::Time::Now(), post_result, response); | |
| cycle->SendProtocolEvent(response_event); | |
| if (post_result.type() != SyncerError::Type::kSuccess) { | |
| LOG(WARNING) << "Post commit failed"; | |
| ReportFullCommitFailure(post_result); | |
| return post_result; | |
| } | |
| if (!response.has_commit()) { | |
| LOG(WARNING) << "Commit response has no commit body!"; | |
| const SyncerError syncer_error = SyncerError::ProtocolViolationError(); | |
| ReportFullCommitFailure(syncer_error); | |
| return syncer_error; | |
| } | |
| size_t message_entries = message_.commit().entries_size(); | |
| size_t response_entries = response.commit().entryresponse_size(); | |
| if (message_entries != response_entries) { | |
| LOG(ERROR) << "Commit response has wrong number of entries! " | |
| << "Expected: " << message_entries << ", " | |
| << "Got: " << response_entries; | |
| const SyncerError syncer_error = SyncerError::ProtocolViolationError(); | |
| ReportFullCommitFailure(syncer_error); | |
| return syncer_error; | |
| } | |
| base::UmaHistogramCounts100("Sync.CommitRequestEntityCount", | |
| message_.commit().entries_size()); | |
| if (cycle->context()->debug_info_getter()) { | |
| // Clear debug info now that we have successfully sent it to the server. | |
| DVLOG(1) << "Clearing client debug info."; | |
| cycle->context()->debug_info_getter()->ClearDebugInfo(); | |
| } | |
| // Let the contributors process the responses to each of their requests. | |
| SyncerError processing_result = SyncerError::Success(); | |
| for (const auto& [type, contributions] : contributions_) { | |
| std::string_view data_type_str = DataTypeToDebugString(type); | |
| TRACE_EVENT1("sync", "ProcessCommitResponse", "type", data_type_str); | |
| SyncerError type_result = | |
| contributions->ProcessCommitResponse(response, status); | |
| if (type_result.type() == SyncerError::Type::kProtocolError && | |
| type_result.GetProtocolErrorOrDie() == | |
| SyncProtocolErrorType::CONFLICT) { | |
| nudge_tracker->RecordCommitConflict(type); | |
| } | |
| if (processing_result.type() == SyncerError::Type::kSuccess && | |
| type_result.type() != SyncerError::Type::kSuccess) { | |
| processing_result = type_result; | |
| } | |
| } | |
| // Handle bookmarks' special extensions activity stats. | |
| if (extensions_activity != nullptr && | |
| cycle->status_controller() | |
| .model_neutral_state() | |
| .num_successful_bookmark_commits == 0) { | |
| extensions_activity->PutRecords(extensions_activity_buffer_); | |
| } | |
| return processing_result; | |
| } | |
| DataTypeSet Commit::GetContributingDataTypes() const { | |
| DataTypeSet contributed_data_types; | |
| for (const auto& [data_type, contribution] : contributions_) { | |
| contributed_data_types.Put(data_type); | |
| } | |
| return contributed_data_types; | |
| } | |
| void Commit::ReportFullCommitFailure(SyncerError syncer_error) { | |
| const SyncCommitError commit_error = GetSyncCommitError(syncer_error); | |
| for (auto& [data_type, contribution] : contributions_) { | |
| contribution->ProcessCommitFailure(commit_error); | |
| } | |
| } | |
| } // namespace syncer |