1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/sync/glue/typed_url_change_processor.h" 6 7#include "base/location.h" 8#include "base/metrics/histogram.h" 9#include "base/strings/string_util.h" 10#include "base/strings/utf_string_conversions.h" 11#include "chrome/browser/chrome_notification_types.h" 12#include "chrome/browser/history/history_backend.h" 13#include "chrome/browser/history/history_notifications.h" 14#include "chrome/browser/profiles/profile.h" 15#include "chrome/browser/sync/glue/typed_url_model_associator.h" 16#include "chrome/browser/sync/profile_sync_service.h" 17#include "content/public/browser/browser_thread.h" 18#include "content/public/browser/notification_service.h" 19#include "sync/internal_api/public/change_record.h" 20#include "sync/internal_api/public/read_node.h" 21#include "sync/internal_api/public/write_node.h" 22#include "sync/internal_api/public/write_transaction.h" 23#include "sync/protocol/typed_url_specifics.pb.h" 24#include "sync/syncable/entry.h" // TODO(tim): Investigating bug 121587. 25 26using content::BrowserThread; 27 28namespace browser_sync { 29 30// This is the threshold at which we start throttling sync updates for typed 31// URLs - any URLs with a typed_count >= this threshold will be throttled. 32static const int kTypedUrlVisitThrottleThreshold = 10; 33 34// This is the multiple we use when throttling sync updates. If the multiple is 35// N, we sync up every Nth update (i.e. when typed_count % N == 0). 36static const int kTypedUrlVisitThrottleMultiple = 10; 37 38TypedUrlChangeProcessor::TypedUrlChangeProcessor( 39 Profile* profile, 40 TypedUrlModelAssociator* model_associator, 41 history::HistoryBackend* history_backend, 42 sync_driver::DataTypeErrorHandler* error_handler) 43 : sync_driver::ChangeProcessor(error_handler), 44 profile_(profile), 45 model_associator_(model_associator), 46 history_backend_(history_backend), 47 backend_loop_(base::MessageLoop::current()), 48 disconnected_(false) { 49 DCHECK(model_associator); 50 DCHECK(history_backend); 51 DCHECK(error_handler); 52 DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI)); 53 // When running in unit tests, there is already a NotificationService object. 54 // Since only one can exist at a time per thread, check first. 55 if (!content::NotificationService::current()) 56 notification_service_.reset(content::NotificationService::Create()); 57} 58 59TypedUrlChangeProcessor::~TypedUrlChangeProcessor() { 60 DCHECK(backend_loop_ == base::MessageLoop::current()); 61} 62 63void TypedUrlChangeProcessor::Observe( 64 int type, 65 const content::NotificationSource& source, 66 const content::NotificationDetails& details) { 67 DCHECK(backend_loop_ == base::MessageLoop::current()); 68 69 base::AutoLock al(disconnect_lock_); 70 if (disconnected_) 71 return; 72 73 DVLOG(1) << "Observed typed_url change."; 74 if (type == chrome::NOTIFICATION_HISTORY_URLS_MODIFIED) { 75 HandleURLsModified( 76 content::Details<history::URLsModifiedDetails>(details).ptr()); 77 } else if (type == chrome::NOTIFICATION_HISTORY_URLS_DELETED) { 78 HandleURLsDeleted( 79 content::Details<history::URLsDeletedDetails>(details).ptr()); 80 } else { 81 DCHECK_EQ(chrome::NOTIFICATION_HISTORY_URL_VISITED, type); 82 HandleURLsVisited( 83 content::Details<history::URLVisitedDetails>(details).ptr()); 84 } 85 UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors", 86 model_associator_->GetErrorPercentage()); 87} 88 89void TypedUrlChangeProcessor::HandleURLsModified( 90 history::URLsModifiedDetails* details) { 91 92 syncer::WriteTransaction trans(FROM_HERE, share_handle()); 93 for (history::URLRows::iterator url = details->changed_urls.begin(); 94 url != details->changed_urls.end(); ++url) { 95 if (url->typed_count() > 0) { 96 // If there were any errors updating the sync node, just ignore them and 97 // continue on to process the next URL. 98 CreateOrUpdateSyncNode(*url, &trans); 99 } 100 } 101} 102 103bool TypedUrlChangeProcessor::CreateOrUpdateSyncNode( 104 history::URLRow url, syncer::WriteTransaction* trans) { 105 DCHECK_GT(url.typed_count(), 0); 106 // Get the visits for this node. 107 history::VisitVector visit_vector; 108 if (!model_associator_->FixupURLAndGetVisits(&url, &visit_vector)) { 109 DLOG(ERROR) << "Could not load visits for url: " << url.url(); 110 return false; 111 } 112 113 syncer::ReadNode typed_url_root(trans); 114 if (typed_url_root.InitTypeRoot(syncer::TYPED_URLS) != 115 syncer::BaseNode::INIT_OK) { 116 syncer::SyncError error(FROM_HERE, 117 syncer::SyncError::DATATYPE_ERROR, 118 "No top level folder", 119 syncer::TYPED_URLS); 120 error_handler()->OnSingleDataTypeUnrecoverableError(error); 121 return false; 122 } 123 124 if (model_associator_->ShouldIgnoreUrl(url.url())) 125 return true; 126 127 DCHECK(!visit_vector.empty()); 128 std::string tag = url.url().spec(); 129 syncer::WriteNode update_node(trans); 130 syncer::BaseNode::InitByLookupResult result = 131 update_node.InitByClientTagLookup(syncer::TYPED_URLS, tag); 132 if (result == syncer::BaseNode::INIT_OK) { 133 model_associator_->WriteToSyncNode(url, visit_vector, &update_node); 134 } else if (result == syncer::BaseNode::INIT_FAILED_DECRYPT_IF_NECESSARY) { 135 syncer::SyncError error(FROM_HERE, 136 syncer::SyncError::DATATYPE_ERROR, 137 "Failed to decrypt.", 138 syncer::TYPED_URLS); 139 error_handler()->OnSingleDataTypeUnrecoverableError(error); 140 return false; 141 } else { 142 syncer::WriteNode create_node(trans); 143 syncer::WriteNode::InitUniqueByCreationResult result = 144 create_node.InitUniqueByCreation(syncer::TYPED_URLS, 145 typed_url_root, tag); 146 if (result != syncer::WriteNode::INIT_SUCCESS) { 147 148 syncer::SyncError error(FROM_HERE, 149 syncer::SyncError::DATATYPE_ERROR, 150 "Failed to create sync node", 151 syncer::TYPED_URLS); 152 error_handler()->OnSingleDataTypeUnrecoverableError(error); 153 return false; 154 } 155 156 create_node.SetTitle(tag); 157 model_associator_->WriteToSyncNode(url, visit_vector, &create_node); 158 } 159 return true; 160} 161 162void TypedUrlChangeProcessor::HandleURLsDeleted( 163 history::URLsDeletedDetails* details) { 164 syncer::WriteTransaction trans(FROM_HERE, share_handle()); 165 166 // Ignore archivals (we don't want to sync them as deletions, to avoid 167 // extra traffic up to the server, and also to make sure that a client with 168 // a bad clock setting won't go on an archival rampage and delete all 169 // history from every client). The server will gracefully age out the sync DB 170 // entries when they've been idle for long enough. 171 if (details->expired) 172 return; 173 174 if (details->all_history) { 175 if (!model_associator_->DeleteAllNodes(&trans)) { 176 syncer::SyncError error(FROM_HERE, 177 syncer::SyncError::DATATYPE_ERROR, 178 "Failed to delete local nodes.", 179 syncer::TYPED_URLS); 180 error_handler()->OnSingleDataTypeUnrecoverableError(error); 181 return; 182 } 183 } else { 184 for (history::URLRows::const_iterator row = details->rows.begin(); 185 row != details->rows.end(); ++row) { 186 syncer::WriteNode sync_node(&trans); 187 // The deleted URL could have been non-typed, so it might not be found 188 // in the sync DB. 189 if (sync_node.InitByClientTagLookup(syncer::TYPED_URLS, 190 row->url().spec()) == 191 syncer::BaseNode::INIT_OK) { 192 sync_node.Tombstone(); 193 } 194 } 195 } 196} 197 198void TypedUrlChangeProcessor::HandleURLsVisited( 199 history::URLVisitedDetails* details) { 200 if (!ShouldSyncVisit(details)) 201 return; 202 203 syncer::WriteTransaction trans(FROM_HERE, share_handle()); 204 CreateOrUpdateSyncNode(details->row, &trans); 205} 206 207bool TypedUrlChangeProcessor::ShouldSyncVisit( 208 history::URLVisitedDetails* details) { 209 int typed_count = details->row.typed_count(); 210 ui::PageTransition transition = 211 ui::PageTransitionStripQualifier(details->transition); 212 213 // Just use an ad-hoc criteria to determine whether to ignore this 214 // notification. For most users, the distribution of visits is roughly a bell 215 // curve with a long tail - there are lots of URLs with < 5 visits so we want 216 // to make sure we sync up every visit to ensure the proper ordering of 217 // suggestions. But there are relatively few URLs with > 10 visits, and those 218 // tend to be more broadly distributed such that there's no need to sync up 219 // every visit to preserve their relative ordering. 220 return (transition == ui::PAGE_TRANSITION_TYPED && 221 typed_count > 0 && 222 (typed_count < kTypedUrlVisitThrottleThreshold || 223 (typed_count % kTypedUrlVisitThrottleMultiple) == 0)); 224} 225 226void TypedUrlChangeProcessor::ApplyChangesFromSyncModel( 227 const syncer::BaseTransaction* trans, 228 int64 model_version, 229 const syncer::ImmutableChangeRecordList& changes) { 230 DCHECK(backend_loop_ == base::MessageLoop::current()); 231 232 base::AutoLock al(disconnect_lock_); 233 if (disconnected_) 234 return; 235 236 syncer::ReadNode typed_url_root(trans); 237 if (typed_url_root.InitTypeRoot(syncer::TYPED_URLS) != 238 syncer::BaseNode::INIT_OK) { 239 syncer::SyncError error(FROM_HERE, 240 syncer::SyncError::DATATYPE_ERROR, 241 "Failed to init type root.", 242 syncer::TYPED_URLS); 243 error_handler()->OnSingleDataTypeUnrecoverableError(error); 244 return; 245 } 246 247 DCHECK(pending_new_urls_.empty() && pending_new_visits_.empty() && 248 pending_deleted_visits_.empty() && pending_updated_urls_.empty() && 249 pending_deleted_urls_.empty()); 250 251 for (syncer::ChangeRecordList::const_iterator it = 252 changes.Get().begin(); it != changes.Get().end(); ++it) { 253 if (syncer::ChangeRecord::ACTION_DELETE == 254 it->action) { 255 DCHECK(it->specifics.has_typed_url()) << 256 "Typed URL delete change does not have necessary specifics."; 257 GURL url(it->specifics.typed_url().url()); 258 pending_deleted_urls_.push_back(url); 259 continue; 260 } 261 262 syncer::ReadNode sync_node(trans); 263 if (sync_node.InitByIdLookup(it->id) != syncer::BaseNode::INIT_OK) { 264 syncer::SyncError error(FROM_HERE, 265 syncer::SyncError::DATATYPE_ERROR, 266 "Failed to init sync node.", 267 syncer::TYPED_URLS); 268 error_handler()->OnSingleDataTypeUnrecoverableError(error); 269 return; 270 } 271 272 // Check that the changed node is a child of the typed_urls folder. 273 DCHECK(typed_url_root.GetId() == sync_node.GetParentId()); 274 DCHECK(syncer::TYPED_URLS == sync_node.GetModelType()); 275 276 const sync_pb::TypedUrlSpecifics& typed_url( 277 sync_node.GetTypedUrlSpecifics()); 278 DCHECK(typed_url.visits_size()); 279 280 if (model_associator_->ShouldIgnoreUrl(GURL(typed_url.url()))) 281 continue; 282 283 sync_pb::TypedUrlSpecifics filtered_url = 284 model_associator_->FilterExpiredVisits(typed_url); 285 if (!filtered_url.visits_size()) { 286 continue; 287 } 288 289 model_associator_->UpdateFromSyncDB( 290 filtered_url, &pending_new_visits_, &pending_deleted_visits_, 291 &pending_updated_urls_, &pending_new_urls_); 292 } 293} 294 295void TypedUrlChangeProcessor::CommitChangesFromSyncModel() { 296 DCHECK(backend_loop_ == base::MessageLoop::current()); 297 298 base::AutoLock al(disconnect_lock_); 299 if (disconnected_) 300 return; 301 302 // Make sure we stop listening for changes while we're modifying the backend, 303 // so we don't try to re-apply these changes to the sync DB. 304 ScopedStopObserving<TypedUrlChangeProcessor> stop_observing(this); 305 if (!pending_deleted_urls_.empty()) 306 history_backend_->DeleteURLs(pending_deleted_urls_); 307 308 model_associator_->WriteToHistoryBackend(&pending_new_urls_, 309 &pending_updated_urls_, 310 &pending_new_visits_, 311 &pending_deleted_visits_); 312 313 pending_new_urls_.clear(); 314 pending_updated_urls_.clear(); 315 pending_new_visits_.clear(); 316 pending_deleted_visits_.clear(); 317 pending_deleted_urls_.clear(); 318 UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors", 319 model_associator_->GetErrorPercentage()); 320} 321 322void TypedUrlChangeProcessor::Disconnect() { 323 base::AutoLock al(disconnect_lock_); 324 disconnected_ = true; 325} 326 327void TypedUrlChangeProcessor::StartImpl() { 328 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 329 DCHECK(history_backend_); 330 DCHECK(backend_loop_); 331 backend_loop_->PostTask(FROM_HERE, 332 base::Bind(&TypedUrlChangeProcessor::StartObserving, 333 base::Unretained(this))); 334} 335 336void TypedUrlChangeProcessor::StartObserving() { 337 DCHECK(backend_loop_ == base::MessageLoop::current()); 338 DCHECK(profile_); 339 notification_registrar_.Add( 340 this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED, 341 content::Source<Profile>(profile_)); 342 notification_registrar_.Add( 343 this, chrome::NOTIFICATION_HISTORY_URLS_DELETED, 344 content::Source<Profile>(profile_)); 345 notification_registrar_.Add( 346 this, chrome::NOTIFICATION_HISTORY_URL_VISITED, 347 content::Source<Profile>(profile_)); 348} 349 350void TypedUrlChangeProcessor::StopObserving() { 351 DCHECK(backend_loop_ == base::MessageLoop::current()); 352 DCHECK(profile_); 353 notification_registrar_.Remove( 354 this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED, 355 content::Source<Profile>(profile_)); 356 notification_registrar_.Remove( 357 this, chrome::NOTIFICATION_HISTORY_URLS_DELETED, 358 content::Source<Profile>(profile_)); 359 notification_registrar_.Remove( 360 this, chrome::NOTIFICATION_HISTORY_URL_VISITED, 361 content::Source<Profile>(profile_)); 362} 363 364} // namespace browser_sync 365