| // 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 |