summaryrefslogtreecommitdiffstats
path: root/sql/recovery.cc
diff options
context:
space:
mode:
authorshess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-07-19 18:25:30 +0000
committershess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-07-19 18:25:30 +0000
commit8d40941e03359f6f56c46c113e117d270dcb57a0 (patch)
tree1f29e17a1beb0548891e66f1d4e9a53ef064ad3d /sql/recovery.cc
parent89849df0a4130db30c99ef13064791b6142b470f (diff)
downloadchromium_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.cc189
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