1// Copyright 2013 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/get_commit_ids.h"
6
7#include <set>
8#include <vector>
9
10#include "base/basictypes.h"
11#include "sync/engine/syncer_util.h"
12#include "sync/syncable/directory.h"
13#include "sync/syncable/entry.h"
14#include "sync/syncable/nigori_handler.h"
15#include "sync/syncable/nigori_util.h"
16#include "sync/syncable/syncable_base_transaction.h"
17#include "sync/syncable/syncable_util.h"
18#include "sync/util/cryptographer.h"
19
20using std::set;
21using std::vector;
22
23namespace syncer {
24
25namespace {
26
27// Forward-declare some helper functions.  This gives us more options for
28// ordering the function defintions within this file.
29
30// Filters |unsynced_handles| to remove all entries that do not belong to the
31// specified |requested_types|, or are not eligible for a commit at this time.
32void FilterUnreadyEntries(
33    syncable::BaseTransaction* trans,
34    ModelTypeSet requested_types,
35    ModelTypeSet encrypted_types,
36    bool passphrase_missing,
37    const syncable::Directory::Metahandles& unsynced_handles,
38    std::set<int64>* ready_unsynced_set);
39
40// Given a set of commit metahandles that are ready for commit
41// (|ready_unsynced_set|), sorts these into commit order and places up to
42// |max_entries| of them in the output parameter |out|.
43//
44// See the header file for an explanation of commit ordering.
45void OrderCommitIds(
46    syncable::BaseTransaction* trans,
47    size_t max_entries,
48    const std::set<int64>& ready_unsynced_set,
49    std::vector<int64>* out);
50
51}  // namespace
52
53void GetCommitIdsForType(
54    syncable::BaseTransaction* trans,
55    ModelType type,
56    size_t max_entries,
57    syncable::Directory::Metahandles* out) {
58  syncable::Directory* dir = trans->directory();
59
60  // Gather the full set of unsynced items and store it in the session. They
61  // are not in the correct order for commit.
62  std::set<int64> ready_unsynced_set;
63  syncable::Directory::Metahandles all_unsynced_handles;
64  GetUnsyncedEntries(trans, &all_unsynced_handles);
65
66  ModelTypeSet encrypted_types;
67  bool passphrase_missing = false;
68  Cryptographer* cryptographer = dir->GetCryptographer(trans);
69  if (cryptographer) {
70    encrypted_types = dir->GetNigoriHandler()->GetEncryptedTypes(trans);
71    passphrase_missing = cryptographer->has_pending_keys();
72  };
73
74  // We filter out all unready entries from the set of unsynced handles. This
75  // new set of ready and unsynced items is then what we use to determine what
76  // is a candidate for commit.  The caller is responsible for ensuring that no
77  // throttled types are included among the requested_types.
78  FilterUnreadyEntries(trans,
79                       ModelTypeSet(type),
80                       encrypted_types,
81                       passphrase_missing,
82                       all_unsynced_handles,
83                       &ready_unsynced_set);
84
85  OrderCommitIds(trans, max_entries, ready_unsynced_set, out);
86
87  for (size_t i = 0; i < out->size(); i++) {
88    DVLOG(1) << "Debug commit batch result:" << (*out)[i];
89  }
90}
91
92namespace {
93
94bool IsEntryInConflict(const syncable::Entry& entry) {
95  if (entry.GetIsUnsynced() &&
96      entry.GetServerVersion() > 0 &&
97      (entry.GetServerVersion() > entry.GetBaseVersion())) {
98    // The local and server versions don't match. The item must be in
99    // conflict, so there's no point in attempting to commit.
100    DCHECK(entry.GetIsUnappliedUpdate());
101    DVLOG(1) << "Excluding entry from commit due to version mismatch "
102             << entry;
103    return true;
104  }
105  return false;
106}
107
108// Return true if this entry has any attachments that haven't yet been uploaded
109// to the server.
110bool HasAttachmentNotOnServer(const syncable::Entry& entry) {
111  // TODO(maniscalco): Add test case (bug 356266).
112  const sync_pb::AttachmentMetadata& metadata = entry.GetAttachmentMetadata();
113  for (int i = 0; i < metadata.record_size(); ++i) {
114    if (!metadata.record(i).is_on_server()) {
115      return true;
116    }
117  }
118  return false;
119}
120
121// An entry is not considered ready for commit if any are true:
122// 1. It's in conflict.
123// 2. It requires encryption (either the type is encrypted but a passphrase
124//    is missing from the cryptographer, or the entry itself wasn't properly
125//    encrypted).
126// 3. It's type is currently throttled.
127// 4. It's a delete but has not been committed.
128bool IsEntryReadyForCommit(ModelTypeSet requested_types,
129                           ModelTypeSet encrypted_types,
130                           bool passphrase_missing,
131                           const syncable::Entry& entry) {
132  DCHECK(entry.GetIsUnsynced());
133  if (IsEntryInConflict(entry))
134    return false;
135
136  const ModelType type = entry.GetModelType();
137  // We special case the nigori node because even though it is considered an
138  // "encrypted type", not all nigori node changes require valid encryption
139  // (ex: sync_tabs).
140  if ((type != NIGORI) && encrypted_types.Has(type) &&
141      (passphrase_missing ||
142       syncable::EntryNeedsEncryption(encrypted_types, entry))) {
143    // This entry requires encryption but is not properly encrypted (possibly
144    // due to the cryptographer not being initialized or the user hasn't
145    // provided the most recent passphrase).
146    DVLOG(1) << "Excluding entry from commit due to lack of encryption "
147             << entry;
148    return false;
149  }
150
151  // Ignore it if it's not in our set of requested types.
152  if (!requested_types.Has(type))
153    return false;
154
155  if (entry.GetIsDel() && !entry.GetId().ServerKnows()) {
156    // New clients (following the resolution of crbug.com/125381) should not
157    // create such items.  Old clients may have left some in the database
158    // (crbug.com/132905), but we should now be cleaning them on startup.
159    NOTREACHED() << "Found deleted and unsynced local item: " << entry;
160    return false;
161  }
162
163  // Extra validity checks.
164  syncable::Id id = entry.GetId();
165  if (id == entry.GetParentId()) {
166    CHECK(id.IsRoot()) << "Non-root item is self parenting." << entry;
167    // If the root becomes unsynced it can cause us problems.
168    NOTREACHED() << "Root item became unsynced " << entry;
169    return false;
170  }
171
172  if (entry.IsRoot()) {
173    NOTREACHED() << "Permanent item became unsynced " << entry;
174    return false;
175  }
176
177  if (HasAttachmentNotOnServer(entry)) {
178    // This entry is not ready to be sent to the server because it has one or
179    // more attachments that have not yet been uploaded to the server.  The idea
180    // here is avoid propagating an entry with dangling attachment references.
181    return false;
182  }
183
184  DVLOG(2) << "Entry is ready for commit: " << entry;
185  return true;
186}
187
188// Filters |unsynced_handles| to remove all entries that do not belong to the
189// specified |requested_types|, or are not eligible for a commit at this time.
190void FilterUnreadyEntries(
191    syncable::BaseTransaction* trans,
192    ModelTypeSet requested_types,
193    ModelTypeSet encrypted_types,
194    bool passphrase_missing,
195    const syncable::Directory::Metahandles& unsynced_handles,
196    std::set<int64>* ready_unsynced_set) {
197  for (syncable::Directory::Metahandles::const_iterator iter =
198       unsynced_handles.begin(); iter != unsynced_handles.end(); ++iter) {
199    syncable::Entry entry(trans, syncable::GET_BY_HANDLE, *iter);
200    // TODO(maniscalco): While we check if entry is ready to be committed, we
201    // also need to check that all of its ancestors (parents, transitive) are
202    // ready to be committed.  Once attachments can prevent an entry from being
203    // committable, this method must ensure all ancestors are ready for commit
204    // (bug 356273).
205    if (IsEntryReadyForCommit(requested_types,
206                              encrypted_types,
207                              passphrase_missing,
208                              entry)) {
209      ready_unsynced_set->insert(*iter);
210    }
211  }
212}
213
214// This class helps to implement OrderCommitIds().  Its members track the
215// progress of a traversal while its methods extend it.  It can return early if
216// the traversal reaches the desired size before the full traversal is complete.
217class Traversal {
218 public:
219  Traversal(
220    syncable::BaseTransaction* trans,
221    int64 max_entries,
222    syncable::Directory::Metahandles* out);
223  ~Traversal();
224
225  // First step of traversal building.  Adds non-deleted items in order.
226  void AddCreatesAndMoves(const std::set<int64>& ready_unsynced_set);
227
228  // Second step of traverals building.  Appends deleted items.
229  void AddDeletes(const std::set<int64>& ready_unsynced_set);
230
231 private:
232  // The following functions do not modify the traversal directly.  They return
233  // their results in the |result| vector instead.
234  bool AddUncommittedParentsAndTheirPredecessors(
235      const std::set<int64>& ready_unsynced_set,
236      const syncable::Entry& item,
237      syncable::Directory::Metahandles* result) const;
238
239  void TryAddItem(const std::set<int64>& ready_unsynced_set,
240                  const syncable::Entry& item,
241                  syncable::Directory::Metahandles* result) const;
242
243  void AddItemThenPredecessors(
244      const std::set<int64>& ready_unsynced_set,
245      const syncable::Entry& item,
246      syncable::Directory::Metahandles* result) const;
247
248  void AddPredecessorsThenItem(
249      const std::set<int64>& ready_unsynced_set,
250      const syncable::Entry& item,
251      syncable::Directory::Metahandles* result) const;
252
253  // Returns true if we've collected enough items.
254  bool IsFull() const;
255
256  // Returns true if the specified handle is already in the traversal.
257  bool HaveItem(int64 handle) const;
258
259  // Adds the specified handles to the traversal.
260  void AppendManyToTraversal(const syncable::Directory::Metahandles& handles);
261
262  // Adds the specifed handle to the traversal.
263  void AppendToTraversal(int64 handle);
264
265  syncable::Directory::Metahandles* out_;
266  std::set<int64> added_handles_;
267  const size_t max_entries_;
268  syncable::BaseTransaction* trans_;
269
270  DISALLOW_COPY_AND_ASSIGN(Traversal);
271};
272
273Traversal::Traversal(
274    syncable::BaseTransaction* trans,
275    int64 max_entries,
276    syncable::Directory::Metahandles* out)
277  : out_(out),
278    max_entries_(max_entries),
279    trans_(trans) { }
280
281Traversal::~Traversal() {}
282
283bool Traversal::AddUncommittedParentsAndTheirPredecessors(
284    const std::set<int64>& ready_unsynced_set,
285    const syncable::Entry& item,
286    syncable::Directory::Metahandles* result) const {
287  syncable::Directory::Metahandles dependencies;
288  syncable::Id parent_id = item.GetParentId();
289
290  // Climb the tree adding entries leaf -> root.
291  while (!parent_id.ServerKnows()) {
292    syncable::Entry parent(trans_, syncable::GET_BY_ID, parent_id);
293    CHECK(parent.good()) << "Bad user-only parent in item path.";
294    int64 handle = parent.GetMetahandle();
295    if (HaveItem(handle)) {
296      // We've already added this parent (and therefore all of its parents).
297      // We can return early.
298      break;
299    }
300    if (IsEntryInConflict(parent)) {
301      // We ignore all entries that are children of a conflicing item.  Return
302      // false immediately to forget the traversal we've built up so far.
303      DVLOG(1) << "Parent was in conflict, omitting " << item;
304      return false;
305    }
306    AddItemThenPredecessors(ready_unsynced_set,
307                            parent,
308                            &dependencies);
309    parent_id = parent.GetParentId();
310  }
311
312  // Reverse what we added to get the correct order.
313  result->insert(result->end(), dependencies.rbegin(), dependencies.rend());
314  return true;
315}
316
317// Adds the given item to the list if it is unsynced and ready for commit.
318void Traversal::TryAddItem(const std::set<int64>& ready_unsynced_set,
319                           const syncable::Entry& item,
320                           syncable::Directory::Metahandles* result) const {
321  DCHECK(item.GetIsUnsynced());
322  int64 item_handle = item.GetMetahandle();
323  if (ready_unsynced_set.count(item_handle) != 0) {
324    result->push_back(item_handle);
325  }
326}
327
328// Adds the given item, and all its unsynced predecessors.  The traversal will
329// be cut short if any item along the traversal is not IS_UNSYNCED, or if we
330// detect that this area of the tree has already been traversed.  Items that are
331// not 'ready' for commit (see IsEntryReadyForCommit()) will not be added to the
332// list, though they will not stop the traversal.
333void Traversal::AddItemThenPredecessors(
334    const std::set<int64>& ready_unsynced_set,
335    const syncable::Entry& item,
336    syncable::Directory::Metahandles* result) const {
337  int64 item_handle = item.GetMetahandle();
338  if (HaveItem(item_handle)) {
339    // We've already added this item to the commit set, and so must have
340    // already added the predecessors as well.
341    return;
342  }
343  TryAddItem(ready_unsynced_set, item, result);
344  if (item.GetIsDel())
345    return;  // Deleted items have no predecessors.
346
347  syncable::Id prev_id = item.GetPredecessorId();
348  while (!prev_id.IsRoot()) {
349    syncable::Entry prev(trans_, syncable::GET_BY_ID, prev_id);
350    CHECK(prev.good()) << "Bad id when walking predecessors.";
351    if (!prev.GetIsUnsynced()) {
352      // We're interested in "runs" of unsynced items.  This item breaks
353      // the streak, so we stop traversing.
354      return;
355    }
356    int64 handle = prev.GetMetahandle();
357    if (HaveItem(handle)) {
358      // We've already added this item to the commit set, and so must have
359      // already added the predecessors as well.
360      return;
361    }
362    TryAddItem(ready_unsynced_set, prev, result);
363    prev_id = prev.GetPredecessorId();
364  }
365}
366
367// Same as AddItemThenPredecessor, but the traversal order will be reversed.
368void Traversal::AddPredecessorsThenItem(
369    const std::set<int64>& ready_unsynced_set,
370    const syncable::Entry& item,
371    syncable::Directory::Metahandles* result) const {
372  syncable::Directory::Metahandles dependencies;
373  AddItemThenPredecessors(ready_unsynced_set, item, &dependencies);
374
375  // Reverse what we added to get the correct order.
376  result->insert(result->end(), dependencies.rbegin(), dependencies.rend());
377}
378
379bool Traversal::IsFull() const {
380  return out_->size() >= max_entries_;
381}
382
383bool Traversal::HaveItem(int64 handle) const {
384  return added_handles_.find(handle) != added_handles_.end();
385}
386
387void Traversal::AppendManyToTraversal(
388    const syncable::Directory::Metahandles& handles) {
389  out_->insert(out_->end(), handles.begin(), handles.end());
390  added_handles_.insert(handles.begin(), handles.end());
391}
392
393void Traversal::AppendToTraversal(int64 metahandle) {
394  out_->push_back(metahandle);
395  added_handles_.insert(metahandle);
396}
397
398void Traversal::AddCreatesAndMoves(
399    const std::set<int64>& ready_unsynced_set) {
400  // Add moves and creates, and prepend their uncommitted parents.
401  for (std::set<int64>::const_iterator iter = ready_unsynced_set.begin();
402       !IsFull() && iter != ready_unsynced_set.end(); ++iter) {
403    int64 metahandle = *iter;
404    if (HaveItem(metahandle))
405      continue;
406
407    syncable::Entry entry(trans_,
408                          syncable::GET_BY_HANDLE,
409                          metahandle);
410    if (!entry.GetIsDel()) {
411      // We only commit an item + its dependencies if it and all its
412      // dependencies are not in conflict.
413      syncable::Directory::Metahandles item_dependencies;
414      if (AddUncommittedParentsAndTheirPredecessors(
415              ready_unsynced_set,
416              entry,
417              &item_dependencies)) {
418        AddPredecessorsThenItem(ready_unsynced_set,
419                                entry,
420                                &item_dependencies);
421        AppendManyToTraversal(item_dependencies);
422      }
423    }
424  }
425
426  // It's possible that we overcommitted while trying to expand dependent
427  // items.  If so, truncate the set down to the allowed size.
428  if (out_->size() > max_entries_)
429    out_->resize(max_entries_);
430}
431
432void Traversal::AddDeletes(
433    const std::set<int64>& ready_unsynced_set) {
434  set<syncable::Id> legal_delete_parents;
435
436  for (std::set<int64>::const_iterator iter = ready_unsynced_set.begin();
437       !IsFull() && iter != ready_unsynced_set.end(); ++iter) {
438    int64 metahandle = *iter;
439    if (HaveItem(metahandle))
440      continue;
441
442    syncable::Entry entry(trans_, syncable::GET_BY_HANDLE,
443                          metahandle);
444
445    if (entry.GetIsDel()) {
446      syncable::Entry parent(trans_, syncable::GET_BY_ID,
447                             entry.GetParentId());
448      // If the parent is deleted and unsynced, then any children of that
449      // parent don't need to be added to the delete queue.
450      //
451      // Note: the parent could be synced if there was an update deleting a
452      // folder when we had a deleted all items in it.
453      // We may get more updates, or we may want to delete the entry.
454      if (parent.good() && parent.GetIsDel() && parent.GetIsUnsynced()) {
455        // However, if an entry is moved, these rules can apply differently.
456        //
457        // If the entry was moved, then the destination parent was deleted,
458        // then we'll miss it in the roll up. We have to add it in manually.
459        // TODO(chron): Unit test for move / delete cases:
460        // Case 1: Locally moved, then parent deleted
461        // Case 2: Server moved, then locally issue recursive delete.
462        if (entry.GetId().ServerKnows() &&
463            entry.GetParentId() != entry.GetServerParentId()) {
464          DVLOG(1) << "Inserting moved and deleted entry, will be missed by "
465                   << "delete roll." << entry.GetId();
466
467          AppendToTraversal(metahandle);
468        }
469
470        // Skip this entry since it's a child of a parent that will be
471        // deleted. The server will unroll the delete and delete the
472        // child as well.
473        continue;
474      }
475
476      legal_delete_parents.insert(entry.GetParentId());
477    }
478  }
479
480  // We could store all the potential entries with a particular parent during
481  // the above scan, but instead we rescan here. This is less efficient, but
482  // we're dropping memory alloc/dealloc in favor of linear scans of recently
483  // examined entries.
484  //
485  // Scan through the UnsyncedMetaHandles again. If we have a deleted
486  // entry, then check if the parent is in legal_delete_parents.
487  //
488  // Parent being in legal_delete_parents means for the child:
489  //   a recursive delete is not currently happening (no recent deletes in same
490  //     folder)
491  //   parent did expect at least one old deleted child
492  //   parent was not deleted
493  for (std::set<int64>::const_iterator iter = ready_unsynced_set.begin();
494       !IsFull() && iter != ready_unsynced_set.end(); ++iter) {
495    int64 metahandle = *iter;
496    if (HaveItem(metahandle))
497      continue;
498    syncable::Entry entry(trans_, syncable::GET_BY_HANDLE, metahandle);
499    if (entry.GetIsDel()) {
500      syncable::Id parent_id = entry.GetParentId();
501      if (legal_delete_parents.count(parent_id)) {
502        AppendToTraversal(metahandle);
503      }
504    }
505  }
506}
507
508void OrderCommitIds(
509    syncable::BaseTransaction* trans,
510    size_t max_entries,
511    const std::set<int64>& ready_unsynced_set,
512    syncable::Directory::Metahandles* out) {
513  // Commits follow these rules:
514  // 1. Moves or creates are preceded by needed folder creates, from
515  //    root to leaf.  For folders whose contents are ordered, moves
516  //    and creates appear in order.
517  // 2. Moves/Creates before deletes.
518  // 3. Deletes, collapsed.
519  // We commit deleted moves under deleted items as moves when collapsing
520  // delete trees.
521
522  Traversal traversal(trans, max_entries, out);
523
524  // Add moves and creates, and prepend their uncommitted parents.
525  traversal.AddCreatesAndMoves(ready_unsynced_set);
526
527  // Add all deletes.
528  traversal.AddDeletes(ready_unsynced_set);
529}
530
531}  // namespace
532
533}  // namespace syncer
534