blob: 1446b0a8bb138366f718ab990834e5e3f492405d [file] [log] [blame]
// Copyright 2016 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/previews/core/previews_opt_out_store_sql.h"
#include <map>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequenced_task_runner.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/previews/core/previews_black_list.h"
#include "components/previews/core/previews_black_list_item.h"
#include "components/previews/core/previews_experiments.h"
#include "sql/connection.h"
#include "sql/recovery.h"
#include "sql/statement.h"
#include "sql/transaction.h"
namespace previews {
namespace {
// Command line switch to change the previews per row DB size.
const char kMaxRowsPerHost[] = "previews-max-opt-out-rows-per-host";
// Command line switch to change the previews DB size.
const char kMaxRows[] = "previews-max-opt-out-rows";
// Returns the maximum number of table rows allowed per host for the previews
// opt out store. This is enforced during insertion of new navigation entries.
int MaxRowsPerHostInOptOutDB() {
std::string max_rows =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
kMaxRowsPerHost);
int value;
return base::StringToInt(max_rows, &value) ? value : 32;
}
// Returns the maximum number of table rows allowed for the previews opt out
// store. This is enforced during load time; thus the database can grow
// larger than this temporarily.
int MaxRowsInOptOutDB() {
std::string max_rows =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(kMaxRows);
int value;
return base::StringToInt(max_rows, &value) ? value : 3200;
}
// Table names use a macro instead of a const, so they can be used inline in
// other SQL statements below.
// The Previews OptOut table holds entries for hosts that should not use a
// specified PreviewsType treatment. Also known as the previews blacklist.
#define PREVIEWS_OPT_OUT_TABLE_NAME "previews_v1"
// The Enabled Previews table hold the list of enabled PreviewsType
// treatments with a version for that enabled treatment. If the version
// changes or the type becomes disabled, then any entries in the OptOut
// table for that treatment type should be cleared.
#define ENABLED_PREVIEWS_TABLE_NAME "enabled_previews_v1"
void CreateSchema(sql::Connection* db) {
const char kSqlCreatePreviewsTable[] =
"CREATE TABLE IF NOT EXISTS " PREVIEWS_OPT_OUT_TABLE_NAME
" (host_name VARCHAR NOT NULL,"
" time INTEGER NOT NULL,"
" opt_out INTEGER NOT NULL,"
" type INTEGER NOT NULL,"
" PRIMARY KEY(host_name, time DESC, opt_out, type))";
if (!db->Execute(kSqlCreatePreviewsTable))
return;
const char kSqlCreateEnabledTypeVersionTable[] =
"CREATE TABLE IF NOT EXISTS " ENABLED_PREVIEWS_TABLE_NAME
" (type INTEGER NOT NULL,"
" version INTEGER NOT NULL,"
" PRIMARY KEY(type))";
if (!db->Execute(kSqlCreateEnabledTypeVersionTable))
return;
}
void DatabaseErrorCallback(sql::Connection* db,
const base::FilePath& db_path,
int extended_error,
sql::Statement* stmt) {
if (sql::Recovery::ShouldRecover(extended_error)) {
// Prevent reentrant calls.
db->reset_error_callback();
// After this call, the |db| handle is poisoned so that future calls will
// return errors until the handle is re-opened.
sql::Recovery::RecoverDatabase(db, db_path);
// The DLOG(FATAL) below is intended to draw immediate attention to errors
// in newly-written code. Database corruption is generally a result of OS
// or hardware issues, not coding errors at the client level, so displaying
// the error would probably lead to confusion. The ignored call signals the
// test-expectation framework that the error was handled.
ignore_result(sql::Connection::IsExpectedSqliteError(extended_error));
return;
}
// The default handling is to assert on debug and to ignore on release.
if (!sql::Connection::IsExpectedSqliteError(extended_error))
DLOG(FATAL) << db->GetErrorMessage();
}
void InitDatabase(sql::Connection* db, base::FilePath path) {
// The entry size should be between 11 and 10 + x bytes, where x is the the
// length of the host name string in bytes.
// The total number of entries per host is bounded at 32, and the total number
// of hosts is currently unbounded (but typically expected to be under 100).
// Assuming average of 100 bytes per entry, and 100 hosts, the total size will
// be 4096 * 78. 250 allows room for extreme cases such as many host names
// or very long host names.
// The average case should be much smaller as users rarely visit hosts that
// are not in their top 20 hosts. It should be closer to 32 * 100 * 20 for
// most users, which is about 4096 * 15.
// The total size of the database will be capped at 3200 entries.
db->set_page_size(4096);
db->set_cache_size(250);
db->set_histogram_tag("PreviewsOptOut");
db->set_exclusive_locking();
db->set_error_callback(base::Bind(&DatabaseErrorCallback, db, path));
base::File::Error err;
if (!base::CreateDirectoryAndGetError(path.DirName(), &err)) {
return;
}
if (!db->Open(path)) {
return;
}
CreateSchema(db);
}
// Adds a new OptOut entry to the data base.
void AddPreviewNavigationToDataBase(sql::Connection* db,
bool opt_out,
const std::string& host_name,
PreviewsType type,
base::Time now) {
// Adds the new entry.
const char kSqlInsert[] = "INSERT INTO " PREVIEWS_OPT_OUT_TABLE_NAME
" (host_name, time, opt_out, type)"
" VALUES "
" (?, ?, ?, ?)";
sql::Statement statement_insert(
db->GetCachedStatement(SQL_FROM_HERE, kSqlInsert));
statement_insert.BindString(0, host_name);
statement_insert.BindInt64(1, now.ToInternalValue());
statement_insert.BindBool(2, opt_out);
statement_insert.BindInt(3, static_cast<int>(type));
statement_insert.Run();
}
// Removes OptOut entries for |host_name| if the per-host row limit is exceeded.
// Removes OptOut entries if per data base row limit is exceeded.
void MaybeEvictHostEntryFromDataBase(sql::Connection* db,
const std::string& host_name) {
// Delete the oldest entries if there are more than |MaxRowsPerHostInOptOutDB|
// for |host_name|.
// DELETE ... LIMIT -1 OFFSET x means delete all but the first x entries.
const char kSqlDeleteByHost[] =
"DELETE FROM " PREVIEWS_OPT_OUT_TABLE_NAME
" WHERE ROWID IN"
" (SELECT ROWID from " PREVIEWS_OPT_OUT_TABLE_NAME
" WHERE host_name == ?"
" ORDER BY time DESC"
" LIMIT -1 OFFSET ?)";
sql::Statement statement_delete_by_host(
db->GetCachedStatement(SQL_FROM_HERE, kSqlDeleteByHost));
statement_delete_by_host.BindString(0, host_name);
statement_delete_by_host.BindInt(1, MaxRowsPerHostInOptOutDB());
statement_delete_by_host.Run();
}
// Deletes every preview navigation/OptOut entry for |type|.
void ClearBlacklistForTypeInDataBase(sql::Connection* db, PreviewsType type) {
const char kSql[] =
"DELETE FROM " PREVIEWS_OPT_OUT_TABLE_NAME " WHERE type == ?";
sql::Statement statement(db->GetUniqueStatement(kSql));
statement.BindInt(0, static_cast<int>(type));
statement.Run();
}
// Retrieves the list of previously enabled previews types with their version
// from the Enabled Previews table.
std::unique_ptr<std::map<PreviewsType, int>> GetStoredPreviews(
sql::Connection* db) {
const char kSqlLoadEnabledPreviewsVersions[] =
"SELECT type, version FROM " ENABLED_PREVIEWS_TABLE_NAME;
sql::Statement statement(
db->GetUniqueStatement(kSqlLoadEnabledPreviewsVersions));
std::unique_ptr<std::map<PreviewsType, int>> stored_previews(
new std::map<PreviewsType, int>());
while (statement.Step()) {
PreviewsType type = static_cast<PreviewsType>(statement.ColumnInt(0));
int version = statement.ColumnInt(1);
stored_previews->insert({type, version});
}
return stored_previews;
}
// Adds a newly enabled |type| with its |version| to the Enabled Previews table.
void InsertEnabledPreviewInDataBase(sql::Connection* db,
PreviewsType type,
int version) {
const char kSqlInsert[] = "INSERT INTO " ENABLED_PREVIEWS_TABLE_NAME
" (type, version)"
" VALUES "
" (?, ?)";
sql::Statement statement_insert(db->GetUniqueStatement(kSqlInsert));
statement_insert.BindInt(0, static_cast<int>(type));
statement_insert.BindInt(1, version);
statement_insert.Run();
}
// Updates the |version| of an enabled previews |type| in the Enabled Previews
// table.
void UpdateEnabledPreviewInDataBase(sql::Connection* db,
PreviewsType type,
int version) {
const char kSqlUpdate[] = "UPDATE " ENABLED_PREVIEWS_TABLE_NAME
" SET version = ?"
" WHERE type = ?";
sql::Statement statement_update(
db->GetCachedStatement(SQL_FROM_HERE, kSqlUpdate));
statement_update.BindInt(0, version);
statement_update.BindInt(1, static_cast<int>(type));
statement_update.Run();
}
// Deletes a previously enabled previews |type| from the Enabled Previews table.
void DeleteEnabledPreviewInDataBase(sql::Connection* db, PreviewsType type) {
const char kSqlDelete[] =
"DELETE FROM " ENABLED_PREVIEWS_TABLE_NAME " WHERE type == ?";
sql::Statement statement_delete(db->GetUniqueStatement(kSqlDelete));
statement_delete.BindInt(0, static_cast<int>(type));
statement_delete.Run();
}
// Checks the current set of enabled previews (with their current version)
// and where a preview is now disabled or has a different version, cleans up
// any associated blacklist entries.
void CheckAndReconcileEnabledPreviewsWithDataBase(
sql::Connection* db,
PreviewsTypeList* enabled_previews) {
std::unique_ptr<std::map<PreviewsType, int>> stored_previews(
GetStoredPreviews(db));
for (auto enabled_it = enabled_previews->begin();
enabled_it != enabled_previews->end(); ++enabled_it) {
PreviewsType type = enabled_it->first;
int current_version = enabled_it->second;
auto stored_it = stored_previews->find(type);
if (stored_it == stored_previews->end()) {
InsertEnabledPreviewInDataBase(db, type, current_version);
} else {
if (stored_it->second != current_version) {
DCHECK_GE(current_version, stored_it->second);
ClearBlacklistForTypeInDataBase(db, type);
UpdateEnabledPreviewInDataBase(db, type, current_version);
}
// Erase entry from the local map to detect any newly disabled types.
stored_previews->erase(stored_it);
}
}
// Now check for any types that are no longer enabled.
for (auto stored_it = stored_previews->begin();
stored_it != stored_previews->end(); ++stored_it) {
PreviewsType type = stored_it->first;
ClearBlacklistForTypeInDataBase(db, type);
DeleteEnabledPreviewInDataBase(db, type);
}
}
void LoadBlackListFromDataBase(
sql::Connection* db,
PreviewsTypeList* enabled_previews,
scoped_refptr<base::SingleThreadTaskRunner> runner,
LoadBlackListCallback callback) {
// First handle any update needed wrt enabled previews and their versions.
CheckAndReconcileEnabledPreviewsWithDataBase(db, enabled_previews);
// Gets the table sorted by host and time. Limits the number of hosts using
// most recent opt_out time as the limiting function. Sorting is free due to
// the table structure, and it improves performance in the loop below.
const char kSql[] =
"SELECT host_name, time, opt_out"
" FROM " PREVIEWS_OPT_OUT_TABLE_NAME " ORDER BY host_name, time DESC";
sql::Statement statement(db->GetUniqueStatement(kSql));
std::unique_ptr<BlackListItemMap> black_list_item_map(new BlackListItemMap());
std::unique_ptr<PreviewsBlackListItem> host_indifferent_black_list_item =
PreviewsBlackList::CreateHostIndifferentBlackListItem();
int count = 0;
// Add the host name, the visit time, and opt out history to
// |black_list_item_map|.
while (statement.Step()) {
++count;
std::string host_name = statement.ColumnString(0);
PreviewsBlackListItem* black_list_item =
PreviewsBlackList::GetOrCreateBlackListItemForMap(
black_list_item_map.get(), host_name);
DCHECK_LE(black_list_item_map->size(),
params::MaxInMemoryHostsInBlackList());
// Allows the internal logic of PreviewsBlackListItem to determine how to
// evict entries when there are more than
// |StoredHistoryLengthForBlackList()| for the host.
black_list_item->AddPreviewNavigation(
statement.ColumnBool(2),
base::Time::FromInternalValue(statement.ColumnInt64(1)));
// Allows the internal logic of PreviewsBlackListItem to determine what
// items to evict.
host_indifferent_black_list_item->AddPreviewNavigation(
statement.ColumnBool(2),
base::Time::FromInternalValue(statement.ColumnInt64(1)));
}
UMA_HISTOGRAM_COUNTS_10000("Previews.OptOut.DBRowCount", count);
if (count > MaxRowsInOptOutDB()) {
// Delete the oldest entries if there are more than |kMaxEntriesInDB|.
// DELETE ... LIMIT -1 OFFSET x means delete all but the first x entries.
const char kSqlDeleteByDBSize[] =
"DELETE FROM " PREVIEWS_OPT_OUT_TABLE_NAME
" WHERE ROWID IN"
" (SELECT ROWID from " PREVIEWS_OPT_OUT_TABLE_NAME
" ORDER BY time DESC"
" LIMIT -1 OFFSET ?)";
sql::Statement statement_delete(
db->GetCachedStatement(SQL_FROM_HERE, kSqlDeleteByDBSize));
statement_delete.BindInt(0, MaxRowsInOptOutDB());
statement_delete.Run();
}
runner->PostTask(FROM_HERE,
base::Bind(callback, base::Passed(&black_list_item_map),
base::Passed(&host_indifferent_black_list_item)));
}
// Synchronous implementations, these are run on the background thread
// and actually do the work to access the SQL data base.
void LoadBlackListSync(sql::Connection* db,
const base::FilePath& path,
std::unique_ptr<PreviewsTypeList> enabled_previews,
scoped_refptr<base::SingleThreadTaskRunner> runner,
LoadBlackListCallback callback) {
if (!db->is_open())
InitDatabase(db, path);
LoadBlackListFromDataBase(db, enabled_previews.get(), runner, callback);
}
// Deletes every row in the table that has entry time between |begin_time| and
// |end_time|.
void ClearBlackListSync(sql::Connection* db,
base::Time begin_time,
base::Time end_time) {
const char kSql[] = "DELETE FROM " PREVIEWS_OPT_OUT_TABLE_NAME
" WHERE time >= ? and time <= ?";
sql::Statement statement(db->GetUniqueStatement(kSql));
statement.BindInt64(0, begin_time.ToInternalValue());
statement.BindInt64(1, end_time.ToInternalValue());
statement.Run();
}
void AddPreviewNavigationSync(bool opt_out,
const std::string& host_name,
PreviewsType type,
base::Time now,
sql::Connection* db) {
sql::Transaction transaction(db);
if (!transaction.Begin())
return;
AddPreviewNavigationToDataBase(db, opt_out, host_name, type, now);
MaybeEvictHostEntryFromDataBase(db, host_name);
transaction.Commit();
}
} // namespace
PreviewsOptOutStoreSQL::PreviewsOptOutStoreSQL(
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
scoped_refptr<base::SequencedTaskRunner> background_task_runner,
const base::FilePath& path,
std::unique_ptr<PreviewsTypeList> enabled_previews)
: io_task_runner_(io_task_runner),
background_task_runner_(background_task_runner),
db_file_path_(path),
enabled_previews_(std::move(enabled_previews)) {
DCHECK(enabled_previews_);
}
PreviewsOptOutStoreSQL::~PreviewsOptOutStoreSQL() {
DCHECK(io_task_runner_->BelongsToCurrentThread());
if (db_.get()) {
background_task_runner_->DeleteSoon(FROM_HERE, db_.release());
}
}
void PreviewsOptOutStoreSQL::AddPreviewNavigation(bool opt_out,
const std::string& host_name,
PreviewsType type,
base::Time now) {
DCHECK(io_task_runner_->BelongsToCurrentThread());
DCHECK(db_.get());
background_task_runner_->PostTask(
FROM_HERE, base::Bind(&AddPreviewNavigationSync, opt_out, host_name, type,
now, db_.get()));
}
void PreviewsOptOutStoreSQL::ClearBlackList(base::Time begin_time,
base::Time end_time) {
DCHECK(io_task_runner_->BelongsToCurrentThread());
DCHECK(db_.get());
background_task_runner_->PostTask(
FROM_HERE,
base::Bind(&ClearBlackListSync, db_.get(), begin_time, end_time));
}
void PreviewsOptOutStoreSQL::LoadBlackList(LoadBlackListCallback callback) {
DCHECK(io_task_runner_->BelongsToCurrentThread());
if (!db_)
db_ = base::MakeUnique<sql::Connection>();
std::unique_ptr<PreviewsTypeList> enabled_previews =
base::MakeUnique<PreviewsTypeList>(*enabled_previews_);
background_task_runner_->PostTask(
FROM_HERE, base::Bind(&LoadBlackListSync, db_.get(), db_file_path_,
base::Passed(std::move(enabled_previews)),
base::ThreadTaskRunnerHandle::Get(), callback));
}
} // namespace previews