OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2006-2009 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 <vector> |
| 9 |
| 10 #include "base/basictypes.h" |
| 11 #include "chrome/browser/sync/engine/syncer_util.h" |
| 12 #include "chrome/browser/sync/engine/syncer_proto_util.h" |
| 13 #include "chrome/browser/sync/engine/syncer_session.h" |
| 14 #include "chrome/browser/sync/engine/syncer_status.h" |
| 15 #include "chrome/browser/sync/engine/syncproto.h" |
| 16 #include "chrome/browser/sync/syncable/directory_manager.h" |
| 17 #include "chrome/browser/sync/syncable/syncable.h" |
| 18 #include "chrome/browser/sync/util/character_set_converters.h" |
| 19 |
| 20 using syncable::ScopedDirLookup; |
| 21 using syncable::WriteTransaction; |
| 22 using syncable::MutableEntry; |
| 23 using syncable::Entry; |
| 24 using syncable::Name; |
| 25 using syncable::SyncName; |
| 26 using syncable::DBName; |
| 27 |
| 28 using std::set; |
| 29 using std::vector; |
| 30 |
| 31 using syncable::BASE_VERSION; |
| 32 using syncable::GET_BY_ID; |
| 33 using syncable::ID; |
| 34 using syncable::IS_DEL; |
| 35 using syncable::IS_DIR; |
| 36 using syncable::IS_UNAPPLIED_UPDATE; |
| 37 using syncable::IS_UNSYNCED; |
| 38 using syncable::PARENT_ID; |
| 39 using syncable::SERVER_PARENT_ID; |
| 40 using syncable::SERVER_POSITION_IN_PARENT; |
| 41 using syncable::SYNCER; |
| 42 using syncable::SYNCING; |
| 43 |
| 44 namespace browser_sync { |
| 45 |
| 46 void IncrementErrorCounters(SyncerStatus status) { |
| 47 status.increment_consecutive_problem_commits(); |
| 48 status.increment_consecutive_errors(); |
| 49 } |
| 50 void ResetErrorCounters(SyncerStatus status) { |
| 51 status.zero_consecutive_problem_commits(); |
| 52 status.zero_consecutive_errors(); |
| 53 } |
| 54 |
| 55 ProcessCommitResponseCommand::ProcessCommitResponseCommand() {} |
| 56 ProcessCommitResponseCommand::~ProcessCommitResponseCommand() {} |
| 57 |
| 58 void ProcessCommitResponseCommand::ModelChangingExecuteImpl( |
| 59 SyncerSession *session) { |
| 60 // TODO(sync): This function returns if it sees problems. We probably want |
| 61 // to flag the need for an update or similar. |
| 62 ScopedDirLookup dir(session->dirman(), session->account_name()); |
| 63 if (!dir.good()) { |
| 64 LOG(ERROR) << "Scoped dir lookup failed!"; |
| 65 return; |
| 66 } |
| 67 const ClientToServerResponse& response = session->commit_response(); |
| 68 const vector<syncable::Id>& commit_ids = session->commit_ids(); |
| 69 |
| 70 // TODO(sync): move counters out of here. |
| 71 SyncerStatus status(session); |
| 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; |
| 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; |
| 93 } |
| 94 |
| 95 // If we try to commit a parent and child together and the parent conflicts |
| 96 // the child will have a bad parent causing an error. As this is not a |
| 97 // critical error, we trap it and don't LOG(ERROR). To enable this we keep |
| 98 // a map of conflicting new folders. |
| 99 int transient_error_commits = 0; |
| 100 int conflicting_commits = 0; |
| 101 int error_commits = 0; |
| 102 int successes = 0; |
| 103 bool over_quota = false; |
| 104 set<syncable::Id> conflicting_new_folder_ids; |
| 105 set<syncable::Id> deleted_folders; |
| 106 bool truncated_commit_logged = false; |
| 107 { // Scope for WriteTransaction |
| 108 WriteTransaction trans(dir, SYNCER, __FILE__, __LINE__); |
| 109 for (int i = 0; i < cr.entryresponse_size(); i++) { |
| 110 CommitResponse::RESPONSE_TYPE response_type = |
| 111 ProcessSingleCommitResponse(&trans, cr.entryresponse(i), |
| 112 commit_ids[i], |
| 113 &conflicting_new_folder_ids, |
| 114 &deleted_folders, session); |
| 115 switch (response_type) { |
| 116 case CommitResponse::INVALID_MESSAGE: |
| 117 ++error_commits; |
| 118 break; |
| 119 case CommitResponse::CONFLICT: |
| 120 ++conflicting_commits; |
| 121 session->AddCommitConflict(commit_ids[i]); |
| 122 break; |
| 123 case CommitResponse::SUCCESS: |
| 124 // TODO(sync): worry about sync_rate_ rate calc? |
| 125 ++successes; |
| 126 status.increment_successful_commits(); |
| 127 break; |
| 128 case CommitResponse::OVER_QUOTA: |
| 129 over_quota = true; |
| 130 // We handle over quota like a retry. |
| 131 case CommitResponse::RETRY: |
| 132 session->AddBlockedItem(commit_ids[i]); |
| 133 break; |
| 134 case CommitResponse::TRANSIENT_ERROR: |
| 135 ++transient_error_commits; |
| 136 break; |
| 137 default: |
| 138 LOG(FATAL) << "Bad return from ProcessSingleCommitResponse"; |
| 139 } |
| 140 } |
| 141 } |
| 142 |
| 143 // TODO(sync): move status reporting elsewhere. |
| 144 status.set_conflicting_commits(conflicting_commits); |
| 145 status.set_error_commits(error_commits); |
| 146 if (0 == successes) { |
| 147 status.increment_consecutive_transient_error_commits_by( |
| 148 transient_error_commits); |
| 149 status.increment_consecutive_errors_by(transient_error_commits); |
| 150 } else { |
| 151 status.zero_consecutive_transient_error_commits(); |
| 152 status.zero_consecutive_errors(); |
| 153 } |
| 154 // If all commits are errors count it as an error. |
| 155 if (commit_count == error_commits) { |
| 156 // A larger error step than normal because a POST just succeeded. |
| 157 status.TallyBigNewError(); |
| 158 } |
| 159 if (commit_count != (conflicting_commits + error_commits + |
| 160 transient_error_commits)) { |
| 161 ResetErrorCounters(status); |
| 162 } |
| 163 SyncerUtil::MarkDeletedChildrenSynced(dir, &deleted_folders); |
| 164 session->set_over_quota(over_quota); |
| 165 |
| 166 return; |
| 167 } |
| 168 |
| 169 void LogServerError(const CommitResponse_EntryResponse& res) { |
| 170 if (res.has_error_message()) |
| 171 LOG(ERROR) << " " << res.error_message(); |
| 172 else |
| 173 LOG(ERROR) << " No detailed error message returned from server"; |
| 174 } |
| 175 |
| 176 CommitResponse::RESPONSE_TYPE |
| 177 ProcessCommitResponseCommand::ProcessSingleCommitResponse( |
| 178 syncable::WriteTransaction* trans, |
| 179 const sync_pb::CommitResponse_EntryResponse& pb_server_entry, |
| 180 const syncable::Id& pre_commit_id, |
| 181 std::set<syncable::Id>* conflicting_new_folder_ids, |
| 182 set<syncable::Id>* deleted_folders, |
| 183 SyncerSession* const session) { |
| 184 |
| 185 const CommitResponse_EntryResponse& server_entry = |
| 186 *static_cast<const CommitResponse_EntryResponse*>(&pb_server_entry); |
| 187 MutableEntry local_entry(trans, GET_BY_ID, pre_commit_id); |
| 188 CHECK(local_entry.good()); |
| 189 bool syncing_was_set = local_entry.Get(SYNCING); |
| 190 local_entry.Put(SYNCING, false); |
| 191 |
| 192 CommitResponse::RESPONSE_TYPE response = (CommitResponse::RESPONSE_TYPE) |
| 193 server_entry.response_type(); |
| 194 if (!CommitResponse::RESPONSE_TYPE_IsValid(response)) { |
| 195 LOG(ERROR) << "Commit response has unknown response type! Possibly out " |
| 196 "of date client?"; |
| 197 return CommitResponse::INVALID_MESSAGE; |
| 198 } |
| 199 if (CommitResponse::TRANSIENT_ERROR == response) { |
| 200 LOG(INFO) << "Transient Error Committing: " << local_entry; |
| 201 LogServerError(server_entry); |
| 202 return CommitResponse::TRANSIENT_ERROR; |
| 203 } |
| 204 if (CommitResponse::INVALID_MESSAGE == response) { |
| 205 LOG(ERROR) << "Error Commiting: " << local_entry; |
| 206 LogServerError(server_entry); |
| 207 return response; |
| 208 } |
| 209 if (CommitResponse::CONFLICT == response) { |
| 210 LOG(INFO) << "Conflict Committing: " << local_entry; |
| 211 if (!pre_commit_id.ServerKnows() && local_entry.Get(IS_DIR)) { |
| 212 conflicting_new_folder_ids->insert(pre_commit_id); |
| 213 } |
| 214 return response; |
| 215 } |
| 216 if (CommitResponse::RETRY == response) { |
| 217 LOG(INFO) << "Retry Committing: " << local_entry; |
| 218 return response; |
| 219 } |
| 220 if (CommitResponse::OVER_QUOTA == response) { |
| 221 LOG(INFO) << "Hit Quota Committing: " << local_entry; |
| 222 return response; |
| 223 } |
| 224 if (!server_entry.has_id_string()) { |
| 225 LOG(ERROR) << "Commit response has no id"; |
| 226 return CommitResponse::INVALID_MESSAGE; |
| 227 } |
| 228 |
| 229 // implied by the IsValid call above, but here for clarity. |
| 230 DCHECK_EQ(CommitResponse::SUCCESS, response) << response; |
| 231 // Check to see if we've been given the ID of an existing entry. If so treat |
| 232 // it as an error response and retry later. |
| 233 if (pre_commit_id != server_entry.id()) { |
| 234 Entry e(trans, GET_BY_ID, server_entry.id()); |
| 235 if (e.good()) { |
| 236 LOG(ERROR) << "Got duplicate id when commiting id: " << pre_commit_id << |
| 237 ". Treating as an error return"; |
| 238 return CommitResponse::INVALID_MESSAGE; |
| 239 } |
| 240 } |
| 241 |
| 242 if (server_entry.version() == 0) { |
| 243 LOG(WARNING) << "Server returned a zero version on a commit response."; |
| 244 } |
| 245 |
| 246 ProcessSuccessfulCommitResponse(trans, server_entry, pre_commit_id, |
| 247 &local_entry, syncing_was_set, |
| 248 deleted_folders, session); |
| 249 return response; |
| 250 } |
| 251 |
| 252 void ProcessCommitResponseCommand::ProcessSuccessfulCommitResponse( |
| 253 syncable::WriteTransaction* trans, |
| 254 const CommitResponse_EntryResponse& server_entry, |
| 255 const syncable::Id& pre_commit_id, syncable::MutableEntry* local_entry, |
| 256 bool syncing_was_set, set<syncable::Id>* deleted_folders, |
| 257 SyncerSession* const session) { |
| 258 int64 old_version = local_entry->Get(BASE_VERSION); |
| 259 int64 new_version = server_entry.version(); |
| 260 bool bad_commit_version = false; |
| 261 // TODO(sync): The !server_entry.has_id_string() clauses below were |
| 262 // introduced when working with the new protocol. |
| 263 if (!pre_commit_id.ServerKnows()) |
| 264 bad_commit_version = 0 == new_version; |
| 265 else |
| 266 bad_commit_version = old_version > new_version; |
| 267 if (bad_commit_version) { |
| 268 LOG(ERROR) << "Bad version in commit return for " << *local_entry << |
| 269 " new_id:" << server_entry.id() << " new_version:" << |
| 270 server_entry.version(); |
| 271 return; |
| 272 } |
| 273 if (server_entry.id() != pre_commit_id) { |
| 274 if (pre_commit_id.ServerKnows()) { |
| 275 // TODO(sync): In future it's possible that we'll want the opportunity |
| 276 // to do a server triggered move aside here. |
| 277 LOG(ERROR) << " ID change but not committing a new entry. " << |
| 278 pre_commit_id << " became " << server_entry.id() << "."; |
| 279 return; |
| 280 } |
| 281 if (!server_entry.id().ServerKnows()) { |
| 282 LOG(ERROR) << " New entries id < 0." << pre_commit_id << " became " << |
| 283 server_entry.id() << "."; |
| 284 return; |
| 285 } |
| 286 MutableEntry same_id(trans, GET_BY_ID, server_entry.id()); |
| 287 // We should trap this before this function. |
| 288 CHECK(!same_id.good()) << "ID clash with id " << server_entry.id() << |
| 289 " during commit " << same_id; |
| 290 SyncerUtil::ChangeEntryIDAndUpdateChildren( |
| 291 trans, local_entry, server_entry.id()); |
| 292 LOG(INFO) << "Changing ID to " << server_entry.id(); |
| 293 } |
| 294 |
| 295 local_entry->Put(BASE_VERSION, new_version); |
| 296 LOG(INFO) << "Commit is changing base version of " << |
| 297 local_entry->Get(ID) << " to: " << new_version; |
| 298 |
| 299 if (local_entry->Get(IS_UNAPPLIED_UPDATE)) { |
| 300 // This is possible, but very unlikely. |
| 301 local_entry->Put(IS_UNAPPLIED_UPDATE, false); |
| 302 } |
| 303 |
| 304 if (server_entry.has_name()) { |
| 305 if (syncing_was_set) { |
| 306 PerformCommitTimeNameAside(trans, server_entry, local_entry); |
| 307 } else { |
| 308 // IS_UNSYNCED will ensure that this entry gets committed again, |
| 309 // even if we skip this name aside. IS_UNSYNCED was probably previously |
| 310 // set, but let's just set it anyway. |
| 311 local_entry->Put(IS_UNSYNCED, true); |
| 312 LOG(INFO) << "Skipping commit time name aside because" << |
| 313 " entry was changed during commit."; |
| 314 } |
| 315 } |
| 316 |
| 317 if (syncing_was_set && server_entry.has_position_in_parent()) { |
| 318 // The server has the final say on positioning, so apply the absolute |
| 319 // position that it returns. |
| 320 local_entry->Put(SERVER_POSITION_IN_PARENT, |
| 321 server_entry.position_in_parent()); |
| 322 |
| 323 // We just committed successfully, so we assume that the position |
| 324 // value we got applies to the PARENT_ID we submitted. |
| 325 syncable::Id new_prev = SyncerUtil::ComputePrevIdFromServerPosition( |
| 326 trans, local_entry, local_entry->Get(PARENT_ID)); |
| 327 if (!local_entry->PutPredecessor(new_prev)) { |
| 328 LOG(WARNING) << "PutPredecessor failed after successful commit"; |
| 329 } |
| 330 } |
| 331 |
| 332 if (syncing_was_set) { |
| 333 local_entry->Put(IS_UNSYNCED, false); |
| 334 } |
| 335 if (local_entry->Get(IS_DIR) && local_entry->Get(IS_DEL)) { |
| 336 deleted_folders->insert(local_entry->Get(ID)); |
| 337 } |
| 338 } |
| 339 |
| 340 void ProcessCommitResponseCommand::PerformCommitTimeNameAside( |
| 341 syncable::WriteTransaction* trans, |
| 342 const CommitResponse_EntryResponse& server_entry, |
| 343 syncable::MutableEntry* local_entry) { |
| 344 Name old_name(local_entry->GetName()); |
| 345 |
| 346 // Ensure that we don't collide with an existing entry. |
| 347 SyncName server_name = |
| 348 SyncerProtoUtil::NameFromCommitEntryResponse(server_entry); |
| 349 |
| 350 LOG(INFO) << "Server provided committed name:" << server_name.value(); |
| 351 if (!server_name.value().empty() && |
| 352 static_cast<SyncName&>(old_name) != server_name) { |
| 353 LOG(INFO) << "Server name differs from local name, attempting" |
| 354 << " commit time name aside."; |
| 355 |
| 356 DBName db_name(server_name.value()); |
| 357 db_name.MakeOSLegal(); |
| 358 |
| 359 // This is going to produce ~1 names instead of (Edited) names. |
| 360 // Since this should be EXTREMELY rare, we do this for now. |
| 361 db_name.MakeNoncollidingForEntry(trans, local_entry->Get(SERVER_PARENT_ID), |
| 362 local_entry); |
| 363 |
| 364 CHECK(!db_name.empty()); |
| 365 |
| 366 LOG(INFO) << "Server commit moved aside entry: " << old_name.db_value() |
| 367 << " to new name " << db_name; |
| 368 |
| 369 // Should be safe since we're in a "commit lock." |
| 370 local_entry->PutName(Name::FromDBNameAndSyncName(db_name, server_name)); |
| 371 } |
| 372 } |
| 373 |
| 374 } // namespace browser_sync |
OLD | NEW |