cae4550a创建于 2012年8月25日历史提交
// Copyright (c) 2012 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_command.h"

#include <set>
#include <utility>
#include <vector>

#include "sync/engine/syncer_util.h"
#include "sync/engine/throttled_data_type_tracker.h"
#include "sync/syncable/entry.h"
#include "sync/syncable/mutable_entry.h"
#include "sync/syncable/nigori_handler.h"
#include "sync/syncable/nigori_util.h"
#include "sync/syncable/syncable_util.h"
#include "sync/syncable/write_transaction.h"
#include "sync/util/cryptographer.h"

using std::set;
using std::vector;

namespace syncer {

using sessions::OrderedCommitSet;
using sessions::SyncSession;
using sessions::StatusController;

GetCommitIdsCommand::GetCommitIdsCommand(
    const size_t commit_batch_size,
    sessions::OrderedCommitSet* commit_set)
    : requested_commit_batch_size_(commit_batch_size),
      commit_set_(commit_set) {
}

GetCommitIdsCommand::~GetCommitIdsCommand() {}

SyncerError GetCommitIdsCommand::ExecuteImpl(SyncSession* session) {
  // Gather the full set of unsynced items and store it in the session. They
  // are not in the correct order for commit.
  std::set<int64> ready_unsynced_set;
  syncable::Directory::UnsyncedMetaHandles all_unsynced_handles;
  GetUnsyncedEntries(session->write_transaction(),
                     &all_unsynced_handles);

  ModelTypeSet encrypted_types;
  bool passphrase_missing = false;
  Cryptographer* cryptographer =
      session->context()->
      directory()->GetCryptographer(session->write_transaction());
  if (cryptographer) {
    encrypted_types = session->context()->directory()->GetNigoriHandler()->
        GetEncryptedTypes(session->write_transaction());
    passphrase_missing = cryptographer->has_pending_keys();
  };

  const ModelTypeSet throttled_types =
       session->context()->throttled_data_type_tracker()->GetThrottledTypes();
  // We filter out all unready entries from the set of unsynced handles. This
  // new set of ready and unsynced items (which excludes throttled items as
  // well) is then what we use to determine what is a candidate for commit.
  FilterUnreadyEntries(session->write_transaction(),
                       throttled_types,
                       encrypted_types,
                       passphrase_missing,
                       all_unsynced_handles,
                       &ready_unsynced_set);

  BuildCommitIds(session->write_transaction(),
                 session->routing_info(),
                 ready_unsynced_set);

  const vector<syncable::Id>& verified_commit_ids =
      commit_set_->GetAllCommitIds();

  for (size_t i = 0; i < verified_commit_ids.size(); i++)
    DVLOG(1) << "Debug commit batch result:" << verified_commit_ids[i];

  return SYNCER_OK;
}

namespace {

bool IsEntryInConflict(const syncable::Entry& entry) {
  if (entry.Get(syncable::IS_UNSYNCED) &&
      entry.Get(syncable::SERVER_VERSION) > 0 &&
      (entry.Get(syncable::SERVER_VERSION) >
       entry.Get(syncable::BASE_VERSION))) {
    // 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.Get(syncable::IS_UNAPPLIED_UPDATE));
    DVLOG(1) << "Excluding entry from commit due to version mismatch "
             << entry;
    return true;
  }
  return false;
}

// An entry is not considered ready for commit if any are true:
// 1. It's in conflict.
// 2. It requires encryption (either the type is encrypted but a passphrase
//    is missing from the cryptographer, or the entry itself wasn't properly
//    encrypted).
// 3. It's type is currently throttled.
// 4. It's a delete but has not been committed.
bool IsEntryReadyForCommit(ModelTypeSet throttled_types,
                           ModelTypeSet encrypted_types,
                           bool passphrase_missing,
                           const syncable::Entry& entry) {
  DCHECK(entry.Get(syncable::IS_UNSYNCED));
  if (IsEntryInConflict(entry))
    return false;

  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;
  }

  // Look at the throttled types.
  if (throttled_types.Has(type))
    return false;

  if (entry.Get(syncable::IS_DEL) && !entry.Get(syncable::ID).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.Get(syncable::ID);
  if (id == entry.Get(syncable::PARENT_ID)) {
    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;
  }

  DVLOG(2) << "Entry is ready for commit: " << entry;
  return true;
}

}  // namespace

void GetCommitIdsCommand::FilterUnreadyEntries(
    syncable::BaseTransaction* trans,
    ModelTypeSet throttled_types,
    ModelTypeSet encrypted_types,
    bool passphrase_missing,
    const syncable::Directory::UnsyncedMetaHandles& unsynced_handles,
    std::set<int64>* ready_unsynced_set) {
  for (syncable::Directory::UnsyncedMetaHandles::const_iterator iter =
           unsynced_handles.begin(); iter != unsynced_handles.end(); ++iter) {
    syncable::Entry entry(trans, syncable::GET_BY_HANDLE, *iter);
    if (IsEntryReadyForCommit(throttled_types,
                              encrypted_types,
                              passphrase_missing,
                              entry)) {
      ready_unsynced_set->insert(*iter);
    }
  }
}

bool GetCommitIdsCommand::AddUncommittedParentsAndTheirPredecessors(
    syncable::BaseTransaction* trans,
    const ModelSafeRoutingInfo& routes,
    const std::set<int64>& ready_unsynced_set,
    const syncable::Entry& item,
    sessions::OrderedCommitSet* result) const {
  OrderedCommitSet item_dependencies(routes);
  syncable::Id parent_id = item.Get(syncable::PARENT_ID);

  // 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 handle = parent.Get(syncable::META_HANDLE);
    if (commit_set_->HaveCommitItem(handle)) {
      // We've already added this parent (and therefore all of its parents).
      // We can return early.
      break;
    }
    if (!AddItemThenPredecessors(trans, ready_unsynced_set, parent,
                                 &item_dependencies)) {
      // There was a parent/predecessor in conflict. We return without adding
      // anything to |commit_set|.
      DVLOG(1) << "Parent or parent's predecessor was in conflict, omitting "
               << item;
      return false;
    }
    parent_id = parent.Get(syncable::PARENT_ID);
  }

  // Reverse what we added to get the correct order.
  result->AppendReverse(item_dependencies);
  return true;
}

bool GetCommitIdsCommand::AddItem(const std::set<int64>& ready_unsynced_set,
                                  const syncable::Entry& item,
                                  OrderedCommitSet* result) const {
  DCHECK(item.Get(syncable::IS_UNSYNCED));
  // An item in conflict means that dependent items (successors and children)
  // cannot be added either.
  if (IsEntryInConflict(item))
    return false;
  int64 item_handle = item.Get(syncable::META_HANDLE);
  if (ready_unsynced_set.count(item_handle) == 0) {
    // It's not in conflict, but not ready for commit. Just return true without
    // adding it to the commit set.
    return true;
  }
  result->AddCommitItem(item_handle, item.Get(syncable::ID),
                        item.GetModelType());
  return true;
}

bool GetCommitIdsCommand::AddItemThenPredecessors(
    syncable::BaseTransaction* trans,
    const std::set<int64>& ready_unsynced_set,
    const syncable::Entry& item,
    OrderedCommitSet* result) const {
  int64 item_handle = item.Get(syncable::META_HANDLE);
  if (commit_set_->HaveCommitItem(item_handle)) {
    // We've already added this item to the commit set, and so must have
    // already added the predecessors as well.
    return true;
  }
  if (!AddItem(ready_unsynced_set, item, result))
    return false;  // Item is in conflict.
  if (item.Get(syncable::IS_DEL))
    return true;  // Deleted items have no predecessors.

  syncable::Id prev_id = item.Get(syncable::PREV_ID);
  while (!prev_id.IsRoot()) {
    syncable::Entry prev(trans, syncable::GET_BY_ID, prev_id);
    CHECK(prev.good()) << "Bad id when walking predecessors.";
    if (!prev.Get(syncable::IS_UNSYNCED))
      break;
    int64 handle = prev.Get(syncable::META_HANDLE);
    if (commit_set_->HaveCommitItem(handle)) {
      // We've already added this item to the commit set, and so must have
      // already added the predecessors as well.
      return true;
    }
    if (!AddItem(ready_unsynced_set, prev, result))
      return false;  // Item is in conflict.
    prev_id = prev.Get(syncable::PREV_ID);
  }
  return true;
}

bool GetCommitIdsCommand::AddPredecessorsThenItem(
    syncable::BaseTransaction* trans,
    const ModelSafeRoutingInfo& routes,
    const std::set<int64>& ready_unsynced_set,
    const syncable::Entry& item,
    OrderedCommitSet* result) const {
  OrderedCommitSet item_dependencies(routes);
  if (!AddItemThenPredecessors(trans, ready_unsynced_set, item,
                               &item_dependencies)) {
    // Either the item or its predecessors are in conflict, so don't add any
    // items to the commit set.
    DVLOG(1) << "Predecessor was in conflict, omitting " << item;
    return false;
  }

  // Reverse what we added to get the correct order.
  result->AppendReverse(item_dependencies);
  return true;
}

bool GetCommitIdsCommand::IsCommitBatchFull() const {
  return commit_set_->Size() >= requested_commit_batch_size_;
}

void GetCommitIdsCommand::AddCreatesAndMoves(
    syncable::WriteTransaction* write_transaction,
    const ModelSafeRoutingInfo& routes,
    const std::set<int64>& ready_unsynced_set) {
  // Add moves and creates, and prepend their uncommitted parents.
  for (std::set<int64>::const_iterator iter = ready_unsynced_set.begin();
       !IsCommitBatchFull() && iter != ready_unsynced_set.end(); ++iter) {
    int64 metahandle = *iter;
    if (commit_set_->HaveCommitItem(metahandle))
      continue;

    syncable::Entry entry(write_transaction,
                          syncable::GET_BY_HANDLE,
                          metahandle);
    if (!entry.Get(syncable::IS_DEL)) {
      // We only commit an item + its dependencies if it and all its
      // dependencies are not in conflict.
      OrderedCommitSet item_dependencies(routes);
      if (AddUncommittedParentsAndTheirPredecessors(
              write_transaction,
              routes,
              ready_unsynced_set,
              entry,
              &item_dependencies) &&
          AddPredecessorsThenItem(write_transaction,
                                  routes,
                                  ready_unsynced_set,
                                  entry,
                                  &item_dependencies)) {
        commit_set_->Append(item_dependencies);
      }
    }
  }

  // It's possible that we overcommitted while trying to expand dependent
  // items.  If so, truncate the set down to the allowed size.
  commit_set_->Truncate(requested_commit_batch_size_);
}

void GetCommitIdsCommand::AddDeletes(
    syncable::WriteTransaction* write_transaction,
    const std::set<int64>& ready_unsynced_set) {
  set<syncable::Id> legal_delete_parents;

  for (std::set<int64>::const_iterator iter = ready_unsynced_set.begin();
       !IsCommitBatchFull() && iter != ready_unsynced_set.end(); ++iter) {
    int64 metahandle = *iter;
    if (commit_set_->HaveCommitItem(metahandle))
      continue;

    syncable::Entry entry(write_transaction, syncable::GET_BY_HANDLE,
                          metahandle);

    if (entry.Get(syncable::IS_DEL)) {
      syncable::Entry parent(write_transaction, syncable::GET_BY_ID,
                             entry.Get(syncable::PARENT_ID));
      // If the parent is deleted and unsynced, then any children of that
      // parent don't need to be added to the delete queue.
      //
      // Note: the parent could be synced if there was an update deleting a
      // folder when we had a deleted all items in it.
      // We may get more updates, or we may want to delete the entry.
      if (parent.good() &&
          parent.Get(syncable::IS_DEL) &&
          parent.Get(syncable::IS_UNSYNCED)) {
        // However, if an entry is moved, these rules can apply differently.
        //
        // If the entry was moved, then the destination parent was deleted,
        // then we'll miss it in the roll up. We have to add it in manually.
        // TODO(chron): Unit test for move / delete cases:
        // Case 1: Locally moved, then parent deleted
        // Case 2: Server moved, then locally issue recursive delete.
        if (entry.Get(syncable::ID).ServerKnows() &&
            entry.Get(syncable::PARENT_ID) !=
                entry.Get(syncable::SERVER_PARENT_ID)) {
          DVLOG(1) << "Inserting moved and deleted entry, will be missed by "
                   << "delete roll." << entry.Get(syncable::ID);

          commit_set_->AddCommitItem(metahandle,
              entry.Get(syncable::ID),
              entry.GetModelType());
        }

        // Skip this entry since it's a child of a parent that will be
        // deleted. The server will unroll the delete and delete the
        // child as well.
        continue;
      }

      legal_delete_parents.insert(entry.Get(syncable::PARENT_ID));
    }
  }

  // We could store all the potential entries with a particular parent during
  // the above scan, but instead we rescan here. This is less efficient, but
  // we're dropping memory alloc/dealloc in favor of linear scans of recently
  // examined entries.
  //
  // Scan through the UnsyncedMetaHandles again. If we have a deleted
  // entry, then check if the parent is in legal_delete_parents.
  //
  // Parent being in legal_delete_parents means for the child:
  //   a recursive delete is not currently happening (no recent deletes in same
  //     folder)
  //   parent did expect at least one old deleted child
  //   parent was not deleted
  for (std::set<int64>::const_iterator iter = ready_unsynced_set.begin();
       !IsCommitBatchFull() && iter != ready_unsynced_set.end(); ++iter) {
    int64 metahandle = *iter;
    if (commit_set_->HaveCommitItem(metahandle))
      continue;
    syncable::MutableEntry entry(write_transaction, syncable::GET_BY_HANDLE,
                                 metahandle);
    if (entry.Get(syncable::IS_DEL)) {
      syncable::Id parent_id = entry.Get(syncable::PARENT_ID);
      if (legal_delete_parents.count(parent_id)) {
        commit_set_->AddCommitItem(metahandle, entry.Get(syncable::ID),
            entry.GetModelType());
      }
    }
  }
}

void GetCommitIdsCommand::BuildCommitIds(
    syncable::WriteTransaction* write_transaction,
    const ModelSafeRoutingInfo& routes,
    const std::set<int64>& ready_unsynced_set) {
  // Commits follow these rules:
  // 1. Moves or creates are preceded by needed folder creates, from
  //    root to leaf.  For folders whose contents are ordered, moves
  //    and creates appear in order.
  // 2. Moves/Creates before deletes.
  // 3. Deletes, collapsed.
  // We commit deleted moves under deleted items as moves when collapsing
  // delete trees.

  // Add moves and creates, and prepend their uncommitted parents.
  AddCreatesAndMoves(write_transaction, routes, ready_unsynced_set);

  // Add all deletes.
  AddDeletes(write_transaction, ready_unsynced_set);
}

}  // namespace syncer