Commit 553ec6da authored by Mohamed Amir Yosef's avatar Mohamed Amir Yosef Committed by Commit Bot

[Sync::USS] Bookmark sync conflict resolution

This patch addresses the problem of conflict in bookmarks upon
receiving a remote update for a node that has local changes.

The general polic that is implemented is that server wins except in
case of deletion.

Bug: 516866
Change-Id: I50a85bde068dd9414e4e22bc1a33529fb3dfffa1
Reviewed-on: https://chromium-review.googlesource.com/1157006
Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
Reviewed-by: default avatarMikel Astiz <mastiz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#580436}
parent b87da49f
...@@ -1679,7 +1679,7 @@ IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest, MC_DeleteBookmark) { ...@@ -1679,7 +1679,7 @@ IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest, MC_DeleteBookmark) {
// Test a scenario of updating the name of the same bookmark from two clients at // Test a scenario of updating the name of the same bookmark from two clients at
// the same time. // the same time.
IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest, IN_PROC_BROWSER_TEST_P(TwoClientBookmarksSyncTestIncludingUssTests,
MC_BookmarkNameChangeConflict) { MC_BookmarkNameChangeConflict) {
ASSERT_TRUE(SetupSync()) << "SetupSync() failed."; ASSERT_TRUE(SetupSync()) << "SetupSync() failed.";
...@@ -1704,7 +1704,7 @@ IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest, ...@@ -1704,7 +1704,7 @@ IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest,
// Test a scenario of updating the URL of the same bookmark from two clients at // Test a scenario of updating the URL of the same bookmark from two clients at
// the same time. // the same time.
IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest, IN_PROC_BROWSER_TEST_P(TwoClientBookmarksSyncTestIncludingUssTests,
MC_BookmarkURLChangeConflict) { MC_BookmarkURLChangeConflict) {
ASSERT_TRUE(SetupSync()) << "SetupSync() failed."; ASSERT_TRUE(SetupSync()) << "SetupSync() failed.";
...@@ -1731,7 +1731,7 @@ IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest, ...@@ -1731,7 +1731,7 @@ IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest,
// Test a scenario of updating the BM Folder name from two clients at the same // Test a scenario of updating the BM Folder name from two clients at the same
// time. // time.
IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest, IN_PROC_BROWSER_TEST_P(TwoClientBookmarksSyncTestIncludingUssTests,
MC_FolderNameChangeConflict) { MC_FolderNameChangeConflict) {
ASSERT_TRUE(SetupClients()) << "SetupClients() failed."; ASSERT_TRUE(SetupClients()) << "SetupClients() failed.";
DisableVerifier(); DisableVerifier();
...@@ -1796,6 +1796,101 @@ IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest, ...@@ -1796,6 +1796,101 @@ IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest,
ASSERT_FALSE(ContainsDuplicateBookmarks(0)); ASSERT_FALSE(ContainsDuplicateBookmarks(0));
} }
// Test a scenario of updating an empty BM Folder name from one client and
// deleting it from another client at the same time.
IN_PROC_BROWSER_TEST_P(TwoClientBookmarksSyncTestIncludingUssTests,
MC_EmptyFolderNameChangeAndDeletionConflict) {
ASSERT_TRUE(SetupClients()) << "SetupClients() failed.";
DisableVerifier();
const BookmarkNode* folders[2];
// Create empty folder on both clients.
folders[0] = AddFolder(0, IndexedFolderName(0));
ASSERT_NE(nullptr, folders[0]);
folders[1] = AddFolder(1, IndexedFolderName(0));
ASSERT_NE(nullptr, folders[1]);
ASSERT_TRUE(SetupSync()) << "SetupSync() failed.";
ASSERT_TRUE(BookmarksMatchChecker().Wait());
ASSERT_FALSE(ContainsDuplicateBookmarks(0));
// Simultaneously rename the folder and delete it.
SetTitle(0, folders[0], "Folder A++");
Remove(1, GetBookmarkBarNode(1), 0);
// Both model should match.
ASSERT_TRUE(BookmarksMatchChecker().Wait());
ASSERT_FALSE(ContainsDuplicateBookmarks(0));
}
// Test a scenario of updating a non-empty BM Folder name from one client and
// deleting it from another client at the same time.
IN_PROC_BROWSER_TEST_P(TwoClientBookmarksSyncTestIncludingUssTests,
MC_NonEmptyFolderNameChangeAndDeletionConflict) {
ASSERT_TRUE(SetupClients()) << "SetupClients() failed.";
DisableVerifier();
const BookmarkNode* folderA[2];
const BookmarkNode* folderB[2];
// Create empty folder A on both clients.
folderA[0] = AddFolder(0, IndexedFolderName(0));
ASSERT_NE(nullptr, folderA[0]);
folderA[1] = AddFolder(1, IndexedFolderName(0));
ASSERT_NE(nullptr, folderA[1]);
// Create folder B under folder A on both clients.
folderB[0] = AddFolder(0, folderA[0], 0, IndexedFolderName(1));
ASSERT_NE(nullptr, folderB[0]);
folderB[1] = AddFolder(1, folderA[1], 0, IndexedFolderName(1));
ASSERT_NE(nullptr, folderB[1]);
ASSERT_TRUE(SetupSync()) << "SetupSync() failed.";
ASSERT_TRUE(BookmarksMatchChecker().Wait());
ASSERT_FALSE(ContainsDuplicateBookmarks(0));
// Simultaneously delete folder A from one client and delete it on the other.
SetTitle(0, folderA[0], "Folder A++");
Remove(1, GetBookmarkBarNode(1), 0);
ASSERT_TRUE(BookmarksMatchChecker().Wait());
ASSERT_FALSE(ContainsDuplicateBookmarks(0));
}
// Test a scenario of deleting a parent BM Folder name from one client and
// renaming a child in another client at the same time. Deleting the parent will
// also delete the child and hence there would be a conflict at the child node.
IN_PROC_BROWSER_TEST_P(TwoClientBookmarksSyncTestIncludingUssTests,
MC_ParentDeleteChildRenameConflict) {
ASSERT_TRUE(SetupClients()) << "SetupClients() failed.";
DisableVerifier();
const BookmarkNode* folderA[2];
const BookmarkNode* folderB[2];
// Create empty folder A on both clients.
folderA[0] = AddFolder(0, IndexedFolderName(0));
ASSERT_NE(nullptr, folderA[0]);
folderA[1] = AddFolder(1, IndexedFolderName(0));
ASSERT_NE(nullptr, folderA[1]);
// Create folder B under folder A on both clients.
folderB[0] = AddFolder(0, folderA[0], 0, IndexedFolderName(1));
ASSERT_NE(nullptr, folderB[0]);
folderB[1] = AddFolder(1, folderA[1], 0, IndexedFolderName(1));
ASSERT_NE(nullptr, folderB[1]);
ASSERT_TRUE(SetupSync()) << "SetupSync() failed.";
ASSERT_TRUE(BookmarksMatchChecker().Wait());
ASSERT_FALSE(ContainsDuplicateBookmarks(0));
// Simultaneously delete folder A from one client and rename folder B on the
// other.
SetTitle(0, folderB[0], "Folder B++");
Remove(1, GetBookmarkBarNode(1), 0);
ASSERT_TRUE(BookmarksMatchChecker().Wait());
ASSERT_FALSE(ContainsDuplicateBookmarks(0));
}
IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest, IN_PROC_BROWSER_TEST_F(TwoClientBookmarksSyncTest,
SingleClientEnabledEncryption) { SingleClientEnabledEncryption) {
ASSERT_TRUE(SetupSync()) << "SetupSync() failed."; ASSERT_TRUE(SetupSync()) << "SetupSync() failed.";
......
...@@ -9,10 +9,12 @@ ...@@ -9,10 +9,12 @@
#include <unordered_map> #include <unordered_map>
#include <utility> #include <utility>
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_piece.h" #include "base/strings/string_piece.h"
#include "components/bookmarks/browser/bookmark_model.h" #include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h" #include "components/bookmarks/browser/bookmark_node.h"
#include "components/sync/base/unique_position.h" #include "components/sync/base/unique_position.h"
#include "components/sync/model/conflict_resolution.h"
#include "components/sync/protocol/unique_position.pb.h" #include "components/sync/protocol/unique_position.pb.h"
#include "components/sync_bookmarks/bookmark_specifics_conversions.h" #include "components/sync_bookmarks/bookmark_specifics_conversions.h"
...@@ -71,6 +73,46 @@ int ComputeChildNodeIndex(const bookmarks::BookmarkNode* parent, ...@@ -71,6 +73,46 @@ int ComputeChildNodeIndex(const bookmarks::BookmarkNode* parent,
return parent->child_count(); return parent->child_count();
} }
void ApplyRemoteUpdate(
const syncer::UpdateResponseData& update,
const SyncedBookmarkTracker::Entity* tracked_entity,
const SyncedBookmarkTracker::Entity* new_parent_tracked_entity,
bookmarks::BookmarkModel* model,
SyncedBookmarkTracker* tracker) {
const syncer::EntityData& update_entity = update.entity.value();
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
const bookmarks::BookmarkNode* old_parent = node->parent();
const bookmarks::BookmarkNode* new_parent =
new_parent_tracked_entity->bookmark_node();
if (update_entity.is_folder != node->is_folder()) {
DLOG(ERROR) << "Could not update node. Remote node is a "
<< (update_entity.is_folder ? "folder" : "bookmark")
<< " while local node is a "
<< (node->is_folder() ? "folder" : "bookmark");
return;
}
UpdateBookmarkNodeFromSpecifics(update_entity.specifics.bookmark(), node,
model);
// Compute index information before updating the |tracker|.
const int old_index = old_parent->GetIndexOf(node);
const int new_index =
ComputeChildNodeIndex(new_parent, update_entity.unique_position, tracker);
tracker->Update(update_entity.id, update.response_version,
update_entity.modification_time,
update_entity.unique_position, update_entity.specifics);
if (new_parent == old_parent &&
(new_index == old_index || new_index == old_index + 1)) {
// Node hasn't moved. No more work to do.
return;
}
// Node has moved to another position under the same parent. Update the model.
// BookmarkModel takes care of placing the node in the correct position if the
// node is move to the left. (i.e. no need to subtract one from |new_index|).
model->Move(node, new_parent, new_index);
}
} // namespace } // namespace
BookmarkRemoteUpdatesHandler::BookmarkRemoteUpdatesHandler( BookmarkRemoteUpdatesHandler::BookmarkRemoteUpdatesHandler(
...@@ -85,12 +127,30 @@ void BookmarkRemoteUpdatesHandler::Process( ...@@ -85,12 +127,30 @@ void BookmarkRemoteUpdatesHandler::Process(
const syncer::UpdateResponseDataList& updates) { const syncer::UpdateResponseDataList& updates) {
for (const syncer::UpdateResponseData* update : ReorderUpdates(&updates)) { for (const syncer::UpdateResponseData* update : ReorderUpdates(&updates)) {
const syncer::EntityData& update_entity = update->entity.value(); const syncer::EntityData& update_entity = update->entity.value();
// TODO(crbug.com/516866): Check |update_entity| for sanity. if (!update_entity.is_deleted()) {
// 1. Has bookmark specifics or no specifics in case of delete. if (!IsValidBookmarkSpecifics(update_entity.specifics.bookmark(),
// 2. All meta info entries in the specifics have unique keys. update_entity.is_folder)) {
// 3. Unique position is valid. // Ignore updates with invalid specifics.
DLOG(ERROR)
<< "Couldn't process an update bookmark with an invalid specifics.";
continue;
}
if (!syncer::UniquePosition::FromProto(update_entity.unique_position)
.IsValid()) {
// Ignore updates with invalid unique position.
DLOG(ERROR) << "Couldn't process an update bookmark with an invalid "
"unique position.";
continue;
}
}
const SyncedBookmarkTracker::Entity* tracked_entity = const SyncedBookmarkTracker::Entity* tracked_entity =
bookmark_tracker_->GetEntityForSyncId(update_entity.id); bookmark_tracker_->GetEntityForSyncId(update_entity.id);
// TODO(crbug.com/516866): Handle the case of conflict as a result of
// re-encryption request.
if (tracked_entity && tracked_entity->IsUnsynced()) {
ProcessConflict(*update, tracked_entity);
continue;
}
if (update_entity.is_deleted()) { if (update_entity.is_deleted()) {
ProcessRemoteDelete(update_entity, tracked_entity); ProcessRemoteDelete(update_entity, tracked_entity);
continue; continue;
...@@ -212,12 +272,6 @@ void BookmarkRemoteUpdatesHandler::ProcessRemoteCreate( ...@@ -212,12 +272,6 @@ void BookmarkRemoteUpdatesHandler::ProcessRemoteCreate(
"should have been merged during intial sync."; "should have been merged during intial sync.";
return; return;
} }
if (!IsValidBookmarkSpecifics(update_entity.specifics.bookmark(),
update_entity.is_folder)) {
// Ignore creations with invalid specifics.
DLOG(ERROR) << "Couldn't add bookmark with an invalid specifics.";
return;
}
const bookmarks::BookmarkNode* parent_node = GetParentNode(update_entity); const bookmarks::BookmarkNode* parent_node = GetParentNode(update_entity);
if (!parent_node) { if (!parent_node) {
// If we cannot find the parent, we can do nothing. // If we cannot find the parent, we can do nothing.
...@@ -255,16 +309,6 @@ void BookmarkRemoteUpdatesHandler::ProcessRemoteUpdate( ...@@ -255,16 +309,6 @@ void BookmarkRemoteUpdatesHandler::ProcessRemoteUpdate(
bookmark_tracker_->GetEntityForSyncId(update_entity.id)); bookmark_tracker_->GetEntityForSyncId(update_entity.id));
// Must not be a deletion. // Must not be a deletion.
DCHECK(!update_entity.is_deleted()); DCHECK(!update_entity.is_deleted());
if (!IsValidBookmarkSpecifics(update_entity.specifics.bookmark(),
update_entity.is_folder)) {
// Ignore updates with invalid specifics.
DLOG(ERROR) << "Couldn't update bookmark with an invalid specifics.";
return;
}
if (tracked_entity->IsUnsynced()) {
// TODO(crbug.com/516866): Handle conflict resolution.
return;
}
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node(); const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
const bookmarks::BookmarkNode* old_parent = node->parent(); const bookmarks::BookmarkNode* old_parent = node->parent();
...@@ -295,33 +339,8 @@ void BookmarkRemoteUpdatesHandler::ProcessRemoteUpdate( ...@@ -295,33 +339,8 @@ void BookmarkRemoteUpdatesHandler::ProcessRemoteUpdate(
update_entity.specifics); update_entity.specifics);
return; return;
} }
if (update_entity.is_folder != node->is_folder()) { ApplyRemoteUpdate(update, tracked_entity, new_parent_entity, bookmark_model_,
DLOG(ERROR) << "Could not update node. Remote node is a " bookmark_tracker_);
<< (update_entity.is_folder ? "folder" : "bookmark")
<< " while local node is a "
<< (node->is_folder() ? "folder" : "bookmark");
return;
}
UpdateBookmarkNodeFromSpecifics(update_entity.specifics.bookmark(), node,
bookmark_model_);
// Compute index information before updating the |bookmark_tracker_|.
const int old_index = old_parent->GetIndexOf(node);
const int new_index = ComputeChildNodeIndex(
new_parent, update_entity.unique_position, bookmark_tracker_);
bookmark_tracker_->Update(update_entity.id, update.response_version,
update_entity.modification_time,
update_entity.unique_position,
update_entity.specifics);
if (new_parent == old_parent &&
(new_index == old_index || new_index == old_index + 1)) {
// Node hasn't moved. No more work to do.
return;
}
// Node has moved to another position under the same parent. Update the model.
// BookmarkModel takes care of placing the node in the correct position if the
// node is move to the left. (i.e. no need to subtract one from |new_index|).
bookmark_model_->Move(node, new_parent, new_index);
} }
void BookmarkRemoteUpdatesHandler::ProcessRemoteDelete( void BookmarkRemoteUpdatesHandler::ProcessRemoteDelete(
...@@ -353,6 +372,106 @@ void BookmarkRemoteUpdatesHandler::ProcessRemoteDelete( ...@@ -353,6 +372,106 @@ void BookmarkRemoteUpdatesHandler::ProcessRemoteDelete(
bookmark_model_->Remove(node); bookmark_model_->Remove(node);
} }
void BookmarkRemoteUpdatesHandler::ProcessConflict(
const syncer::UpdateResponseData& update,
const SyncedBookmarkTracker::Entity* tracked_entity) {
const syncer::EntityData& update_entity = update.entity.value();
// TODO(crbug.com/516866): Add basic unit test for this function.
// Can only conflict with existing nodes.
DCHECK(tracked_entity);
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
if (tracked_entity->metadata()->is_deleted() && update_entity.is_deleted()) {
// Both have been deleted, delete the corresponding entity from the tracker.
bookmark_tracker_->Remove(update_entity.id);
UMA_HISTOGRAM_ENUMERATION("Sync.ResolveConflict",
syncer::ConflictResolution::CHANGES_MATCH,
syncer::ConflictResolution::TYPE_SIZE);
return;
}
if (update_entity.is_deleted()) {
// Only remote has been deleted. Local wins. Record that we received the
// update from the server but leave the pending commit intact.
bookmark_tracker_->UpdateServerVersion(update_entity.id,
update.response_version);
UMA_HISTOGRAM_ENUMERATION("Sync.ResolveConflict",
syncer::ConflictResolution::USE_LOCAL,
syncer::ConflictResolution::TYPE_SIZE);
return;
}
if (tracked_entity->metadata()->is_deleted()) {
// Only local node has been deleted. It should be restored from the server
// data as a remote creation.
bookmark_tracker_->Remove(update_entity.id);
ProcessRemoteCreate(update);
UMA_HISTOGRAM_ENUMERATION("Sync.ResolveConflict",
syncer::ConflictResolution::USE_REMOTE,
syncer::ConflictResolution::TYPE_SIZE);
return;
}
// No deletions, there are potentially conflicting updates.
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
const bookmarks::BookmarkNode* old_parent = node->parent();
const SyncedBookmarkTracker::Entity* new_parent_entity =
bookmark_tracker_->GetEntityForSyncId(update_entity.parent_id);
// The |new_parent_entity| could be null in some racy conditions. For
// example, when a client A moves a node and deletes the old parent and
// commits, and then updates the node again, and at the same time client B
// updates before receiving the move updates. The client B update will arrive
// at client A after the parent entity has been deleted already.
if (!new_parent_entity) {
DLOG(ERROR) << "Could not update node. Parent node doesn't exist: "
<< update_entity.parent_id;
return;
}
const bookmarks::BookmarkNode* new_parent =
new_parent_entity->bookmark_node();
// |new_parent| would be null if the parent has been deleted locally and not
// committed yet. Deletions are excuted recusively, so a parent deletions
// entails child deletion, and if this child has been updated on another
// client, this would cause conflict.
if (!new_parent) {
DLOG(ERROR)
<< "Could not update node. Parent node has been deleted already.";
return;
}
// Either local and remote data match or server wins, and in both cases we
// should squash any pending commits.
bookmark_tracker_->AckSequenceNumber(update_entity.id);
// Node update could be either in the node data (e.g. title or
// unique_position), or it could be that the node has moved under another
// parent without any data change. Should check both the data and the parent
// to confirm that no updates to the model are needed.
if (tracked_entity->MatchesDataIgnoringParent(update_entity) &&
new_parent == old_parent) {
bookmark_tracker_->Update(update_entity.id, update.response_version,
update_entity.modification_time,
update_entity.unique_position,
update_entity.specifics);
// The changes are identical so there isn't a real conflict.
UMA_HISTOGRAM_ENUMERATION("Sync.ResolveConflict",
syncer::ConflictResolution::CHANGES_MATCH,
syncer::ConflictResolution::TYPE_SIZE);
return;
}
// Conflict where data don't match and no remote deletion, and hence server
// wins. Update the model from server data.
UMA_HISTOGRAM_ENUMERATION("Sync.ResolveConflict",
syncer::ConflictResolution::USE_REMOTE,
syncer::ConflictResolution::TYPE_SIZE);
ApplyRemoteUpdate(update, tracked_entity, new_parent_entity, bookmark_model_,
bookmark_tracker_);
}
void BookmarkRemoteUpdatesHandler::RemoveEntityAndChildrenFromTracker( void BookmarkRemoteUpdatesHandler::RemoveEntityAndChildrenFromTracker(
const bookmarks::BookmarkNode* node) { const bookmarks::BookmarkNode* node) {
const SyncedBookmarkTracker::Entity* entity = const SyncedBookmarkTracker::Entity* entity =
......
...@@ -65,7 +65,7 @@ class BookmarkRemoteUpdatesHandler { ...@@ -65,7 +65,7 @@ class BookmarkRemoteUpdatesHandler {
void ProcessRemoteUpdate(const syncer::UpdateResponseData& update, void ProcessRemoteUpdate(const syncer::UpdateResponseData& update,
const SyncedBookmarkTracker::Entity* tracked_entity); const SyncedBookmarkTracker::Entity* tracked_entity);
// Process a remote delete of a bookmark node. |update_entity| must not be a // Processes a remote delete of a bookmark node. |update_entity| must not be a
// deletion. |tracked_entity| is the tracked entity for that server_id. It is // deletion. |tracked_entity| is the tracked entity for that server_id. It is
// passed as a dependency instead of performing a lookup inside // passed as a dependency instead of performing a lookup inside
// ProcessRemoteDelete() to avoid wasting CPU cycles for doing another lookup // ProcessRemoteDelete() to avoid wasting CPU cycles for doing another lookup
...@@ -73,6 +73,15 @@ class BookmarkRemoteUpdatesHandler { ...@@ -73,6 +73,15 @@ class BookmarkRemoteUpdatesHandler {
void ProcessRemoteDelete(const syncer::EntityData& update_entity, void ProcessRemoteDelete(const syncer::EntityData& update_entity,
const SyncedBookmarkTracker::Entity* tracked_entity); const SyncedBookmarkTracker::Entity* tracked_entity);
// Processes a conflict where the bookmark has been changed both locally and
// remotely. It applies the general policy the server wins expcet in the case
// of remote deletions in which local wins. |tracked_entity| is the tracked
// entity for that server_id. It is passed as a dependency instead of
// performing a lookup inside ProcessRemoteDelete() to avoid wasting CPU
// cycles for doing another lookup (this code runs on the UI thread).
void ProcessConflict(const syncer::UpdateResponseData& update,
const SyncedBookmarkTracker::Entity* tracked_entity);
// Recursively removes the entities corresponding to |node| and its children // Recursively removes the entities corresponding to |node| and its children
// from |bookmark_tracker_|. // from |bookmark_tracker_|.
void RemoveEntityAndChildrenFromTracker(const bookmarks::BookmarkNode* node); void RemoveEntityAndChildrenFromTracker(const bookmarks::BookmarkNode* node);
......
...@@ -136,7 +136,7 @@ void SyncedBookmarkTracker::Update( ...@@ -136,7 +136,7 @@ void SyncedBookmarkTracker::Update(
auto it = sync_id_to_entities_map_.find(sync_id); auto it = sync_id_to_entities_map_.find(sync_id);
Entity* entity = it->second.get(); Entity* entity = it->second.get();
DCHECK(entity); DCHECK(entity);
entity->metadata()->set_server_id(sync_id); DCHECK_EQ(entity->metadata()->server_id(), sync_id);
entity->metadata()->set_server_version(server_version); entity->metadata()->set_server_version(server_version);
entity->metadata()->set_modification_time( entity->metadata()->set_modification_time(
syncer::TimeToProtoTime(modification_time)); syncer::TimeToProtoTime(modification_time));
...@@ -146,6 +146,14 @@ void SyncedBookmarkTracker::Update( ...@@ -146,6 +146,14 @@ void SyncedBookmarkTracker::Update(
// |ordered_local_tombstones_| as well if it has been locally deleted. // |ordered_local_tombstones_| as well if it has been locally deleted.
} }
void SyncedBookmarkTracker::UpdateServerVersion(const std::string& sync_id,
int64_t server_version) {
auto it = sync_id_to_entities_map_.find(sync_id);
Entity* entity = it->second.get();
DCHECK(entity);
entity->metadata()->set_server_version(server_version);
}
void SyncedBookmarkTracker::MarkDeleted(const std::string& sync_id) { void SyncedBookmarkTracker::MarkDeleted(const std::string& sync_id) {
auto it = sync_id_to_entities_map_.find(sync_id); auto it = sync_id_to_entities_map_.find(sync_id);
Entity* entity = it->second.get(); Entity* entity = it->second.get();
...@@ -225,7 +233,7 @@ SyncedBookmarkTracker::GetEntitiesWithLocalChanges(size_t max_entries) const { ...@@ -225,7 +233,7 @@ SyncedBookmarkTracker::GetEntitiesWithLocalChanges(size_t max_entries) const {
sync_id_to_entities_map_) { sync_id_to_entities_map_) {
Entity* entity = pair.second.get(); Entity* entity = pair.second.get();
if (entity->metadata()->is_deleted()) { if (entity->metadata()->is_deleted()) {
// Deletion are stored sorted in |ordered_local_tombstones_| and will be // Deletions are stored sorted in |ordered_local_tombstones_| and will be
// added later. // added later.
continue; continue;
} }
...@@ -344,6 +352,18 @@ void SyncedBookmarkTracker::UpdateUponCommitResponse( ...@@ -344,6 +352,18 @@ void SyncedBookmarkTracker::UpdateUponCommitResponse(
} }
} }
void SyncedBookmarkTracker::AckSequenceNumber(const std::string& sync_id) {
auto it = sync_id_to_entities_map_.find(sync_id);
Entity* entity =
it != sync_id_to_entities_map_.end() ? it->second.get() : nullptr;
if (!entity) {
DLOG(WARNING) << "Trying to update a non existing entity.";
return;
}
entity->metadata()->set_acked_sequence_number(
entity->metadata()->sequence_number());
}
bool SyncedBookmarkTracker::IsEmpty() const { bool SyncedBookmarkTracker::IsEmpty() const {
return sync_id_to_entities_map_.empty(); return sync_id_to_entities_map_.empty();
} }
......
...@@ -106,6 +106,9 @@ class SyncedBookmarkTracker { ...@@ -106,6 +106,9 @@ class SyncedBookmarkTracker {
const sync_pb::UniquePosition& unique_position, const sync_pb::UniquePosition& unique_position,
const sync_pb::EntitySpecifics& specifics); const sync_pb::EntitySpecifics& specifics);
// Updates the server version of an existing entry for the |sync_id|.
void UpdateServerVersion(const std::string& sync_id, int64_t server_version);
// This class maintains the order of calls to this method and the same order // This class maintains the order of calls to this method and the same order
// is gauaranteed when returning local changes in // is gauaranteed when returning local changes in
// GetEntitiesWithLocalChanges() as well as in BuildBookmarkModelMetadata(). // GetEntitiesWithLocalChanges() as well as in BuildBookmarkModelMetadata().
...@@ -146,6 +149,11 @@ class SyncedBookmarkTracker { ...@@ -146,6 +149,11 @@ class SyncedBookmarkTracker {
int64_t acked_sequence_number, int64_t acked_sequence_number,
int64_t server_version); int64_t server_version);
// Set the value of |EntityMetadata.acked_sequence_number| in the entity with
// |sync_id| to be equal to |EntityMetadata.sequence_number| such that it is
// not returned in GetEntitiesWithLocalChanges().
void AckSequenceNumber(const std::string& sync_id);
// Whether the tracker is empty or not. // Whether the tracker is empty or not.
bool IsEmpty() const; bool IsEmpty() const;
......
...@@ -150,6 +150,38 @@ TEST(SyncedBookmarkTrackerTest, ...@@ -150,6 +150,38 @@ TEST(SyncedBookmarkTrackerTest,
// request in a separate test probably. // request in a separate test probably.
} }
TEST(SyncedBookmarkTrackerTest, ShouldAckSequenceNumber) {
SyncedBookmarkTracker tracker(std::vector<NodeMetadataPair>(),
std::make_unique<sync_pb::ModelTypeState>());
const std::string kSyncId = "SYNC_ID";
const int64_t kId = 1;
const int64_t kServerVersion = 1000;
const base::Time kModificationTime(base::Time::Now() -
base::TimeDelta::FromSeconds(1));
const sync_pb::UniquePosition unique_position;
const sync_pb::EntitySpecifics specifics =
GenerateSpecifics(/*title=*/std::string(), /*url=*/std::string());
bookmarks::BookmarkNode node(kId, GURL());
tracker.Add(kSyncId, &node, kServerVersion, kModificationTime,
unique_position, specifics);
// Test simple scenario of ack'ing an incrememented sequence number.
EXPECT_THAT(tracker.HasLocalChanges(), Eq(false));
tracker.IncrementSequenceNumber(kSyncId);
EXPECT_THAT(tracker.HasLocalChanges(), Eq(true));
tracker.AckSequenceNumber(kSyncId);
EXPECT_THAT(tracker.HasLocalChanges(), Eq(false));
// Test ack'ing of a mutliple times incremented sequence number.
tracker.IncrementSequenceNumber(kSyncId);
EXPECT_THAT(tracker.HasLocalChanges(), Eq(true));
tracker.IncrementSequenceNumber(kSyncId);
tracker.IncrementSequenceNumber(kSyncId);
EXPECT_THAT(tracker.HasLocalChanges(), Eq(true));
tracker.AckSequenceNumber(kSyncId);
EXPECT_THAT(tracker.HasLocalChanges(), Eq(false));
}
TEST(SyncedBookmarkTrackerTest, ShouldUpdateUponCommitResponseWithNewId) { TEST(SyncedBookmarkTrackerTest, ShouldUpdateUponCommitResponseWithNewId) {
SyncedBookmarkTracker tracker(std::vector<NodeMetadataPair>(), SyncedBookmarkTracker tracker(std::vector<NodeMetadataPair>(),
std::make_unique<sync_pb::ModelTypeState>()); std::make_unique<sync_pb::ModelTypeState>());
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment