1 // Copyright (c) 2010 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 "chrome/browser/sync/engine/process_commit_response_command.h" 6 7 #include <set> 8 #include <string> 9 #include <vector> 10 11 #include "base/basictypes.h" 12 #include "chrome/browser/sync/engine/syncer_proto_util.h" 13 #include "chrome/browser/sync/engine/syncer_util.h" 14 #include "chrome/browser/sync/engine/syncproto.h" 15 #include "chrome/browser/sync/sessions/sync_session.h" 16 #include "chrome/browser/sync/syncable/directory_manager.h" 17 #include "chrome/browser/sync/syncable/syncable.h" 18 19 using syncable::ScopedDirLookup; 20 using syncable::WriteTransaction; 21 using syncable::MutableEntry; 22 using syncable::Entry; 23 24 using std::set; 25 using std::string; 26 using std::vector; 27 28 using syncable::BASE_VERSION; 29 using syncable::GET_BY_ID; 30 using syncable::ID; 31 using syncable::IS_DEL; 32 using syncable::IS_DIR; 33 using syncable::IS_UNAPPLIED_UPDATE; 34 using syncable::IS_UNSYNCED; 35 using syncable::PARENT_ID; 36 using syncable::SERVER_IS_DEL; 37 using syncable::SERVER_PARENT_ID; 38 using syncable::SERVER_POSITION_IN_PARENT; 39 using syncable::SERVER_VERSION; 40 using syncable::SYNCER; 41 using syncable::SYNCING; 42 43 namespace browser_sync { 44 45 using sessions::OrderedCommitSet; 46 using sessions::StatusController; 47 using sessions::SyncSession; 48 using sessions::ConflictProgress; 49 50 void IncrementErrorCounters(StatusController* status) { 51 status->increment_num_consecutive_errors(); 52 } 53 void ResetErrorCounters(StatusController* status) { 54 status->set_num_consecutive_errors(0); 55 } 56 57 ProcessCommitResponseCommand::ProcessCommitResponseCommand() {} 58 ProcessCommitResponseCommand::~ProcessCommitResponseCommand() {} 59 60 bool ProcessCommitResponseCommand::ModelNeutralExecuteImpl( 61 sessions::SyncSession* session) { 62 ScopedDirLookup dir(session->context()->directory_manager(), 63 session->context()->account_name()); 64 if (!dir.good()) { 65 LOG(ERROR) << "Scoped dir lookup failed!"; 66 return false; 67 } 68 69 StatusController* status = session->status_controller(); 70 const ClientToServerResponse& response(status->commit_response()); 71 const vector<syncable::Id>& commit_ids(status->commit_ids()); 72 73 if (!response.has_commit()) { 74 // TODO(sync): What if we didn't try to commit anything? 75 LOG(WARNING) << "Commit response has no commit body!"; 76 IncrementErrorCounters(status); 77 return false; 78 } 79 80 const CommitResponse& cr = response.commit(); 81 int commit_count = commit_ids.size(); 82 if (cr.entryresponse_size() != commit_count) { 83 LOG(ERROR) << "Commit response has wrong number of entries! Expected:" << 84 commit_count << " Got:" << cr.entryresponse_size(); 85 for (int i = 0 ; i < cr.entryresponse_size() ; i++) { 86 LOG(ERROR) << "Response #" << i << " Value: " << 87 cr.entryresponse(i).response_type(); 88 if (cr.entryresponse(i).has_error_message()) 89 LOG(ERROR) << " " << cr.entryresponse(i).error_message(); 90 } 91 IncrementErrorCounters(status); 92 return false; 93 } 94 return true; 95 } 96 97 void ProcessCommitResponseCommand::ModelChangingExecuteImpl( 98 SyncSession* session) { 99 ProcessCommitResponse(session); 100 ExtensionsActivityMonitor* monitor = session->context()->extensions_monitor(); 101 if (session->status_controller()->HasBookmarkCommitActivity() && 102 session->status_controller()->syncer_status() 103 .num_successful_bookmark_commits == 0) { 104 monitor->PutRecords(session->extensions_activity()); 105 session->mutable_extensions_activity()->clear(); 106 } 107 } 108 109 void ProcessCommitResponseCommand::ProcessCommitResponse( 110 SyncSession* session) { 111 // TODO(sync): This function returns if it sees problems. We probably want 112 // to flag the need for an update or similar. 113 ScopedDirLookup dir(session->context()->directory_manager(), 114 session->context()->account_name()); 115 if (!dir.good()) { 116 LOG(ERROR) << "Scoped dir lookup failed!"; 117 return; 118 } 119 120 StatusController* status = session->status_controller(); 121 const ClientToServerResponse& response(status->commit_response()); 122 const CommitResponse& cr = response.commit(); 123 const sync_pb::CommitMessage& commit_message = 124 status->commit_message().commit(); 125 126 // If we try to commit a parent and child together and the parent conflicts 127 // the child will have a bad parent causing an error. As this is not a 128 // critical error, we trap it and don't LOG(ERROR). To enable this we keep 129 // a map of conflicting new folders. 130 int transient_error_commits = 0; 131 int conflicting_commits = 0; 132 int error_commits = 0; 133 int successes = 0; 134 set<syncable::Id> conflicting_new_folder_ids; 135 set<syncable::Id> deleted_folders; 136 ConflictProgress* conflict_progress = status->mutable_conflict_progress(); 137 OrderedCommitSet::Projection proj = status->commit_id_projection(); 138 if (!proj.empty()) { // Scope for WriteTransaction. 139 WriteTransaction trans(dir, SYNCER, __FILE__, __LINE__); 140 for (size_t i = 0; i < proj.size(); i++) { 141 CommitResponse::ResponseType response_type = 142 ProcessSingleCommitResponse(&trans, cr.entryresponse(proj[i]), 143 commit_message.entries(proj[i]), 144 status->GetCommitIdAt(proj[i]), 145 &conflicting_new_folder_ids, 146 &deleted_folders); 147 switch (response_type) { 148 case CommitResponse::INVALID_MESSAGE: 149 ++error_commits; 150 break; 151 case CommitResponse::CONFLICT: 152 ++conflicting_commits; 153 // Only server CONFLICT responses will activate conflict resolution. 154 conflict_progress->AddConflictingItemById( 155 status->GetCommitIdAt(proj[i])); 156 break; 157 case CommitResponse::SUCCESS: 158 // TODO(sync): worry about sync_rate_ rate calc? 159 ++successes; 160 if (status->GetCommitIdModelTypeAt(proj[i]) == syncable::BOOKMARKS) 161 status->increment_num_successful_bookmark_commits(); 162 status->increment_num_successful_commits(); 163 break; 164 case CommitResponse::OVER_QUOTA: 165 // We handle over quota like a retry, which is same as transient. 166 case CommitResponse::RETRY: 167 case CommitResponse::TRANSIENT_ERROR: 168 // TODO(tim): Now that we have SyncSession::Delegate, we 169 // should plumb this directly for exponential backoff purposes rather 170 // than trying to infer from HasMoreToSync(). See 171 // SyncerThread::CalculatePollingWaitTime. 172 ++transient_error_commits; 173 break; 174 default: 175 LOG(FATAL) << "Bad return from ProcessSingleCommitResponse"; 176 } 177 } 178 } 179 180 // TODO(sync): move status reporting elsewhere. 181 status->increment_num_conflicting_commits_by(conflicting_commits); 182 if (0 == successes) { 183 status->increment_num_consecutive_transient_error_commits_by( 184 transient_error_commits); 185 status->increment_num_consecutive_errors_by(transient_error_commits); 186 } else { 187 status->set_num_consecutive_transient_error_commits(0); 188 status->set_num_consecutive_errors(0); 189 } 190 int commit_count = static_cast<int>(proj.size()); 191 if (commit_count != (conflicting_commits + error_commits + 192 transient_error_commits)) { 193 ResetErrorCounters(status); 194 } 195 SyncerUtil::MarkDeletedChildrenSynced(dir, &deleted_folders); 196 197 return; 198 } 199 200 void LogServerError(const CommitResponse_EntryResponse& res) { 201 if (res.has_error_message()) 202 LOG(WARNING) << " " << res.error_message(); 203 else 204 LOG(WARNING) << " No detailed error message returned from server"; 205 } 206 207 CommitResponse::ResponseType 208 ProcessCommitResponseCommand::ProcessSingleCommitResponse( 209 syncable::WriteTransaction* trans, 210 const sync_pb::CommitResponse_EntryResponse& pb_server_entry, 211 const sync_pb::SyncEntity& commit_request_entry, 212 const syncable::Id& pre_commit_id, 213 std::set<syncable::Id>* conflicting_new_folder_ids, 214 set<syncable::Id>* deleted_folders) { 215 216 const CommitResponse_EntryResponse& server_entry = 217 *static_cast<const CommitResponse_EntryResponse*>(&pb_server_entry); 218 MutableEntry local_entry(trans, GET_BY_ID, pre_commit_id); 219 CHECK(local_entry.good()); 220 bool syncing_was_set = local_entry.Get(SYNCING); 221 local_entry.Put(SYNCING, false); 222 223 CommitResponse::ResponseType response = (CommitResponse::ResponseType) 224 server_entry.response_type(); 225 if (!CommitResponse::ResponseType_IsValid(response)) { 226 LOG(ERROR) << "Commit response has unknown response type! Possibly out " 227 "of date client?"; 228 return CommitResponse::INVALID_MESSAGE; 229 } 230 if (CommitResponse::TRANSIENT_ERROR == response) { 231 VLOG(1) << "Transient Error Committing: " << local_entry; 232 LogServerError(server_entry); 233 return CommitResponse::TRANSIENT_ERROR; 234 } 235 if (CommitResponse::INVALID_MESSAGE == response) { 236 LOG(ERROR) << "Error Commiting: " << local_entry; 237 LogServerError(server_entry); 238 return response; 239 } 240 if (CommitResponse::CONFLICT == response) { 241 VLOG(1) << "Conflict Committing: " << local_entry; 242 // TODO(nick): conflicting_new_folder_ids is a purposeless anachronism. 243 if (!pre_commit_id.ServerKnows() && local_entry.Get(IS_DIR)) { 244 conflicting_new_folder_ids->insert(pre_commit_id); 245 } 246 return response; 247 } 248 if (CommitResponse::RETRY == response) { 249 VLOG(1) << "Retry Committing: " << local_entry; 250 return response; 251 } 252 if (CommitResponse::OVER_QUOTA == response) { 253 LOG(WARNING) << "Hit deprecated OVER_QUOTA Committing: " << local_entry; 254 return response; 255 } 256 if (!server_entry.has_id_string()) { 257 LOG(ERROR) << "Commit response has no id"; 258 return CommitResponse::INVALID_MESSAGE; 259 } 260 261 // Implied by the IsValid call above, but here for clarity. 262 DCHECK_EQ(CommitResponse::SUCCESS, response) << response; 263 // Check to see if we've been given the ID of an existing entry. If so treat 264 // it as an error response and retry later. 265 if (pre_commit_id != server_entry.id()) { 266 Entry e(trans, GET_BY_ID, server_entry.id()); 267 if (e.good()) { 268 LOG(ERROR) << "Got duplicate id when commiting id: " << pre_commit_id << 269 ". Treating as an error return"; 270 return CommitResponse::INVALID_MESSAGE; 271 } 272 } 273 274 if (server_entry.version() == 0) { 275 LOG(WARNING) << "Server returned a zero version on a commit response."; 276 } 277 278 ProcessSuccessfulCommitResponse(commit_request_entry, server_entry, 279 pre_commit_id, &local_entry, syncing_was_set, deleted_folders); 280 return response; 281 } 282 283 const string& ProcessCommitResponseCommand::GetResultingPostCommitName( 284 const sync_pb::SyncEntity& committed_entry, 285 const CommitResponse_EntryResponse& entry_response) { 286 const string& response_name = 287 SyncerProtoUtil::NameFromCommitEntryResponse(entry_response); 288 if (!response_name.empty()) 289 return response_name; 290 return SyncerProtoUtil::NameFromSyncEntity(committed_entry); 291 } 292 293 bool ProcessCommitResponseCommand::UpdateVersionAfterCommit( 294 const sync_pb::SyncEntity& committed_entry, 295 const CommitResponse_EntryResponse& entry_response, 296 const syncable::Id& pre_commit_id, 297 syncable::MutableEntry* local_entry) { 298 int64 old_version = local_entry->Get(BASE_VERSION); 299 int64 new_version = entry_response.version(); 300 bool bad_commit_version = false; 301 if (committed_entry.deleted() && 302 !local_entry->Get(syncable::UNIQUE_CLIENT_TAG).empty()) { 303 // If the item was deleted, and it's undeletable (uses the client tag), 304 // change the version back to zero. We must set the version to zero so 305 // that the server knows to re-create the item if it gets committed 306 // later for undeletion. 307 new_version = 0; 308 } else if (!pre_commit_id.ServerKnows()) { 309 bad_commit_version = 0 == new_version; 310 } else { 311 bad_commit_version = old_version > new_version; 312 } 313 if (bad_commit_version) { 314 LOG(ERROR) << "Bad version in commit return for " << *local_entry 315 << " new_id:" << entry_response.id() << " new_version:" 316 << entry_response.version(); 317 return false; 318 } 319 320 // Update the base version and server version. The base version must change 321 // here, even if syncing_was_set is false; that's because local changes were 322 // on top of the successfully committed version. 323 local_entry->Put(BASE_VERSION, new_version); 324 VLOG(1) << "Commit is changing base version of " << local_entry->Get(ID) 325 << " to: " << new_version; 326 local_entry->Put(SERVER_VERSION, new_version); 327 return true; 328 } 329 330 bool ProcessCommitResponseCommand::ChangeIdAfterCommit( 331 const CommitResponse_EntryResponse& entry_response, 332 const syncable::Id& pre_commit_id, 333 syncable::MutableEntry* local_entry) { 334 syncable::WriteTransaction* trans = local_entry->write_transaction(); 335 if (entry_response.id() != pre_commit_id) { 336 if (pre_commit_id.ServerKnows()) { 337 // The server can sometimes generate a new ID on commit; for example, 338 // when committing an undeletion. 339 VLOG(1) << " ID changed while committing an old entry. " 340 << pre_commit_id << " became " << entry_response.id() << "."; 341 } 342 MutableEntry same_id(trans, GET_BY_ID, entry_response.id()); 343 // We should trap this before this function. 344 if (same_id.good()) { 345 LOG(ERROR) << "ID clash with id " << entry_response.id() 346 << " during commit " << same_id; 347 return false; 348 } 349 SyncerUtil::ChangeEntryIDAndUpdateChildren( 350 trans, local_entry, entry_response.id()); 351 VLOG(1) << "Changing ID to " << entry_response.id(); 352 } 353 return true; 354 } 355 356 void ProcessCommitResponseCommand::UpdateServerFieldsAfterCommit( 357 const sync_pb::SyncEntity& committed_entry, 358 const CommitResponse_EntryResponse& entry_response, 359 syncable::MutableEntry* local_entry) { 360 361 // We just committed an entry successfully, and now we want to make our view 362 // of the server state consistent with the server state. We must be careful; 363 // |entry_response| and |committed_entry| have some identically named 364 // fields. We only want to consider fields from |committed_entry| when there 365 // is not an overriding field in the |entry_response|. We do not want to 366 // update the server data from the local data in the entry -- it's possible 367 // that the local data changed during the commit, and even if not, the server 368 // has the last word on the values of several properties. 369 370 local_entry->Put(SERVER_IS_DEL, committed_entry.deleted()); 371 if (committed_entry.deleted()) { 372 // Don't clobber any other fields of deleted objects. 373 return; 374 } 375 376 local_entry->Put(syncable::SERVER_IS_DIR, 377 (committed_entry.folder() || 378 committed_entry.bookmarkdata().bookmark_folder())); 379 local_entry->Put(syncable::SERVER_SPECIFICS, 380 committed_entry.specifics()); 381 local_entry->Put(syncable::SERVER_MTIME, 382 committed_entry.mtime()); 383 local_entry->Put(syncable::SERVER_CTIME, 384 committed_entry.ctime()); 385 local_entry->Put(syncable::SERVER_POSITION_IN_PARENT, 386 entry_response.position_in_parent()); 387 // TODO(nick): The server doesn't set entry_response.server_parent_id in 388 // practice; to update SERVER_PARENT_ID appropriately here we'd need to 389 // get the post-commit ID of the parent indicated by 390 // committed_entry.parent_id_string(). That should be inferrable from the 391 // information we have, but it's a bit convoluted to pull it out directly. 392 // Getting this right is important: SERVER_PARENT_ID gets fed back into 393 // old_parent_id during the next commit. 394 local_entry->Put(syncable::SERVER_PARENT_ID, 395 local_entry->Get(syncable::PARENT_ID)); 396 local_entry->Put(syncable::SERVER_NON_UNIQUE_NAME, 397 GetResultingPostCommitName(committed_entry, entry_response)); 398 399 if (local_entry->Get(IS_UNAPPLIED_UPDATE)) { 400 // This shouldn't happen; an unapplied update shouldn't be committed, and 401 // if it were, the commit should have failed. But if it does happen: we've 402 // just overwritten the update info, so clear the flag. 403 local_entry->Put(IS_UNAPPLIED_UPDATE, false); 404 } 405 } 406 407 void ProcessCommitResponseCommand::OverrideClientFieldsAfterCommit( 408 const sync_pb::SyncEntity& committed_entry, 409 const CommitResponse_EntryResponse& entry_response, 410 syncable::MutableEntry* local_entry) { 411 if (committed_entry.deleted()) { 412 // If an entry's been deleted, nothing else matters. 413 DCHECK(local_entry->Get(IS_DEL)); 414 return; 415 } 416 417 // Update the name. 418 const string& server_name = 419 GetResultingPostCommitName(committed_entry, entry_response); 420 const string& old_name = 421 local_entry->Get(syncable::NON_UNIQUE_NAME); 422 423 if (!server_name.empty() && old_name != server_name) { 424 VLOG(1) << "During commit, server changed name: " << old_name 425 << " to new name: " << server_name; 426 local_entry->Put(syncable::NON_UNIQUE_NAME, server_name); 427 } 428 429 // The server has the final say on positioning, so apply the absolute 430 // position that it returns. 431 if (entry_response.has_position_in_parent()) { 432 // The SERVER_ field should already have been written. 433 DCHECK_EQ(entry_response.position_in_parent(), 434 local_entry->Get(SERVER_POSITION_IN_PARENT)); 435 436 // We just committed successfully, so we assume that the position 437 // value we got applies to the PARENT_ID we submitted. 438 syncable::Id new_prev = local_entry->ComputePrevIdFromServerPosition( 439 local_entry->Get(PARENT_ID)); 440 if (!local_entry->PutPredecessor(new_prev)) 441 LOG(WARNING) << "PutPredecessor failed after successful commit"; 442 } 443 } 444 445 void ProcessCommitResponseCommand::ProcessSuccessfulCommitResponse( 446 const sync_pb::SyncEntity& committed_entry, 447 const CommitResponse_EntryResponse& entry_response, 448 const syncable::Id& pre_commit_id, syncable::MutableEntry* local_entry, 449 bool syncing_was_set, set<syncable::Id>* deleted_folders) { 450 DCHECK(local_entry->Get(IS_UNSYNCED)); 451 452 // Update SERVER_VERSION and BASE_VERSION. 453 if (!UpdateVersionAfterCommit(committed_entry, entry_response, pre_commit_id, 454 local_entry)) { 455 LOG(ERROR) << "Bad version in commit return for " << *local_entry 456 << " new_id:" << entry_response.id() << " new_version:" 457 << entry_response.version(); 458 return; 459 } 460 461 // If the server gave us a new ID, apply it. 462 if (!ChangeIdAfterCommit(entry_response, pre_commit_id, local_entry)) { 463 return; 464 } 465 466 // Update our stored copy of the server state. 467 UpdateServerFieldsAfterCommit(committed_entry, entry_response, local_entry); 468 469 // If the item doesn't need to be committed again (an item might need to be 470 // committed again if it changed locally during the commit), we can remove 471 // it from the unsynced list. Also, we should change the locally- 472 // visible properties to apply any canonicalizations or fixups 473 // that the server introduced during the commit. 474 if (syncing_was_set) { 475 OverrideClientFieldsAfterCommit(committed_entry, entry_response, 476 local_entry); 477 local_entry->Put(IS_UNSYNCED, false); 478 } 479 480 // Make a note of any deleted folders, whose children would have 481 // been recursively deleted. 482 // TODO(nick): Here, commit_message.deleted() would be more correct than 483 // local_entry->Get(IS_DEL). For example, an item could be renamed, and then 484 // deleted during the commit of the rename. Unit test & fix. 485 if (local_entry->Get(IS_DIR) && local_entry->Get(IS_DEL)) { 486 deleted_folders->insert(local_entry->Get(ID)); 487 } 488 } 489 490 } // namespace browser_sync 491