| Index: components/history/core/browser/top_sites_database.cc
|
| diff --git a/components/history/core/browser/top_sites_database.cc b/components/history/core/browser/top_sites_database.cc
|
| index 7e9fc819a0a5f4c92013add5553ac60c6bd45215..ebfeb4a5d340016bc91095203ccc302390b26861 100644
|
| --- a/components/history/core/browser/top_sites_database.cc
|
| +++ b/components/history/core/browser/top_sites_database.cc
|
| @@ -57,16 +57,13 @@ namespace {
|
| // anyhow).
|
|
|
| // Version 3: b6d6a783/r231648 by beaudoin@chromium.org on 2013-10-29
|
| -// Version 2: eb0b24e6/r87284 by satorux@chromium.org on 2011-05-31
|
| +// Version 2: eb0b24e6/r87284 by satorux@chromium.org on 2011-05-31 (deprecated)
|
| // Version 1: 809cc4d8/r64072 by sky@chromium.org on 2010-10-27 (deprecated)
|
|
|
| // NOTE(shess): When changing the version, add a new golden file for
|
| // the new version and a test to verify that Init() works with it.
|
| -// NOTE(shess): RecoverDatabaseOrRaze() depends on the specific
|
| -// version number. The code is subtle and in development, contact me
|
| -// if the necessary changes are not obvious.
|
| static const int kVersionNumber = 3;
|
| -static const int kDeprecatedVersionNumber = 1; // and earlier.
|
| +static const int kDeprecatedVersionNumber = 2; // and earlier.
|
|
|
| bool InitTables(sql::Connection* db) {
|
| const char kThumbnailsSql[] =
|
| @@ -107,9 +104,8 @@ void SetRedirects(const std::string& redirects, MostVisitedURL* url) {
|
| // Track various failure (and success) cases in recovery code.
|
| //
|
| // TODO(shess): The recovery code is complete, but by nature runs in challenging
|
| -// circumstances, so initially the default error response is to leave the
|
| -// existing database in place. This histogram is intended to expose the
|
| -// failures seen in the fleet. Frequent failure cases can be explored more
|
| +// circumstances, so errors will happen. This histogram is intended to expose
|
| +// the failures seen in the fleet. Frequent failure cases can be explored more
|
| // deeply to see if the complexity to fix them is warranted. Infrequent failure
|
| // cases can be resolved by marking the database unrecoverable (which will
|
| // delete the data).
|
| @@ -125,12 +121,12 @@ enum RecoveryEventType {
|
|
|
| // Sqlite.RecoveryEvent can usually be used to get more detail about the
|
| // specific failure (see sql/recovery.cc).
|
| - RECOVERY_EVENT_FAILED_SCOPER,
|
| + OBSOLETE_RECOVERY_EVENT_FAILED_SCOPER,
|
| RECOVERY_EVENT_FAILED_META_VERSION,
|
| RECOVERY_EVENT_FAILED_META_WRONG_VERSION,
|
| - RECOVERY_EVENT_FAILED_META_INIT,
|
| - RECOVERY_EVENT_FAILED_SCHEMA_INIT,
|
| - RECOVERY_EVENT_FAILED_AUTORECOVER_THUMBNAILS,
|
| + OBSOLETE_RECOVERY_EVENT_FAILED_META_INIT,
|
| + OBSOLETE_RECOVERY_EVENT_FAILED_SCHEMA_INIT,
|
| + OBSOLETE_RECOVERY_EVENT_FAILED_AUTORECOVER_THUMBNAILS,
|
| RECOVERY_EVENT_FAILED_COMMIT,
|
|
|
| // Track invariants resolved by FixThumbnailsTable().
|
| @@ -138,6 +134,9 @@ enum RecoveryEventType {
|
| RECOVERY_EVENT_INVARIANT_REDIRECT,
|
| RECOVERY_EVENT_INVARIANT_CONTIGUOUS,
|
|
|
| + // Track automated full-database recovery.
|
| + RECOVERY_EVENT_FAILED_AUTORECOVER,
|
| +
|
| // Always keep this at the end.
|
| RECOVERY_EVENT_MAX,
|
| };
|
| @@ -202,89 +201,48 @@ void FixThumbnailsTable(sql::Connection* db) {
|
| RecordRecoveryEvent(RECOVERY_EVENT_INVARIANT_CONTIGUOUS);
|
| }
|
|
|
| -// Recover the database to the extent possible, razing it if recovery is not
|
| -// possible.
|
| -void RecoverDatabaseOrRaze(sql::Connection* db, const base::FilePath& db_path) {
|
| +// Recover the database to the extent possible, then fixup any broken
|
| +// constraints.
|
| +void RecoverAndFixup(sql::Connection* db, const base::FilePath& db_path) {
|
| // NOTE(shess): If the version changes, review this code.
|
| DCHECK_EQ(3, kVersionNumber);
|
|
|
| - // It is almost certain that some operation against |db| will fail, prevent
|
| - // reentry.
|
| - db->reset_error_callback();
|
| -
|
| - // For generating histogram stats.
|
| - size_t thumbnails_recovered = 0;
|
| - int64_t original_size = 0;
|
| - base::GetFileSize(db_path, &original_size);
|
| -
|
| - std::unique_ptr<sql::Recovery> recovery = sql::Recovery::Begin(db, db_path);
|
| + std::unique_ptr<sql::Recovery> recovery =
|
| + sql::Recovery::BeginRecoverDatabase(db, db_path);
|
| if (!recovery) {
|
| - RecordRecoveryEvent(RECOVERY_EVENT_FAILED_SCOPER);
|
| + RecordRecoveryEvent(RECOVERY_EVENT_FAILED_AUTORECOVER);
|
| return;
|
| }
|
|
|
| - // Setup the meta recovery table and fetch the version number from the corrupt
|
| - // database.
|
| + // If the [meta] table does not exist, or the [version] key cannot be found,
|
| + // then the schema is indeterminate. The only plausible approach would be to
|
| + // validate that the schema contains all of the tables and indices and columns
|
| + // expected, but that complexity may not be warranted, this case has only been
|
| + // seen for a few thousand database files.
|
| int version = 0;
|
| if (!recovery->SetupMeta() || !recovery->GetMetaVersionNumber(&version)) {
|
| - // TODO(shess): Prior histograms indicate all failures are in creating the
|
| - // recover virtual table for corrupt.meta. The table may not exist, or the
|
| - // database may be too far gone. Either way, unclear how to resolve.
|
| - sql::Recovery::Rollback(std::move(recovery));
|
| + sql::Recovery::Unrecoverable(std::move(recovery));
|
| RecordRecoveryEvent(RECOVERY_EVENT_FAILED_META_VERSION);
|
| return;
|
| }
|
|
|
| - // This code runs in a context which may be able to read version information
|
| - // that the regular deprecation path cannot. The effect of this code will be
|
| - // to raze the database.
|
| + // In this case the next open will clear the database anyhow.
|
| if (version <= kDeprecatedVersionNumber) {
|
| sql::Recovery::Unrecoverable(std::move(recovery));
|
| RecordRecoveryEvent(RECOVERY_EVENT_DEPRECATED);
|
| return;
|
| }
|
|
|
| - // TODO(shess): Earlier versions have been deprecated, later versions should
|
| - // be impossible. Unrecoverable() seems like a feasible response if this is
|
| - // infrequent enough.
|
| - if (version != 2 && version != 3) {
|
| + // TODO(shess): Consider marking corrupt databases from the future
|
| + // Unrecoverable(), since this histogram value has never been seen. OTOH,
|
| + // this may be too risky, because if future code was correlated with
|
| + // corruption then rollback would be a sensible response.
|
| + if (version > kVersionNumber) {
|
| RecordRecoveryEvent(RECOVERY_EVENT_FAILED_META_WRONG_VERSION);
|
| sql::Recovery::Rollback(std::move(recovery));
|
| return;
|
| }
|
|
|
| - // Both v2 and v3 recover to current schema version.
|
| - sql::MetaTable recover_meta_table;
|
| - if (!recover_meta_table.Init(recovery->db(), kVersionNumber,
|
| - kVersionNumber)) {
|
| - sql::Recovery::Rollback(std::move(recovery));
|
| - RecordRecoveryEvent(RECOVERY_EVENT_FAILED_META_INIT);
|
| - return;
|
| - }
|
| -
|
| - // Create a fresh version of the schema. The recovery code uses
|
| - // conflict-resolution to handle duplicates, so any indices are necessary.
|
| - if (!InitTables(recovery->db())) {
|
| - // TODO(shess): Unable to create the new schema in the new database. The
|
| - // new database should be a temporary file, so being unable to work with it
|
| - // is pretty unclear.
|
| - //
|
| - // What are the potential responses, even? The recovery database could be
|
| - // opened as in-memory. If the temp database had a filesystem problem and
|
| - // the temp filesystem differs from the main database, then that could fix
|
| - // it.
|
| - sql::Recovery::Rollback(std::move(recovery));
|
| - RecordRecoveryEvent(RECOVERY_EVENT_FAILED_SCHEMA_INIT);
|
| - return;
|
| - }
|
| -
|
| - // In the v2 case the missing column will get default values.
|
| - if (!recovery->AutoRecoverTable("thumbnails", &thumbnails_recovered)) {
|
| - sql::Recovery::Rollback(std::move(recovery));
|
| - RecordRecoveryEvent(RECOVERY_EVENT_FAILED_AUTORECOVER_THUMBNAILS);
|
| - return;
|
| - }
|
| -
|
| // TODO(shess): Inline this?
|
| FixThumbnailsTable(recovery->db());
|
|
|
| @@ -296,23 +254,6 @@ void RecoverDatabaseOrRaze(sql::Connection* db, const base::FilePath& db_path) {
|
| return;
|
| }
|
|
|
| - // Track the size of the recovered database relative to the size of the input
|
| - // database. The size should almost always be smaller, unless the input
|
| - // database was empty to start with. If the percentage results are very low,
|
| - // something is awry.
|
| - int64_t final_size = 0;
|
| - if (original_size > 0 && base::GetFileSize(db_path, &final_size) &&
|
| - final_size > 0) {
|
| - UMA_HISTOGRAM_PERCENTAGE("History.TopSitesRecoveredPercentage",
|
| - final_size * 100 / original_size);
|
| - }
|
| -
|
| - // Using 10,000 because these cases mostly care about "none recovered" and
|
| - // "lots recovered". More than 10,000 rows recovered probably means there's
|
| - // something wrong with the profile.
|
| - UMA_HISTOGRAM_COUNTS_10000("History.TopSitesRecoveredRowsThumbnails",
|
| - static_cast<int>(thumbnails_recovered));
|
| -
|
| RecordRecoveryEvent(RECOVERY_EVENT_RECOVERED);
|
| }
|
|
|
| @@ -324,11 +265,21 @@ void DatabaseErrorCallback(sql::Connection* db,
|
| // be the history thread, but at this level I can't see how to reach that.
|
|
|
| // Attempt to recover corrupt databases.
|
| - int error = (extended_error & 0xFF);
|
| - if (error == SQLITE_CORRUPT ||
|
| - error == SQLITE_CANTOPEN ||
|
| - error == SQLITE_NOTADB) {
|
| - RecoverDatabaseOrRaze(db, db_path);
|
| + 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.
|
| + RecoverAndFixup(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;
|
| }
|
|
|
| // TODO(shess): This database's error histograms look like:
|
|
|