Home | History | Annotate | Download | only in engine
      1 // Copyright 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "sync/engine/process_commit_response_command.h"
      6 
      7 #include <cstddef>
      8 #include <set>
      9 #include <string>
     10 #include <vector>
     11 
     12 #include "base/basictypes.h"
     13 #include "base/location.h"
     14 #include "sync/engine/syncer_proto_util.h"
     15 #include "sync/engine/syncer_util.h"
     16 #include "sync/internal_api/public/base/unique_position.h"
     17 #include "sync/sessions/sync_session.h"
     18 #include "sync/syncable/entry.h"
     19 #include "sync/syncable/mutable_entry.h"
     20 #include "sync/syncable/syncable_proto_util.h"
     21 #include "sync/syncable/syncable_read_transaction.h"
     22 #include "sync/syncable/syncable_util.h"
     23 #include "sync/syncable/syncable_write_transaction.h"
     24 #include "sync/util/time.h"
     25 
     26 using std::set;
     27 using std::string;
     28 using std::vector;
     29 using sync_pb::CommitResponse;
     30 
     31 namespace syncer {
     32 
     33 using sessions::OrderedCommitSet;
     34 using sessions::StatusController;
     35 using sessions::SyncSession;
     36 using syncable::WriteTransaction;
     37 using syncable::MutableEntry;
     38 using syncable::Entry;
     39 using syncable::BASE_VERSION;
     40 using syncable::GET_BY_ID;
     41 using syncable::ID;
     42 using syncable::IS_DEL;
     43 using syncable::IS_DIR;
     44 using syncable::IS_UNAPPLIED_UPDATE;
     45 using syncable::IS_UNSYNCED;
     46 using syncable::PARENT_ID;
     47 using syncable::SERVER_IS_DEL;
     48 using syncable::SERVER_PARENT_ID;
     49 using syncable::SERVER_VERSION;
     50 using syncable::SYNCER;
     51 using syncable::SYNCING;
     52 
     53 ProcessCommitResponseCommand::ProcessCommitResponseCommand(
     54       const sessions::OrderedCommitSet& commit_set,
     55       const sync_pb::ClientToServerMessage& commit_message,
     56       const sync_pb::ClientToServerResponse& commit_response)
     57   : commit_set_(commit_set),
     58     commit_message_(commit_message),
     59     commit_response_(commit_response) {
     60 }
     61 
     62 ProcessCommitResponseCommand::~ProcessCommitResponseCommand() {}
     63 
     64 std::set<ModelSafeGroup> ProcessCommitResponseCommand::GetGroupsToChange(
     65     const sessions::SyncSession& session) const {
     66   std::set<ModelSafeGroup> groups_with_commits;
     67 
     68   syncable::Directory* dir = session.context()->directory();
     69   syncable::ReadTransaction trans(FROM_HERE, dir);
     70   for (size_t i = 0; i < commit_set_.Size(); ++i) {
     71     groups_with_commits.insert(
     72         GetGroupForModelType(commit_set_.GetModelTypeAt(i),
     73                              session.context()->routing_info()));
     74   }
     75 
     76   return groups_with_commits;
     77 }
     78 
     79 
     80 SyncerError ProcessCommitResponseCommand::ModelChangingExecuteImpl(
     81     SyncSession* session) {
     82   syncable::Directory* dir = session->context()->directory();
     83   StatusController* status = session->mutable_status_controller();
     84   const CommitResponse& cr = commit_response_.commit();
     85   const sync_pb::CommitMessage& commit_message = commit_message_.commit();
     86 
     87   int transient_error_commits = 0;
     88   int conflicting_commits = 0;
     89   int error_commits = 0;
     90   int successes = 0;
     91 
     92   set<syncable::Id> deleted_folders;
     93   OrderedCommitSet::Projection proj = status->commit_id_projection(
     94       commit_set_);
     95 
     96   if (!proj.empty()) { // Scope for WriteTransaction.
     97     WriteTransaction trans(FROM_HERE, SYNCER, dir);
     98     for (size_t i = 0; i < proj.size(); i++) {
     99       CommitResponse::ResponseType response_type = ProcessSingleCommitResponse(
    100           &trans,
    101           cr.entryresponse(proj[i]),
    102           commit_message.entries(proj[i]),
    103           commit_set_.GetCommitIdAt(proj[i]),
    104           &deleted_folders);
    105       switch (response_type) {
    106         case CommitResponse::INVALID_MESSAGE:
    107           ++error_commits;
    108           break;
    109         case CommitResponse::CONFLICT:
    110           ++conflicting_commits;
    111           status->increment_num_server_conflicts();
    112           break;
    113         case CommitResponse::SUCCESS:
    114           // TODO(sync): worry about sync_rate_ rate calc?
    115           ++successes;
    116           if (commit_set_.GetModelTypeAt(proj[i]) == BOOKMARKS)
    117             status->increment_num_successful_bookmark_commits();
    118           status->increment_num_successful_commits();
    119           break;
    120         case CommitResponse::OVER_QUOTA:
    121           // We handle over quota like a retry, which is same as transient.
    122         case CommitResponse::RETRY:
    123         case CommitResponse::TRANSIENT_ERROR:
    124           ++transient_error_commits;
    125           break;
    126         default:
    127           LOG(FATAL) << "Bad return from ProcessSingleCommitResponse";
    128       }
    129     }
    130   }
    131 
    132   MarkDeletedChildrenSynced(dir, &deleted_folders);
    133 
    134   int commit_count = static_cast<int>(proj.size());
    135   if (commit_count == successes) {
    136     return SYNCER_OK;
    137   } else if (error_commits > 0) {
    138     return SERVER_RETURN_UNKNOWN_ERROR;
    139   } else if (transient_error_commits > 0) {
    140     return SERVER_RETURN_TRANSIENT_ERROR;
    141   } else if (conflicting_commits > 0) {
    142     // This means that the server already has an item with this version, but
    143     // we haven't seen that update yet.
    144     //
    145     // A well-behaved client should respond to this by proceeding to the
    146     // download updates phase, fetching the conflicting items, then attempting
    147     // to resolve the conflict.  That's not what this client does.
    148     //
    149     // We don't currently have any code to support that exceptional control
    150     // flow.  Instead, we abort the current sync cycle and start a new one.  The
    151     // end result is the same.
    152     return SERVER_RETURN_CONFLICT;
    153   } else {
    154     LOG(FATAL) << "Inconsistent counts when processing commit response";
    155     return SYNCER_OK;
    156   }
    157 }
    158 
    159 void LogServerError(const sync_pb::CommitResponse_EntryResponse& res) {
    160   if (res.has_error_message())
    161     LOG(WARNING) << "  " << res.error_message();
    162   else
    163     LOG(WARNING) << "  No detailed error message returned from server";
    164 }
    165 
    166 CommitResponse::ResponseType
    167 ProcessCommitResponseCommand::ProcessSingleCommitResponse(
    168     syncable::WriteTransaction* trans,
    169     const sync_pb::CommitResponse_EntryResponse& server_entry,
    170     const sync_pb::SyncEntity& commit_request_entry,
    171     const syncable::Id& pre_commit_id,
    172     set<syncable::Id>* deleted_folders) {
    173 
    174   MutableEntry local_entry(trans, GET_BY_ID, pre_commit_id);
    175   CHECK(local_entry.good());
    176   bool syncing_was_set = local_entry.Get(SYNCING);
    177   local_entry.Put(SYNCING, false);
    178 
    179   CommitResponse::ResponseType response = (CommitResponse::ResponseType)
    180       server_entry.response_type();
    181   if (!CommitResponse::ResponseType_IsValid(response)) {
    182     LOG(ERROR) << "Commit response has unknown response type! Possibly out "
    183                "of date client?";
    184     return CommitResponse::INVALID_MESSAGE;
    185   }
    186   if (CommitResponse::TRANSIENT_ERROR == response) {
    187     DVLOG(1) << "Transient Error Committing: " << local_entry;
    188     LogServerError(server_entry);
    189     return CommitResponse::TRANSIENT_ERROR;
    190   }
    191   if (CommitResponse::INVALID_MESSAGE == response) {
    192     LOG(ERROR) << "Error Commiting: " << local_entry;
    193     LogServerError(server_entry);
    194     return response;
    195   }
    196   if (CommitResponse::CONFLICT == response) {
    197     DVLOG(1) << "Conflict Committing: " << local_entry;
    198     return response;
    199   }
    200   if (CommitResponse::RETRY == response) {
    201     DVLOG(1) << "Retry Committing: " << local_entry;
    202     return response;
    203   }
    204   if (CommitResponse::OVER_QUOTA == response) {
    205     LOG(WARNING) << "Hit deprecated OVER_QUOTA Committing: " << local_entry;
    206     return response;
    207   }
    208   if (!server_entry.has_id_string()) {
    209     LOG(ERROR) << "Commit response has no id";
    210     return CommitResponse::INVALID_MESSAGE;
    211   }
    212 
    213   // Implied by the IsValid call above, but here for clarity.
    214   DCHECK_EQ(CommitResponse::SUCCESS, response) << response;
    215   // Check to see if we've been given the ID of an existing entry. If so treat
    216   // it as an error response and retry later.
    217   const syncable::Id& server_entry_id =
    218       SyncableIdFromProto(server_entry.id_string());
    219   if (pre_commit_id != server_entry_id) {
    220     Entry e(trans, GET_BY_ID, server_entry_id);
    221     if (e.good()) {
    222       LOG(ERROR) << "Got duplicate id when commiting id: " << pre_commit_id <<
    223                  ". Treating as an error return";
    224       return CommitResponse::INVALID_MESSAGE;
    225     }
    226   }
    227 
    228   if (server_entry.version() == 0) {
    229     LOG(WARNING) << "Server returned a zero version on a commit response.";
    230   }
    231 
    232   ProcessSuccessfulCommitResponse(commit_request_entry, server_entry,
    233       pre_commit_id, &local_entry, syncing_was_set, deleted_folders);
    234   return response;
    235 }
    236 
    237 const string& ProcessCommitResponseCommand::GetResultingPostCommitName(
    238     const sync_pb::SyncEntity& committed_entry,
    239     const sync_pb::CommitResponse_EntryResponse& entry_response) {
    240   const string& response_name =
    241       SyncerProtoUtil::NameFromCommitEntryResponse(entry_response);
    242   if (!response_name.empty())
    243     return response_name;
    244   return SyncerProtoUtil::NameFromSyncEntity(committed_entry);
    245 }
    246 
    247 bool ProcessCommitResponseCommand::UpdateVersionAfterCommit(
    248     const sync_pb::SyncEntity& committed_entry,
    249     const sync_pb::CommitResponse_EntryResponse& entry_response,
    250     const syncable::Id& pre_commit_id,
    251     syncable::MutableEntry* local_entry) {
    252   int64 old_version = local_entry->Get(BASE_VERSION);
    253   int64 new_version = entry_response.version();
    254   bool bad_commit_version = false;
    255   if (committed_entry.deleted() &&
    256       !local_entry->Get(syncable::UNIQUE_CLIENT_TAG).empty()) {
    257     // If the item was deleted, and it's undeletable (uses the client tag),
    258     // change the version back to zero.  We must set the version to zero so
    259     // that the server knows to re-create the item if it gets committed
    260     // later for undeletion.
    261     new_version = 0;
    262   } else if (!pre_commit_id.ServerKnows()) {
    263     bad_commit_version = 0 == new_version;
    264   } else {
    265     bad_commit_version = old_version > new_version;
    266   }
    267   if (bad_commit_version) {
    268     LOG(ERROR) << "Bad version in commit return for " << *local_entry
    269                << " new_id:" << SyncableIdFromProto(entry_response.id_string())
    270                << " new_version:" << entry_response.version();
    271     return false;
    272   }
    273 
    274   // Update the base version and server version.  The base version must change
    275   // here, even if syncing_was_set is false; that's because local changes were
    276   // on top of the successfully committed version.
    277   local_entry->Put(BASE_VERSION, new_version);
    278   DVLOG(1) << "Commit is changing base version of " << local_entry->Get(ID)
    279            << " to: " << new_version;
    280   local_entry->Put(SERVER_VERSION, new_version);
    281   return true;
    282 }
    283 
    284 bool ProcessCommitResponseCommand::ChangeIdAfterCommit(
    285     const sync_pb::CommitResponse_EntryResponse& entry_response,
    286     const syncable::Id& pre_commit_id,
    287     syncable::MutableEntry* local_entry) {
    288   syncable::WriteTransaction* trans = local_entry->write_transaction();
    289   const syncable::Id& entry_response_id =
    290       SyncableIdFromProto(entry_response.id_string());
    291   if (entry_response_id != pre_commit_id) {
    292     if (pre_commit_id.ServerKnows()) {
    293       // The server can sometimes generate a new ID on commit; for example,
    294       // when committing an undeletion.
    295       DVLOG(1) << " ID changed while committing an old entry. "
    296                << pre_commit_id << " became " << entry_response_id << ".";
    297     }
    298     MutableEntry same_id(trans, GET_BY_ID, entry_response_id);
    299     // We should trap this before this function.
    300     if (same_id.good()) {
    301       LOG(ERROR) << "ID clash with id " << entry_response_id
    302                  << " during commit " << same_id;
    303       return false;
    304     }
    305     ChangeEntryIDAndUpdateChildren(trans, local_entry, entry_response_id);
    306     DVLOG(1) << "Changing ID to " << entry_response_id;
    307   }
    308   return true;
    309 }
    310 
    311 void ProcessCommitResponseCommand::UpdateServerFieldsAfterCommit(
    312     const sync_pb::SyncEntity& committed_entry,
    313     const sync_pb::CommitResponse_EntryResponse& entry_response,
    314     syncable::MutableEntry* local_entry) {
    315 
    316   // We just committed an entry successfully, and now we want to make our view
    317   // of the server state consistent with the server state. We must be careful;
    318   // |entry_response| and |committed_entry| have some identically named
    319   // fields.  We only want to consider fields from |committed_entry| when there
    320   // is not an overriding field in the |entry_response|.  We do not want to
    321   // update the server data from the local data in the entry -- it's possible
    322   // that the local data changed during the commit, and even if not, the server
    323   // has the last word on the values of several properties.
    324 
    325   local_entry->Put(SERVER_IS_DEL, committed_entry.deleted());
    326   if (committed_entry.deleted()) {
    327     // Don't clobber any other fields of deleted objects.
    328     return;
    329   }
    330 
    331   local_entry->Put(syncable::SERVER_IS_DIR,
    332       (committed_entry.folder() ||
    333        committed_entry.bookmarkdata().bookmark_folder()));
    334   local_entry->Put(syncable::SERVER_SPECIFICS,
    335       committed_entry.specifics());
    336   local_entry->Put(syncable::SERVER_MTIME,
    337                    ProtoTimeToTime(committed_entry.mtime()));
    338   local_entry->Put(syncable::SERVER_CTIME,
    339                    ProtoTimeToTime(committed_entry.ctime()));
    340   if (committed_entry.has_unique_position()) {
    341     local_entry->Put(syncable::SERVER_UNIQUE_POSITION,
    342                      UniquePosition::FromProto(
    343                          committed_entry.unique_position()));
    344   }
    345 
    346   // TODO(nick): The server doesn't set entry_response.server_parent_id in
    347   // practice; to update SERVER_PARENT_ID appropriately here we'd need to
    348   // get the post-commit ID of the parent indicated by
    349   // committed_entry.parent_id_string(). That should be inferrable from the
    350   // information we have, but it's a bit convoluted to pull it out directly.
    351   // Getting this right is important: SERVER_PARENT_ID gets fed back into
    352   // old_parent_id during the next commit.
    353   local_entry->Put(syncable::SERVER_PARENT_ID,
    354       local_entry->Get(syncable::PARENT_ID));
    355   local_entry->Put(syncable::SERVER_NON_UNIQUE_NAME,
    356       GetResultingPostCommitName(committed_entry, entry_response));
    357 
    358   if (local_entry->Get(IS_UNAPPLIED_UPDATE)) {
    359     // This shouldn't happen; an unapplied update shouldn't be committed, and
    360     // if it were, the commit should have failed.  But if it does happen: we've
    361     // just overwritten the update info, so clear the flag.
    362     local_entry->Put(IS_UNAPPLIED_UPDATE, false);
    363   }
    364 }
    365 
    366 void ProcessCommitResponseCommand::OverrideClientFieldsAfterCommit(
    367     const sync_pb::SyncEntity& committed_entry,
    368     const sync_pb::CommitResponse_EntryResponse& entry_response,
    369     syncable::MutableEntry* local_entry) {
    370   if (committed_entry.deleted()) {
    371     // If an entry's been deleted, nothing else matters.
    372     DCHECK(local_entry->Get(IS_DEL));
    373     return;
    374   }
    375 
    376   // Update the name.
    377   const string& server_name =
    378       GetResultingPostCommitName(committed_entry, entry_response);
    379   const string& old_name =
    380       local_entry->Get(syncable::NON_UNIQUE_NAME);
    381 
    382   if (!server_name.empty() && old_name != server_name) {
    383     DVLOG(1) << "During commit, server changed name: " << old_name
    384              << " to new name: " << server_name;
    385     local_entry->Put(syncable::NON_UNIQUE_NAME, server_name);
    386   }
    387 }
    388 
    389 void ProcessCommitResponseCommand::ProcessSuccessfulCommitResponse(
    390     const sync_pb::SyncEntity& committed_entry,
    391     const sync_pb::CommitResponse_EntryResponse& entry_response,
    392     const syncable::Id& pre_commit_id, syncable::MutableEntry* local_entry,
    393     bool syncing_was_set, set<syncable::Id>* deleted_folders) {
    394   DCHECK(local_entry->Get(IS_UNSYNCED));
    395 
    396   // Update SERVER_VERSION and BASE_VERSION.
    397   if (!UpdateVersionAfterCommit(committed_entry, entry_response, pre_commit_id,
    398                                 local_entry)) {
    399     LOG(ERROR) << "Bad version in commit return for " << *local_entry
    400                << " new_id:" << SyncableIdFromProto(entry_response.id_string())
    401                << " new_version:" << entry_response.version();
    402     return;
    403   }
    404 
    405   // If the server gave us a new ID, apply it.
    406   if (!ChangeIdAfterCommit(entry_response, pre_commit_id, local_entry)) {
    407     return;
    408   }
    409 
    410   // Update our stored copy of the server state.
    411   UpdateServerFieldsAfterCommit(committed_entry, entry_response, local_entry);
    412 
    413   // If the item doesn't need to be committed again (an item might need to be
    414   // committed again if it changed locally during the commit), we can remove
    415   // it from the unsynced list.  Also, we should change the locally-
    416   // visible properties to apply any canonicalizations or fixups
    417   // that the server introduced during the commit.
    418   if (syncing_was_set) {
    419     OverrideClientFieldsAfterCommit(committed_entry, entry_response,
    420                                     local_entry);
    421     local_entry->Put(IS_UNSYNCED, false);
    422   }
    423 
    424   // Make a note of any deleted folders, whose children would have
    425   // been recursively deleted.
    426   // TODO(nick): Here, commit_message.deleted() would be more correct than
    427   // local_entry->Get(IS_DEL).  For example, an item could be renamed, and then
    428   // deleted during the commit of the rename.  Unit test & fix.
    429   if (local_entry->Get(IS_DIR) && local_entry->Get(IS_DEL)) {
    430     deleted_folders->insert(local_entry->Get(ID));
    431   }
    432 }
    433 
    434 }  // namespace syncer
    435