| Index: sql/recovery.cc
|
| diff --git a/sql/recovery.cc b/sql/recovery.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..a8e5217d9baebdcf3585d8241fbcc153605d520d
|
| --- /dev/null
|
| +++ b/sql/recovery.cc
|
| @@ -0,0 +1,189 @@
|
| +// Copyright (c) 2013 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 "sql/recovery.h"
|
| +
|
| +#include "base/files/file_path.h"
|
| +#include "base/logging.h"
|
| +#include "base/metrics/sparse_histogram.h"
|
| +#include "sql/connection.h"
|
| +#include "third_party/sqlite/sqlite3.h"
|
| +
|
| +namespace sql {
|
| +
|
| +// static
|
| +scoped_ptr<Recovery> Recovery::Begin(
|
| + Connection* connection,
|
| + const base::FilePath& db_path) {
|
| + scoped_ptr<Recovery> r(new Recovery(connection));
|
| + if (!r->Init(db_path)) {
|
| + // TODO(shess): Should Init() failure result in Raze()?
|
| + r->Shutdown(POISON);
|
| + return scoped_ptr<Recovery>();
|
| + }
|
| +
|
| + return r.Pass();
|
| +}
|
| +
|
| +// static
|
| +bool Recovery::Recovered(scoped_ptr<Recovery> r) {
|
| + return r->Backup();
|
| +}
|
| +
|
| +// static
|
| +void Recovery::Unrecoverable(scoped_ptr<Recovery> r) {
|
| + CHECK(r->db_);
|
| + // ~Recovery() will RAZE_AND_POISON.
|
| +}
|
| +
|
| +Recovery::Recovery(Connection* connection)
|
| + : db_(connection),
|
| + recover_db_() {
|
| + // Result should keep the page size specified earlier.
|
| + if (db_->page_size_)
|
| + recover_db_.set_page_size(db_->page_size_);
|
| +
|
| + // TODO(shess): This may not handle cases where the default page
|
| + // size is used, but the default has changed. I do not think this
|
| + // has ever happened. This could be handled by using "PRAGMA
|
| + // page_size", at the cost of potential additional failure cases.
|
| +}
|
| +
|
| +Recovery::~Recovery() {
|
| + Shutdown(RAZE_AND_POISON);
|
| +}
|
| +
|
| +bool Recovery::Init(const base::FilePath& db_path) {
|
| + // Prevent the possibility of re-entering this code due to errors
|
| + // which happen while executing this code.
|
| + DCHECK(!db_->has_error_callback());
|
| +
|
| + // Break any outstanding transactions on the original database to
|
| + // prevent deadlocks reading through the attached version.
|
| + // TODO(shess): A client may legitimately wish to recover from
|
| + // within the transaction context, because it would potentially
|
| + // preserve any in-flight changes. Unfortunately, any attach-based
|
| + // system could not handle that. A system which manually queried
|
| + // one database and stored to the other possibly could, but would be
|
| + // more complicated.
|
| + db_->RollbackAllTransactions();
|
| +
|
| + if (!recover_db_.OpenTemporary())
|
| + return false;
|
| +
|
| + // Turn on |SQLITE_RecoveryMode| for the handle, which allows
|
| + // reading certain broken databases.
|
| + if (!recover_db_.Execute("PRAGMA writable_schema=1"))
|
| + return false;
|
| +
|
| + if (!recover_db_.AttachDatabase(db_path, "corrupt"))
|
| + return false;
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool Recovery::Backup() {
|
| + CHECK(db_);
|
| + CHECK(recover_db_.is_open());
|
| +
|
| + // TODO(shess): Some of the failure cases here may need further
|
| + // exploration. Just as elsewhere, persistent problems probably
|
| + // need to be razed, while anything which might succeed on a future
|
| + // run probably should be allowed to try. But since Raze() uses the
|
| + // same approach, even that wouldn't work when this code fails.
|
| + //
|
| + // The documentation for the backup system indicate a relatively
|
| + // small number of errors are expected:
|
| + // SQLITE_BUSY - cannot lock the destination database. This should
|
| + // only happen if someone has another handle to the
|
| + // database, Chromium generally doesn't do that.
|
| + // SQLITE_LOCKED - someone locked the source database. Should be
|
| + // impossible (perhaps anti-virus could?).
|
| + // SQLITE_READONLY - destination is read-only.
|
| + // SQLITE_IOERR - since source database is temporary, probably
|
| + // indicates that the destination contains blocks
|
| + // throwing errors, or gross filesystem errors.
|
| + // SQLITE_NOMEM - out of memory, should be transient.
|
| + //
|
| + // AFAICT, SQLITE_BUSY and SQLITE_NOMEM could perhaps be considered
|
| + // transient, with SQLITE_LOCKED being unclear.
|
| + //
|
| + // SQLITE_READONLY and SQLITE_IOERR are probably persistent, with a
|
| + // strong chance that Raze() would not resolve them. If Delete()
|
| + // deletes the database file, the code could then re-open the file
|
| + // and attempt the backup again.
|
| + //
|
| + // For now, this code attempts a best effort and records histograms
|
| + // to inform future development.
|
| +
|
| + // Backup the original db from the recovered db.
|
| + const char* kMain = "main";
|
| + sqlite3_backup* backup = sqlite3_backup_init(db_->db_, kMain,
|
| + recover_db_.db_, kMain);
|
| + if (!backup) {
|
| + // Error code is in the destination database handle.
|
| + int err = sqlite3_errcode(db_->db_);
|
| + UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryHandle", err);
|
| + LOG(ERROR) << "sqlite3_backup_init() failed: "
|
| + << sqlite3_errmsg(db_->db_);
|
| + return false;
|
| + }
|
| +
|
| + // -1 backs up the entire database.
|
| + int rc = sqlite3_backup_step(backup, -1);
|
| + int pages = sqlite3_backup_pagecount(backup);
|
| + // TODO(shess): sqlite3_backup_finish() appears to allow returning a
|
| + // different value from sqlite3_backup_step(). Circle back and
|
| + // figure out if that can usefully inform the decision of whether to
|
| + // retry or not.
|
| + sqlite3_backup_finish(backup);
|
| + DCHECK_GT(pages, 0);
|
| +
|
| + if (rc != SQLITE_DONE) {
|
| + UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryStep", rc);
|
| + LOG(ERROR) << "sqlite3_backup_step() failed: "
|
| + << sqlite3_errmsg(db_->db_);
|
| + }
|
| +
|
| + // The destination database was locked. Give up, but leave the data
|
| + // in place. Maybe it won't be locked next time.
|
| + if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {
|
| + Shutdown(POISON);
|
| + return false;
|
| + }
|
| +
|
| + // Running out of memory should be transient, retry later.
|
| + if (rc == SQLITE_NOMEM) {
|
| + Shutdown(POISON);
|
| + return false;
|
| + }
|
| +
|
| + // TODO(shess): For now, leave the original database alone, pending
|
| + // results from Sqlite.RecoveryStep. Some errors should probably
|
| + // route to RAZE_AND_POISON.
|
| + if (rc != SQLITE_DONE) {
|
| + Shutdown(POISON);
|
| + return false;
|
| + }
|
| +
|
| + // Clean up the recovery db, and terminate the main database
|
| + // connection.
|
| + Shutdown(POISON);
|
| + return true;
|
| +}
|
| +
|
| +void Recovery::Shutdown(Recovery::Disposition raze) {
|
| + if (!db_)
|
| + return;
|
| +
|
| + recover_db_.Close();
|
| + if (raze == RAZE_AND_POISON) {
|
| + db_->RazeAndClose();
|
| + } else if (raze == POISON) {
|
| + db_->Poison();
|
| + }
|
| + db_ = NULL;
|
| +}
|
| +
|
| +} // namespace sql
|
|
|