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 |