// 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