普通文本  |  520行  |  19.92 KB

// Copyright (c) 2010 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 "chrome/browser/sync/engine/conflict_resolver.h"

#include <map>
#include <set>

#include "chrome/browser/sync/engine/syncer.h"
#include "chrome/browser/sync/engine/syncer_util.h"
#include "chrome/browser/sync/protocol/service_constants.h"
#include "chrome/browser/sync/sessions/status_controller.h"
#include "chrome/browser/sync/syncable/directory_manager.h"
#include "chrome/browser/sync/syncable/syncable.h"
#include "chrome/common/deprecated/event_sys-inl.h"

using std::map;
using std::set;
using syncable::BaseTransaction;
using syncable::Directory;
using syncable::Entry;
using syncable::Id;
using syncable::MutableEntry;
using syncable::ScopedDirLookup;
using syncable::WriteTransaction;

namespace browser_sync {

using sessions::ConflictProgress;
using sessions::StatusController;

const int SYNC_CYCLES_BEFORE_ADMITTING_DEFEAT = 8;

ConflictResolver::ConflictResolver() {
}

ConflictResolver::~ConflictResolver() {
}

void ConflictResolver::IgnoreLocalChanges(MutableEntry* entry) {
  // An update matches local actions, merge the changes.
  // This is a little fishy because we don't actually merge them.
  // In the future we should do a 3-way merge.
  VLOG(1) << "Server and local changes match, merging:" << entry;
  // With IS_UNSYNCED false, changes should be merged.
  // METRIC simple conflict resolved by merge.
  entry->Put(syncable::IS_UNSYNCED, false);
}

void ConflictResolver::OverwriteServerChanges(WriteTransaction* trans,
                                              MutableEntry * entry) {
  // This is similar to an overwrite from the old client.
  // This is equivalent to a scenario where we got the update before we'd
  // made our local client changes.
  // TODO(chron): This is really a general property clobber. We clobber
  // the server side property. Perhaps we should actually do property merging.
  entry->Put(syncable::BASE_VERSION, entry->Get(syncable::SERVER_VERSION));
  entry->Put(syncable::IS_UNAPPLIED_UPDATE, false);
  // METRIC conflict resolved by overwrite.
}

ConflictResolver::ProcessSimpleConflictResult
ConflictResolver::ProcessSimpleConflict(WriteTransaction* trans,
                                        const Id& id) {
  MutableEntry entry(trans, syncable::GET_BY_ID, id);
  // Must be good as the entry won't have been cleaned up.
  CHECK(entry.good());
  // If an update fails, locally we have to be in a set or unsynced. We're not
  // in a set here, so we must be unsynced.
  if (!entry.Get(syncable::IS_UNSYNCED)) {
    return NO_SYNC_PROGRESS;
  }

  if (!entry.Get(syncable::IS_UNAPPLIED_UPDATE)) {
    if (!entry.Get(syncable::PARENT_ID).ServerKnows()) {
      VLOG(1) << "Item conflicting because its parent not yet committed. Id: "
              << id;
    } else {
      VLOG(1) << "No set for conflicting entry id " << id << ". There should "
                 "be an update/commit that will fix this soon. This message "
                 "should not repeat.";
    }
    return NO_SYNC_PROGRESS;
  }

  if (entry.Get(syncable::IS_DEL) && entry.Get(syncable::SERVER_IS_DEL)) {
    // we've both deleted it, so lets just drop the need to commit/update this
    // entry.
    entry.Put(syncable::IS_UNSYNCED, false);
    entry.Put(syncable::IS_UNAPPLIED_UPDATE, false);
    // we've made changes, but they won't help syncing progress.
    // METRIC simple conflict resolved by merge.
    return NO_SYNC_PROGRESS;
  }

  if (!entry.Get(syncable::SERVER_IS_DEL)) {
    // This logic determines "client wins" vs. "server wins" strategy picking.
    // TODO(nick): The current logic is arbitrary; instead, it ought to be made
    // consistent with the ModelAssociator behavior for a datatype.  It would
    // be nice if we could route this back to ModelAssociator code to pick one
    // of three options: CLIENT, SERVER, or MERGE.  Some datatypes (autofill)
    // are easily mergeable.
    bool name_matches = entry.Get(syncable::NON_UNIQUE_NAME) ==
                        entry.Get(syncable::SERVER_NON_UNIQUE_NAME);
    bool parent_matches = entry.Get(syncable::PARENT_ID) ==
                                    entry.Get(syncable::SERVER_PARENT_ID);
    bool entry_deleted = entry.Get(syncable::IS_DEL);

    if (!entry_deleted && name_matches && parent_matches) {
      VLOG(1) << "Resolving simple conflict, ignoring local changes for:"
              << entry;
      IgnoreLocalChanges(&entry);
    } else {
      VLOG(1) << "Resolving simple conflict, overwriting server changes for:"
              << entry;
      OverwriteServerChanges(trans, &entry);
    }
    return SYNC_PROGRESS;
  } else {  // SERVER_IS_DEL is true
    // If a server deleted folder has local contents we should be in a set.
    if (entry.Get(syncable::IS_DIR)) {
      Directory::ChildHandles children;
      trans->directory()->GetChildHandles(trans,
                                          entry.Get(syncable::ID),
                                          &children);
      if (0 != children.size()) {
        VLOG(1) << "Entry is a server deleted directory with local contents, "
                   "should be in a set. (race condition).";
        return NO_SYNC_PROGRESS;
      }
    }

    // The entry is deleted on the server but still exists locally.
    if (!entry.Get(syncable::UNIQUE_CLIENT_TAG).empty()) {
      // If we've got a client-unique tag, we can undelete while retaining
      // our present ID.
      DCHECK_EQ(entry.Get(syncable::SERVER_VERSION), 0) << "For the server to "
          "know to re-create, client-tagged items should revert to version 0 "
          "when server-deleted.";
      OverwriteServerChanges(trans, &entry);
      // Clobber the versions, just in case the above DCHECK is violated.
      entry.Put(syncable::SERVER_VERSION, 0);
      entry.Put(syncable::BASE_VERSION, 0);
    } else {
      // Otherwise, we've got to undelete by creating a new locally
      // uncommitted entry.
      SyncerUtil::SplitServerInformationIntoNewEntry(trans, &entry);

      MutableEntry server_update(trans, syncable::GET_BY_ID, id);
      CHECK(server_update.good());
      CHECK(server_update.Get(syncable::META_HANDLE) !=
            entry.Get(syncable::META_HANDLE))
          << server_update << entry;
    }
    return SYNC_PROGRESS;
  }
}

ConflictResolver::ConflictSetCountMapKey ConflictResolver::GetSetKey(
    ConflictSet* set) {
  // TODO(sync): Come up with a better scheme for set hashing. This scheme
  // will make debugging easy.
  // If this call to sort is removed, we need to add one before we use
  // binary_search in ProcessConflictSet.
  sort(set->begin(), set->end());
  std::stringstream rv;
  for (ConflictSet::iterator i = set->begin() ; i != set->end() ; ++i )
    rv << *i << ".";
  return rv.str();
}

namespace {

bool AttemptToFixCircularConflict(WriteTransaction* trans,
                                  ConflictSet* conflict_set) {
  ConflictSet::const_iterator i, j;
  for (i = conflict_set->begin() ; i != conflict_set->end() ; ++i) {
    MutableEntry entryi(trans, syncable::GET_BY_ID, *i);
    if (entryi.Get(syncable::PARENT_ID) ==
            entryi.Get(syncable::SERVER_PARENT_ID) ||
        !entryi.Get(syncable::IS_UNAPPLIED_UPDATE) ||
        !entryi.Get(syncable::IS_DIR)) {
      continue;
    }
    Id parentid = entryi.Get(syncable::SERVER_PARENT_ID);
    // Create the entry here as it's the only place we could ever get a parentid
    // that doesn't correspond to a real entry.
    Entry parent(trans, syncable::GET_BY_ID, parentid);
    if (!parent.good())  // server parent update not received yet
      continue;
    // This loop walks upwards from the server parent. If we hit the root (0)
    // all is well. If we hit the entry we're examining it means applying the
    // parent id would cause a loop. We don't need more general loop detection
    // because we know our local tree is valid.
    while (!parentid.IsRoot()) {
      Entry parent(trans, syncable::GET_BY_ID, parentid);
      CHECK(parent.good());
      if (parentid == *i)
        break;  // It's a loop.
      parentid = parent.Get(syncable::PARENT_ID);
    }
    if (parentid.IsRoot())
      continue;
    VLOG(1) << "Overwriting server changes to avoid loop: " << entryi;
    entryi.Put(syncable::BASE_VERSION, entryi.Get(syncable::SERVER_VERSION));
    entryi.Put(syncable::IS_UNSYNCED, true);
    entryi.Put(syncable::IS_UNAPPLIED_UPDATE, false);
    // METRIC conflict resolved by breaking dir loop.
    return true;
  }
  return false;
}

bool AttemptToFixUnsyncedEntryInDeletedServerTree(WriteTransaction* trans,
                                                  ConflictSet* conflict_set,
                                                  const Entry& entry) {
  if (!entry.Get(syncable::IS_UNSYNCED) || entry.Get(syncable::IS_DEL))
    return false;
  Id parentid = entry.Get(syncable::PARENT_ID);
  MutableEntry parent(trans, syncable::GET_BY_ID, parentid);
  if (!parent.good() || !parent.Get(syncable::IS_UNAPPLIED_UPDATE) ||
      !parent.Get(syncable::SERVER_IS_DEL) ||
      !binary_search(conflict_set->begin(), conflict_set->end(), parentid))
    return false;
  // We're trying to commit into a directory tree that's been deleted. To
  // solve this we recreate the directory tree.
  //
  // We do this in two parts, first we ensure the tree is unaltered since the
  // conflict was detected.
  Id id = parentid;
  while (!id.IsRoot()) {
    if (!binary_search(conflict_set->begin(), conflict_set->end(), id))
      break;
    Entry parent(trans, syncable::GET_BY_ID, id);
    if (!parent.good() || !parent.Get(syncable::IS_UNAPPLIED_UPDATE) ||
        !parent.Get(syncable::SERVER_IS_DEL))
      return false;
    id = parent.Get(syncable::PARENT_ID);
  }
  // Now we fix up the entries.
  id = parentid;
  while (!id.IsRoot()) {
    MutableEntry parent(trans, syncable::GET_BY_ID, id);
    if (!binary_search(conflict_set->begin(), conflict_set->end(), id))
      break;
    VLOG(1) << "Giving directory a new id so we can undelete it " << parent;
    ClearServerData(&parent);
    SyncerUtil::ChangeEntryIDAndUpdateChildren(trans, &parent,
        trans->directory()->NextId());
    parent.Put(syncable::BASE_VERSION, 0);
    parent.Put(syncable::IS_UNSYNCED, true);
    id = parent.Get(syncable::PARENT_ID);
    // METRIC conflict resolved by recreating dir tree.
  }
  return true;
}

// TODO(chron): needs unit test badly
bool AttemptToFixUpdateEntryInDeletedLocalTree(WriteTransaction* trans,
                                               ConflictSet* conflict_set,
                                               const Entry& entry) {
  if (!entry.Get(syncable::IS_UNAPPLIED_UPDATE) ||
      entry.Get(syncable::SERVER_IS_DEL))
    return false;
  Id parent_id = entry.Get(syncable::SERVER_PARENT_ID);
  MutableEntry parent(trans, syncable::GET_BY_ID, parent_id);
  if (!parent.good() || !parent.Get(syncable::IS_DEL) ||
    !binary_search(conflict_set->begin(), conflict_set->end(), parent_id)) {
    return false;
  }
  // We've deleted a directory tree that's got contents on the server. We
  // recreate the directory to solve the problem.
  //
  // We do this in two parts, first we ensure the tree is unaltered since
  // the conflict was detected.
  Id id = parent_id;
  // As we will be crawling the path of deleted entries there's a chance we'll
  // end up having to reparent an item as there will be an invalid parent.
  Id reroot_id = syncable::kNullId;
  // Similarly crawling deleted items means we risk loops.
  int loop_detection = conflict_set->size();
  while (!id.IsRoot() && --loop_detection >= 0) {
    Entry parent(trans, syncable::GET_BY_ID, id);
    // If we get a bad parent, or a parent that's deleted on client and server
    // we recreate the hierarchy in the root.
    if (!parent.good()) {
      reroot_id = id;
      break;
    }
    CHECK(parent.Get(syncable::IS_DIR));
    if (!binary_search(conflict_set->begin(), conflict_set->end(), id)) {
      // We've got to an entry that's not in the set. If it has been deleted
      // between set building and this point in time we return false. If it had
      // been deleted earlier it would have been in the set.
      // TODO(sync): Revisit syncer code organization to see if conflict
      // resolution can be done in the same transaction as set building.
      if (parent.Get(syncable::IS_DEL))
        return false;
      break;
    }
    if (!parent.Get(syncable::IS_DEL) ||
        parent.Get(syncable::SERVER_IS_DEL) ||
        !parent.Get(syncable::IS_UNSYNCED)) {
      return false;
    }
    id = parent.Get(syncable::PARENT_ID);
  }
  // If we find we've been looping we re-root the hierarchy.
  if (loop_detection < 0) {
    if (id == entry.Get(syncable::ID))
      reroot_id = entry.Get(syncable::PARENT_ID);
    else
      reroot_id = id;
  }
  // Now we fix things up by undeleting all the folders in the item's path.
  id = parent_id;
  while (!id.IsRoot() && id != reroot_id) {
    if (!binary_search(conflict_set->begin(), conflict_set->end(), id)) {
      break;
    }
    MutableEntry entry(trans, syncable::GET_BY_ID, id);

    VLOG(1) << "Undoing our deletion of " << entry
            << ", will have name " << entry.Get(syncable::NON_UNIQUE_NAME);

    Id parent_id = entry.Get(syncable::PARENT_ID);
    if (parent_id == reroot_id) {
      parent_id = trans->root_id();
    }
    entry.Put(syncable::PARENT_ID, parent_id);
    entry.Put(syncable::IS_DEL, false);
    id = entry.Get(syncable::PARENT_ID);
    // METRIC conflict resolved by recreating dir tree.
  }
  return true;
}

bool AttemptToFixRemovedDirectoriesWithContent(WriteTransaction* trans,
                                               ConflictSet* conflict_set) {
  ConflictSet::const_iterator i,j;
  for (i = conflict_set->begin() ; i != conflict_set->end() ; ++i) {
    Entry entry(trans, syncable::GET_BY_ID, *i);
    if (AttemptToFixUnsyncedEntryInDeletedServerTree(trans,
        conflict_set, entry)) {
      return true;
    }
    if (AttemptToFixUpdateEntryInDeletedLocalTree(trans, conflict_set, entry))
      return true;
  }
  return false;
}

}  // namespace

// TODO(sync): Eliminate conflict sets. They're not necessary.
bool ConflictResolver::ProcessConflictSet(WriteTransaction* trans,
                                          ConflictSet* conflict_set,
                                          int conflict_count) {
  int set_size = conflict_set->size();
  if (set_size < 2) {
    LOG(WARNING) << "Skipping conflict set because it has size " << set_size;
    // We can end up with sets of size one if we have a new item in a set that
    // we tried to commit transactionally. This should not be a persistent
    // situation.
    return false;
  }
  if (conflict_count < 3) {
    // Avoid resolving sets that could be the result of transient conflicts.
    // Transient conflicts can occur because the client or server can be
    // slightly out of date.
    return false;
  }

  VLOG(1) << "Fixing a set containing " << set_size << " items";

  // Fix circular conflicts.
  if (AttemptToFixCircularConflict(trans, conflict_set))
    return true;
  // Check for problems involving contents of removed folders.
  if (AttemptToFixRemovedDirectoriesWithContent(trans, conflict_set))
    return true;
  return false;
}

template <typename InputIt>
bool ConflictResolver::LogAndSignalIfConflictStuck(
    BaseTransaction* trans,
    int attempt_count,
    InputIt begin,
    InputIt end,
    StatusController* status) {
  if (attempt_count < SYNC_CYCLES_BEFORE_ADMITTING_DEFEAT) {
    return false;
  }

  // Don't signal stuck if we're not up to date.
  if (status->num_server_changes_remaining() > 0) {
    return false;
  }

  LOG(ERROR) << "[BUG] Conflict set cannot be resolved, has "
             << end - begin << " items:";
  for (InputIt i = begin ; i != end ; ++i) {
    Entry e(trans, syncable::GET_BY_ID, *i);
    if (e.good())
      LOG(ERROR) << "  " << e;
    else
      LOG(ERROR) << "  Bad ID:" << *i;
  }

  status->set_syncer_stuck(true);

  return true;
  // TODO(sync): If we're stuck for a while we need to alert the user, clear
  // cache or reset syncing. At the very least we should stop trying something
  // that's obviously not working.
}

bool ConflictResolver::ResolveSimpleConflicts(const ScopedDirLookup& dir,
                                              StatusController* status) {
  WriteTransaction trans(dir, syncable::SYNCER, __FILE__, __LINE__);
  bool forward_progress = false;
  const ConflictProgress& progress = status->conflict_progress();
  // First iterate over simple conflict items (those that belong to no set).
  set<Id>::const_iterator conflicting_item_it;
  for (conflicting_item_it = progress.ConflictingItemsBeginConst();
       conflicting_item_it != progress.ConflictingItemsEnd();
       ++conflicting_item_it) {
    Id id = *conflicting_item_it;
    map<Id, ConflictSet*>::const_iterator item_set_it =
        progress.IdToConflictSetFind(id);
    if (item_set_it == progress.IdToConflictSetEnd() ||
        0 == item_set_it->second) {
      // We have a simple conflict.
      switch (ProcessSimpleConflict(&trans, id)) {
        case NO_SYNC_PROGRESS:
          {
            int conflict_count = (simple_conflict_count_map_[id] += 2);
            LogAndSignalIfConflictStuck(&trans, conflict_count,
                                        &id, &id + 1, status);
            break;
          }
        case SYNC_PROGRESS:
          forward_progress = true;
          break;
      }
    }
  }
  // Reduce the simple_conflict_count for each item currently tracked.
  SimpleConflictCountMap::iterator i = simple_conflict_count_map_.begin();
  while (i != simple_conflict_count_map_.end()) {
    if (0 == --(i->second))
      simple_conflict_count_map_.erase(i++);
    else
      ++i;
  }
  return forward_progress;
}

bool ConflictResolver::ResolveConflicts(const ScopedDirLookup& dir,
                                        StatusController* status) {
  const ConflictProgress& progress = status->conflict_progress();
  bool rv = false;
  if (ResolveSimpleConflicts(dir, status))
    rv = true;
  WriteTransaction trans(dir, syncable::SYNCER, __FILE__, __LINE__);
  set<Id> children_of_dirs_merged_last_round;
  std::swap(children_of_merged_dirs_, children_of_dirs_merged_last_round);
  set<ConflictSet*>::const_iterator set_it;
  for (set_it = progress.ConflictSetsBegin();
       set_it != progress.ConflictSetsEnd();
       set_it++) {
    ConflictSet* conflict_set = *set_it;
    ConflictSetCountMapKey key = GetSetKey(conflict_set);
    conflict_set_count_map_[key] += 2;
    int conflict_count = conflict_set_count_map_[key];
    // Keep a metric for new sets.
    if (2 == conflict_count) {
      // METRIC conflict sets seen ++
    }
    // See if this set contains entries whose parents were merged last round.
    if (SortedCollectionsIntersect(children_of_dirs_merged_last_round.begin(),
                                   children_of_dirs_merged_last_round.end(),
                                   conflict_set->begin(),
                                   conflict_set->end())) {
      VLOG(1) << "Accelerating resolution for hierarchical merge.";
      conflict_count += 2;
    }
    // See if we should process this set.
    if (ProcessConflictSet(&trans, conflict_set, conflict_count)) {
      rv = true;
    }
    LogAndSignalIfConflictStuck(&trans, conflict_count,
                                conflict_set->begin(),
                                conflict_set->end(), status);
  }
  if (rv) {
    // This code means we don't signal that syncing is stuck when any conflict
    // resolution has occured.
    // TODO(sync): As this will also reduce our sensitivity to problem
    // conditions and increase the time for cascading resolutions we may have to
    // revisit this code later, doing something more intelligent.
    conflict_set_count_map_.clear();
    simple_conflict_count_map_.clear();
  }
  ConflictSetCountMap::iterator i = conflict_set_count_map_.begin();
  while (i != conflict_set_count_map_.end()) {
    if (0 == --i->second) {
      conflict_set_count_map_.erase(i++);
      // METRIC self resolved conflict sets ++.
    } else {
      ++i;
    }
  }
  return rv;
}

}  // namespace browser_sync