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"
9 #include "base/memory/scoped_ptr.h"
10 #include "base/message_loop/message_loop_proxy.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 "components/pref_registry/pref_registry_syncable.h"
17 #include "components/suggestions/blacklist_store.h"
18 #include "components/suggestions/suggestions_store.h"
19 #include "components/variations/net/variations_http_header_provider.h"
20 #include "components/variations/variations_associated_data.h"
21 #include "net/base/escape.h"
22 #include "net/base/load_flags.h"
23 #include "net/base/net_errors.h"
24 #include "net/base/url_util.h"
25 #include "net/http/http_response_headers.h"
26 #include "net/http/http_status_code.h"
27 #include "net/http/http_util.h"
28 #include "net/url_request/url_fetcher.h"
29 #include "net/url_request/url_request_status.h"
32 using base::CancelableClosure
;
34 namespace suggestions
{
38 // Used to UMA log the state of the last response from the server.
39 enum SuggestionsResponseState
{
46 // Will log the supplied response |state|.
47 void LogResponseState(SuggestionsResponseState state
) {
48 UMA_HISTOGRAM_ENUMERATION("Suggestions.ResponseState", state
,
52 // Obtains the experiment parameter under the supplied |key|, or empty string
53 // if the parameter does not exist.
54 std::string
GetExperimentParam(const std::string
& key
) {
55 return variations::GetVariationParamValue(kSuggestionsFieldTrialName
, key
);
58 GURL
BuildBlacklistRequestURL(const std::string
& blacklist_url_prefix
,
59 const GURL
& candidate_url
) {
60 return GURL(blacklist_url_prefix
+
61 net::EscapeQueryParamValue(candidate_url
.spec(), true));
64 // Runs each callback in |requestors| on |suggestions|, then deallocates
66 void DispatchRequestsAndClear(
67 const SuggestionsProfile
& suggestions
,
68 std::vector
<SuggestionsService::ResponseCallback
>* requestors
) {
69 std::vector
<SuggestionsService::ResponseCallback
> temp_requestors
;
70 temp_requestors
.swap(*requestors
);
71 std::vector
<SuggestionsService::ResponseCallback
>::iterator it
;
72 for (it
= temp_requestors
.begin(); it
!= temp_requestors
.end(); ++it
) {
73 if (!it
->is_null()) it
->Run(suggestions
);
77 // Default delay used when scheduling a blacklist request.
78 const int kBlacklistDefaultDelaySec
= 1;
80 // Multiplier on the delay used when scheduling a blacklist request, in case the
81 // last observed request was unsuccessful.
82 const int kBlacklistBackoffMultiplier
= 2;
84 // Maximum valid delay for scheduling a request. Candidate delays larger than
85 // this are rejected. This means the maximum backoff is at least 300 / 2, i.e.
87 const int kBlacklistMaxDelaySec
= 300; // 5 minutes
91 const char kSuggestionsFieldTrialName
[] = "ChromeSuggestions";
92 const char kSuggestionsFieldTrialControlParam
[] = "control";
93 const char kSuggestionsFieldTrialStateEnabled
[] = "enabled";
95 // TODO(mathp): Put this in TemplateURL.
96 const char kSuggestionsURL
[] = "https://www.google.com/chromesuggestions?t=2";
97 const char kSuggestionsBlacklistURLPrefix
[] =
98 "https://www.google.com/chromesuggestions/blacklist?t=2&url=";
99 const char kSuggestionsBlacklistURLParam
[] = "url";
101 // The default expiry timeout is 72 hours.
102 const int64 kDefaultExpiryUsec
= 72 * base::Time::kMicrosecondsPerHour
;
104 SuggestionsService::SuggestionsService(
105 net::URLRequestContextGetter
* url_request_context
,
106 scoped_ptr
<SuggestionsStore
> suggestions_store
,
107 scoped_ptr
<ImageManager
> thumbnail_manager
,
108 scoped_ptr
<BlacklistStore
> blacklist_store
)
109 : url_request_context_(url_request_context
),
110 suggestions_store_(suggestions_store
.Pass()),
111 thumbnail_manager_(thumbnail_manager
.Pass()),
112 blacklist_store_(blacklist_store
.Pass()),
113 blacklist_delay_sec_(kBlacklistDefaultDelaySec
),
114 suggestions_url_(kSuggestionsURL
),
115 blacklist_url_prefix_(kSuggestionsBlacklistURLPrefix
),
116 weak_ptr_factory_(this) {}
118 SuggestionsService::~SuggestionsService() {}
121 bool SuggestionsService::IsControlGroup() {
122 return GetExperimentParam(kSuggestionsFieldTrialControlParam
) ==
123 kSuggestionsFieldTrialStateEnabled
;
126 void SuggestionsService::FetchSuggestionsData(
127 SyncState sync_state
,
128 SuggestionsService::ResponseCallback callback
) {
129 DCHECK(thread_checker_
.CalledOnValidThread());
130 waiting_requestors_
.push_back(callback
);
131 if (sync_state
== SYNC_OR_HISTORY_SYNC_DISABLED
) {
132 // Cancel any ongoing request, to stop interacting with the server.
133 pending_request_
.reset(NULL
);
134 suggestions_store_
->ClearSuggestions();
135 DispatchRequestsAndClear(SuggestionsProfile(), &waiting_requestors_
);
136 } else if (sync_state
== INITIALIZED_ENABLED_HISTORY
||
137 sync_state
== NOT_INITIALIZED_ENABLED
) {
138 // Sync is enabled. Serve previously cached suggestions if available, else
139 // an empty set of suggestions.
142 // Issue a network request to refresh the suggestions in the cache.
143 IssueRequestIfNoneOngoing(suggestions_url_
);
149 void SuggestionsService::GetPageThumbnail(
151 base::Callback
<void(const GURL
&, const SkBitmap
*)> callback
) {
152 thumbnail_manager_
->GetImageForURL(url
, callback
);
155 void SuggestionsService::BlacklistURL(
156 const GURL
& candidate_url
,
157 const SuggestionsService::ResponseCallback
& callback
) {
158 DCHECK(thread_checker_
.CalledOnValidThread());
159 waiting_requestors_
.push_back(callback
);
161 // Blacklist locally for immediate effect and serve the requestors.
162 blacklist_store_
->BlacklistUrl(candidate_url
);
165 // Send blacklisting request. Even if this request ends up not being sent
166 // because of an ongoing request, a blacklist request is later scheduled.
167 // TODO(mathp): Currently, this will not send a request if there is already
168 // a request in flight (for suggestions or blacklist). Should we prioritize
169 // blacklist requests since they actually carry a payload?
170 IssueRequestIfNoneOngoing(
171 BuildBlacklistRequestURL(blacklist_url_prefix_
, candidate_url
));
175 bool SuggestionsService::GetBlacklistedUrl(const net::URLFetcher
& request
,
177 bool is_blacklist_request
= StartsWithASCII(request
.GetOriginalURL().spec(),
178 kSuggestionsBlacklistURLPrefix
,
180 if (!is_blacklist_request
) return false;
182 // Extract the blacklisted URL from the blacklist request.
183 std::string blacklisted
;
184 if (!net::GetValueForKeyInQuery(
185 request
.GetOriginalURL(),
186 kSuggestionsBlacklistURLParam
,
191 GURL
blacklisted_url(blacklisted
);
192 blacklisted_url
.Swap(url
);
197 void SuggestionsService::RegisterProfilePrefs(
198 user_prefs::PrefRegistrySyncable
* registry
) {
199 SuggestionsStore::RegisterProfilePrefs(registry
);
200 BlacklistStore::RegisterProfilePrefs(registry
);
203 void SuggestionsService::SetDefaultExpiryTimestamp(
204 SuggestionsProfile
* suggestions
, int64 default_timestamp_usec
) {
205 for (int i
= 0; i
< suggestions
->suggestions_size(); ++i
) {
206 ChromeSuggestion
* suggestion
= suggestions
->mutable_suggestions(i
);
207 // Do not set expiry if the server has already provided a more specific
208 // expiry time for this suggestion.
209 if (!suggestion
->has_expiry_ts()) {
210 suggestion
->set_expiry_ts(default_timestamp_usec
);
215 void SuggestionsService::IssueRequestIfNoneOngoing(const GURL
& url
) {
216 // If there is an ongoing request, let it complete.
217 if (pending_request_
.get()) {
220 pending_request_
.reset(CreateSuggestionsRequest(url
));
221 pending_request_
->Start();
222 last_request_started_time_
= base::TimeTicks::Now();
225 net::URLFetcher
* SuggestionsService::CreateSuggestionsRequest(const GURL
& url
) {
226 net::URLFetcher
* request
=
227 net::URLFetcher::Create(0, url
, net::URLFetcher::GET
, this);
228 request
->SetLoadFlags(net::LOAD_DISABLE_CACHE
);
229 request
->SetRequestContext(url_request_context_
);
230 // Add Chrome experiment state to the request headers.
231 net::HttpRequestHeaders headers
;
232 variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders(
233 request
->GetOriginalURL(), false, false, &headers
);
234 request
->SetExtraRequestHeaders(headers
.ToString());
238 void SuggestionsService::OnURLFetchComplete(const net::URLFetcher
* source
) {
239 DCHECK(thread_checker_
.CalledOnValidThread());
240 DCHECK_EQ(pending_request_
.get(), source
);
242 // The fetcher will be deleted when the request is handled.
243 scoped_ptr
<const net::URLFetcher
> request(pending_request_
.release());
245 const net::URLRequestStatus
& request_status
= request
->GetStatus();
246 if (request_status
.status() != net::URLRequestStatus::SUCCESS
) {
247 // This represents network errors (i.e. the server did not provide a
249 UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FailedRequestErrorCode",
250 -request_status
.error());
251 DVLOG(1) << "Suggestions server request failed with error: "
252 << request_status
.error() << ": "
253 << net::ErrorToString(request_status
.error());
254 ScheduleBlacklistUpload(false);
258 const int response_code
= request
->GetResponseCode();
259 UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FetchResponseCode", response_code
);
260 if (response_code
!= net::HTTP_OK
) {
261 // A non-200 response code means that server has no (longer) suggestions for
262 // this user. Aggressively clear the cache.
263 suggestions_store_
->ClearSuggestions();
264 ScheduleBlacklistUpload(false);
268 const base::TimeDelta latency
=
269 base::TimeTicks::Now() - last_request_started_time_
;
270 UMA_HISTOGRAM_MEDIUM_TIMES("Suggestions.FetchSuccessLatency", latency
);
272 // Handle a successful blacklisting.
273 GURL blacklisted_url
;
274 if (GetBlacklistedUrl(*source
, &blacklisted_url
)) {
275 blacklist_store_
->RemoveUrl(blacklisted_url
);
278 std::string suggestions_data
;
279 bool success
= request
->GetResponseAsString(&suggestions_data
);
282 // Parse the received suggestions and update the cache, or take proper action
283 // in the case of invalid response.
284 SuggestionsProfile suggestions
;
285 if (suggestions_data
.empty()) {
286 LogResponseState(RESPONSE_EMPTY
);
287 suggestions_store_
->ClearSuggestions();
288 } else if (suggestions
.ParseFromString(suggestions_data
)) {
289 LogResponseState(RESPONSE_VALID
);
290 int64 now_usec
= (base::Time::NowFromSystemTime() - base::Time::UnixEpoch())
292 SetDefaultExpiryTimestamp(&suggestions
, now_usec
+ kDefaultExpiryUsec
);
293 suggestions_store_
->StoreSuggestions(suggestions
);
295 LogResponseState(RESPONSE_INVALID
);
298 ScheduleBlacklistUpload(true);
301 void SuggestionsService::Shutdown() {
302 // Cancel pending request, then serve existing requestors from cache.
303 pending_request_
.reset(NULL
);
307 void SuggestionsService::ServeFromCache() {
308 SuggestionsProfile suggestions
;
309 // In case of empty cache or error, |suggestions| stays empty.
310 suggestions_store_
->LoadSuggestions(&suggestions
);
311 thumbnail_manager_
->Initialize(suggestions
);
312 FilterAndServe(&suggestions
);
315 void SuggestionsService::FilterAndServe(SuggestionsProfile
* suggestions
) {
316 blacklist_store_
->FilterSuggestions(suggestions
);
317 DispatchRequestsAndClear(*suggestions
, &waiting_requestors_
);
320 void SuggestionsService::ScheduleBlacklistUpload(bool last_request_successful
) {
321 DCHECK(thread_checker_
.CalledOnValidThread());
323 UpdateBlacklistDelay(last_request_successful
);
325 // Schedule a blacklist upload task.
327 if (blacklist_store_
->GetFirstUrlFromBlacklist(&blacklist_url
)) {
328 base::Closure blacklist_cb
=
329 base::Bind(&SuggestionsService::UploadOneFromBlacklist
,
330 weak_ptr_factory_
.GetWeakPtr());
331 base::MessageLoopProxy::current()->PostDelayedTask(
332 FROM_HERE
, blacklist_cb
,
333 base::TimeDelta::FromSeconds(blacklist_delay_sec_
));
337 void SuggestionsService::UploadOneFromBlacklist() {
338 DCHECK(thread_checker_
.CalledOnValidThread());
341 if (!blacklist_store_
->GetFirstUrlFromBlacklist(&blacklist_url
))
342 return; // Local blacklist is empty.
344 // Send blacklisting request. Even if this request ends up not being sent
345 // because of an ongoing request, a blacklist request is later scheduled.
346 IssueRequestIfNoneOngoing(
347 BuildBlacklistRequestURL(blacklist_url_prefix_
, blacklist_url
));
350 void SuggestionsService::UpdateBlacklistDelay(bool last_request_successful
) {
351 DCHECK(thread_checker_
.CalledOnValidThread());
353 if (last_request_successful
) {
354 blacklist_delay_sec_
= kBlacklistDefaultDelaySec
;
356 int candidate_delay
= blacklist_delay_sec_
* kBlacklistBackoffMultiplier
;
357 if (candidate_delay
< kBlacklistMaxDelaySec
)
358 blacklist_delay_sec_
= candidate_delay
;
362 } // namespace suggestions