// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/suggestions/suggestions_service.h"
#include <sstream>
#include <string>
#include "base/memory/scoped_ptr.h"
#include "base/message_loop/message_loop_proxy.h"
#include "base/metrics/histogram.h"
#include "base/metrics/sparse_histogram.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/suggestions/blacklist_store.h"
#include "components/suggestions/suggestions_store.h"
#include "components/variations/variations_associated_data.h"
#include "components/variations/variations_http_header_provider.h"
#include "net/base/escape.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/base/url_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_request_status.h"
#include "url/gurl.h"
using base::CancelableClosure;
namespace suggestions {
namespace {
// Used to UMA log the state of the last response from the server.
enum SuggestionsResponseState {
RESPONSE_EMPTY,
RESPONSE_INVALID,
RESPONSE_VALID,
RESPONSE_STATE_SIZE
};
// Will log the supplied response |state|.
void LogResponseState(SuggestionsResponseState state) {
UMA_HISTOGRAM_ENUMERATION("Suggestions.ResponseState", state,
RESPONSE_STATE_SIZE);
}
// Obtains the experiment parameter under the supplied |key|, or empty string
// if the parameter does not exist.
std::string GetExperimentParam(const std::string& key) {
return variations::GetVariationParamValue(kSuggestionsFieldTrialName, key);
}
GURL BuildBlacklistRequestURL(const std::string& blacklist_url_prefix,
const GURL& candidate_url) {
return GURL(blacklist_url_prefix +
net::EscapeQueryParamValue(candidate_url.spec(), true));
}
// Runs each callback in |requestors| on |suggestions|, then deallocates
// |requestors|.
void DispatchRequestsAndClear(
const SuggestionsProfile& suggestions,
std::vector<SuggestionsService::ResponseCallback>* requestors) {
std::vector<SuggestionsService::ResponseCallback>::iterator it;
for (it = requestors->begin(); it != requestors->end(); ++it) {
if (!it->is_null()) it->Run(suggestions);
}
std::vector<SuggestionsService::ResponseCallback>().swap(*requestors);
}
const int kDefaultRequestTimeoutMs = 200;
// Default delay used when scheduling a blacklist request.
const int kBlacklistDefaultDelaySec = 1;
// Multiplier on the delay used when scheduling a blacklist request, in case the
// last observed request was unsuccessful.
const int kBlacklistBackoffMultiplier = 2;
// Maximum valid delay for scheduling a request. Candidate delays larger than
// this are rejected. This means the maximum backoff is at least 300 / 2, i.e.
// 2.5 minutes.
const int kBlacklistMaxDelaySec = 300; // 5 minutes
} // namespace
const char kSuggestionsFieldTrialName[] = "ChromeSuggestions";
const char kSuggestionsFieldTrialURLParam[] = "url";
const char kSuggestionsFieldTrialCommonParamsParam[] = "common_params";
const char kSuggestionsFieldTrialBlacklistPathParam[] = "blacklist_path";
const char kSuggestionsFieldTrialBlacklistUrlParam[] = "blacklist_url_param";
const char kSuggestionsFieldTrialStateParam[] = "state";
const char kSuggestionsFieldTrialControlParam[] = "control";
const char kSuggestionsFieldTrialStateEnabled[] = "enabled";
const char kSuggestionsFieldTrialTimeoutMs[] = "timeout_ms";
// The default expiry timeout is 72 hours.
const int64 kDefaultExpiryUsec = 72 * base::Time::kMicrosecondsPerHour;
namespace {
std::string GetBlacklistUrlPrefix() {
std::stringstream blacklist_url_prefix_stream;
blacklist_url_prefix_stream
<< GetExperimentParam(kSuggestionsFieldTrialURLParam)
<< GetExperimentParam(kSuggestionsFieldTrialBlacklistPathParam) << "?"
<< GetExperimentParam(kSuggestionsFieldTrialCommonParamsParam) << "&"
<< GetExperimentParam(kSuggestionsFieldTrialBlacklistUrlParam) << "=";
return blacklist_url_prefix_stream.str();
}
} // namespace
SuggestionsService::SuggestionsService(
net::URLRequestContextGetter* url_request_context,
scoped_ptr<SuggestionsStore> suggestions_store,
scoped_ptr<ImageManager> thumbnail_manager,
scoped_ptr<BlacklistStore> blacklist_store)
: suggestions_store_(suggestions_store.Pass()),
blacklist_store_(blacklist_store.Pass()),
thumbnail_manager_(thumbnail_manager.Pass()),
url_request_context_(url_request_context),
blacklist_delay_sec_(kBlacklistDefaultDelaySec),
request_timeout_ms_(kDefaultRequestTimeoutMs),
weak_ptr_factory_(this) {
// Obtain various parameters from Variations.
suggestions_url_ =
GURL(GetExperimentParam(kSuggestionsFieldTrialURLParam) + "?" +
GetExperimentParam(kSuggestionsFieldTrialCommonParamsParam));
blacklist_url_prefix_ = GetBlacklistUrlPrefix();
std::string timeout = GetExperimentParam(kSuggestionsFieldTrialTimeoutMs);
int temp_timeout;
if (!timeout.empty() && base::StringToInt(timeout, &temp_timeout)) {
request_timeout_ms_ = temp_timeout;
}
}
SuggestionsService::~SuggestionsService() {}
// static
bool SuggestionsService::IsEnabled() {
return GetExperimentParam(kSuggestionsFieldTrialStateParam) ==
kSuggestionsFieldTrialStateEnabled;
}
// static
bool SuggestionsService::IsControlGroup() {
return GetExperimentParam(kSuggestionsFieldTrialControlParam) ==
kSuggestionsFieldTrialStateEnabled;
}
void SuggestionsService::FetchSuggestionsData(
SyncState sync_state,
SuggestionsService::ResponseCallback callback) {
DCHECK(thread_checker_.CalledOnValidThread());
if (sync_state == NOT_INITIALIZED_ENABLED) {
// Sync is not initialized yet, but enabled. Serve previously cached
// suggestions if available.
waiting_requestors_.push_back(callback);
ServeFromCache();
return;
} else if (sync_state == SYNC_OR_HISTORY_SYNC_DISABLED) {
// Cancel any ongoing request (and the timeout closure). We must no longer
// interact with the server.
pending_request_.reset(NULL);
pending_timeout_closure_.reset(NULL);
suggestions_store_->ClearSuggestions();
callback.Run(SuggestionsProfile());
DispatchRequestsAndClear(SuggestionsProfile(), &waiting_requestors_);
return;
}
FetchSuggestionsDataNoTimeout(callback);
// Post a task to serve the cached suggestions if the request hasn't completed
// after some time. Cancels the previous such task, if one existed.
pending_timeout_closure_.reset(new CancelableClosure(base::Bind(
&SuggestionsService::OnRequestTimeout, weak_ptr_factory_.GetWeakPtr())));
base::MessageLoopProxy::current()->PostDelayedTask(
FROM_HERE, pending_timeout_closure_->callback(),
base::TimeDelta::FromMilliseconds(request_timeout_ms_));
}
void SuggestionsService::GetPageThumbnail(
const GURL& url,
base::Callback<void(const GURL&, const SkBitmap*)> callback) {
thumbnail_manager_->GetImageForURL(url, callback);
}
void SuggestionsService::BlacklistURL(
const GURL& candidate_url,
const SuggestionsService::ResponseCallback& callback) {
DCHECK(thread_checker_.CalledOnValidThread());
waiting_requestors_.push_back(callback);
// Blacklist locally, for immediate effect.
if (!blacklist_store_->BlacklistUrl(candidate_url)) {
DVLOG(1) << "Failed blacklisting attempt.";
return;
}
// If there's an ongoing request, let it complete.
if (pending_request_.get()) return;
IssueRequest(BuildBlacklistRequestURL(blacklist_url_prefix_, candidate_url));
}
// static
bool SuggestionsService::GetBlacklistedUrl(const net::URLFetcher& request,
GURL* url) {
bool is_blacklist_request = StartsWithASCII(request.GetOriginalURL().spec(),
GetBlacklistUrlPrefix(), true);
if (!is_blacklist_request) return false;
// Extract the blacklisted URL from the blacklist request.
std::string blacklisted;
if (!net::GetValueForKeyInQuery(
request.GetOriginalURL(),
GetExperimentParam(kSuggestionsFieldTrialBlacklistUrlParam),
&blacklisted))
return false;
GURL blacklisted_url(blacklisted);
blacklisted_url.Swap(url);
return true;
}
// static
void SuggestionsService::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
SuggestionsStore::RegisterProfilePrefs(registry);
BlacklistStore::RegisterProfilePrefs(registry);
}
void SuggestionsService::SetDefaultExpiryTimestamp(
SuggestionsProfile* suggestions, int64 default_timestamp_usec) {
for (int i = 0; i < suggestions->suggestions_size(); ++i) {
ChromeSuggestion* suggestion = suggestions->mutable_suggestions(i);
// Do not set expiry if the server has already provided a more specific
// expiry time for this suggestion.
if (!suggestion->has_expiry_ts()) {
suggestion->set_expiry_ts(default_timestamp_usec);
}
}
}
void SuggestionsService::FetchSuggestionsDataNoTimeout(
SuggestionsService::ResponseCallback callback) {
DCHECK(thread_checker_.CalledOnValidThread());
if (pending_request_.get()) {
// Request already exists, so just add requestor to queue.
waiting_requestors_.push_back(callback);
return;
}
// Form new request.
DCHECK(waiting_requestors_.empty());
waiting_requestors_.push_back(callback);
IssueRequest(suggestions_url_);
}
void SuggestionsService::IssueRequest(const GURL& url) {
pending_request_.reset(CreateSuggestionsRequest(url));
pending_request_->Start();
last_request_started_time_ = base::TimeTicks::Now();
}
net::URLFetcher* SuggestionsService::CreateSuggestionsRequest(const GURL& url) {
net::URLFetcher* request =
net::URLFetcher::Create(0, url, net::URLFetcher::GET, this);
request->SetLoadFlags(net::LOAD_DISABLE_CACHE);
request->SetRequestContext(url_request_context_);
// Add Chrome experiment state to the request headers.
net::HttpRequestHeaders headers;
variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders(
request->GetOriginalURL(), false, false, &headers);
request->SetExtraRequestHeaders(headers.ToString());
return request;
}
void SuggestionsService::OnRequestTimeout() {
DCHECK(thread_checker_.CalledOnValidThread());
ServeFromCache();
}
void SuggestionsService::OnURLFetchComplete(const net::URLFetcher* source) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK_EQ(pending_request_.get(), source);
// We no longer need the timeout closure. Delete it whether or not it has run.
// If it hasn't, this cancels it.
pending_timeout_closure_.reset();
// The fetcher will be deleted when the request is handled.
scoped_ptr<const net::URLFetcher> request(pending_request_.release());
const net::URLRequestStatus& request_status = request->GetStatus();
if (request_status.status() != net::URLRequestStatus::SUCCESS) {
UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FailedRequestErrorCode",
-request_status.error());
DVLOG(1) << "Suggestions server request failed with error: "
<< request_status.error() << ": "
<< net::ErrorToString(request_status.error());
// Dispatch the cached profile on error.
ServeFromCache();
ScheduleBlacklistUpload(false);
return;
}
// Log the response code.
const int response_code = request->GetResponseCode();
UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FetchResponseCode", response_code);
if (response_code != net::HTTP_OK) {
// Aggressively clear the store.
suggestions_store_->ClearSuggestions();
DispatchRequestsAndClear(SuggestionsProfile(), &waiting_requestors_);
ScheduleBlacklistUpload(false);
return;
}
const base::TimeDelta latency =
base::TimeTicks::Now() - last_request_started_time_;
UMA_HISTOGRAM_MEDIUM_TIMES("Suggestions.FetchSuccessLatency", latency);
// Handle a successful blacklisting.
GURL blacklisted_url;
if (GetBlacklistedUrl(*source, &blacklisted_url)) {
blacklist_store_->RemoveUrl(blacklisted_url);
}
std::string suggestions_data;
bool success = request->GetResponseAsString(&suggestions_data);
DCHECK(success);
// Compute suggestions, and dispatch them to requestors. On error still
// dispatch empty suggestions.
SuggestionsProfile suggestions;
if (suggestions_data.empty()) {
LogResponseState(RESPONSE_EMPTY);
suggestions_store_->ClearSuggestions();
} else if (suggestions.ParseFromString(suggestions_data)) {
LogResponseState(RESPONSE_VALID);
thumbnail_manager_->Initialize(suggestions);
int64 now_usec = (base::Time::NowFromSystemTime() - base::Time::UnixEpoch())
.ToInternalValue();
SetDefaultExpiryTimestamp(&suggestions, now_usec + kDefaultExpiryUsec);
suggestions_store_->StoreSuggestions(suggestions);
} else {
LogResponseState(RESPONSE_INVALID);
suggestions_store_->LoadSuggestions(&suggestions);
thumbnail_manager_->Initialize(suggestions);
}
FilterAndServe(&suggestions);
ScheduleBlacklistUpload(true);
}
void SuggestionsService::Shutdown() {
// Cancel pending request and timeout closure, then serve existing requestors
// from cache.
pending_request_.reset(NULL);
pending_timeout_closure_.reset(NULL);
ServeFromCache();
}
void SuggestionsService::ServeFromCache() {
SuggestionsProfile suggestions;
suggestions_store_->LoadSuggestions(&suggestions);
thumbnail_manager_->Initialize(suggestions);
FilterAndServe(&suggestions);
}
void SuggestionsService::FilterAndServe(SuggestionsProfile* suggestions) {
blacklist_store_->FilterSuggestions(suggestions);
DispatchRequestsAndClear(*suggestions, &waiting_requestors_);
}
void SuggestionsService::ScheduleBlacklistUpload(bool last_request_successful) {
DCHECK(thread_checker_.CalledOnValidThread());
UpdateBlacklistDelay(last_request_successful);
// Schedule a blacklist upload task.
GURL blacklist_url;
if (blacklist_store_->GetFirstUrlFromBlacklist(&blacklist_url)) {
base::Closure blacklist_cb =
base::Bind(&SuggestionsService::UploadOneFromBlacklist,
weak_ptr_factory_.GetWeakPtr());
base::MessageLoopProxy::current()->PostDelayedTask(
FROM_HERE, blacklist_cb,
base::TimeDelta::FromSeconds(blacklist_delay_sec_));
}
}
void SuggestionsService::UploadOneFromBlacklist() {
DCHECK(thread_checker_.CalledOnValidThread());
// If there's an ongoing request, let it complete.
if (pending_request_.get()) return;
GURL blacklist_url;
if (!blacklist_store_->GetFirstUrlFromBlacklist(&blacklist_url))
return; // Local blacklist is empty.
// Send blacklisting request.
IssueRequest(BuildBlacklistRequestURL(blacklist_url_prefix_, blacklist_url));
}
void SuggestionsService::UpdateBlacklistDelay(bool last_request_successful) {
DCHECK(thread_checker_.CalledOnValidThread());
if (last_request_successful) {
blacklist_delay_sec_ = kBlacklistDefaultDelaySec;
} else {
int candidate_delay = blacklist_delay_sec_ * kBlacklistBackoffMultiplier;
if (candidate_delay < kBlacklistMaxDelaySec)
blacklist_delay_sec_ = candidate_delay;
}
}
} // namespace suggestions