summaryrefslogtreecommitdiff
path: root/chromium/components/previews/core/previews_opt_out_store_sql.cc
blob: 4b9b97b67d95012d504e09a6a81e06c361d54074 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
// 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/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