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