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.
5 #include "components/suggestions/suggestions_service.h"
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"
33 using base::CancelableClosure
;
35 namespace suggestions
{
39 // Used to UMA log the state of the last response from the server.
40 enum SuggestionsResponseState
{
47 // Will log the supplied response |state|.
48 void LogResponseState(SuggestionsResponseState state
) {
49 UMA_HISTOGRAM_ENUMERATION("Suggestions.ResponseState", state
,
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
);
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));
65 // Runs each callback in |requestors| on |suggestions|, then deallocates
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
);
74 std::vector
<SuggestionsService::ResponseCallback
>().swap(*requestors
);
77 const int kDefaultRequestTimeoutMs
= 200;
79 // Default delay used when scheduling a blacklist request.
80 const int kBlacklistDefaultDelaySec
= 1;
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;
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.
89 const int kBlacklistMaxDelaySec
= 300; // 5 minutes
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";
103 // The default expiry timeout is 72 hours.
104 const int64 kDefaultExpiryUsec
= 72 * base::Time::kMicrosecondsPerHour
;
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();
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 weak_ptr_factory_(this),
131 request_timeout_ms_(kDefaultRequestTimeoutMs
) {
132 // Obtain various parameters from Variations.
134 GURL(GetExperimentParam(kSuggestionsFieldTrialURLParam
) + "?" +
135 GetExperimentParam(kSuggestionsFieldTrialCommonParamsParam
));
136 blacklist_url_prefix_
= GetBlacklistUrlPrefix();
137 std::string timeout
= GetExperimentParam(kSuggestionsFieldTrialTimeoutMs
);
139 if (!timeout
.empty() && base::StringToInt(timeout
, &temp_timeout
)) {
140 request_timeout_ms_
= temp_timeout
;
144 SuggestionsService::~SuggestionsService() {}
147 bool SuggestionsService::IsEnabled() {
148 return GetExperimentParam(kSuggestionsFieldTrialStateParam
) ==
149 kSuggestionsFieldTrialStateEnabled
;
153 bool SuggestionsService::IsControlGroup() {
154 return GetExperimentParam(kSuggestionsFieldTrialControlParam
) ==
155 kSuggestionsFieldTrialStateEnabled
;
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
);
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_
);
179 FetchSuggestionsDataNoTimeout(callback
);
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_
));
190 void SuggestionsService::GetPageThumbnail(
192 base::Callback
<void(const GURL
&, const SkBitmap
*)> callback
) {
193 thumbnail_manager_
->GetImageForURL(url
, callback
);
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
);
202 // Blacklist locally, for immediate effect.
203 if (!blacklist_store_
->BlacklistUrl(candidate_url
)) {
204 DVLOG(1) << "Failed blacklisting attempt.";
208 // If there's an ongoing request, let it complete.
209 if (pending_request_
.get()) return;
210 IssueRequest(BuildBlacklistRequestURL(blacklist_url_prefix_
, candidate_url
));
214 bool SuggestionsService::GetBlacklistedUrl(const net::URLFetcher
& request
,
216 bool is_blacklist_request
= StartsWithASCII(request
.GetOriginalURL().spec(),
217 GetBlacklistUrlPrefix(), true);
218 if (!is_blacklist_request
) return false;
220 // Extract the blacklisted URL from the blacklist request.
221 std::string blacklisted
;
222 if (!net::GetValueForKeyInQuery(
223 request
.GetOriginalURL(),
224 GetExperimentParam(kSuggestionsFieldTrialBlacklistUrlParam
),
228 GURL
blacklisted_url(blacklisted
);
229 blacklisted_url
.Swap(url
);
234 void SuggestionsService::RegisterProfilePrefs(
235 user_prefs::PrefRegistrySyncable
* registry
) {
236 SuggestionsStore::RegisterProfilePrefs(registry
);
237 BlacklistStore::RegisterProfilePrefs(registry
);
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
);
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
);
262 DCHECK(waiting_requestors_
.empty());
263 waiting_requestors_
.push_back(callback
);
264 IssueRequest(suggestions_url_
);
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();
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());
286 void SuggestionsService::OnRequestTimeout() {
287 DCHECK(thread_checker_
.CalledOnValidThread());
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();
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.
309 ScheduleBlacklistUpload(false);
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);
324 const base::TimeDelta latency
=
325 base::TimeTicks::Now() - last_request_started_time_
;
326 UMA_HISTOGRAM_MEDIUM_TIMES("Suggestions.FetchSuccessLatency", latency
);
328 // Handle a successful blacklisting.
329 GURL blacklisted_url
;
330 if (GetBlacklistedUrl(*source
, &blacklisted_url
)) {
331 blacklist_store_
->RemoveUrl(blacklisted_url
);
334 std::string suggestions_data
;
335 bool success
= request
->GetResponseAsString(&suggestions_data
);
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
);
348 int64 now_usec
= (base::Time::NowFromSystemTime() - base::Time::UnixEpoch())
350 SetDefaultExpiryTimestamp(&suggestions
, now_usec
+ kDefaultExpiryUsec
);
351 suggestions_store_
->StoreSuggestions(suggestions
);
353 LogResponseState(RESPONSE_INVALID
);
354 suggestions_store_
->LoadSuggestions(&suggestions
);
355 thumbnail_manager_
->Initialize(suggestions
);
358 FilterAndServe(&suggestions
);
359 ScheduleBlacklistUpload(true);
362 void SuggestionsService::Shutdown() {
363 // Cancel pending request and timeout closure, then serve existing requestors
365 pending_request_
.reset(NULL
);
366 pending_timeout_closure_
.reset(NULL
);
370 void SuggestionsService::ServeFromCache() {
371 SuggestionsProfile suggestions
;
372 suggestions_store_
->LoadSuggestions(&suggestions
);
373 thumbnail_manager_
->Initialize(suggestions
);
374 FilterAndServe(&suggestions
);
377 void SuggestionsService::FilterAndServe(SuggestionsProfile
* suggestions
) {
378 blacklist_store_
->FilterSuggestions(suggestions
);
379 DispatchRequestsAndClear(*suggestions
, &waiting_requestors_
);
382 void SuggestionsService::ScheduleBlacklistUpload(bool last_request_successful
) {
383 DCHECK(thread_checker_
.CalledOnValidThread());
385 UpdateBlacklistDelay(last_request_successful
);
387 // Schedule a blacklist upload task.
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_
));
399 void SuggestionsService::UploadOneFromBlacklist() {
400 DCHECK(thread_checker_
.CalledOnValidThread());
402 // If there's an ongoing request, let it complete.
403 if (pending_request_
.get()) return;
406 if (!blacklist_store_
->GetFirstUrlFromBlacklist(&blacklist_url
))
407 return; // Local blacklist is empty.
409 // Send blacklisting request.
410 IssueRequest(BuildBlacklistRequestURL(blacklist_url_prefix_
, blacklist_url
));
413 void SuggestionsService::UpdateBlacklistDelay(bool last_request_successful
) {
414 DCHECK(thread_checker_
.CalledOnValidThread());
416 if (last_request_successful
) {
417 blacklist_delay_sec_
= kBlacklistDefaultDelaySec
;
419 int candidate_delay
= blacklist_delay_sec_
* kBlacklistBackoffMultiplier
;
420 if (candidate_delay
< kBlacklistMaxDelaySec
)
421 blacklist_delay_sec_
= candidate_delay
;
425 } // namespace suggestions