Bug 1411538 - Try to recover tables from a corrupt places.sqlite. r=standard8
MozReview-Commit-ID: FvuKwIqSt7b
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -39,22 +39,26 @@
// Time between corrupt database backups.
#define RECENT_BACKUP_TIME_MICROSEC (int64_t)86400 * PR_USEC_PER_SEC // 24H
// Filename of the database.
#define DATABASE_FILENAME NS_LITERAL_STRING("places.sqlite")
// Filename used to backup corrupt databases.
#define DATABASE_CORRUPT_FILENAME NS_LITERAL_STRING("places.sqlite.corrupt")
+#define DATABASE_RECOVER_FILENAME NS_LITERAL_STRING("places.sqlite.recover")
// Filename of the icons database.
#define DATABASE_FAVICONS_FILENAME NS_LITERAL_STRING("favicons.sqlite")
// Set when the database file was found corrupt by a previous maintenance.
#define PREF_FORCE_DATABASE_REPLACEMENT "places.database.replaceOnStartup"
+// Whether on corruption we should try to fix the database by cloning it.
+#define PREF_DATABASE_CLONEONCORRUPTION "places.database.cloneOnCorruption"
+
// Set to specify the size of the places database growth increments in kibibytes
#define PREF_GROWTH_INCREMENT_KIB "places.database.growthIncrementKiB"
// Set to disable the default robust storage and use volatile, in-memory
// storage without robust transaction flushing guarantees. This makes
// SQLite use much less I/O at the cost of losing data when things crash.
// The pref is only honored if an environment variable is set. The env
// variable is intentionally named something scary to help prevent someone
@@ -570,33 +574,37 @@ Database::EnsureConnection()
bool databaseCreated = false;
nsresult rv = InitDatabaseFile(storage, &databaseCreated);
if (NS_SUCCEEDED(rv) && databaseCreated) {
mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CREATE;
}
else if (rv == NS_ERROR_FILE_CORRUPTED) {
// The database is corrupt, backup and replace it with a new one.
mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
- rv = BackupAndReplaceDatabaseFile(storage);
+ rv = BackupAndReplaceDatabaseFile(storage, true);
// Fallback to catch-all handler.
}
NS_ENSURE_SUCCESS(rv, rv);
// Ensure the icons database exists.
rv = EnsureFaviconsDatabaseFile(storage);
NS_ENSURE_SUCCESS(rv, rv);
// Initialize the database schema. In case of failure the existing schema is
// is corrupt or incoherent, thus the database should be replaced.
bool databaseMigrated = false;
rv = SetupDatabaseConnection(storage);
+ bool shouldTryToCloneDb = true;
if (NS_SUCCEEDED(rv)) {
// Failing to initialize the schema may indicate a corruption.
rv = InitSchema(&databaseMigrated);
if (NS_FAILED(rv)) {
+ // Cloning the db on a schema migration may not be a good idea, since we
+ // may end up cloning the schema problems.
+ shouldTryToCloneDb = false;
if (rv == NS_ERROR_STORAGE_BUSY ||
rv == NS_ERROR_FILE_IS_LOCKED ||
rv == NS_ERROR_FILE_NO_DEVICE_SPACE ||
rv == NS_ERROR_OUT_OF_MEMORY) {
// The database is not corrupt, though some migration step failed.
// This may be caused by concurrent use of sync and async Storage APIs
// or by a system issue.
// The best we can do is trying again. If it should still fail, Places
@@ -612,17 +620,17 @@ Database::EnsureConnection()
}
if (NS_WARN_IF(NS_FAILED(rv))) {
if (rv != NS_ERROR_FILE_IS_LOCKED) {
mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
}
// Some errors may not indicate a database corruption, for those cases we
// just bail out without throwing away a possibly valid places.sqlite.
if (rv == NS_ERROR_FILE_CORRUPTED) {
- rv = BackupAndReplaceDatabaseFile(storage);
+ rv = BackupAndReplaceDatabaseFile(storage, shouldTryToCloneDb);
NS_ENSURE_SUCCESS(rv, rv);
// Try to initialize the new database again.
rv = SetupDatabaseConnection(storage);
NS_ENSURE_SUCCESS(rv, rv);
rv = InitSchema(&databaseMigrated);
}
// Bail out if we couldn't fix the database.
NS_ENSURE_SUCCESS(rv, rv);
@@ -764,17 +772,18 @@ Database::InitDatabaseFile(nsCOMPtr<mozI
rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn));
NS_ENSURE_SUCCESS(rv, rv);
*aNewDatabaseCreated = !databaseFileExists;
return NS_OK;
}
nsresult
-Database::BackupAndReplaceDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage)
+Database::BackupAndReplaceDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage,
+ bool aTryToClone)
{
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIFile> profDir;
nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
getter_AddRefs(profDir));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> databaseFile;
rv = profDir->Clone(getter_AddRefs(databaseFile));
@@ -797,17 +806,19 @@ Database::BackupAndReplaceDatabaseFile(n
// database file, and there's not much more we can do.
// The only thing we can try to do is to replace the database on the next
// startup, and report the problem through telemetry.
{
enum eCorruptDBReplaceStage : int8_t {
stage_closing = 0,
stage_removing,
stage_reopening,
- stage_replaced
+ stage_replaced,
+ stage_cloning,
+ stage_cloned
};
eCorruptDBReplaceStage stage = stage_closing;
auto guard = MakeScopeExit([&]() {
if (stage != stage_replaced) {
// Reaching this point means the database is corrupt and we failed to
// replace it. For this session part of the application related to
// bookmarks and history will misbehave. The frontend may show a
// "locked" notification to the user though.
@@ -827,26 +838,150 @@ Database::BackupAndReplaceDatabaseFile(n
// Remove the broken database.
stage = stage_removing;
rv = databaseFile->Remove(false);
if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
return rv;
}
- // Create a new database file.
+ // Create a new database file and try to clone tables from the corrupt one.
+ bool cloned = false;
+ if (aTryToClone && Preferences::GetBool(PREF_DATABASE_CLONEONCORRUPTION, true)) {
+ stage = stage_cloning;
+ rv = TryToCloneTablesFromCorruptDatabase(aStorage);
+ if (NS_SUCCEEDED(rv)) {
+ mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_OK;
+ cloned = true;
+ }
+ }
+
// Use an unshared connection, it will consume more memory but avoid shared
// cache contentions across threads.
stage = stage_reopening;
rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn));
NS_ENSURE_SUCCESS(rv, rv);
- stage = stage_replaced;
+ stage = cloned ? stage_cloned : stage_replaced;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Database::TryToCloneTablesFromCorruptDatabase(nsCOMPtr<mozIStorageService>& aStorage)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIFile> profDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> corruptFile;
+ rv = profDir->Clone(getter_AddRefs(corruptFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = corruptFile->Append(DATABASE_CORRUPT_FILENAME);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString path;
+ rv = corruptFile->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> recoverFile;
+ rv = profDir->Clone(getter_AddRefs(recoverFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = recoverFile->Append(DATABASE_RECOVER_FILENAME);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Ensure there's no previous recover file.
+ rv = recoverFile->Remove(false);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST &&
+ rv != NS_ERROR_FILE_NOT_FOUND) {
+ return rv;
}
+ nsCOMPtr<mozIStorageConnection> conn;
+ auto guard = MakeScopeExit([&]() {
+ if (conn) {
+ Unused << conn->Close();
+ }
+ Unused << recoverFile->Remove(false);
+ });
+
+ rv = aStorage->OpenUnsharedDatabase(recoverFile, getter_AddRefs(conn));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = AttachDatabase(conn, NS_ConvertUTF16toUTF8(path),
+ NS_LITERAL_CSTRING("corrupt"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozStorageTransaction transaction(conn, false);
+
+ // Copy the schema version.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ (void)conn->CreateStatement(NS_LITERAL_CSTRING("PRAGMA corrupt.user_version"),
+ getter_AddRefs(stmt));
+ NS_ENSURE_TRUE(stmt, NS_ERROR_OUT_OF_MEMORY);
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t schemaVersion = stmt->AsInt32(0);
+ rv = conn->SetSchemaVersion(schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Recreate the tables.
+ rv = conn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT name, sql FROM corrupt.sqlite_master "
+ "WHERE type = 'table' AND name BETWEEN 'moz_' AND 'moza'"
+ ), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ nsAutoCString name;
+ rv = stmt->GetUTF8String(0, name);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString query;
+ rv = stmt->GetUTF8String(1, query);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = conn->ExecuteSimpleSQL(query);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Copy the table contents.
+ rv = conn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("INSERT INTO main.") +
+ name + NS_LITERAL_CSTRING(" SELECT * FROM corrupt.") + name);
+ if (NS_FAILED(rv)) {
+ rv = conn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("INSERT INTO main.") +
+ name + NS_LITERAL_CSTRING(" SELECT * FROM corrupt.") + name +
+ NS_LITERAL_CSTRING(" ORDER BY rowid DESC"));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Recreate the indices. Doing this after data addition is faster.
+ rv = conn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT sql FROM corrupt.sqlite_master "
+ "WHERE type <> 'table' AND name BETWEEN 'moz_' AND 'moza'"
+ ), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ hasResult = false;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ nsAutoCString query;
+ rv = stmt->GetUTF8String(0, query);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = conn->ExecuteSimpleSQL(query);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = stmt->Finalize();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ Unused << conn->Close();
+ conn = nullptr;
+ rv = recoverFile->RenameTo(profDir, DATABASE_FILENAME);
+ NS_ENSURE_SUCCESS(rv, rv);
+ Unused << corruptFile->Remove(false);
+
+ guard.release();
return NS_OK;
}
nsresult
Database::SetupDatabaseConnection(nsCOMPtr<mozIStorageService>& aStorage)
{
MOZ_ASSERT(NS_IsMainThread());
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -236,18 +236,29 @@ protected:
nsresult EnsureFaviconsDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage);
/**
* Creates a database backup and replaces the original file with a new
* one.
*
* @param aStorage
* mozStorage service instance.
+ * @param aTryToClone
+ * whether we should try to clone a corrupt database.
*/
- nsresult BackupAndReplaceDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage);
+ nsresult BackupAndReplaceDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage,
+ bool aTryToClone);
+
+ /**
+ * Tries to recover tables and their contents from a corrupt database.
+ *
+ * @param aStorage
+ * mozStorage service instance.
+ */
+ nsresult TryToCloneTablesFromCorruptDatabase(nsCOMPtr<mozIStorageService>& aStorage);
/**
* Set up the connection environment through PRAGMAs.
* Will return NS_ERROR_FILE_CORRUPTED if any critical setting fails.
*
* @param aStorage
* mozStorage service instance.
*/
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -23,43 +23,32 @@ const TRANSITION_REDIRECT_TEMPORARY = Ci
const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD;
const TRANSITION_RELOAD = Ci.nsINavHistoryService.TRANSITION_RELOAD;
const TITLE_LENGTH_MAX = 4096;
Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
- "resource://gre/modules/FileUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
- "resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
- "resource://gre/modules/PromiseUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
- "resource://gre/modules/Services.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
- "resource://gre/modules/BookmarkJSONUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
- "resource://gre/modules/BookmarkHTMLUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
- "resource://gre/modules/PlacesBackups.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesSyncUtils",
- "resource://gre/modules/PlacesSyncUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
- "resource://testing-common/PlacesTestUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
- "resource://gre/modules/PlacesTransactions.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS",
- "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
- "resource://gre/modules/Sqlite.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "TestUtils",
- "resource://testing-common/TestUtils.jsm");
-
+XPCOMUtils.defineLazyModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.jsm",
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.jsm",
+ BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.jsm",
+ PlacesBackups: "resource://gre/modules/PlacesBackups.jsm",
+ PlacesSyncUtils: "resource://gre/modules/PlacesSyncUtils.jsm",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
+ PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
+ OS: "resource://gre/modules/osfile.jsm",
+ Sqlite: "resource://gre/modules/Sqlite.jsm",
+ TestUtils: "resource://testing-common/TestUtils.jsm",
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+});
// This imports various other objects in addition to PlacesUtils.
Cu.import("resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() {
return NetUtil.newURI(
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
"AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==");
});
deleted file mode 100644
index 8fbd3bc9acab0abff5305d493d9568a10b36d08b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
--- a/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
+++ b/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
@@ -1,46 +1,60 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that history initialization correctly handles a request to forcibly
// replace the current database.
-function run_test() {
- // Ensure that our database doesn't already exist.
- let dbFile = gProfD.clone();
- dbFile.append("places.sqlite");
- Assert.ok(!dbFile.exists());
+add_task(async function() {
+ registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("places.database.cloneOnCorruption");
+ });
+ test_replacement(true);
+ test_replacement(false);
+});
- dbFile = gProfD.clone();
- dbFile.append("places.sqlite.corrupt");
- Assert.ok(!dbFile.exists());
+async function test_replacement(shouldClone) {
+ Services.prefs.setBoolPref("places.database.cloneOnCorruption", shouldClone);
+ // Ensure that our databases don't exist yet.
+ let sane = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
+ Assert.ok(!await OS.File.exists(sane), "The db should not exist initially");
+ let corrupt = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite.corrupt");
+ Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist initially");
- let file = do_get_file("default.sqlite");
+ let file = do_get_file("../migration/places_v38.sqlite");
file.copyToFollowingLinks(gProfD, "places.sqlite");
file = gProfD.clone();
file.append("places.sqlite");
// Create some unique stuff to check later.
let db = Services.storage.openUnsharedDatabase(file);
- db.executeSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)");
+ db.executeSimpleSQL("CREATE TABLE moz_cloned(id INTEGER PRIMARY KEY)");
+ db.executeSimpleSQL("CREATE TABLE not_cloned (id INTEGER PRIMARY KEY)");
+ db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
db.close();
+ // Open the database with Places.
Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
- Assert.equal(PlacesUtils.history.databaseStatus,
- PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+ let expectedStatus = shouldClone ? PlacesUtils.history.DATABASE_STATUS_UPGRADED
+ : PlacesUtils.history.DATABASE_STATUS_CORRUPT;
+ Assert.equal(PlacesUtils.history.databaseStatus, expectedStatus);
- dbFile = gProfD.clone();
- dbFile.append("places.sqlite");
- Assert.ok(dbFile.exists());
+ Assert.ok(await OS.File.exists(sane), "The database should exist");
+
+ // Check the new database still contains our special data.
+ db = Services.storage.openUnsharedDatabase(file);
+ if (shouldClone) {
+ db.executeSimpleSQL("DELETE FROM moz_cloned"); // Shouldn't throw.
+ }
// Check the new database is really a new one.
- db = Services.storage.openUnsharedDatabase(file);
- try {
- db.executeSimpleSQL("DELETE * FROM test");
- do_throw("The new database should not have our unique content");
- } catch (ex) {}
+ Assert.throws(() => {
+ db.executeSimpleSQL("DELETE FROM not_cloned");
+ }, "The database should have been replaced");
db.close();
- dbFile = gProfD.clone();
- dbFile.append("places.sqlite.corrupt");
- Assert.ok(dbFile.exists());
+ if (shouldClone) {
+ Assert.ok(!await OS.File.exists(corrupt), "The corrupt db should not exist");
+ } else {
+ Assert.ok(await OS.File.exists(corrupt), "The corrupt db should exist");
+ }
}
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -4,17 +4,16 @@ firefox-appdir = browser
support-files =
bookmarks.corrupt.html
bookmarks.json
bookmarks_corrupt.json
bookmarks.preplaces.html
bookmarks_html_singleframe.html
bug476292.sqlite
corruptDB.sqlite
- default.sqlite
livemark.xml
mobile_bookmarks_folder_import.json
mobile_bookmarks_folder_merge.json
mobile_bookmarks_multiple_folders.json
mobile_bookmarks_root_import.json
mobile_bookmarks_root_merge.json
nsDummyObserver.js
nsDummyObserver.manifest