| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include"sql/recovery.h" |
| |
| #include<stddef.h> |
| |
| #include<string> |
| #include<tuple> |
| |
| #include"base/check.h" |
| #include"base/check_op.h" |
| #include"base/logging.h" |
| #include"base/metrics/histogram_functions.h" |
| #include"base/notreached.h" |
| #include"base/strings/strcat.h" |
| #include"build/build_config.h" |
| #include"sql/database.h" |
| #include"sql/error_delegate_util.h" |
| #include"sql/internal_api_token.h" |
| #include"sql/meta_table.h" |
| #include"sql/sqlite_result_code.h" |
| #include"third_party/sqlite/sqlite3.h" |
| |
| namespace sql{ |
| |
| namespace{ |
| |
| constexprchar kMainDatabaseName[]="main"; |
| |
| }// namespace |
| |
| // static |
| boolRecovery::ShouldAttemptRecovery(Database* database,int extended_error){ |
| return database&& database->is_open()&& |
| !database->DbPath(InternalApiToken()).empty()&& |
| #if BUILDFLAG(IS_FUCHSIA) |
| // Recovering WAL databases is not supported on Fuchsia. |
| !database->UseWALMode()&& |
| #endif// BUILDFLAG(IS_FUCHSIA) |
| IsErrorCatastrophic(extended_error); |
| } |
| |
| // static |
| SqliteResultCodeRecovery::RecoverDatabase(Database* database, |
| Strategy strategy){ |
| auto recovery=Recovery(database, strategy); |
| return recovery.RecoverAndReplaceDatabase(); |
| } |
| |
| // static |
| boolRecovery::RecoverIfPossible(Database* database, |
| int extended_error, |
| Strategy strategy){ |
| if(!ShouldAttemptRecovery(database, extended_error)){ |
| returnfalse; |
| } |
| |
| // Recovery should be attempted. Since recovery must only be attempted from |
| // within a database error callback, reset the error callback to prevent |
| // re-entry. |
| database->reset_error_callback(); |
| |
| auto result=Recovery::RecoverDatabase(database, strategy); |
| if(!IsSqliteSuccessCode(result)){ |
| DLOG(ERROR)<<"Database recovery failed with result code: "<< result; |
| } |
| |
| returntrue; |
| } |
| |
| Recovery::Recovery(Database* database,Strategy strategy) |
| : strategy_(strategy), |
| db_(database), |
| recover_db_( |
| DatabaseOptions().set_page_size(database? database->page_size():0), |
| "Recovery"){ |
| CHECK(db_); |
| CHECK(db_->is_open()); |
| // Recovery is likely to be used in error handling. To prevent re-entry due to |
| // errors while attempting to recover the database, the error callback must |
| // not be set. |
| CHECK(!db_->has_error_callback()); |
| |
| auto db_path= db_->DbPath(InternalApiToken()); |
| |
| // Corruption recovery for in-memory databases is not supported. |
| CHECK(!db_path.empty()); |
| |
| // Cache the database's histogram tag while the database is open. |
| database_uma_name_= db_->histogram_tag(); |
| |
| recovery_database_path_= db_path.AddExtensionASCII(".recovery"); |
| |
| // Break any outstanding transactions on the original database, since the |
| // recovery module opens a transaction on the database while recovery is in |
| // progress. |
| db_->RollbackAllTransactions(); |
| } |
| |
| Recovery::~Recovery(){ |
| // Recovery result must be set before we reach this point. |
| CHECK_NE(result_,Result::kUnknown); |
| |
| base::UmaHistogramEnumeration("Sql.Recovery.Result", result_); |
| UmaHistogramSqliteResult("Sql.Recovery.ResultCode", |
| static_cast<int>(sqlite_result_code_)); |
| |
| if(!database_uma_name_.empty()){ |
| base::UmaHistogramEnumeration( |
| base::StrCat({"Sql.Recovery.Result.", database_uma_name_}), result_); |
| UmaHistogramSqliteResult( |
| base::StrCat({"Sql.Recovery.ResultCode.", database_uma_name_}), |
| static_cast<int>(sqlite_result_code_)); |
| } |
| |
| if(db_){ |
| if(result_==Result::kSuccess){ |
| // Poison the original handle, but don't raze the database. |
| db_->Poison(); |
| }else{ |
| db_->RazeAndPoison(); |
| } |
| } |
| |
| db_=nullptr; |
| |
| if(recover_db_.is_open()){ |
| recover_db_.Close(); |
| } |
| // TODO(crbug.com/40061775): Don't always delete the recovery db if we |
| // are ever to keep around successfully-recovered, but unsuccessfully-restored |
| // databases. |
| sql::Database::Delete(recovery_database_path_); |
| } |
| |
| voidRecovery::SetRecoverySucceeded(){ |
| // Recovery result must only be set once. |
| CHECK_EQ(result_,Result::kUnknown); |
| |
| result_=Result::kSuccess; |
| } |
| |
| voidRecovery::SetRecoveryFailed(Result failure_result, |
| SqliteResultCode result_code){ |
| // Recovery result must only be set once. |
| CHECK_EQ(result_,Result::kUnknown); |
| |
| switch(failure_result){ |
| caseResult::kUnknown: |
| caseResult::kSuccess: |
| NOTREACHED(); |
| caseResult::kFailedRecoveryInit: |
| caseResult::kFailedRecoveryRun: |
| caseResult::kFailedToOpenRecoveredDatabase: |
| caseResult::kFailedMetaTableDoesNotExist: |
| caseResult::kFailedMetaTableInit: |
| caseResult::kFailedMetaTableVersionWasInvalid: |
| caseResult::kFailedBackupInit: |
| caseResult::kFailedBackupRun: |
| break; |
| } |
| |
| result_= failure_result; |
| sqlite_result_code_= result_code; |
| } |
| |
| SqliteResultCodeRecovery::RecoverAndReplaceDatabase(){ |
| auto sqlite_result_code=AttemptToRecoverDatabaseToBackup(); |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| return sqlite_result_code; |
| } |
| |
| // Open a connection to the newly-created recovery database. |
| if(!recover_db_.Open(recovery_database_path_)){ |
| DVLOG(1)<<"Unable to open recovery database."; |
| |
| // TODO(crbug.com/40061775): It's unfortunate to give up now, after |
| // we've successfully recovered the database to a backup. Consider falling |
| // back to base::Move(). |
| SetRecoveryFailed(Result::kFailedToOpenRecoveredDatabase, |
| ToSqliteResultCode(recover_db_.GetErrorCode())); |
| returnSqliteResultCode::kError; |
| } |
| |
| if(strategy_==Strategy::kRecoverWithMetaVersionOrRaze&& |
| !RecoveredDbHasValidMetaTable()){ |
| DVLOG(1)<<"Could not read valid version number from recovery database."; |
| returnSqliteResultCode::kError; |
| } |
| |
| returnReplaceOriginalWithRecoveredDb(); |
| } |
| |
| SqliteResultCodeRecovery::AttemptToRecoverDatabaseToBackup(){ |
| CHECK(db_->is_open()); |
| CHECK(!recover_db_.is_open()); |
| |
| // See full documentation for the corruption recovery module in |
| // https://sqlite.org/src/file/ext/recover/sqlite3recover.h |
| |
| // sqlite3_recover_init() create a new sqlite3_recover handle, with data being |
| // recovered into a new database. This should very rarely fail - e.g. if |
| // memory for the recovery object itself could not be allocated. If it does |
| // fail, `recover` will be nullptr and an error code will surface when |
| // attempting to configure the recovery object below. |
| sqlite3_recover* recover= |
| sqlite3_recover_init(db_->db(InternalApiToken()), kMainDatabaseName, |
| recovery_database_path_.AsUTF8Unsafe().c_str()); |
| |
| // sqlite3_recover_config() configures the sqlite3_recover object. |
| // |
| // These functions should only fail if the above initialization failed, or if |
| // invalid parameters are passed. |
| |
| // Don't bother creating a lost-and-found table. |
| sqlite3_recover_config(recover, SQLITE_RECOVER_LOST_AND_FOUND,nullptr); |
| // Do not attempt to recover records from pages that appear to be linked to |
| // the freelist, to avoid "recovering" deleted records. |
| int kRecoverFreelist=0; |
| sqlite3_recover_config(recover, SQLITE_RECOVER_FREELIST_CORRUPT, |
| static_cast<void*>(&kRecoverFreelist)); |
| // Attempt to recover ROWID values that are not INTEGER PRIMARY KEY. |
| int kRecoverRowIds=1; |
| sqlite3_recover_config(recover, SQLITE_RECOVER_ROWIDS, |
| static_cast<void*>(&kRecoverRowIds)); |
| |
| auto sqlite_result_code= |
| ToSqliteResultCode(sqlite3_recover_errcode(recover)); |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| CHECK_NE(sqlite_result_code,SqliteResultCode::kApiMisuse); |
| |
| // The recovery could not be configured. |
| // TODO(crbug.com/40061775): This is likely a transient issue, so we |
| // could consider keeping the database intact in case the caller wants to |
| // try again later. For now, we'll always raze. |
| SetRecoveryFailed(Result::kFailedRecoveryInit, sqlite_result_code); |
| |
| DVLOG(1)<<"recovery config error: "<< sqlite_result_code |
| << sqlite3_recover_errcode(recover); |
| |
| // Clean up the recovery object. |
| sqlite3_recover_finish(recover); |
| return sqlite_result_code; |
| } |
| |
| // sqlite3_recover_run() attempts to construct an copy of the database with |
| // data corruption handled. It returns SQLITE_OK if recovery was successful. |
| sqlite_result_code=ToSqliteResultCode(sqlite3_recover_run(recover)); |
| |
| // sqlite3_recover_finish() cleans up the recovery object. It should return |
| // the same error code as from sqlite3_recover_run(). |
| auto finish_result_code=ToSqliteResultCode(sqlite3_recover_finish(recover)); |
| CHECK_EQ(finish_result_code, sqlite_result_code); |
| |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| // Could not recover the database. |
| SetRecoveryFailed(Result::kFailedRecoveryRun, sqlite_result_code); |
| |
| DVLOG(1)<<"recovery error: "<< sqlite_result_code |
| << sqlite3_recover_errmsg(recover); |
| } |
| |
| return sqlite_result_code; |
| } |
| |
| SqliteResultCodeRecovery::ReplaceOriginalWithRecoveredDb(){ |
| CHECK(db_->is_open()); |
| CHECK(recover_db_.is_open()); |
| |
| // sqlite3_backup_init() fails if a transaction is ongoing. This should be |
| // rare, since we rolled back all transactions in this object's constructor. |
| sqlite3_backup* backup= sqlite3_backup_init( |
| db_->db(InternalApiToken()), kMainDatabaseName, |
| recover_db_.db(InternalApiToken()), kMainDatabaseName); |
| if(!backup){ |
| // Error code is in the destination database handle. |
| DVLOG(1)<<"sqlite3_backup_init() failed: " |
| << sqlite3_errmsg(db_->db(InternalApiToken())); |
| |
| auto result_code= |
| ToSqliteResultCode(sqlite3_errcode(db_->db(InternalApiToken()))); |
| |
| // TODO(crbug.com/40061775): It's unfortunate to give up now, after |
| // we've successfully recovered the database. Consider falling back to |
| // base::Move(). |
| SetRecoveryFailed(Result::kFailedBackupInit, result_code); |
| return result_code; |
| } |
| |
| // sqlite3_backup_step() copies pages from the source to the destination |
| // database. It returns SQLITE_DONE if copying successfully completed, or some |
| // other error on failure. |
| // TODO(crbug.com/40061775): Some of these errors are transient and the |
| // operation could feasibly succeed at a later time. Consider keeping around |
| // successfully-recovered, but unsuccessfully-restored databases or falling |
| // back to base::Move(). |
| constexprint kUnlimitedPageCount=-1;// Back up entire database. |
| auto sqlite_result_code= |
| ToSqliteResultCode(sqlite3_backup_step(backup, kUnlimitedPageCount)); |
| |
| // sqlite3_backup_remaining() returns the number of pages still to be backed |
| // up, which should be zero if sqlite3_backup_step() completed successfully. |
| int pages_remaining= sqlite3_backup_remaining(backup); |
| |
| // sqlite3_backup_finish() releases the sqlite3_backup object. |
| // |
| // It returns an error code only if the backup encountered a permanent error. |
| // We use the the sqlite3_backup_step() result instead, because it also tells |
| // us about temporary errors, like SQLITE_BUSY. |
| // |
| // We pass the sqlite3_backup_finish() result code through |
| // ToSqliteResultCode() to catch codes that should never occur, like |
| // SQLITE_MISUSE. |
| std::ignore=ToSqliteResultCode(sqlite3_backup_finish(backup)); |
| |
| if(sqlite_result_code!=SqliteResultCode::kDone){ |
| CHECK_NE(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_backup_step() returned SQLITE_OK (instead of SQLITE_DONE) " |
| <<"when asked to back up the entire database"; |
| |
| DVLOG(1)<<"sqlite3_backup_step() failed: " |
| << sqlite3_errmsg(db_->db(InternalApiToken())); |
| SetRecoveryFailed(Result::kFailedBackupRun, sqlite_result_code); |
| return sqlite_result_code; |
| } |
| |
| // The original database was successfully recovered and replaced. Hooray! |
| SetRecoverySucceeded(); |
| |
| CHECK_EQ(pages_remaining,0); |
| returnSqliteResultCode::kOk; |
| } |
| |
| boolRecovery::RecoveredDbHasValidMetaTable(){ |
| CHECK(recover_db_.is_open()); |
| |
| if(!MetaTable::DoesTableExist(&recover_db_)){ |
| DVLOG(1)<<"Meta table does not exist in recovery database."; |
| SetRecoveryFailed(Result::kFailedMetaTableDoesNotExist, |
| ToSqliteResultCode(recover_db_.GetErrorCode())); |
| returnfalse; |
| } |
| |
| // MetaTable::Init will not create a meta table if one already exists. |
| sql::MetaTable meta_table; |
| if(!meta_table.Init(&recover_db_,/*version=*/1, |
| /*compatible_version=*/1)){ |
| SetRecoveryFailed(Result::kFailedMetaTableInit, |
| ToSqliteResultCode(recover_db_.GetErrorCode())); |
| returnfalse; |
| } |
| |
| // Confirm that we can read a valid version number from the recovered table. |
| if(meta_table.GetVersionNumber()<=0){ |
| SetRecoveryFailed(Result::kFailedMetaTableVersionWasInvalid, |
| ToSqliteResultCode(recover_db_.GetErrorCode())); |
| returnfalse; |
| } |
| |
| returntrue; |
| } |
| |
| }// namespace sql |