1 // Copyright 2014 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 "components/suggestions/suggestions_service.h" 6 7 #include <sstream> 8 #include <string> 9 10 #include "base/memory/scoped_ptr.h" 11 #include "base/message_loop/message_loop_proxy.h" 12 #include "base/metrics/histogram.h" 13 #include "base/metrics/sparse_histogram.h" 14 #include "base/strings/string_number_conversions.h" 15 #include "base/strings/string_util.h" 16 #include "base/time/time.h" 17 #include "components/pref_registry/pref_registry_syncable.h" 18 #include "components/suggestions/blacklist_store.h" 19 #include "components/suggestions/suggestions_store.h" 20 #include "components/variations/variations_associated_data.h" 21 #include "components/variations/variations_http_header_provider.h" 22 #include "net/base/escape.h" 23 #include "net/base/load_flags.h" 24 #include "net/base/net_errors.h" 25 #include "net/base/url_util.h" 26 #include "net/http/http_response_headers.h" 27 #include "net/http/http_status_code.h" 28 #include "net/http/http_util.h" 29 #include "net/url_request/url_fetcher.h" 30 #include "net/url_request/url_request_status.h" 31 #include "url/gurl.h" 32 33 using base::CancelableClosure; 34 35 namespace suggestions { 36 37 namespace { 38 39 // Used to UMA log the state of the last response from the server. 40 enum SuggestionsResponseState { 41 RESPONSE_EMPTY, 42 RESPONSE_INVALID, 43 RESPONSE_VALID, 44 RESPONSE_STATE_SIZE 45 }; 46 47 // Will log the supplied response |state|. 48 void LogResponseState(SuggestionsResponseState state) { 49 UMA_HISTOGRAM_ENUMERATION("Suggestions.ResponseState", state, 50 RESPONSE_STATE_SIZE); 51 } 52 53 // Obtains the experiment parameter under the supplied |key|, or empty string 54 // if the parameter does not exist. 55 std::string GetExperimentParam(const std::string& key) { 56 return variations::GetVariationParamValue(kSuggestionsFieldTrialName, key); 57 } 58 59 GURL BuildBlacklistRequestURL(const std::string& blacklist_url_prefix, 60 const GURL& candidate_url) { 61 return GURL(blacklist_url_prefix + 62 net::EscapeQueryParamValue(candidate_url.spec(), true)); 63 } 64 65 // Runs each callback in |requestors| on |suggestions|, then deallocates 66 // |requestors|. 67 void DispatchRequestsAndClear( 68 const SuggestionsProfile& suggestions, 69 std::vector<SuggestionsService::ResponseCallback>* requestors) { 70 std::vector<SuggestionsService::ResponseCallback>::iterator it; 71 for (it = requestors->begin(); it != requestors->end(); ++it) { 72 if (!it->is_null()) it->Run(suggestions); 73 } 74 std::vector<SuggestionsService::ResponseCallback>().swap(*requestors); 75 } 76 77 const int kDefaultRequestTimeoutMs = 200; 78 79 // Default delay used when scheduling a blacklist request. 80 const int kBlacklistDefaultDelaySec = 1; 81 82 // Multiplier on the delay used when scheduling a blacklist request, in case the 83 // last observed request was unsuccessful. 84 const int kBlacklistBackoffMultiplier = 2; 85 86 // Maximum valid delay for scheduling a request. Candidate delays larger than 87 // this are rejected. This means the maximum backoff is at least 300 / 2, i.e. 88 // 2.5 minutes. 89 const int kBlacklistMaxDelaySec = 300; // 5 minutes 90 91 } // namespace 92 93 const char kSuggestionsFieldTrialName[] = "ChromeSuggestions"; 94 const char kSuggestionsFieldTrialURLParam[] = "url"; 95 const char kSuggestionsFieldTrialCommonParamsParam[] = "common_params"; 96 const char kSuggestionsFieldTrialBlacklistPathParam[] = "blacklist_path"; 97 const char kSuggestionsFieldTrialBlacklistUrlParam[] = "blacklist_url_param"; 98 const char kSuggestionsFieldTrialStateParam[] = "state"; 99 const char kSuggestionsFieldTrialControlParam[] = "control"; 100 const char kSuggestionsFieldTrialStateEnabled[] = "enabled"; 101 const char kSuggestionsFieldTrialTimeoutMs[] = "timeout_ms"; 102 103 // The default expiry timeout is 72 hours. 104 const int64 kDefaultExpiryUsec = 72 * base::Time::kMicrosecondsPerHour; 105 106 namespace { 107 108 std::string GetBlacklistUrlPrefix() { 109 std::stringstream blacklist_url_prefix_stream; 110 blacklist_url_prefix_stream 111 << GetExperimentParam(kSuggestionsFieldTrialURLParam) 112 << GetExperimentParam(kSuggestionsFieldTrialBlacklistPathParam) << "?" 113 << GetExperimentParam(kSuggestionsFieldTrialCommonParamsParam) << "&" 114 << GetExperimentParam(kSuggestionsFieldTrialBlacklistUrlParam) << "="; 115 return blacklist_url_prefix_stream.str(); 116 } 117 118 } // namespace 119 120 SuggestionsService::SuggestionsService( 121 net::URLRequestContextGetter* url_request_context, 122 scoped_ptr<SuggestionsStore> suggestions_store, 123 scoped_ptr<ImageManager> thumbnail_manager, 124 scoped_ptr<BlacklistStore> blacklist_store) 125 : suggestions_store_(suggestions_store.Pass()), 126 blacklist_store_(blacklist_store.Pass()), 127 thumbnail_manager_(thumbnail_manager.Pass()), 128 url_request_context_(url_request_context), 129 blacklist_delay_sec_(kBlacklistDefaultDelaySec), 130 request_timeout_ms_(kDefaultRequestTimeoutMs), 131 weak_ptr_factory_(this) { 132 // Obtain various parameters from Variations. 133 suggestions_url_ = 134 GURL(GetExperimentParam(kSuggestionsFieldTrialURLParam) + "?" + 135 GetExperimentParam(kSuggestionsFieldTrialCommonParamsParam)); 136 blacklist_url_prefix_ = GetBlacklistUrlPrefix(); 137 std::string timeout = GetExperimentParam(kSuggestionsFieldTrialTimeoutMs); 138 int temp_timeout; 139 if (!timeout.empty() && base::StringToInt(timeout, &temp_timeout)) { 140 request_timeout_ms_ = temp_timeout; 141 } 142 } 143 144 SuggestionsService::~SuggestionsService() {} 145 146 // static 147 bool SuggestionsService::IsEnabled() { 148 return GetExperimentParam(kSuggestionsFieldTrialStateParam) == 149 kSuggestionsFieldTrialStateEnabled; 150 } 151 152 // static 153 bool SuggestionsService::IsControlGroup() { 154 return GetExperimentParam(kSuggestionsFieldTrialControlParam) == 155 kSuggestionsFieldTrialStateEnabled; 156 } 157 158 void SuggestionsService::FetchSuggestionsData( 159 SyncState sync_state, 160 SuggestionsService::ResponseCallback callback) { 161 DCHECK(thread_checker_.CalledOnValidThread()); 162 if (sync_state == NOT_INITIALIZED_ENABLED) { 163 // Sync is not initialized yet, but enabled. Serve previously cached 164 // suggestions if available. 165 waiting_requestors_.push_back(callback); 166 ServeFromCache(); 167 return; 168 } else if (sync_state == SYNC_OR_HISTORY_SYNC_DISABLED) { 169 // Cancel any ongoing request (and the timeout closure). We must no longer 170 // interact with the server. 171 pending_request_.reset(NULL); 172 pending_timeout_closure_.reset(NULL); 173 suggestions_store_->ClearSuggestions(); 174 callback.Run(SuggestionsProfile()); 175 DispatchRequestsAndClear(SuggestionsProfile(), &waiting_requestors_); 176 return; 177 } 178 179 FetchSuggestionsDataNoTimeout(callback); 180 181 // Post a task to serve the cached suggestions if the request hasn't completed 182 // after some time. Cancels the previous such task, if one existed. 183 pending_timeout_closure_.reset(new CancelableClosure(base::Bind( 184 &SuggestionsService::OnRequestTimeout, weak_ptr_factory_.GetWeakPtr()))); 185 base::MessageLoopProxy::current()->PostDelayedTask( 186 FROM_HERE, pending_timeout_closure_->callback(), 187 base::TimeDelta::FromMilliseconds(request_timeout_ms_)); 188 } 189 190 void SuggestionsService::GetPageThumbnail( 191 const GURL& url, 192 base::Callback<void(const GURL&, const SkBitmap*)> callback) { 193 thumbnail_manager_->GetImageForURL(url, callback); 194 } 195 196 void SuggestionsService::BlacklistURL( 197 const GURL& candidate_url, 198 const SuggestionsService::ResponseCallback& callback) { 199 DCHECK(thread_checker_.CalledOnValidThread()); 200 waiting_requestors_.push_back(callback); 201 202 // Blacklist locally, for immediate effect. 203 if (!blacklist_store_->BlacklistUrl(candidate_url)) { 204 DVLOG(1) << "Failed blacklisting attempt."; 205 return; 206 } 207 208 // If there's an ongoing request, let it complete. 209 if (pending_request_.get()) return; 210 IssueRequest(BuildBlacklistRequestURL(blacklist_url_prefix_, candidate_url)); 211 } 212 213 // static 214 bool SuggestionsService::GetBlacklistedUrl(const net::URLFetcher& request, 215 GURL* url) { 216 bool is_blacklist_request = StartsWithASCII(request.GetOriginalURL().spec(), 217 GetBlacklistUrlPrefix(), true); 218 if (!is_blacklist_request) return false; 219 220 // Extract the blacklisted URL from the blacklist request. 221 std::string blacklisted; 222 if (!net::GetValueForKeyInQuery( 223 request.GetOriginalURL(), 224 GetExperimentParam(kSuggestionsFieldTrialBlacklistUrlParam), 225 &blacklisted)) 226 return false; 227 228 GURL blacklisted_url(blacklisted); 229 blacklisted_url.Swap(url); 230 return true; 231 } 232 233 // static 234 void SuggestionsService::RegisterProfilePrefs( 235 user_prefs::PrefRegistrySyncable* registry) { 236 SuggestionsStore::RegisterProfilePrefs(registry); 237 BlacklistStore::RegisterProfilePrefs(registry); 238 } 239 240 void SuggestionsService::SetDefaultExpiryTimestamp( 241 SuggestionsProfile* suggestions, int64 default_timestamp_usec) { 242 for (int i = 0; i < suggestions->suggestions_size(); ++i) { 243 ChromeSuggestion* suggestion = suggestions->mutable_suggestions(i); 244 // Do not set expiry if the server has already provided a more specific 245 // expiry time for this suggestion. 246 if (!suggestion->has_expiry_ts()) { 247 suggestion->set_expiry_ts(default_timestamp_usec); 248 } 249 } 250 } 251 252 void SuggestionsService::FetchSuggestionsDataNoTimeout( 253 SuggestionsService::ResponseCallback callback) { 254 DCHECK(thread_checker_.CalledOnValidThread()); 255 if (pending_request_.get()) { 256 // Request already exists, so just add requestor to queue. 257 waiting_requestors_.push_back(callback); 258 return; 259 } 260 261 // Form new request. 262 DCHECK(waiting_requestors_.empty()); 263 waiting_requestors_.push_back(callback); 264 IssueRequest(suggestions_url_); 265 } 266 267 void SuggestionsService::IssueRequest(const GURL& url) { 268 pending_request_.reset(CreateSuggestionsRequest(url)); 269 pending_request_->Start(); 270 last_request_started_time_ = base::TimeTicks::Now(); 271 } 272 273 net::URLFetcher* SuggestionsService::CreateSuggestionsRequest(const GURL& url) { 274 net::URLFetcher* request = 275 net::URLFetcher::Create(0, url, net::URLFetcher::GET, this); 276 request->SetLoadFlags(net::LOAD_DISABLE_CACHE); 277 request->SetRequestContext(url_request_context_); 278 // Add Chrome experiment state to the request headers. 279 net::HttpRequestHeaders headers; 280 variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders( 281 request->GetOriginalURL(), false, false, &headers); 282 request->SetExtraRequestHeaders(headers.ToString()); 283 return request; 284 } 285 286 void SuggestionsService::OnRequestTimeout() { 287 DCHECK(thread_checker_.CalledOnValidThread()); 288 ServeFromCache(); 289 } 290 291 void SuggestionsService::OnURLFetchComplete(const net::URLFetcher* source) { 292 DCHECK(thread_checker_.CalledOnValidThread()); 293 DCHECK_EQ(pending_request_.get(), source); 294 // We no longer need the timeout closure. Delete it whether or not it has run. 295 // If it hasn't, this cancels it. 296 pending_timeout_closure_.reset(); 297 298 // The fetcher will be deleted when the request is handled. 299 scoped_ptr<const net::URLFetcher> request(pending_request_.release()); 300 const net::URLRequestStatus& request_status = request->GetStatus(); 301 if (request_status.status() != net::URLRequestStatus::SUCCESS) { 302 UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FailedRequestErrorCode", 303 -request_status.error()); 304 DVLOG(1) << "Suggestions server request failed with error: " 305 << request_status.error() << ": " 306 << net::ErrorToString(request_status.error()); 307 // Dispatch the cached profile on error. 308 ServeFromCache(); 309 ScheduleBlacklistUpload(false); 310 return; 311 } 312 313 // Log the response code. 314 const int response_code = request->GetResponseCode(); 315 UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FetchResponseCode", response_code); 316 if (response_code != net::HTTP_OK) { 317 // Aggressively clear the store. 318 suggestions_store_->ClearSuggestions(); 319 DispatchRequestsAndClear(SuggestionsProfile(), &waiting_requestors_); 320 ScheduleBlacklistUpload(false); 321 return; 322 } 323 324 const base::TimeDelta latency = 325 base::TimeTicks::Now() - last_request_started_time_; 326 UMA_HISTOGRAM_MEDIUM_TIMES("Suggestions.FetchSuccessLatency", latency); 327 328 // Handle a successful blacklisting. 329 GURL blacklisted_url; 330 if (GetBlacklistedUrl(*source, &blacklisted_url)) { 331 blacklist_store_->RemoveUrl(blacklisted_url); 332 } 333 334 std::string suggestions_data; 335 bool success = request->GetResponseAsString(&suggestions_data); 336 DCHECK(success); 337 338 // Compute suggestions, and dispatch them to requestors. On error still 339 // dispatch empty suggestions. 340 SuggestionsProfile suggestions; 341 if (suggestions_data.empty()) { 342 LogResponseState(RESPONSE_EMPTY); 343 suggestions_store_->ClearSuggestions(); 344 } else if (suggestions.ParseFromString(suggestions_data)) { 345 LogResponseState(RESPONSE_VALID); 346 thumbnail_manager_->Initialize(suggestions); 347 348 int64 now_usec = (base::Time::NowFromSystemTime() - base::Time::UnixEpoch()) 349 .ToInternalValue(); 350 SetDefaultExpiryTimestamp(&suggestions, now_usec + kDefaultExpiryUsec); 351 suggestions_store_->StoreSuggestions(suggestions); 352 } else { 353 LogResponseState(RESPONSE_INVALID); 354 suggestions_store_->LoadSuggestions(&suggestions); 355 thumbnail_manager_->Initialize(suggestions); 356 } 357 358 FilterAndServe(&suggestions); 359 ScheduleBlacklistUpload(true); 360 } 361 362 void SuggestionsService::Shutdown() { 363 // Cancel pending request and timeout closure, then serve existing requestors 364 // from cache. 365 pending_request_.reset(NULL); 366 pending_timeout_closure_.reset(NULL); 367 ServeFromCache(); 368 } 369 370 void SuggestionsService::ServeFromCache() { 371 SuggestionsProfile suggestions; 372 suggestions_store_->LoadSuggestions(&suggestions); 373 thumbnail_manager_->Initialize(suggestions); 374 FilterAndServe(&suggestions); 375 } 376 377 void SuggestionsService::FilterAndServe(SuggestionsProfile* suggestions) { 378 blacklist_store_->FilterSuggestions(suggestions); 379 DispatchRequestsAndClear(*suggestions, &waiting_requestors_); 380 } 381 382 void SuggestionsService::ScheduleBlacklistUpload(bool last_request_successful) { 383 DCHECK(thread_checker_.CalledOnValidThread()); 384 385 UpdateBlacklistDelay(last_request_successful); 386 387 // Schedule a blacklist upload task. 388 GURL blacklist_url; 389 if (blacklist_store_->GetFirstUrlFromBlacklist(&blacklist_url)) { 390 base::Closure blacklist_cb = 391 base::Bind(&SuggestionsService::UploadOneFromBlacklist, 392 weak_ptr_factory_.GetWeakPtr()); 393 base::MessageLoopProxy::current()->PostDelayedTask( 394 FROM_HERE, blacklist_cb, 395 base::TimeDelta::FromSeconds(blacklist_delay_sec_)); 396 } 397 } 398 399 void SuggestionsService::UploadOneFromBlacklist() { 400 DCHECK(thread_checker_.CalledOnValidThread()); 401 402 // If there's an ongoing request, let it complete. 403 if (pending_request_.get()) return; 404 405 GURL blacklist_url; 406 if (!blacklist_store_->GetFirstUrlFromBlacklist(&blacklist_url)) 407 return; // Local blacklist is empty. 408 409 // Send blacklisting request. 410 IssueRequest(BuildBlacklistRequestURL(blacklist_url_prefix_, blacklist_url)); 411 } 412 413 void SuggestionsService::UpdateBlacklistDelay(bool last_request_successful) { 414 DCHECK(thread_checker_.CalledOnValidThread()); 415 416 if (last_request_successful) { 417 blacklist_delay_sec_ = kBlacklistDefaultDelaySec; 418 } else { 419 int candidate_delay = blacklist_delay_sec_ * kBlacklistBackoffMultiplier; 420 if (candidate_delay < kBlacklistMaxDelaySec) 421 blacklist_delay_sec_ = candidate_delay; 422 } 423 } 424 425 } // namespace suggestions 426