diff options
author | maxbogue <maxbogue@chromium.org> | 2015-10-22 16:34:32 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-10-22 23:35:14 +0000 |
commit | ae4d4cd14920a9ec246406b99d5f49c2a06c450c (patch) | |
tree | 24a38e19c29c1617d624d4972c9309a007543307 /components/history | |
parent | 67349c42be488047259ba0b919d882a234b49e64 (diff) | |
download | chromium_src-ae4d4cd14920a9ec246406b99d5f49c2a06c450c.zip chromium_src-ae4d4cd14920a9ec246406b99d5f49c2a06c450c.tar.gz chromium_src-ae4d4cd14920a9ec246406b99d5f49c2a06c450c.tar.bz2 |
[Sync] Remove history and favicon deps from sync_driver.
All datatype specific code is being moved out of sync_driver.
BUG=543199, 512038
Review URL: https://codereview.chromium.org/1402153003
Cr-Commit-Position: refs/heads/master@{#355672}
Diffstat (limited to 'components/history')
-rw-r--r-- | components/history/DEPS | 1 | ||||
-rw-r--r-- | components/history/core/browser/BUILD.gn | 5 | ||||
-rw-r--r-- | components/history/core/browser/history_model_worker.cc | 131 | ||||
-rw-r--r-- | components/history/core/browser/history_model_worker.h | 62 | ||||
-rw-r--r-- | components/history/core/browser/typed_url_model_associator.cc | 858 | ||||
-rw-r--r-- | components/history/core/browser/typed_url_model_associator.h | 208 |
6 files changed, 1265 insertions, 0 deletions
diff --git a/components/history/DEPS b/components/history/DEPS index 58b0b17..9111271 100644 --- a/components/history/DEPS +++ b/components/history/DEPS @@ -3,6 +3,7 @@ include_rules = [ "+components/keyed_service", "+components/query_parser", "+components/signin/core", + "+components/sync_driver", "+google_apis/gaia", "+net", "+sql", diff --git a/components/history/core/browser/BUILD.gn b/components/history/core/browser/BUILD.gn index 4ce6421..2da4805 100644 --- a/components/history/core/browser/BUILD.gn +++ b/components/history/core/browser/BUILD.gn @@ -31,6 +31,8 @@ static_library("browser") { "history_db_task.h", "history_match.cc", "history_match.h", + "history_model_worker.cc", + "history_model_worker.h", "history_service.cc", "history_service.h", "history_service_observer.h", @@ -58,6 +60,8 @@ static_library("browser") { "top_sites_impl.cc", "top_sites_impl.h", "top_sites_observer.h", + "typed_url_model_associator.cc", + "typed_url_model_associator.h", "typed_url_syncable_service.cc", "typed_url_syncable_service.h", "url_database.cc", @@ -93,6 +97,7 @@ static_library("browser") { "//components/keyed_service/core", "//components/query_parser", "//components/signin/core/browser", + "//components/sync_driver", "//components/url_formatter", "//google_apis", "//net", diff --git a/components/history/core/browser/history_model_worker.cc b/components/history/core/browser/history_model_worker.cc new file mode 100644 index 0000000..76f4200 --- /dev/null +++ b/components/history/core/browser/history_model_worker.cc @@ -0,0 +1,131 @@ +// Copyright 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 "components/history/core/browser/history_model_worker.h" + +#include "base/memory/ref_counted.h" +#include "base/message_loop/message_loop.h" +#include "base/synchronization/waitable_event.h" + +using base::WaitableEvent; + +namespace browser_sync { + +class WorkerTask : public history::HistoryDBTask { + public: + WorkerTask( + const syncer::WorkCallback& work, + WaitableEvent* done, + syncer::SyncerError* error) + : work_(work), done_(done), error_(error) {} + + bool RunOnDBThread(history::HistoryBackend* backend, + history::HistoryDatabase* db) override { + *error_ = work_.Run(); + done_->Signal(); + return true; + } + + // Since the DoWorkAndWaitUntilDone() is synchronous, we don't need to run + // any code asynchronously on the main thread after completion. + void DoneRunOnMainThread() override {} + + protected: + ~WorkerTask() override {} + + syncer::WorkCallback work_; + WaitableEvent* done_; + syncer::SyncerError* error_; +}; + +class AddDBThreadObserverTask : public history::HistoryDBTask { + public: + explicit AddDBThreadObserverTask(base::Closure register_callback) + : register_callback_(register_callback) {} + + bool RunOnDBThread(history::HistoryBackend* backend, + history::HistoryDatabase* db) override { + register_callback_.Run(); + return true; + } + + void DoneRunOnMainThread() override {} + + private: + ~AddDBThreadObserverTask() override {} + + base::Closure register_callback_; +}; + +namespace { + +// Post the work task on |history_service|'s DB thread from the UI +// thread. +void PostWorkerTask( + const base::WeakPtr<history::HistoryService>& history_service, + const syncer::WorkCallback& work, + base::CancelableTaskTracker* cancelable_tracker, + WaitableEvent* done, + syncer::SyncerError* error) { + if (history_service.get()) { + scoped_ptr<history::HistoryDBTask> task(new WorkerTask(work, done, error)); + history_service->ScheduleDBTask(task.Pass(), cancelable_tracker); + } else { + *error = syncer::CANNOT_DO_WORK; + done->Signal(); + } +} + +} // namespace + +HistoryModelWorker::HistoryModelWorker( + const base::WeakPtr<history::HistoryService>& history_service, + const scoped_refptr<base::SingleThreadTaskRunner>& ui_thread, + syncer::WorkerLoopDestructionObserver* observer) + : syncer::ModelSafeWorker(observer), + history_service_(history_service), + ui_thread_(ui_thread) { + CHECK(history_service.get()); + DCHECK(ui_thread_->BelongsToCurrentThread()); + cancelable_tracker_.reset(new base::CancelableTaskTracker); +} + +void HistoryModelWorker::RegisterForLoopDestruction() { + CHECK(history_service_.get()); + history_service_->ScheduleDBTask( + scoped_ptr<history::HistoryDBTask>(new AddDBThreadObserverTask( + base::Bind(&HistoryModelWorker::RegisterOnDBThread, this))), + cancelable_tracker_.get()); +} + +void HistoryModelWorker::RegisterOnDBThread() { + SetWorkingLoopToCurrent(); +} + +syncer::SyncerError HistoryModelWorker::DoWorkAndWaitUntilDoneImpl( + const syncer::WorkCallback& work) { + syncer::SyncerError error = syncer::UNSET; + if (ui_thread_->PostTask(FROM_HERE, + base::Bind(&PostWorkerTask, history_service_, work, + cancelable_tracker_.get(), + work_done_or_stopped(), &error))) { + work_done_or_stopped()->Wait(); + } else { + error = syncer::CANNOT_DO_WORK; + } + return error; +} + +syncer::ModelSafeGroup HistoryModelWorker::GetModelSafeGroup() { + return syncer::GROUP_HISTORY; +} + +HistoryModelWorker::~HistoryModelWorker() { + // The base::CancelableTaskTracker class is not thread-safe and must only be + // used from a single thread but the current object may not be destroyed from + // the UI thread, so delete it from the UI thread. + ui_thread_->DeleteSoon(FROM_HERE, cancelable_tracker_.release()); +} + +} // namespace browser_sync diff --git a/components/history/core/browser/history_model_worker.h b/components/history/core/browser/history_model_worker.h new file mode 100644 index 0000000..e16d756 --- /dev/null +++ b/components/history/core/browser/history_model_worker.h @@ -0,0 +1,62 @@ +// Copyright 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. + +#ifndef COMPONENTS_HISTORY_CORE_BROWSER_HISTORY_MODEL_WORKER_H_ +#define COMPONENTS_HISTORY_CORE_BROWSER_HISTORY_MODEL_WORKER_H_ + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/task/cancelable_task_tracker.h" +#include "components/history/core/browser/history_db_task.h" +#include "components/history/core/browser/history_service.h" +#include "sync/internal_api/public/engine/model_safe_worker.h" + +namespace history { +class HistoryService; +} + +namespace browser_sync { + +// A syncer::ModelSafeWorker for history models that accepts requests +// from the syncapi that need to be fulfilled on the history thread. +class HistoryModelWorker : public syncer::ModelSafeWorker { + public: + explicit HistoryModelWorker( + const base::WeakPtr<history::HistoryService>& history_service, + const scoped_refptr<base::SingleThreadTaskRunner>& ui_thread, + syncer::WorkerLoopDestructionObserver* observer); + + // syncer::ModelSafeWorker implementation. Called on syncapi SyncerThread. + void RegisterForLoopDestruction() override; + syncer::ModelSafeGroup GetModelSafeGroup() override; + + // Called on history DB thread to register HistoryModelWorker to observe + // destruction of history backend loop. + void RegisterOnDBThread(); + + protected: + syncer::SyncerError DoWorkAndWaitUntilDoneImpl( + const syncer::WorkCallback& work) override; + + private: + ~HistoryModelWorker() override; + + const base::WeakPtr<history::HistoryService> history_service_; + + // A reference to the UI thread's task runner. + const scoped_refptr<base::SingleThreadTaskRunner> ui_thread_; + + // Helper object to make sure we don't leave tasks running on the history + // thread. + scoped_ptr<base::CancelableTaskTracker> cancelable_tracker_; + + DISALLOW_COPY_AND_ASSIGN(HistoryModelWorker); +}; + +} // namespace browser_sync + +#endif // COMPONENTS_HISTORY_CORE_BROWSER_HISTORY_MODEL_WORKER_H_ diff --git a/components/history/core/browser/typed_url_model_associator.cc b/components/history/core/browser/typed_url_model_associator.cc new file mode 100644 index 0000000..38be8e8 --- /dev/null +++ b/components/history/core/browser/typed_url_model_associator.cc @@ -0,0 +1,858 @@ +// Copyright 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 "components/history/core/browser/typed_url_model_associator.h" + +#include <algorithm> +#include <set> + +#include "base/location.h" +#include "base/logging.h" +#include "base/metrics/histogram.h" +#include "base/strings/utf_string_conversions.h" +#include "components/history/core/browser/history_backend.h" +#include "components/sync_driver/sync_service.h" +#include "net/base/net_util.h" +#include "sync/api/sync_error.h" +#include "sync/api/sync_merge_result.h" +#include "sync/internal_api/public/read_node.h" +#include "sync/internal_api/public/read_transaction.h" +#include "sync/internal_api/public/write_node.h" +#include "sync/internal_api/public/write_transaction.h" +#include "sync/protocol/typed_url_specifics.pb.h" + +namespace browser_sync { + +// The server backend can't handle arbitrarily large node sizes, so to keep +// the size under control we limit the visit array. +static const int kMaxTypedUrlVisits = 100; + +// There's no limit on how many visits the history DB could have for a given +// typed URL, so we limit how many we fetch from the DB to avoid crashes due to +// running out of memory (http://crbug.com/89793). This value is different +// from kMaxTypedUrlVisits, as some of the visits fetched from the DB may be +// RELOAD visits, which will be stripped. +static const int kMaxVisitsToFetch = 1000; + +static bool CheckVisitOrdering(const history::VisitVector& visits) { + int64 previous_visit_time = 0; + for (history::VisitVector::const_iterator visit = visits.begin(); + visit != visits.end(); ++visit) { + if (visit != visits.begin()) { + // We allow duplicate visits here - they shouldn't really be allowed, but + // they still seem to show up sometimes and we haven't figured out the + // source, so we just log an error instead of failing an assertion. + // (http://crbug.com/91473). + if (previous_visit_time == visit->visit_time.ToInternalValue()) + DVLOG(1) << "Duplicate visit time encountered"; + else if (previous_visit_time > visit->visit_time.ToInternalValue()) + return false; + } + + previous_visit_time = visit->visit_time.ToInternalValue(); + } + return true; +} + +TypedUrlModelAssociator::TypedUrlModelAssociator( + sync_driver::SyncService* sync_service, + history::HistoryBackend* history_backend, + sync_driver::DataTypeErrorHandler* error_handler) + : sync_service_(sync_service), + history_backend_(history_backend), + expected_loop_(base::MessageLoop::current()), + abort_requested_(false), + error_handler_(error_handler), + num_db_accesses_(0), + num_db_errors_(0) { + DCHECK(sync_service_); + // history_backend_ may be null for unit tests (since it's not mockable). +} + +TypedUrlModelAssociator::~TypedUrlModelAssociator() {} + + +bool TypedUrlModelAssociator::FixupURLAndGetVisits( + history::URLRow* url, + history::VisitVector* visits) { + ++num_db_accesses_; + CHECK(history_backend_); + if (!history_backend_->GetMostRecentVisitsForURL( + url->id(), kMaxVisitsToFetch, visits)) { + ++num_db_errors_; + return false; + } + + // Sometimes (due to a bug elsewhere in the history or sync code, or due to + // a crash between adding a URL to the history database and updating the + // visit DB) the visit vector for a URL can be empty. If this happens, just + // create a new visit whose timestamp is the same as the last_visit time. + // This is a workaround for http://crbug.com/84258. + if (visits->empty()) { + DVLOG(1) << "Found empty visits for URL: " << url->url(); + + if (url->last_visit().is_null()) { + // If modified URL is bookmarked, history backend treats it as modified + // even if all its visits are deleted. Return false to stop further + // processing because sync expects valid visit time for modified entry. + return false; + } + + history::VisitRow visit( + url->id(), url->last_visit(), 0, ui::PAGE_TRANSITION_TYPED, 0); + visits->push_back(visit); + } + + // GetMostRecentVisitsForURL() returns the data in the opposite order that + // we need it, so reverse it. + std::reverse(visits->begin(), visits->end()); + + // Sometimes, the last_visit field in the URL doesn't match the timestamp of + // the last visit in our visit array (they come from different tables, so + // crashes/bugs can cause them to mismatch), so just set it here. + url->set_last_visit(visits->back().visit_time); + DCHECK(CheckVisitOrdering(*visits)); + return true; +} + +bool TypedUrlModelAssociator::ShouldIgnoreUrl(const GURL& url) { + // Ignore empty URLs. Not sure how this can happen (maybe import from other + // busted browsers, or misuse of the history API, or just plain bugs) but we + // can't deal with them. + if (url.spec().empty()) + return true; + + // Ignore local file URLs. + if (url.SchemeIsFile()) + return true; + + // Ignore localhost URLs. + if (net::IsLocalhost(url.host())) + return true; + + return false; +} + +bool TypedUrlModelAssociator::ShouldIgnoreVisits( + const history::VisitVector& visits) { + // We ignore URLs that were imported, but have never been visited by + // chromium. + static const int kLastImportedSource = history::SOURCE_EXTENSION; + history::VisitSourceMap map; + if (!history_backend_->GetVisitsSource(visits, &map)) + return false; // If we can't read the visit, assume it's not imported. + + // Walk the list of visits and look for a non-imported item. + for (history::VisitVector::const_iterator it = visits.begin(); + it != visits.end(); ++it) { + if (map.count(it->visit_id) == 0 || + map[it->visit_id] <= kLastImportedSource) { + return false; + } + } + // We only saw imported visits, so tell the caller to ignore them. + return true; +} + +syncer::SyncError TypedUrlModelAssociator::AssociateModels( + syncer::SyncMergeResult* local_merge_result, + syncer::SyncMergeResult* syncer_merge_result) { + ClearErrorStats(); + syncer::SyncError error = + DoAssociateModels(local_merge_result, syncer_merge_result); + UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlModelAssociationErrors", + GetErrorPercentage()); + ClearErrorStats(); + return error; +} + +void TypedUrlModelAssociator::ClearErrorStats() { + num_db_accesses_ = 0; + num_db_errors_ = 0; +} + +int TypedUrlModelAssociator::GetErrorPercentage() const { + return num_db_accesses_ ? (100 * num_db_errors_ / num_db_accesses_) : 0; +} + +syncer::SyncError TypedUrlModelAssociator::DoAssociateModels( + syncer::SyncMergeResult* local_merge_result, + syncer::SyncMergeResult* syncer_merge_result) { + DVLOG(1) << "Associating TypedUrl Models"; + DCHECK(expected_loop_ == base::MessageLoop::current()); + + history::URLRows typed_urls; + ++num_db_accesses_; + bool query_succeeded = + history_backend_ && history_backend_->GetAllTypedURLs(&typed_urls); + + history::URLRows new_urls; + history::URLRows updated_urls; + TypedUrlVisitVector new_visits; + { + base::AutoLock au(abort_lock_); + if (abort_requested_) { + return syncer::SyncError(FROM_HERE, + syncer::SyncError::DATATYPE_ERROR, + "Association was aborted.", + model_type()); + } + + // Must lock and check first to make sure |error_handler_| is valid. + if (!query_succeeded) { + ++num_db_errors_; + return error_handler_->CreateAndUploadError( + FROM_HERE, + "Could not get the typed_url entries.", + model_type()); + } + local_merge_result->set_num_items_before_association(typed_urls.size()); + + // Get all the visits. + std::map<history::URLID, history::VisitVector> visit_vectors; + for (history::URLRows::iterator ix = typed_urls.begin(); + ix != typed_urls.end();) { + DCHECK_EQ(0U, visit_vectors.count(ix->id())); + if (!FixupURLAndGetVisits(&(*ix), &(visit_vectors[ix->id()])) || + ShouldIgnoreUrl(ix->url()) || + ShouldIgnoreVisits(visit_vectors[ix->id()])) { + // Ignore this URL if we couldn't load the visits or if there's some + // other problem with it (it was empty, or imported and never visited). + ix = typed_urls.erase(ix); + } else { + ++ix; + } + } + + syncer::WriteTransaction trans(FROM_HERE, sync_service_->GetUserShare()); + syncer::ReadNode typed_url_root(&trans); + if (typed_url_root.InitTypeRoot(syncer::TYPED_URLS) != + syncer::BaseNode::INIT_OK) { + return error_handler_->CreateAndUploadError( + FROM_HERE, + "Server did not create the top-level typed_url node. We " + "might be running against an out-of-date server.", + model_type()); + } + syncer_merge_result->set_num_items_before_association( + typed_url_root.GetTotalNodeCount()); + + std::set<std::string> current_urls; + for (history::URLRows::iterator ix = typed_urls.begin(); + ix != typed_urls.end(); ++ix) { + std::string tag = ix->url().spec(); + // Empty URLs should be filtered out by ShouldIgnoreUrl() previously. + DCHECK(!tag.empty()); + history::VisitVector& visits = visit_vectors[ix->id()]; + + syncer::ReadNode node(&trans); + if (node.InitByClientTagLookup(syncer::TYPED_URLS, tag) == + syncer::BaseNode::INIT_OK) { + // Same URL exists in sync data and in history data - compare the + // entries to see if there's any difference. + sync_pb::TypedUrlSpecifics typed_url( + FilterExpiredVisits(node.GetTypedUrlSpecifics())); + DCHECK_EQ(tag, typed_url.url()); + + // Initialize fields in |new_url| to the same values as the fields in + // the existing URLRow in the history DB. This is needed because we + // overwrite the existing value below in WriteToHistoryBackend(), but + // some of the values in that structure are not synced (like + // typed_count). + history::URLRow new_url(*ix); + + std::vector<history::VisitInfo> added_visits; + MergeResult difference = + MergeUrls(typed_url, *ix, &visits, &new_url, &added_visits); + if (difference & DIFF_UPDATE_NODE) { + syncer::WriteNode write_node(&trans); + if (write_node.InitByClientTagLookup(syncer::TYPED_URLS, tag) != + syncer::BaseNode::INIT_OK) { + return error_handler_->CreateAndUploadError( + FROM_HERE, + "Failed to edit typed_url sync node.", + model_type()); + } + // We don't want to resurrect old visits that have been aged out by + // other clients, so remove all visits that are older than the + // earliest existing visit in the sync node. + if (typed_url.visits_size() > 0) { + base::Time earliest_visit = + base::Time::FromInternalValue(typed_url.visits(0)); + for (history::VisitVector::iterator it = visits.begin(); + it != visits.end() && it->visit_time < earliest_visit; ) { + it = visits.erase(it); + } + // Should never be possible to delete all the items, since the + // visit vector contains all the items in typed_url.visits. + DCHECK_GT(visits.size(), 0u); + } + DCHECK_EQ(new_url.last_visit().ToInternalValue(), + visits.back().visit_time.ToInternalValue()); + WriteToSyncNode(new_url, visits, &write_node); + syncer_merge_result->set_num_items_modified( + syncer_merge_result->num_items_modified() + 1); + } + if (difference & DIFF_LOCAL_ROW_CHANGED) { + DCHECK_EQ(ix->id(), new_url.id()); + updated_urls.push_back(new_url); + } + if (difference & DIFF_LOCAL_VISITS_ADDED) { + new_visits.push_back( + std::pair<GURL, std::vector<history::VisitInfo> >(ix->url(), + added_visits)); + } + } else { + // Sync has never seen this URL before. + syncer::WriteNode node(&trans); + syncer::WriteNode::InitUniqueByCreationResult result = + node.InitUniqueByCreation(syncer::TYPED_URLS, tag); + if (result != syncer::WriteNode::INIT_SUCCESS) { + return error_handler_->CreateAndUploadError( + FROM_HERE, + "Failed to create typed_url sync node: " + tag, + model_type()); + } + + node.SetTitle(tag); + WriteToSyncNode(*ix, visits, &node); + syncer_merge_result->set_num_items_added( + syncer_merge_result->num_items_added() + 1); + } + + current_urls.insert(tag); + } + + // Now walk the sync nodes and detect any URLs that exist there, but not in + // the history DB, so we can add them to our local history DB. + std::vector<int64> obsolete_nodes; + std::vector<int64> sync_ids; + typed_url_root.GetChildIds(&sync_ids); + + for (std::vector<int64>::const_iterator it = sync_ids.begin(); + it != sync_ids.end(); ++it) { + syncer::ReadNode sync_child_node(&trans); + if (sync_child_node.InitByIdLookup(*it) != syncer::BaseNode::INIT_OK) { + return error_handler_->CreateAndUploadError( + FROM_HERE, + "Failed to fetch child node.", + model_type()); + } + const sync_pb::TypedUrlSpecifics& typed_url( + sync_child_node.GetTypedUrlSpecifics()); + + // Ignore old sync nodes that don't have any transition data stored with + // them, or transition data that does not match the visit data (will be + // deleted below). + if (typed_url.visit_transitions_size() == 0 || + typed_url.visit_transitions_size() != typed_url.visits_size()) { + // Generate a debug assertion to help track down http://crbug.com/91473, + // even though we gracefully handle this case by throwing away this + // node. + DCHECK_EQ(typed_url.visits_size(), typed_url.visit_transitions_size()); + DVLOG(1) << "Deleting obsolete sync node with no visit " + << "transition info."; + obsolete_nodes.push_back(sync_child_node.GetId()); + continue; + } + + if (typed_url.url().empty()) { + DVLOG(1) << "Ignoring empty URL in sync DB"; + continue; + } + + // Now, get rid of the expired visits, and if there are no un-expired + // visits left, just ignore this node. + sync_pb::TypedUrlSpecifics filtered_url = FilterExpiredVisits(typed_url); + if (filtered_url.visits_size() == 0) { + DVLOG(1) << "Ignoring expired URL in sync DB: " << filtered_url.url(); + continue; + } + + if (current_urls.find(filtered_url.url()) == current_urls.end()) { + // Update the local DB from the sync DB. Since we are doing our + // initial model association, we don't want to remove any of the + // existing visits (pass NULL as |visits_to_remove|). + UpdateFromSyncDB(filtered_url, + &new_visits, + NULL, + &updated_urls, + &new_urls); + local_merge_result->set_num_items_added( + local_merge_result->num_items_added() + 1); + } + } + + // If we encountered any obsolete nodes, remove them so they don't hang + // around and confuse people looking at the sync node browser. + if (!obsolete_nodes.empty()) { + for (std::vector<int64>::const_iterator it = obsolete_nodes.begin(); + it != obsolete_nodes.end(); + ++it) { + syncer::WriteNode sync_node(&trans); + if (sync_node.InitByIdLookup(*it) != syncer::BaseNode::INIT_OK) { + return error_handler_->CreateAndUploadError( + FROM_HERE, + "Failed to fetch obsolete node.", + model_type()); + } + sync_node.Tombstone(); + } + } + syncer_merge_result->set_num_items_after_association( + typed_url_root.GetTotalNodeCount()); + } + + // Since we're on the history thread, we don't have to worry about updating + // the history database after closing the write transaction, since + // this is the only thread that writes to the database. We also don't have + // to worry about the sync model getting out of sync, because changes are + // propagated to the ChangeProcessor on this thread. + WriteToHistoryBackend(&new_urls, &updated_urls, &new_visits, NULL); + local_merge_result->set_num_items_modified(updated_urls.size()); + local_merge_result->set_num_items_after_association( + local_merge_result->num_items_before_association() + + local_merge_result->num_items_added()); + return syncer::SyncError(); +} + +void TypedUrlModelAssociator::UpdateFromSyncDB( + const sync_pb::TypedUrlSpecifics& typed_url, + TypedUrlVisitVector* visits_to_add, + history::VisitVector* visits_to_remove, + history::URLRows* updated_urls, + history::URLRows* new_urls) { + history::URLRow new_url(GURL(typed_url.url())); + history::VisitVector existing_visits; + bool existing_url = history_backend_->GetURL(new_url.url(), &new_url); + if (existing_url) { + // This URL already exists locally - fetch the visits so we can + // merge them below. + if (!FixupURLAndGetVisits(&new_url, &existing_visits)) { + // Couldn't load the visits for this URL due to some kind of DB error. + // Don't bother writing this URL to the history DB (if we ignore the + // error and continue, we might end up duplicating existing visits). + DLOG(ERROR) << "Could not load visits for url: " << new_url.url(); + return; + } + } + visits_to_add->push_back(std::pair<GURL, std::vector<history::VisitInfo> >( + new_url.url(), std::vector<history::VisitInfo>())); + + // Update the URL with information from the typed URL. + UpdateURLRowFromTypedUrlSpecifics(typed_url, &new_url); + + // Figure out which visits we need to add. + DiffVisits(existing_visits, typed_url, &visits_to_add->back().second, + visits_to_remove); + + if (existing_url) { + updated_urls->push_back(new_url); + } else { + new_urls->push_back(new_url); + } +} + +sync_pb::TypedUrlSpecifics TypedUrlModelAssociator::FilterExpiredVisits( + const sync_pb::TypedUrlSpecifics& source) { + // Make a copy of the source, then regenerate the visits. + sync_pb::TypedUrlSpecifics specifics(source); + specifics.clear_visits(); + specifics.clear_visit_transitions(); + for (int i = 0; i < source.visits_size(); ++i) { + base::Time time = base::Time::FromInternalValue(source.visits(i)); + if (!history_backend_->IsExpiredVisitTime(time)) { + specifics.add_visits(source.visits(i)); + specifics.add_visit_transitions(source.visit_transitions(i)); + } + } + DCHECK(specifics.visits_size() == specifics.visit_transitions_size()); + return specifics; +} + +bool TypedUrlModelAssociator::DeleteAllNodes( + syncer::WriteTransaction* trans) { + DCHECK(expected_loop_ == base::MessageLoop::current()); + + // Just walk through all our child nodes and delete them. + syncer::ReadNode typed_url_root(trans); + if (typed_url_root.InitTypeRoot(syncer::TYPED_URLS) != + syncer::BaseNode::INIT_OK) { + LOG(ERROR) << "Could not lookup root node"; + return false; + } + + std::vector<int64> sync_ids; + typed_url_root.GetChildIds(&sync_ids); + + for (std::vector<int64>::const_iterator it = sync_ids.begin(); + it != sync_ids.end(); ++it) { + syncer::WriteNode sync_child_node(trans); + if (sync_child_node.InitByIdLookup(*it) != syncer::BaseNode::INIT_OK) { + LOG(ERROR) << "Typed url node lookup failed."; + return false; + } + sync_child_node.Tombstone(); + } + return true; +} + +syncer::SyncError TypedUrlModelAssociator::DisassociateModels() { + return syncer::SyncError(); +} + +void TypedUrlModelAssociator::AbortAssociation() { + base::AutoLock lock(abort_lock_); + abort_requested_ = true; +} + +bool TypedUrlModelAssociator::SyncModelHasUserCreatedNodes(bool* has_nodes) { + DCHECK(has_nodes); + *has_nodes = false; + syncer::ReadTransaction trans(FROM_HERE, sync_service_->GetUserShare()); + syncer::ReadNode sync_node(&trans); + if (sync_node.InitTypeRoot(syncer::TYPED_URLS) != syncer::BaseNode::INIT_OK) { + LOG(ERROR) << "Server did not create the top-level typed_url node. We " + << "might be running against an out-of-date server."; + return false; + } + + // The sync model has user created nodes if the typed_url folder has any + // children. + *has_nodes = sync_node.HasChildren(); + return true; +} + +void TypedUrlModelAssociator::WriteToHistoryBackend( + const history::URLRows* new_urls, + const history::URLRows* updated_urls, + const TypedUrlVisitVector* new_visits, + const history::VisitVector* deleted_visits) { + if (new_urls) { + history_backend_->AddPagesWithDetails(*new_urls, history::SOURCE_SYNCED); + } + if (updated_urls) { + ++num_db_accesses_; + // These are existing entries in the URL database. We don't verify the + // visit_count or typed_count values here, because either one (or both) + // could be zero in the case of bookmarks, or in the case of a URL + // transitioning from non-typed to typed as a result of this sync. + // In the field we sometimes run into errors on specific URLs. It's OK to + // just continue, as we can try writing again on the next model association. + size_t num_successful_updates = history_backend_->UpdateURLs(*updated_urls); + num_db_errors_ += updated_urls->size() - num_successful_updates; + } + if (new_visits) { + for (TypedUrlVisitVector::const_iterator visits = new_visits->begin(); + visits != new_visits->end(); ++visits) { + // If there are no visits to add, just skip this. + if (visits->second.empty()) + continue; + ++num_db_accesses_; + if (!history_backend_->AddVisits(visits->first, visits->second, + history::SOURCE_SYNCED)) { + ++num_db_errors_; + DLOG(ERROR) << "Could not add visits."; + } + } + } + if (deleted_visits) { + ++num_db_accesses_; + if (!history_backend_->RemoveVisits(*deleted_visits)) { + ++num_db_errors_; + DLOG(ERROR) << "Could not remove visits."; + // This is bad news, since it means we may end up resurrecting history + // entries on the next reload. It's unavoidable so we'll just keep on + // syncing. + } + } +} + +// static +TypedUrlModelAssociator::MergeResult TypedUrlModelAssociator::MergeUrls( + const sync_pb::TypedUrlSpecifics& node, + const history::URLRow& url, + history::VisitVector* visits, + history::URLRow* new_url, + std::vector<history::VisitInfo>* new_visits) { + DCHECK(new_url); + DCHECK(!node.url().compare(url.url().spec())); + DCHECK(!node.url().compare(new_url->url().spec())); + DCHECK(visits->size()); + CHECK_EQ(node.visits_size(), node.visit_transitions_size()); + + // If we have an old-format node (before we added the visits and + // visit_transitions arrays to the protobuf) or else the node only contained + // expired visits, so just overwrite it with our local history data. + if (node.visits_size() == 0) + return DIFF_UPDATE_NODE; + + // Convert these values only once. + base::string16 node_title(base::UTF8ToUTF16(node.title())); + base::Time node_last_visit = base::Time::FromInternalValue( + node.visits(node.visits_size() - 1)); + + // This is a bitfield representing what we'll need to update with the output + // value. + MergeResult different = DIFF_NONE; + + // Check if the non-incremented values changed. + if ((node_title.compare(url.title()) != 0) || + (node.hidden() != url.hidden())) { + // Use the values from the most recent visit. + if (node_last_visit >= url.last_visit()) { + new_url->set_title(node_title); + new_url->set_hidden(node.hidden()); + different |= DIFF_LOCAL_ROW_CHANGED; + } else { + new_url->set_title(url.title()); + new_url->set_hidden(url.hidden()); + different |= DIFF_UPDATE_NODE; + } + } else { + // No difference. + new_url->set_title(url.title()); + new_url->set_hidden(url.hidden()); + } + + size_t node_num_visits = node.visits_size(); + size_t history_num_visits = visits->size(); + size_t node_visit_index = 0; + size_t history_visit_index = 0; + base::Time earliest_history_time = (*visits)[0].visit_time; + // Walk through the two sets of visits and figure out if any new visits were + // added on either side. + while (node_visit_index < node_num_visits || + history_visit_index < history_num_visits) { + // Time objects are initialized to "earliest possible time". + base::Time node_time, history_time; + if (node_visit_index < node_num_visits) + node_time = base::Time::FromInternalValue(node.visits(node_visit_index)); + if (history_visit_index < history_num_visits) + history_time = (*visits)[history_visit_index].visit_time; + if (node_visit_index >= node_num_visits || + (history_visit_index < history_num_visits && + node_time > history_time)) { + // We found a visit in the history DB that doesn't exist in the sync DB, + // so mark the node as modified so the caller will update the sync node. + different |= DIFF_UPDATE_NODE; + ++history_visit_index; + } else if (history_visit_index >= history_num_visits || + node_time < history_time) { + // Found a visit in the sync node that doesn't exist in the history DB, so + // add it to our list of new visits and set the appropriate flag so the + // caller will update the history DB. + // If the node visit is older than any existing visit in the history DB, + // don't re-add it - this keeps us from resurrecting visits that were + // aged out locally. + if (node_time > earliest_history_time) { + different |= DIFF_LOCAL_VISITS_ADDED; + new_visits->push_back(history::VisitInfo( + node_time, + ui::PageTransitionFromInt( + node.visit_transitions(node_visit_index)))); + } + // This visit is added to visits below. + ++node_visit_index; + } else { + // Same (already synced) entry found in both DBs - no need to do anything. + ++node_visit_index; + ++history_visit_index; + } + } + + DCHECK(CheckVisitOrdering(*visits)); + if (different & DIFF_LOCAL_VISITS_ADDED) { + // Insert new visits into the apropriate place in the visits vector. + history::VisitVector::iterator visit_ix = visits->begin(); + for (std::vector<history::VisitInfo>::iterator new_visit = + new_visits->begin(); + new_visit != new_visits->end(); ++new_visit) { + while (visit_ix != visits->end() && + new_visit->first > visit_ix->visit_time) { + ++visit_ix; + } + visit_ix = visits->insert(visit_ix, + history::VisitRow(url.id(), new_visit->first, + 0, new_visit->second, 0)); + ++visit_ix; + } + } + DCHECK(CheckVisitOrdering(*visits)); + + new_url->set_last_visit(visits->back().visit_time); + return different; +} + +// static +void TypedUrlModelAssociator::WriteToSyncNode( + const history::URLRow& url, + const history::VisitVector& visits, + syncer::WriteNode* node) { + sync_pb::TypedUrlSpecifics typed_url; + WriteToTypedUrlSpecifics(url, visits, &typed_url); + node->SetTypedUrlSpecifics(typed_url); +} + +void TypedUrlModelAssociator::WriteToTypedUrlSpecifics( + const history::URLRow& url, + const history::VisitVector& visits, + sync_pb::TypedUrlSpecifics* typed_url) { + + DCHECK(!url.last_visit().is_null()); + DCHECK(!visits.empty()); + DCHECK_EQ(url.last_visit().ToInternalValue(), + visits.back().visit_time.ToInternalValue()); + + typed_url->set_url(url.url().spec()); + typed_url->set_title(base::UTF16ToUTF8(url.title())); + typed_url->set_hidden(url.hidden()); + + DCHECK(CheckVisitOrdering(visits)); + + bool only_typed = false; + int skip_count = 0; + + if (visits.size() > static_cast<size_t>(kMaxTypedUrlVisits)) { + int typed_count = 0; + int total = 0; + // Walk the passed-in visit vector and count the # of typed visits. + for (history::VisitVector::const_iterator visit = visits.begin(); + visit != visits.end(); ++visit) { + ui::PageTransition transition = + ui::PageTransitionStripQualifier(visit->transition); + // We ignore reload visits. + if (transition == ui::PAGE_TRANSITION_RELOAD) + continue; + ++total; + if (transition == ui::PAGE_TRANSITION_TYPED) + ++typed_count; + } + // We should have at least one typed visit. This can sometimes happen if + // the history DB has an inaccurate count for some reason (there's been + // bugs in the history code in the past which has left users in the wild + // with incorrect counts - http://crbug.com/84258). + DCHECK_GT(typed_count, 0); + + if (typed_count > kMaxTypedUrlVisits) { + only_typed = true; + skip_count = typed_count - kMaxTypedUrlVisits; + } else if (total > kMaxTypedUrlVisits) { + skip_count = total - kMaxTypedUrlVisits; + } + } + + + for (history::VisitVector::const_iterator visit = visits.begin(); + visit != visits.end(); ++visit) { + ui::PageTransition transition = + ui::PageTransitionStripQualifier(visit->transition); + // Skip reload visits. + if (transition == ui::PAGE_TRANSITION_RELOAD) + continue; + + // If we only have room for typed visits, then only add typed visits. + if (only_typed && transition != ui::PAGE_TRANSITION_TYPED) + continue; + + if (skip_count > 0) { + // We have too many entries to fit, so we need to skip the oldest ones. + // Only skip typed URLs if there are too many typed URLs to fit. + if (only_typed || transition != ui::PAGE_TRANSITION_TYPED) { + --skip_count; + continue; + } + } + typed_url->add_visits(visit->visit_time.ToInternalValue()); + typed_url->add_visit_transitions(visit->transition); + } + DCHECK_EQ(skip_count, 0); + + if (typed_url->visits_size() == 0) { + // If we get here, it's because we don't actually have any TYPED visits + // even though the visit's typed_count > 0 (corrupted typed_count). So + // let's go ahead and add a RELOAD visit at the most recent visit since + // it's not legal to have an empty visit array (yet another workaround + // for http://crbug.com/84258). + typed_url->add_visits(url.last_visit().ToInternalValue()); + typed_url->add_visit_transitions(ui::PAGE_TRANSITION_RELOAD); + } + CHECK_GT(typed_url->visits_size(), 0); + CHECK_LE(typed_url->visits_size(), kMaxTypedUrlVisits); + CHECK_EQ(typed_url->visits_size(), typed_url->visit_transitions_size()); +} + +// static +void TypedUrlModelAssociator::DiffVisits( + const history::VisitVector& old_visits, + const sync_pb::TypedUrlSpecifics& new_url, + std::vector<history::VisitInfo>* new_visits, + history::VisitVector* removed_visits) { + DCHECK(new_visits); + size_t old_visit_count = old_visits.size(); + size_t new_visit_count = new_url.visits_size(); + size_t old_index = 0; + size_t new_index = 0; + while (old_index < old_visit_count && new_index < new_visit_count) { + base::Time new_visit_time = + base::Time::FromInternalValue(new_url.visits(new_index)); + if (old_visits[old_index].visit_time < new_visit_time) { + if (new_index > 0 && removed_visits) { + // If there are visits missing from the start of the node, that + // means that they were probably clipped off due to our code that + // limits the size of the sync nodes - don't delete them from our + // local history. + removed_visits->push_back(old_visits[old_index]); + } + ++old_index; + } else if (old_visits[old_index].visit_time > new_visit_time) { + new_visits->push_back(history::VisitInfo( + new_visit_time, + ui::PageTransitionFromInt( + new_url.visit_transitions(new_index)))); + ++new_index; + } else { + ++old_index; + ++new_index; + } + } + + if (removed_visits) { + for ( ; old_index < old_visit_count; ++old_index) { + removed_visits->push_back(old_visits[old_index]); + } + } + + for ( ; new_index < new_visit_count; ++new_index) { + new_visits->push_back(history::VisitInfo( + base::Time::FromInternalValue(new_url.visits(new_index)), + ui::PageTransitionFromInt(new_url.visit_transitions(new_index)))); + } +} + + +// static +void TypedUrlModelAssociator::UpdateURLRowFromTypedUrlSpecifics( + const sync_pb::TypedUrlSpecifics& typed_url, history::URLRow* new_url) { + DCHECK_GT(typed_url.visits_size(), 0); + CHECK_EQ(typed_url.visit_transitions_size(), typed_url.visits_size()); + new_url->set_title(base::UTF8ToUTF16(typed_url.title())); + new_url->set_hidden(typed_url.hidden()); + // Only provide the initial value for the last_visit field - after that, let + // the history code update the last_visit field on its own. + if (new_url->last_visit().is_null()) { + new_url->set_last_visit(base::Time::FromInternalValue( + typed_url.visits(typed_url.visits_size() - 1))); + } +} + +bool TypedUrlModelAssociator::CryptoReadyIfNecessary() { + // We only access the cryptographer while holding a transaction. + syncer::ReadTransaction trans(FROM_HERE, sync_service_->GetUserShare()); + const syncer::ModelTypeSet encrypted_types = trans.GetEncryptedTypes(); + return !encrypted_types.Has(syncer::TYPED_URLS) || + sync_service_->IsCryptographerReady(&trans); +} + +} // namespace browser_sync diff --git a/components/history/core/browser/typed_url_model_associator.h b/components/history/core/browser/typed_url_model_associator.h new file mode 100644 index 0000000..35db2eb --- /dev/null +++ b/components/history/core/browser/typed_url_model_associator.h @@ -0,0 +1,208 @@ +// Copyright 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. + +#ifndef COMPONENTS_HISTORY_CORE_BROWSER_TYPED_URL_MODEL_ASSOCIATOR_H_ +#define COMPONENTS_HISTORY_CORE_BROWSER_TYPED_URL_MODEL_ASSOCIATOR_H_ + +#include <map> +#include <string> +#include <utility> +#include <vector> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/strings/string16.h" +#include "components/history/core/browser/history_types.h" +#include "components/sync_driver/data_type_error_handler.h" +#include "components/sync_driver/model_associator.h" +#include "sync/protocol/typed_url_specifics.pb.h" + +class GURL; + +namespace base { +class MessageLoop; +} + +namespace history { +class HistoryBackend; +class URLRow; +}; + +namespace syncer { +class WriteNode; +class WriteTransaction; +}; + +namespace sync_driver { +class SyncService; +} + +namespace browser_sync { + +// Contains all model association related logic: +// * Algorithm to associate typed_url model and sync model. +// * Persisting model associations and loading them back. +// We do not check if we have local data before this run; we always +// merge and sync. +class TypedUrlModelAssociator : public sync_driver::AssociatorInterface { + public: + typedef std::vector<std::pair<GURL, std::vector<history::VisitInfo> > > + TypedUrlVisitVector; + + static syncer::ModelType model_type() { return syncer::TYPED_URLS; } + TypedUrlModelAssociator(sync_driver::SyncService* sync_service, + history::HistoryBackend* history_backend, + sync_driver::DataTypeErrorHandler* error_handler); + ~TypedUrlModelAssociator() override; + + // AssociatorInterface implementation. + // + // Iterates through the sync model looking for matched pairs of items. + syncer::SyncError AssociateModels( + syncer::SyncMergeResult* local_merge_result, + syncer::SyncMergeResult* syncer_merge_result) override; + + // Clears all associations. + syncer::SyncError DisassociateModels() override; + + // Called from the main thread, to abort the currently active model + // association (for example, if we are shutting down). + void AbortAssociation() override; + + // The has_nodes out param is true if the sync model has nodes other + // than the permanent tagged nodes. + bool SyncModelHasUserCreatedNodes(bool* has_nodes) override; + + bool CryptoReadyIfNecessary() override; + + // Delete all typed url nodes. + bool DeleteAllNodes(syncer::WriteTransaction* trans); + + void WriteToHistoryBackend(const history::URLRows* new_urls, + const history::URLRows* updated_urls, + const TypedUrlVisitVector* new_visits, + const history::VisitVector* deleted_visits); + + // Given a typed URL in the sync DB, looks for an existing entry in the + // local history DB and generates a list of visits to add to the + // history DB to bring it up to date (avoiding duplicates). + // Updates the passed |visits_to_add| and |visits_to_remove| vectors with the + // visits to add to/remove from the history DB, and adds a new entry to either + // |updated_urls| or |new_urls| depending on whether the URL already existed + // in the history DB. + void UpdateFromSyncDB(const sync_pb::TypedUrlSpecifics& typed_url, + TypedUrlVisitVector* visits_to_add, + history::VisitVector* visits_to_remove, + history::URLRows* updated_urls, + history::URLRows* new_urls); + + // Given a TypedUrlSpecifics object, removes all visits that are older than + // the current expiration time. Note that this can result in having no visits + // at all. + sync_pb::TypedUrlSpecifics FilterExpiredVisits( + const sync_pb::TypedUrlSpecifics& specifics); + + // Returns the percentage of DB accesses that have resulted in an error. + int GetErrorPercentage() const; + + // Bitfield returned from MergeUrls to specify the result of the merge. + typedef uint32 MergeResult; + static const MergeResult DIFF_NONE = 0; + static const MergeResult DIFF_UPDATE_NODE = 1 << 0; + static const MergeResult DIFF_LOCAL_ROW_CHANGED = 1 << 1; + static const MergeResult DIFF_LOCAL_VISITS_ADDED = 1 << 2; + + // Merges the URL information in |typed_url| with the URL information from the + // history database in |url| and |visits|, and returns a bitmask with the + // results of the merge: + // DIFF_UPDATE_NODE - changes have been made to |new_url| and |visits| which + // should be persisted to the sync node. + // DIFF_LOCAL_ROW_CHANGED - The history data in |new_url| should be persisted + // to the history DB. + // DIFF_LOCAL_VISITS_ADDED - |new_visits| contains a list of visits that + // should be written to the history DB for this URL. Deletions are not + // written to the DB - each client is left to age out visits on their own. + static MergeResult MergeUrls(const sync_pb::TypedUrlSpecifics& typed_url, + const history::URLRow& url, + history::VisitVector* visits, + history::URLRow* new_url, + std::vector<history::VisitInfo>* new_visits); + static void WriteToSyncNode(const history::URLRow& url, + const history::VisitVector& visits, + syncer::WriteNode* node); + + // Diffs the set of visits between the history DB and the sync DB, using the + // sync DB as the canonical copy. Result is the set of |new_visits| and + // |removed_visits| that can be applied to the history DB to make it match + // the sync DB version. |removed_visits| can be null if the caller does not + // care about which visits to remove. + static void DiffVisits(const history::VisitVector& old_visits, + const sync_pb::TypedUrlSpecifics& new_url, + std::vector<history::VisitInfo>* new_visits, + history::VisitVector* removed_visits); + + // Converts the passed URL information to a TypedUrlSpecifics structure for + // writing to the sync DB + static void WriteToTypedUrlSpecifics(const history::URLRow& url, + const history::VisitVector& visits, + sync_pb::TypedUrlSpecifics* specifics); + + // Fetches visits from the history DB corresponding to the passed URL. This + // function compensates for the fact that the history DB has rather poor data + // integrity (duplicate visits, visit timestamps that don't match the + // last_visit timestamp, huge data sets that exhaust memory when fetched, + // etc) by modifying the passed |url| object and |visits| vector. + // Returns false if we could not fetch the visits for the passed URL, and + // tracks DB error statistics internally for reporting via UMA. + bool FixupURLAndGetVisits(history::URLRow* url, + history::VisitVector* visits); + + // Updates the passed |url_row| based on the values in |specifics|. Fields + // that are not contained in |specifics| (such as typed_count) are left + // unchanged. + static void UpdateURLRowFromTypedUrlSpecifics( + const sync_pb::TypedUrlSpecifics& specifics, history::URLRow* url_row); + + // Helper function that determines if we should ignore a URL for the purposes + // of sync, because it contains invalid data. + bool ShouldIgnoreUrl(const GURL& url); + + protected: + // Helper function that clears our error counters (used to reset stats after + // model association so we can track model association errors separately). + // Overridden by tests. + virtual void ClearErrorStats(); + + private: + // Helper routine that actually does the work of associating models. + syncer::SyncError DoAssociateModels( + syncer::SyncMergeResult* local_merge_result, + syncer::SyncMergeResult* syncer_merge_result); + + // Helper function that determines if we should ignore a URL for the purposes + // of sync, based on the visits the URL had. + bool ShouldIgnoreVisits(const history::VisitVector& visits); + + sync_driver::SyncService* sync_service_; + history::HistoryBackend* history_backend_; + + base::MessageLoop* expected_loop_; + + bool abort_requested_; + base::Lock abort_lock_; + + // Guaranteed to outlive datatypes. + sync_driver::DataTypeErrorHandler* error_handler_; + + // Statistics for the purposes of tracking the percentage of DB accesses that + // fail for each client via UMA. + int num_db_accesses_; + int num_db_errors_; + + DISALLOW_COPY_AND_ASSIGN(TypedUrlModelAssociator); +}; + +} // namespace browser_sync + +#endif // COMPONENTS_HISTORY_CORE_BROWSER_TYPED_URL_MODEL_ASSOCIATOR_H_ |