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