Home | History | Annotate | Download | only in sql
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "sql/recovery.h"
      6 
      7 #include "base/files/file_path.h"
      8 #include "base/logging.h"
      9 #include "base/metrics/sparse_histogram.h"
     10 #include "sql/connection.h"
     11 #include "third_party/sqlite/sqlite3.h"
     12 
     13 namespace sql {
     14 
     15 // static
     16 scoped_ptr<Recovery> Recovery::Begin(
     17     Connection* connection,
     18     const base::FilePath& db_path) {
     19   scoped_ptr<Recovery> r(new Recovery(connection));
     20   if (!r->Init(db_path)) {
     21     // TODO(shess): Should Init() failure result in Raze()?
     22     r->Shutdown(POISON);
     23     return scoped_ptr<Recovery>();
     24   }
     25 
     26   return r.Pass();
     27 }
     28 
     29 // static
     30 bool Recovery::Recovered(scoped_ptr<Recovery> r) {
     31   return r->Backup();
     32 }
     33 
     34 // static
     35 void Recovery::Unrecoverable(scoped_ptr<Recovery> r) {
     36   CHECK(r->db_);
     37   // ~Recovery() will RAZE_AND_POISON.
     38 }
     39 
     40 Recovery::Recovery(Connection* connection)
     41     : db_(connection),
     42       recover_db_() {
     43   // Result should keep the page size specified earlier.
     44   if (db_->page_size_)
     45     recover_db_.set_page_size(db_->page_size_);
     46 
     47   // TODO(shess): This may not handle cases where the default page
     48   // size is used, but the default has changed.  I do not think this
     49   // has ever happened.  This could be handled by using "PRAGMA
     50   // page_size", at the cost of potential additional failure cases.
     51 }
     52 
     53 Recovery::~Recovery() {
     54   Shutdown(RAZE_AND_POISON);
     55 }
     56 
     57 bool Recovery::Init(const base::FilePath& db_path) {
     58   // Prevent the possibility of re-entering this code due to errors
     59   // which happen while executing this code.
     60   DCHECK(!db_->has_error_callback());
     61 
     62   // Break any outstanding transactions on the original database to
     63   // prevent deadlocks reading through the attached version.
     64   // TODO(shess): A client may legitimately wish to recover from
     65   // within the transaction context, because it would potentially
     66   // preserve any in-flight changes.  Unfortunately, any attach-based
     67   // system could not handle that.  A system which manually queried
     68   // one database and stored to the other possibly could, but would be
     69   // more complicated.
     70   db_->RollbackAllTransactions();
     71 
     72   if (!recover_db_.OpenTemporary())
     73     return false;
     74 
     75   // TODO(shess): Figure out a story for USE_SYSTEM_SQLITE.  The
     76   // virtual table implementation relies on SQLite internals for some
     77   // types and functions, which could be copied inline to make it
     78   // standalone.  Or an alternate implementation could try to read
     79   // through errors entirely at the SQLite level.
     80   //
     81   // For now, defer to the caller.  The setup will succeed, but the
     82   // later CREATE VIRTUAL TABLE call will fail, at which point the
     83   // caller can fire Unrecoverable().
     84 #if !defined(USE_SYSTEM_SQLITE)
     85   int rc = recoverVtableInit(recover_db_.db_);
     86   if (rc != SQLITE_OK) {
     87     LOG(ERROR) << "Failed to initialize recover module: "
     88                << recover_db_.GetErrorMessage();
     89     return false;
     90   }
     91 #endif
     92 
     93   // Turn on |SQLITE_RecoveryMode| for the handle, which allows
     94   // reading certain broken databases.
     95   if (!recover_db_.Execute("PRAGMA writable_schema=1"))
     96     return false;
     97 
     98   if (!recover_db_.AttachDatabase(db_path, "corrupt"))
     99     return false;
    100 
    101   return true;
    102 }
    103 
    104 bool Recovery::Backup() {
    105   CHECK(db_);
    106   CHECK(recover_db_.is_open());
    107 
    108   // TODO(shess): Some of the failure cases here may need further
    109   // exploration.  Just as elsewhere, persistent problems probably
    110   // need to be razed, while anything which might succeed on a future
    111   // run probably should be allowed to try.  But since Raze() uses the
    112   // same approach, even that wouldn't work when this code fails.
    113   //
    114   // The documentation for the backup system indicate a relatively
    115   // small number of errors are expected:
    116   // SQLITE_BUSY - cannot lock the destination database.  This should
    117   //               only happen if someone has another handle to the
    118   //               database, Chromium generally doesn't do that.
    119   // SQLITE_LOCKED - someone locked the source database.  Should be
    120   //                 impossible (perhaps anti-virus could?).
    121   // SQLITE_READONLY - destination is read-only.
    122   // SQLITE_IOERR - since source database is temporary, probably
    123   //                indicates that the destination contains blocks
    124   //                throwing errors, or gross filesystem errors.
    125   // SQLITE_NOMEM - out of memory, should be transient.
    126   //
    127   // AFAICT, SQLITE_BUSY and SQLITE_NOMEM could perhaps be considered
    128   // transient, with SQLITE_LOCKED being unclear.
    129   //
    130   // SQLITE_READONLY and SQLITE_IOERR are probably persistent, with a
    131   // strong chance that Raze() would not resolve them.  If Delete()
    132   // deletes the database file, the code could then re-open the file
    133   // and attempt the backup again.
    134   //
    135   // For now, this code attempts a best effort and records histograms
    136   // to inform future development.
    137 
    138   // Backup the original db from the recovered db.
    139   const char* kMain = "main";
    140   sqlite3_backup* backup = sqlite3_backup_init(db_->db_, kMain,
    141                                                recover_db_.db_, kMain);
    142   if (!backup) {
    143     // Error code is in the destination database handle.
    144     int err = sqlite3_errcode(db_->db_);
    145     UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryHandle", err);
    146     LOG(ERROR) << "sqlite3_backup_init() failed: "
    147                << sqlite3_errmsg(db_->db_);
    148     return false;
    149   }
    150 
    151   // -1 backs up the entire database.
    152   int rc = sqlite3_backup_step(backup, -1);
    153   int pages = sqlite3_backup_pagecount(backup);
    154   // TODO(shess): sqlite3_backup_finish() appears to allow returning a
    155   // different value from sqlite3_backup_step().  Circle back and
    156   // figure out if that can usefully inform the decision of whether to
    157   // retry or not.
    158   sqlite3_backup_finish(backup);
    159   DCHECK_GT(pages, 0);
    160 
    161   if (rc != SQLITE_DONE) {
    162     UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryStep", rc);
    163     LOG(ERROR) << "sqlite3_backup_step() failed: "
    164                << sqlite3_errmsg(db_->db_);
    165   }
    166 
    167   // The destination database was locked.  Give up, but leave the data
    168   // in place.  Maybe it won't be locked next time.
    169   if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {
    170     Shutdown(POISON);
    171     return false;
    172   }
    173 
    174   // Running out of memory should be transient, retry later.
    175   if (rc == SQLITE_NOMEM) {
    176     Shutdown(POISON);
    177     return false;
    178   }
    179 
    180   // TODO(shess): For now, leave the original database alone, pending
    181   // results from Sqlite.RecoveryStep.  Some errors should probably
    182   // route to RAZE_AND_POISON.
    183   if (rc != SQLITE_DONE) {
    184     Shutdown(POISON);
    185     return false;
    186   }
    187 
    188   // Clean up the recovery db, and terminate the main database
    189   // connection.
    190   Shutdown(POISON);
    191   return true;
    192 }
    193 
    194 void Recovery::Shutdown(Recovery::Disposition raze) {
    195   if (!db_)
    196     return;
    197 
    198   recover_db_.Close();
    199   if (raze == RAZE_AND_POISON) {
    200     db_->RazeAndClose();
    201   } else if (raze == POISON) {
    202     db_->Poison();
    203   }
    204   db_ = NULL;
    205 }
    206 
    207 }  // namespace sql
    208