diff options
author | shess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-07-19 18:25:30 +0000 |
---|---|---|
committer | shess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-07-19 18:25:30 +0000 |
commit | 8d40941e03359f6f56c46c113e117d270dcb57a0 (patch) | |
tree | 1f29e17a1beb0548891e66f1d4e9a53ef064ad3d /sql/recovery.cc | |
parent | 89849df0a4130db30c99ef13064791b6142b470f (diff) | |
download | chromium_src-8d40941e03359f6f56c46c113e117d270dcb57a0.zip chromium_src-8d40941e03359f6f56c46c113e117d270dcb57a0.tar.gz chromium_src-8d40941e03359f6f56c46c113e117d270dcb57a0.tar.bz2 |
[sql] Scoped recovery framework.
sql::Recovery is intended to be used within a sql::Connection error
callback to either recover the database (*) or indicate that the
database is unrecoverable and should be razed. The intend is that the
code should either progress to a valid database which is composed of
data recovered from the original (likely corrupt) database, or a valid
database which is empty.
This is just the framework without the SQLite-level data-recovery
virtual table.
BUG=109482
Review URL: https://chromiumcodereview.appspot.com/19281002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@212607 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'sql/recovery.cc')
-rw-r--r-- | sql/recovery.cc | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/sql/recovery.cc b/sql/recovery.cc new file mode 100644 index 0000000..e929ed9 --- /dev/null +++ b/sql/recovery.cc @@ -0,0 +1,189 @@ +// Copyright 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 |