// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "sync/engine/get_commit_ids.h" #include #include #include #include #include "base/macros.h" #include "sync/engine/syncer_util.h" #include "sync/syncable/directory.h" #include "sync/syncable/entry.h" #include "sync/syncable/nigori_handler.h" #include "sync/syncable/nigori_util.h" #include "sync/syncable/syncable_base_transaction.h" #include "sync/syncable/syncable_util.h" #include "sync/util/cryptographer.h" using std::set; using std::vector; namespace syncer { namespace { // Forward-declare some helper functions. This gives us more options for // ordering the function defintions within this file. // Filters |unsynced_handles| to remove all entries that do not belong to the // specified |requested_types|, or are not eligible for a commit at this time. void FilterUnreadyEntries( syncable::BaseTransaction* trans, ModelTypeSet requested_types, ModelTypeSet encrypted_types, bool passphrase_missing, const syncable::Directory::Metahandles& unsynced_handles, std::set* ready_unsynced_set); // Given a set of commit metahandles that are ready for commit // (|ready_unsynced_set|), sorts these into commit order and places up to // |max_entries| of them in the output parameter |out|. // // See the header file for an explanation of commit ordering. void OrderCommitIds(syncable::BaseTransaction* trans, size_t max_entries, const std::set& ready_unsynced_set, std::vector* out); } // namespace void GetCommitIdsForType( syncable::BaseTransaction* trans, ModelType type, size_t max_entries, syncable::Directory::Metahandles* out) { syncable::Directory* dir = trans->directory(); // Gather the full set of unsynced items and store it in the session. They // are not in the correct order for commit. std::set ready_unsynced_set; syncable::Directory::Metahandles all_unsynced_handles; GetUnsyncedEntries(trans, &all_unsynced_handles); ModelTypeSet encrypted_types; bool passphrase_missing = false; Cryptographer* cryptographer = dir->GetCryptographer(trans); if (cryptographer) { encrypted_types = dir->GetNigoriHandler()->GetEncryptedTypes(trans); passphrase_missing = cryptographer->has_pending_keys(); } // We filter out all unready entries from the set of unsynced handles. This // new set of ready and unsynced items is then what we use to determine what // is a candidate for commit. The caller is responsible for ensuring that no // throttled types are included among the requested_types. FilterUnreadyEntries(trans, ModelTypeSet(type), encrypted_types, passphrase_missing, all_unsynced_handles, &ready_unsynced_set); OrderCommitIds(trans, max_entries, ready_unsynced_set, out); for (size_t i = 0; i < out->size(); i++) { DVLOG(1) << "Debug commit batch result:" << (*out)[i]; } } namespace { bool IsEntryInConflict(const syncable::Entry& entry) { if (entry.GetIsUnsynced() && entry.GetServerVersion() > 0 && (entry.GetServerVersion() > entry.GetBaseVersion())) { // The local and server versions don't match. The item must be in // conflict, so there's no point in attempting to commit. DCHECK(entry.GetIsUnappliedUpdate()); DVLOG(1) << "Excluding entry from commit due to version mismatch " << entry; return true; } return false; } // Return true if this entry has any attachments that haven't yet been uploaded // to the server. bool HasAttachmentNotOnServer(const syncable::Entry& entry) { const sync_pb::AttachmentMetadata& metadata = entry.GetAttachmentMetadata(); for (int i = 0; i < metadata.record_size(); ++i) { if (!metadata.record(i).is_on_server()) { return true; } } return false; } // An entry may not commit if any are true: // 1. It requires encryption (either the type is encrypted but a passphrase // is missing from the cryptographer, or the entry itself wasn't properly // encrypted). // 2. It's type is currently throttled. // 3. It's a delete but has not been committed. bool MayEntryCommit(ModelTypeSet requested_types, ModelTypeSet encrypted_types, bool passphrase_missing, const syncable::Entry& entry) { DCHECK(entry.GetIsUnsynced()); const ModelType type = entry.GetModelType(); // We special case the nigori node because even though it is considered an // "encrypted type", not all nigori node changes require valid encryption // (ex: sync_tabs). if ((type != NIGORI) && encrypted_types.Has(type) && (passphrase_missing || syncable::EntryNeedsEncryption(encrypted_types, entry))) { // This entry requires encryption but is not properly encrypted (possibly // due to the cryptographer not being initialized or the user hasn't // provided the most recent passphrase). DVLOG(1) << "Excluding entry from commit due to lack of encryption " << entry; return false; } // Ignore it if it's not in our set of requested types. if (!requested_types.Has(type)) return false; if (entry.GetIsDel() && !entry.GetId().ServerKnows()) { // New clients (following the resolution of crbug.com/125381) should not // create such items. Old clients may have left some in the database // (crbug.com/132905), but we should now be cleaning them on startup. NOTREACHED() << "Found deleted and unsynced local item: " << entry; return false; } // Extra validity checks. syncable::Id id = entry.GetId(); if (id == entry.GetParentId()) { CHECK(id.IsRoot()) << "Non-root item is self parenting." << entry; // If the root becomes unsynced it can cause us problems. NOTREACHED() << "Root item became unsynced " << entry; return false; } if (entry.IsRoot()) { NOTREACHED() << "Permanent item became unsynced " << entry; return false; } if (HasAttachmentNotOnServer(entry)) { // This entry is not ready to be sent to the server because it has one or // more attachments that have not yet been uploaded to the server. The idea // here is avoid propagating an entry with dangling attachment references. return false; } DVLOG(2) << "Entry is ready for commit: " << entry; return true; } bool SupportsHierarchy(const syncable::Entry& item) { // Types with explicit server supported hierarchy only. return IsTypeWithServerGeneratedRoot(item.GetModelType()); } // Excludes ancestors of deleted conflicted items from // |ready_unsynced_set|. void ExcludeDeletedAncestors( syncable::BaseTransaction* trans, const std::vector& deleted_conflicted_items, std::set* ready_unsynced_set) { for (auto iter = deleted_conflicted_items.begin(); iter != deleted_conflicted_items.end(); ++iter) { syncable::Entry item(trans, syncable::GET_BY_HANDLE, *iter); syncable::Id parent_id = item.GetParentId(); DCHECK(!parent_id.IsNull()); while (!parent_id.IsRoot()) { syncable::Entry parent(trans, syncable::GET_BY_ID, parent_id); CHECK(parent.good()) << "Bad user-only parent in item path."; int64_t handle = parent.GetMetahandle(); if (!parent.GetIsDel()) break; auto ready_iter = ready_unsynced_set->find(handle); if (ready_iter == ready_unsynced_set->end()) break; // Remove this entry from |ready_unsynced_set|. ready_unsynced_set->erase(ready_iter); parent_id = parent.GetParentId(); } } } // Iterates over children of items from |conflicted_items| list that are in // |ready_unsynced_set|, exludes them from |ready_unsynced_set| and adds them // to |excluded_items| list. void ExcludeChildren(syncable::BaseTransaction* trans, const std::vector& conflicted_items, std::vector* excluded_items, std::set* ready_unsynced_set) { for (auto iter = conflicted_items.begin(); iter != conflicted_items.end(); ++iter) { syncable::Entry entry(trans, syncable::GET_BY_HANDLE, *iter); if (!entry.GetIsDir() || entry.GetIsDel()) continue; std::vector children; entry.GetChildHandles(&children); for (std::vector::const_iterator child_iter = children.begin(); child_iter != children.end(); ++child_iter) { // Collect all child handles that are in |ready_unsynced_set|. int64_t child_handle = *child_iter; auto ready_iter = ready_unsynced_set->find(child_handle); if (ready_iter != ready_unsynced_set->end()) { // Remove this entry from |ready_unsynced_set| and add it // to |excluded_items|. ready_unsynced_set->erase(ready_iter); excluded_items->push_back(child_handle); } } } } // Filters |unsynced_handles| to remove all entries that do not belong to the // specified |requested_types|, or are not eligible for a commit at this time. void FilterUnreadyEntries( syncable::BaseTransaction* trans, ModelTypeSet requested_types, ModelTypeSet encrypted_types, bool passphrase_missing, const syncable::Directory::Metahandles& unsynced_handles, std::set* ready_unsynced_set) { std::vector deleted_conflicted_items; std::vector conflicted_items; // Go over all unsynced handles, filter the ones that might be committed based // on type / encryption, then based on whether they are in conflict add them // to either |ready_unsynced_set| or one of the conflicted lists. for (auto iter = unsynced_handles.begin(); iter != unsynced_handles.end(); ++iter) { syncable::Entry entry(trans, syncable::GET_BY_HANDLE, *iter); // TODO(maniscalco): While we check if entry is ready to be committed, we // also need to check that all of its ancestors (parents, transitive) are // ready to be committed. Once attachments can prevent an entry from being // committable, this method must ensure all ancestors are ready for commit // (bug 356273). if (MayEntryCommit(requested_types, encrypted_types, passphrase_missing, entry)) { if (IsEntryInConflict(entry)) { // Conflicting hierarchical entries might prevent their ancestors or // descendants from being committed. if (SupportsHierarchy(entry)) { if (entry.GetIsDel()) { deleted_conflicted_items.push_back(*iter); } else if (entry.GetIsDir()) { // Populate the initial version of |conflicted_items| with folder // items that are in conflict. conflicted_items.push_back(*iter); } } } else { ready_unsynced_set->insert(*iter); } } } // If there are any deleted conflicted entries, remove their deleted ancestors // from |ready_unsynced_set| as well. ExcludeDeletedAncestors(trans, deleted_conflicted_items, ready_unsynced_set); // Starting with conflicted_items containing conflicted folders go down and // exclude all descendants from |ready_unsynced_set|. while (!conflicted_items.empty()) { std::vector new_list; ExcludeChildren(trans, conflicted_items, &new_list, ready_unsynced_set); conflicted_items.swap(new_list); } } // This class helps to implement OrderCommitIds(). Its members track the // progress of a traversal while its methods extend it. It can return early if // the traversal reaches the desired size before the full traversal is complete. class Traversal { public: Traversal(syncable::BaseTransaction* trans, int64_t max_entries, syncable::Directory::Metahandles* out); ~Traversal(); // First step of traversal building. Adds non-deleted items in order. void AddCreatesAndMoves(const std::set& ready_unsynced_set); // Second step of traverals building. Appends deleted items. void AddDeletes(const std::set& ready_unsynced_set); private: // The following functions do not modify the traversal directly. They return // their results in the |result| vector instead. void AddUncommittedParents(const std::set& ready_unsynced_set, const syncable::Entry& item, syncable::Directory::Metahandles* result) const; bool TryAddItem(const std::set& ready_unsynced_set, const syncable::Entry& item, syncable::Directory::Metahandles* result) const; void AddDeletedParents(const std::set& ready_unsynced_set, const syncable::Entry& item, const syncable::Directory::Metahandles& traversed, syncable::Directory::Metahandles* result) const; // Returns true if we've collected enough items. bool IsFull() const; // Returns true if the specified handle is already in the traversal. bool HaveItem(int64_t handle) const; // Adds the specified handles to the traversal. void AppendManyToTraversal(const syncable::Directory::Metahandles& handles); // Adds the specifed handle to the traversal. void AppendToTraversal(int64_t handle); syncable::Directory::Metahandles* out_; std::set added_handles_; const size_t max_entries_; syncable::BaseTransaction* trans_; DISALLOW_COPY_AND_ASSIGN(Traversal); }; Traversal::Traversal(syncable::BaseTransaction* trans, int64_t max_entries, syncable::Directory::Metahandles* out) : out_(out), max_entries_(max_entries), trans_(trans) {} Traversal::~Traversal() {} void Traversal::AddUncommittedParents( const std::set& ready_unsynced_set, const syncable::Entry& item, syncable::Directory::Metahandles* result) const { DCHECK(SupportsHierarchy(item)); syncable::Directory::Metahandles dependencies; syncable::Id parent_id = item.GetParentId(); // Climb the tree adding entries leaf -> root. while (!parent_id.ServerKnows()) { syncable::Entry parent(trans_, syncable::GET_BY_ID, parent_id); CHECK(parent.good()) << "Bad user-only parent in item path."; int64_t handle = parent.GetMetahandle(); if (HaveItem(handle)) { // We've already added this parent (and therefore all of its parents). // We can return early. break; } if (!TryAddItem(ready_unsynced_set, parent, &dependencies)) { // The parent isn't in |ready_unsynced_set|. break; } parent_id = parent.GetParentId(); } // Reverse what we added to get the correct order. result->insert(result->end(), dependencies.rbegin(), dependencies.rend()); } // Adds the given item to the list if it is unsynced and ready for commit. bool Traversal::TryAddItem(const std::set& ready_unsynced_set, const syncable::Entry& item, syncable::Directory::Metahandles* result) const { DCHECK(item.GetIsUnsynced()); int64_t item_handle = item.GetMetahandle(); if (ready_unsynced_set.count(item_handle) != 0) { result->push_back(item_handle); return true; } return false; } // Traverses the tree from bottom to top, adding the deleted parents of the // given |item|. Stops traversing if it encounters a non-deleted node, or // a node that was already listed in the |traversed| list. // // The result list is reversed before it is returned, so the resulting // traversal is in top to bottom order. Also note that this function appends // to the result list without clearing it. void Traversal::AddDeletedParents( const std::set& ready_unsynced_set, const syncable::Entry& item, const syncable::Directory::Metahandles& traversed, syncable::Directory::Metahandles* result) const { DCHECK(SupportsHierarchy(item)); syncable::Directory::Metahandles dependencies; syncable::Id parent_id = item.GetParentId(); // Climb the tree adding entries leaf -> root. while (!parent_id.IsRoot()) { syncable::Entry parent(trans_, syncable::GET_BY_ID, parent_id); if (!parent.good()) { // This is valid because the parent could have gone away a long time ago // // Consider the case where a folder is server-unknown and locally // deleted, and has a child that is server-known, deleted, and unsynced. // The parent could be dropped from memory at any time, but its child // needs to be committed first. break; } int64_t handle = parent.GetMetahandle(); if (!parent.GetIsUnsynced()) { // In some rare cases, our parent can be both deleted and unsynced. // (ie. the server-unknown parent case). break; } if (!parent.GetIsDel()) { // We're not intersted in non-deleted parents. break; } if (std::find(traversed.begin(), traversed.end(), handle) != traversed.end()) { // We've already added this parent (and therefore all of its parents). // We can return early. break; } if (!TryAddItem(ready_unsynced_set, parent, &dependencies)) { // The parent isn't in ready_unsynced_set. break; } parent_id = parent.GetParentId(); } // Reverse what we added to get the correct order. result->insert(result->end(), dependencies.rbegin(), dependencies.rend()); } bool Traversal::IsFull() const { return out_->size() >= max_entries_; } bool Traversal::HaveItem(int64_t handle) const { return added_handles_.find(handle) != added_handles_.end(); } void Traversal::AppendManyToTraversal( const syncable::Directory::Metahandles& handles) { out_->insert(out_->end(), handles.begin(), handles.end()); added_handles_.insert(handles.begin(), handles.end()); } void Traversal::AppendToTraversal(int64_t metahandle) { out_->push_back(metahandle); added_handles_.insert(metahandle); } void Traversal::AddCreatesAndMoves( const std::set& ready_unsynced_set) { // Add moves and creates, and prepend their uncommitted parents. for (std::set::const_iterator iter = ready_unsynced_set.begin(); !IsFull() && iter != ready_unsynced_set.end(); ++iter) { int64_t metahandle = *iter; if (HaveItem(metahandle)) continue; syncable::Entry entry(trans_, syncable::GET_BY_HANDLE, metahandle); if (!entry.GetIsDel()) { if (SupportsHierarchy(entry)) { // We only commit an item + its dependencies if it and all its // dependencies are not in conflict. syncable::Directory::Metahandles item_dependencies; AddUncommittedParents(ready_unsynced_set, entry, &item_dependencies); TryAddItem(ready_unsynced_set, entry, &item_dependencies); AppendManyToTraversal(item_dependencies); } else { // No hierarchy dependencies, just commit the item itself. AppendToTraversal(metahandle); } } } // It's possible that we overcommitted while trying to expand dependent // items. If so, truncate the set down to the allowed size. if (out_->size() > max_entries_) out_->resize(max_entries_); } void Traversal::AddDeletes(const std::set& ready_unsynced_set) { syncable::Directory::Metahandles deletion_list; // Note: we iterate over all the unsynced set, regardless of the max size. // The max size is only enforced after the top-to-bottom order has been // reversed, in order to ensure children are always deleted before parents. for (std::set::const_iterator iter = ready_unsynced_set.begin(); iter != ready_unsynced_set.end(); ++iter) { int64_t metahandle = *iter; if (HaveItem(metahandle)) continue; if (std::find(deletion_list.begin(), deletion_list.end(), metahandle) != deletion_list.end()) { continue; } syncable::Entry entry(trans_, syncable::GET_BY_HANDLE, metahandle); if (entry.GetIsDel()) { if (SupportsHierarchy(entry)) { syncable::Directory::Metahandles parents; AddDeletedParents(ready_unsynced_set, entry, deletion_list, &parents); // Append parents and chilren in top to bottom order. deletion_list.insert(deletion_list.end(), parents.begin(), parents.end()); } deletion_list.push_back(metahandle); } } // We've been gathering deletions in top to bottom order. Now we reverse the // order as we prepare the list. std::reverse(deletion_list.begin(), deletion_list.end()); AppendManyToTraversal(deletion_list); // It's possible that we overcommitted while trying to expand dependent // items. If so, truncate the set down to the allowed size. if (out_->size() > max_entries_) out_->resize(max_entries_); } void OrderCommitIds(syncable::BaseTransaction* trans, size_t max_entries, const std::set& ready_unsynced_set, syncable::Directory::Metahandles* out) { // Commits follow these rules: // 1. Moves or creates are preceded by needed folder creates, from // root to leaf. // 2. Moves/Creates before deletes. // 3. Deletes, collapsed. // We commit deleted moves under deleted items as moves when collapsing // delete trees. Traversal traversal(trans, max_entries, out); // Add moves and creates, and prepend their uncommitted parents. traversal.AddCreatesAndMoves(ready_unsynced_set); // Add all deletes. traversal.AddDeletes(ready_unsynced_set); } } // namespace } // namespace syncer