From b92cc073908e7769ff18382c0a7f40b06dac491c Mon Sep 17 00:00:00 2001 From: Junior Hsu Date: Tue, 29 Aug 2017 18:16:27 +0800 Subject: [PATCH] Bug 870460 - Part 1: Let cookie db startup-read off-main-thread. r=nwgh, r=jdm, data-r=francois --- .../test/unit/test_cookies_async_failure.js | 115 +-- .../cookie/test/unit/test_cookies_read.js | 3 +- .../test/unit/test_schema_2_migration.js | 13 + .../test/unit/test_schema_3_migration.js | 13 + netwerk/base/nsIOService.cpp | 5 + netwerk/cookie/nsCookieService.cpp | 971 +++++++----------- netwerk/cookie/nsCookieService.h | 75 +- netwerk/cookie/test/unit/test_bug1321912.js | 6 +- toolkit/components/telemetry/Histograms.json | 10 + 9 files changed, 518 insertions(+), 693 deletions(-) diff --git a/extensions/cookie/test/unit/test_cookies_async_failure.js b/extensions/cookie/test/unit/test_cookies_async_failure.js index eaf0bc8d620b..e4b06579ce96 100644 --- a/extensions/cookie/test/unit/test_cookies_async_failure.js +++ b/extensions/cookie/test/unit/test_cookies_async_failure.js @@ -232,20 +232,9 @@ function* run_test_2(generator) // succeeded. do_check_false(do_get_backup_file(profile).exists()); - // Synchronously read in the first cookie. This will cause it to go into the - // cookie table, whereupon it will be written out during database rebuild. - do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1); - - // Wait for the asynchronous read to choke, at which point the backup file - // will be created and the database rebuilt. - new _observer(sub_generator, "cookie-db-rebuilding"); - yield; - do_execute_soon(function() { do_run_generator(sub_generator); }); - yield; - - // At this point, the cookies should still be in memory. - do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1); - do_check_eq(do_count_cookies(), 1); + // Recreate a new database since it was corrupted + do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + do_check_eq(do_count_cookies(), 0); // Close the profile. do_close_profile(sub_generator); @@ -255,13 +244,11 @@ function* run_test_2(generator) do_check_true(do_get_backup_file(profile).exists()); do_check_eq(do_get_backup_file(profile).fileSize, size); let db = Services.storage.openDatabase(do_get_cookie_file(profile)); - do_check_eq(do_count_cookies_in_db(db, "0.com"), 1); db.close(); - // Load the profile, and check that it contains the new cookie. do_load_profile(); - do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1); - do_check_eq(do_count_cookies(), 1); + do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + do_check_eq(do_count_cookies(), 0); // Close the profile. do_close_profile(sub_generator); @@ -310,25 +297,16 @@ function* run_test_3(generator) // succeeded. do_check_false(do_get_backup_file(profile).exists()); - // Synchronously read in the cookies for our two domains. The first should - // succeed, but the second should fail midway through, resulting in none of - // those cookies being present. - do_check_eq(Services.cookiemgr.countCookiesFromHost("hither.com"), 10); + // Recreate a new database since it was corrupted + do_check_eq(Services.cookiemgr.countCookiesFromHost("hither.com"), 0); do_check_eq(Services.cookiemgr.countCookiesFromHost("haithur.com"), 0); - // Wait for the backup file to be created and the database rebuilt. - do_check_false(do_get_backup_file(profile).exists()); - new _observer(sub_generator, "cookie-db-rebuilding"); - yield; - do_execute_soon(function() { do_run_generator(sub_generator); }); - yield; - // Close the profile. do_close_profile(sub_generator); yield; let db = Services.storage.openDatabase(do_get_cookie_file(profile)); - do_check_eq(do_count_cookies_in_db(db, "hither.com"), 10); - do_check_eq(do_count_cookies_in_db(db), 10); + do_check_eq(do_count_cookies_in_db(db, "hither.com"), 0); + do_check_eq(do_count_cookies_in_db(db), 0); db.close(); // Check that the original database was renamed. @@ -346,13 +324,6 @@ function* run_test_3(generator) // Synchronously read in everything. do_check_eq(do_count_cookies(), 0); - // Wait for the backup file to be created and the database rebuilt. - do_check_false(do_get_backup_file(profile).exists()); - new _observer(sub_generator, "cookie-db-rebuilding"); - yield; - do_execute_soon(function() { do_run_generator(sub_generator); }); - yield; - // Close the profile. do_close_profile(sub_generator); yield; @@ -397,26 +368,17 @@ function* run_test_4(generator) // succeeded. do_check_false(do_get_backup_file(profile).exists()); - // Synchronously read in the first cookie. This will cause it to go into the - // cookie table, whereupon it will be written out during database rebuild. - do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1); + // Recreate a new database since it was corrupted + do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0); // Queue up an INSERT for the same base domain. This should also go into // memory and be written out during database rebuild. let uri = NetUtil.newURI("http://0.com/"); Services.cookies.setCookieString(uri, null, "oh2=hai; max-age=1000", null); - // Wait for the asynchronous read to choke and the insert to fail shortly - // thereafter, at which point the backup file will be created and the database - // rebuilt. - new _observer(sub_generator, "cookie-db-rebuilding"); - yield; - do_execute_soon(function() { do_run_generator(sub_generator); }); - yield; - // At this point, the cookies should still be in memory. - do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 2); - do_check_eq(do_count_cookies(), 2); + do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1); + do_check_eq(do_count_cookies(), 1); // Close the profile. do_close_profile(sub_generator); @@ -425,14 +387,11 @@ function* run_test_4(generator) // Check that the original database was renamed. do_check_true(do_get_backup_file(profile).exists()); do_check_eq(do_get_backup_file(profile).fileSize, size); - let db = Services.storage.openDatabase(do_get_cookie_file(profile)); - do_check_eq(do_count_cookies_in_db(db, "0.com"), 2); - db.close(); // Load the profile, and check that it contains the new cookie. do_load_profile(); - do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 2); - do_check_eq(do_count_cookies(), 2); + do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1); + do_check_eq(do_count_cookies(), 1); // Close the profile. do_close_profile(sub_generator); @@ -474,22 +433,10 @@ function* run_test_5(generator) // succeeded. do_check_false(do_get_backup_file(profile).exists()); - // Synchronously read in the first two cookies. This will cause them to go - // into the cookie table, whereupon it will be written out during database - // rebuild. - do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 1); - do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1); - - // Wait for the asynchronous read to choke, at which point the backup file - // will be created and a new connection opened. - new _observer(sub_generator, "cookie-db-rebuilding"); - yield; - - // At this point, the cookies should still be in memory. (Note that these - // calls are re-entrant into the cookie service, but it's OK!) - do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 1); - do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1); - do_check_eq(do_count_cookies(), 2); + // Recreate a new database since it was corrupted + do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 0); + do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + do_check_eq(do_count_cookies(), 0); do_check_true(do_get_backup_file(profile).exists()); do_check_eq(do_get_backup_file(profile).fileSize, size); do_check_false(do_get_rebuild_backup_file(profile).exists()); @@ -502,39 +449,23 @@ function* run_test_5(generator) do_check_eq(do_count_cookies_in_db(db.db), 1); db.close(); - // Wait for the rebuild to bail and the database to be closed. - new _observer(sub_generator, "cookie-db-closed"); - yield; - // Check that the original backup and the database itself are gone. - do_check_true(do_get_rebuild_backup_file(profile).exists()); do_check_true(do_get_backup_file(profile).exists()); do_check_eq(do_get_backup_file(profile).fileSize, size); - do_check_false(do_get_cookie_file(profile).exists()); - // Check that the rebuild backup has the original bar.com cookie, and possibly - // a 0.com cookie depending on whether it got written out first or second. - db = new CookieDatabaseConnection(do_get_rebuild_backup_file(profile), 4); - do_check_eq(do_count_cookies_in_db(db.db, "bar.com"), 1); - let count = do_count_cookies_in_db(db.db); - do_check_true(count == 1 || - count == 2 && do_count_cookies_in_db(db.db, "0.com") == 1); - db.close(); - - do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 1); - do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1); - do_check_eq(do_count_cookies(), 2); + do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 0); + do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + do_check_eq(do_count_cookies(), 0); // Close the profile. We do not need to wait for completion, because the // database has already been closed. do_close_profile(); // Clean up. + do_get_cookie_file(profile).remove(false); do_get_backup_file(profile).remove(false); - do_get_rebuild_backup_file(profile).remove(false); do_check_false(do_get_cookie_file(profile).exists()); do_check_false(do_get_backup_file(profile).exists()); - do_check_false(do_get_rebuild_backup_file(profile).exists()); do_run_generator(generator); } diff --git a/extensions/cookie/test/unit/test_cookies_read.js b/extensions/cookie/test/unit/test_cookies_read.js index db98b714fe66..ca59b87a87a0 100644 --- a/extensions/cookie/test/unit/test_cookies_read.js +++ b/extensions/cookie/test/unit/test_cookies_read.js @@ -27,7 +27,8 @@ function* do_run_test() { Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); // Start the cookieservice, to force creation of a database. - Services.cookies; + // Get the sessionEnumerator to join the initialization in cookie thread + Services.cookiemgr.sessionEnumerator; // Open a database connection now, after synchronous initialization has // completed. We may not be able to open one later once asynchronous writing diff --git a/extensions/cookie/test/unit/test_schema_2_migration.js b/extensions/cookie/test/unit/test_schema_2_migration.js index 5b998d8c5aa8..89d6803ebd19 100644 --- a/extensions/cookie/test/unit/test_schema_2_migration.js +++ b/extensions/cookie/test/unit/test_schema_2_migration.js @@ -22,6 +22,17 @@ function* do_run_test() { // Set up a profile. let profile = do_get_profile(); + // Start the cookieservice, to force creation of a database. + // Get the sessionEnumerator to join the initialization in cookie thread + Services.cookiemgr.sessionEnumerator; + + // Close the profile. + do_close_profile(test_generator); + yield; + + // Remove the cookie file in order to create another database file. + do_get_cookie_file(profile).remove(false); + // Create a schema 2 database. let schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2); @@ -79,6 +90,8 @@ function* do_run_test() { // Load the database, forcing migration to the current schema version. Then // test the expected set of cookies: + do_load_profile(); + // 1) All unexpired, unique cookies exist. do_check_eq(Services.cookiemgr.countCookiesFromHost("foo.com"), 20); diff --git a/extensions/cookie/test/unit/test_schema_3_migration.js b/extensions/cookie/test/unit/test_schema_3_migration.js index 1723a2390084..7754748b3266 100644 --- a/extensions/cookie/test/unit/test_schema_3_migration.js +++ b/extensions/cookie/test/unit/test_schema_3_migration.js @@ -22,6 +22,17 @@ function* do_run_test() { // Set up a profile. let profile = do_get_profile(); + // Start the cookieservice, to force creation of a database. + // Get the sessionEnumerator to join the initialization in cookie thread + Services.cookiemgr.sessionEnumerator; + + // Close the profile. + do_close_profile(test_generator); + yield; + + // Remove the cookie file in order to create another database file. + do_get_cookie_file(profile).remove(false); + // Create a schema 3 database. let schema3db = new CookieDatabaseConnection(do_get_cookie_file(profile), 3); @@ -79,6 +90,8 @@ function* do_run_test() { // Load the database, forcing migration to the current schema version. Then // test the expected set of cookies: + do_load_profile(); + // 1) All unexpired, unique cookies exist. do_check_eq(Services.cookiemgr.countCookiesFromHost("foo.com"), 20); diff --git a/netwerk/base/nsIOService.cpp b/netwerk/base/nsIOService.cpp index beeea4896d34..62889893e251 100644 --- a/netwerk/base/nsIOService.cpp +++ b/netwerk/base/nsIOService.cpp @@ -1489,6 +1489,11 @@ nsIOService::Observe(nsISupports *subject, nsCOMPtr prefBranch; GetPrefBranch(getter_AddRefs(prefBranch)); PrefsChanged(prefBranch, MANAGE_OFFLINE_STATUS_PREF); + + // Bug 870460 - Read cookie database at an early-as-possible time + // off main thread. Hence, we have more chance to finish db query + // before something calls into the cookie service. + nsCOMPtr cookieServ = do_GetService(NS_COOKIESERVICE_CONTRACTID); } } else if (!strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { // Remember we passed XPCOM shutdown notification to prevent any diff --git a/netwerk/cookie/nsCookieService.cpp b/netwerk/cookie/nsCookieService.cpp index 484bb9e92cae..d02094c1a56b 100644 --- a/netwerk/cookie/nsCookieService.cpp +++ b/netwerk/cookie/nsCookieService.cpp @@ -34,6 +34,7 @@ #include "nsILineInputStream.h" #include "nsIEffectiveTLDService.h" #include "nsIIDNService.h" +#include "nsIThread.h" #include "mozIThirdPartyUtil.h" #include "nsTArray.h" @@ -82,8 +83,7 @@ static StaticRefPtr gCookieService; #define COOKIES_FILE "cookies.sqlite" #define COOKIES_SCHEMA_VERSION 9 -// parameter indexes; see EnsureReadDomain, EnsureReadComplete and -// ReadCookieDBListener::HandleResult +// parameter indexes; see |Read| #define IDX_NAME 0 #define IDX_VALUE 1 #define IDX_HOST 2 @@ -457,92 +457,6 @@ public: NS_IMPL_ISUPPORTS(RemoveCookieDBListener, mozIStorageStatementCallback) -/****************************************************************************** - * ReadCookieDBListener impl: - * mozIStorageStatementCallback used to track asynchronous removal operations. - ******************************************************************************/ -class ReadCookieDBListener final : public DBListenerErrorHandler -{ -private: - const char *GetOpType() override { return "READ"; } - bool mCanceled; - - ~ReadCookieDBListener() = default; - -public: - NS_DECL_ISUPPORTS - - explicit ReadCookieDBListener(DBState* dbState) - : DBListenerErrorHandler(dbState) - , mCanceled(false) - { - } - - void Cancel() { mCanceled = true; } - - NS_IMETHOD HandleResult(mozIStorageResultSet *aResult) override - { - nsCOMPtr row; - - while (true) { - DebugOnly rv = aResult->GetNextRow(getter_AddRefs(row)); - NS_ASSERT_SUCCESS(rv); - - if (!row) - break; - - CookieDomainTuple *tuple = mDBState->hostArray.AppendElement(); - row->GetUTF8String(IDX_BASE_DOMAIN, tuple->key.mBaseDomain); - - nsAutoCString suffix; - row->GetUTF8String(IDX_ORIGIN_ATTRIBUTES, suffix); - DebugOnly success = tuple->key.mOriginAttributes.PopulateFromSuffix(suffix); - MOZ_ASSERT(success); - - tuple->cookie = - gCookieService->GetCookieFromRow(row, tuple->key.mOriginAttributes); - } - - return NS_OK; - } - NS_IMETHOD HandleCompletion(uint16_t aReason) override - { - // Process the completion of the read operation. If we have been canceled, - // we cannot assume that the cookieservice still has an open connection - // or that it even refers to the same database, so we must return early. - // Conversely, the cookieservice guarantees that if we have not been - // canceled, the database connection is still alive and we can safely - // operate on it. - - if (mCanceled) { - // We may receive a REASON_FINISHED after being canceled; - // tweak the reason accordingly. - aReason = mozIStorageStatementCallback::REASON_CANCELED; - } - - switch (aReason) { - case mozIStorageStatementCallback::REASON_FINISHED: - gCookieService->AsyncReadComplete(); - break; - case mozIStorageStatementCallback::REASON_CANCELED: - // Nothing more to do here. The partially read data has already been - // thrown away. - COOKIE_LOGSTRING(LogLevel::Debug, ("Read canceled")); - break; - case mozIStorageStatementCallback::REASON_ERROR: - // Nothing more to do here. DBListenerErrorHandler::HandleError() - // can handle it. - COOKIE_LOGSTRING(LogLevel::Debug, ("Read error")); - break; - default: - NS_NOTREACHED("invalid reason"); - } - return NS_OK; - } -}; - -NS_IMPL_ISUPPORTS(ReadCookieDBListener, mozIStorageStatementCallback) - /****************************************************************************** * CloseCookieDBListener imp: * Static mozIStorageCompletionCallback used to notify when the database is @@ -608,17 +522,6 @@ nsCookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const return amount; } -size_t -CookieDomainTuple::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const -{ - size_t amount = 0; - - amount += key.SizeOfExcludingThis(aMallocSizeOf); - amount += cookie->SizeOfIncludingThis(aMallocSizeOf); - - return amount; -} - size_t DBState::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { @@ -626,11 +529,6 @@ DBState::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const amount += aMallocSizeOf(this); amount += hostTable.SizeOfExcludingThis(aMallocSizeOf); - amount += hostArray.ShallowSizeOfExcludingThis(aMallocSizeOf); - for (uint32_t i = 0; i < hostArray.Length(); ++i) { - amount += hostArray[i].SizeOfExcludingThis(aMallocSizeOf); - } - amount += readSet.SizeOfExcludingThis(aMallocSizeOf); return amount; } @@ -706,6 +604,11 @@ nsCookieService::nsCookieService() , mMaxNumberOfCookies(kMaxNumberOfCookies) , mMaxCookiesPerHost(kMaxCookiesPerHost) , mCookiePurgeAge(kCookiePurgeAge) + , mThread(nullptr) + , mMonitor("CookieThread") + , mInitializedDBStates(false) + , mInitializedDBConn(false) + , mAccumulatedWaitTelemetry(false) { } @@ -737,6 +640,9 @@ nsCookieService::Init() mStorageService = do_GetService("@mozilla.org/storage/service;1", &rv); NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewNamedThread("Cookie", getter_AddRefs(mThread)); + NS_ENSURE_SUCCESS(rv, rv); + // Init our default, and possibly private DBStates. InitDBStates(); @@ -763,6 +669,7 @@ nsCookieService::InitDBStates() NS_ASSERTION(!mDBState, "already have a DBState"); NS_ASSERTION(!mDefaultDBState, "already have a default DBState"); NS_ASSERTION(!mPrivateDBState, "already have a private DBState"); + NS_ASSERTION(!mInitializedDBStates, "already initialized"); // Create a new default DBState and set our current one. mDefaultDBState = new DBState(); @@ -777,35 +684,61 @@ nsCookieService::InitDBStates() // We've already set up our DBStates appropriately; nothing more to do. COOKIE_LOGSTRING(LogLevel::Warning, ("InitDBStates(): couldn't get cookie file")); + + mInitializedDBConn = true; + mInitializedDBStates = true; return; } mDefaultDBState->cookieFile->AppendNative(NS_LITERAL_CSTRING(COOKIES_FILE)); - // Attempt to open and read the database. If TryInitDB() returns RESULT_RETRY, - // do so. - OpenDBResult result = TryInitDB(false); - if (result == RESULT_RETRY) { - // Database may be corrupt. Synchronously close the connection, clean up the - // default DBState, and try again. - COOKIE_LOGSTRING(LogLevel::Warning, ("InitDBStates(): retrying TryInitDB()")); - CleanupCachedStatements(); - CleanupDefaultDBConnection(); - result = TryInitDB(true); + nsCOMPtr runnable = NS_NewRunnableFunction("InitDBStates.TryInitDB", [] { + NS_ENSURE_TRUE_VOID(gCookieService && + gCookieService->mDBState && + gCookieService->mDefaultDBState); + + MonitorAutoLock lock(gCookieService->mMonitor); + + // Attempt to open and read the database. If TryInitDB() returns RESULT_RETRY, + // do so. + OpenDBResult result = gCookieService->TryInitDB(false); if (result == RESULT_RETRY) { - // We're done. Change the code to failure so we clean up below. - result = RESULT_FAILURE; + // Database may be corrupt. Synchronously close the connection, clean up the + // default DBState, and try again. + COOKIE_LOGSTRING(LogLevel::Warning, ("InitDBStates(): retrying TryInitDB()")); + gCookieService->CleanupCachedStatements(); + gCookieService->CleanupDefaultDBConnection(); + result = gCookieService->TryInitDB(true); + if (result == RESULT_RETRY) { + // We're done. Change the code to failure so we clean up below. + result = RESULT_FAILURE; + } } - } - if (result == RESULT_FAILURE) { - COOKIE_LOGSTRING(LogLevel::Warning, - ("InitDBStates(): TryInitDB() failed, closing connection")); + if (result == RESULT_FAILURE) { + COOKIE_LOGSTRING(LogLevel::Warning, + ("InitDBStates(): TryInitDB() failed, closing connection")); - // Connection failure is unrecoverable. Clean up our connection. We can run - // fine without persistent storage -- e.g. if there's no profile. - CleanupCachedStatements(); - CleanupDefaultDBConnection(); - } + // Connection failure is unrecoverable. Clean up our connection. We can run + // fine without persistent storage -- e.g. if there's no profile. + gCookieService->CleanupCachedStatements(); + gCookieService->CleanupDefaultDBConnection(); + + // No need to initialize dbConn + gCookieService->mInitializedDBConn = true; + } + + gCookieService->mInitializedDBStates = true; + + NS_DispatchToMainThread( + NS_NewRunnableFunction("TryInitDB.InitDBConn", [] { + NS_ENSURE_TRUE_VOID(gCookieService); + gCookieService->InitDBConn(); + }) + ); + gCookieService->mMonitor.Notify(); + }); + + mThread->Dispatch(runnable, NS_DISPATCH_NORMAL); } namespace { @@ -930,6 +863,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) NS_ASSERTION(!mDefaultDBState->stmtInsert, "nonnull stmtInsert"); NS_ASSERTION(!mDefaultDBState->insertListener, "nonnull insertListener"); NS_ASSERTION(!mDefaultDBState->syncConn, "nonnull syncConn"); + NS_ASSERTION(NS_GetCurrentThread() == mThread, "non cookie thread"); // Ditch an existing db, if we've been told to (i.e. it's corrupt). We don't // want to delete it outright, since it may be useful for debugging purposes, @@ -953,21 +887,12 @@ nsCookieService::TryInitDB(bool aRecreateDB) // and statements upon success. The connection is opened unshared to eliminate // cache contention between the main and background threads. rv = mStorageService->OpenUnsharedDatabase(mDefaultDBState->cookieFile, - getter_AddRefs(mDefaultDBState->dbConn)); + getter_AddRefs(mDefaultDBState->syncConn)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } - // Set up our listeners. - mDefaultDBState->insertListener = new InsertCookieDBListener(mDefaultDBState); - mDefaultDBState->updateListener = new UpdateCookieDBListener(mDefaultDBState); - mDefaultDBState->removeListener = new RemoveCookieDBListener(mDefaultDBState); - mDefaultDBState->closeListener = new CloseCookieDBListener(mDefaultDBState); - - // Grow cookie db in 512KB increments - mDefaultDBState->dbConn->SetGrowthIncrement(512 * 1024, EmptyCString()); - bool tableExists = false; - mDefaultDBState->dbConn->TableExists(NS_LITERAL_CSTRING("moz_cookies"), + mDefaultDBState->syncConn->TableExists(NS_LITERAL_CSTRING("moz_cookies"), &tableExists); if (!tableExists) { rv = CreateTable(); @@ -976,11 +901,11 @@ nsCookieService::TryInitDB(bool aRecreateDB) } else { // table already exists; check the schema version before reading int32_t dbSchemaVersion; - rv = mDefaultDBState->dbConn->GetSchemaVersion(&dbSchemaVersion); + rv = mDefaultDBState->syncConn->GetSchemaVersion(&dbSchemaVersion); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Start a transaction for the whole migration block. - mozStorageTransaction transaction(mDefaultDBState->dbConn, true); + mozStorageTransaction transaction(mDefaultDBState->syncConn, true); switch (dbSchemaVersion) { // Upgrading. @@ -992,7 +917,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) case 1: { // Add the lastAccessed column to the table. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_cookies ADD lastAccessed INTEGER")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } @@ -1002,7 +927,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) case 2: { // Add the baseDomain column and index to the table. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_cookies ADD baseDomain TEXT")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1012,12 +937,12 @@ nsCookieService::TryInitDB(bool aRecreateDB) const int64_t SCHEMA2_IDX_ID = 0; const int64_t SCHEMA2_IDX_HOST = 1; nsCOMPtr select; - rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT id, host FROM moz_cookies"), getter_AddRefs(select)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); nsCOMPtr update; - rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( "UPDATE moz_cookies SET baseDomain = :baseDomain WHERE id = :id"), getter_AddRefs(update)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1051,7 +976,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) } // Create an index on baseDomain. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } @@ -1076,14 +1001,14 @@ nsCookieService::TryInitDB(bool aRecreateDB) const int64_t SCHEMA3_IDX_HOST = 2; const int64_t SCHEMA3_IDX_PATH = 3; nsCOMPtr select; - rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT id, name, host, path FROM moz_cookies " "ORDER BY name ASC, host ASC, path ASC, expiry ASC"), getter_AddRefs(select)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); nsCOMPtr deleteExpired; - rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_cookies WHERE id = :id"), getter_AddRefs(deleteExpired)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1136,18 +1061,18 @@ nsCookieService::TryInitDB(bool aRecreateDB) } // Add the creationTime column to the table. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_cookies ADD creationTime INTEGER")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Copy the id of each row into the new creationTime column. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE moz_cookies SET creationTime = " "(SELECT id WHERE id = moz_cookies.id)")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Create a unique index on (name, host, path) to allow fast lookup. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE UNIQUE INDEX moz_uniqueid " "ON moz_cookies (name, host, path)")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1169,12 +1094,12 @@ nsCookieService::TryInitDB(bool aRecreateDB) // the only namespace used by a non-Firefox-OS implementation. // Rename existing table - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_cookies RENAME TO moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop existing index (CreateTable will create new one for new table) - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX moz_basedomain")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1183,7 +1108,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Copy data from old table, using appId/inBrowser=0 for existing rows - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO moz_cookies " "(baseDomain, appId, inBrowserElement, name, value, host, path, expiry," " lastAccessed, creationTime, isSecure, isHttpOnly) " @@ -1193,7 +1118,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop old table - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1219,12 +1144,12 @@ nsCookieService::TryInitDB(bool aRecreateDB) // inBrowserElement to originAttributes in the meantime. // Rename existing table. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_cookies RENAME TO moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop existing index (CreateTable will create new one for new table). - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX moz_basedomain")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1241,11 +1166,11 @@ nsCookieService::TryInitDB(bool aRecreateDB) NS_NAMED_LITERAL_CSTRING(convertToOriginAttrsName, "CONVERT_TO_ORIGIN_ATTRIBUTES"); - rv = mDefaultDBState->dbConn->CreateFunction(convertToOriginAttrsName, + rv = mDefaultDBState->syncConn->CreateFunction(convertToOriginAttrsName, 2, convertToOriginAttrs); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO moz_cookies " "(baseDomain, originAttributes, name, value, host, path, expiry," " lastAccessed, creationTime, isSecure, isHttpOnly) " @@ -1256,11 +1181,11 @@ nsCookieService::TryInitDB(bool aRecreateDB) "FROM moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - rv = mDefaultDBState->dbConn->RemoveFunction(convertToOriginAttrsName); + rv = mDefaultDBState->syncConn->RemoveFunction(convertToOriginAttrsName); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop old table - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1279,11 +1204,11 @@ nsCookieService::TryInitDB(bool aRecreateDB) // This version simply restores appId and inBrowserElement columns in // order to fix downgrading issue even though these two columns are no // longer used in the latest schema. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_cookies ADD appId INTEGER DEFAULT 0;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_cookies ADD inBrowserElement INTEGER DEFAULT 0;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1295,7 +1220,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) NS_NAMED_LITERAL_CSTRING(setAppIdName, "SET_APP_ID"); - rv = mDefaultDBState->dbConn->CreateFunction(setAppIdName, 1, setAppId); + rv = mDefaultDBState->syncConn->CreateFunction(setAppIdName, 1, setAppId); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); nsCOMPtr @@ -1304,20 +1229,20 @@ nsCookieService::TryInitDB(bool aRecreateDB) NS_NAMED_LITERAL_CSTRING(setInBrowserName, "SET_IN_BROWSER"); - rv = mDefaultDBState->dbConn->CreateFunction(setInBrowserName, 1, + rv = mDefaultDBState->syncConn->CreateFunction(setInBrowserName, 1, setInBrowser); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE moz_cookies SET appId = SET_APP_ID(originAttributes), " "inBrowserElement = SET_IN_BROWSER(originAttributes);" )); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - rv = mDefaultDBState->dbConn->RemoveFunction(setAppIdName); + rv = mDefaultDBState->syncConn->RemoveFunction(setAppIdName); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - rv = mDefaultDBState->dbConn->RemoveFunction(setInBrowserName); + rv = mDefaultDBState->syncConn->RemoveFunction(setInBrowserName); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, @@ -1334,7 +1259,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) // https://www.sqlite.org/lang_altertable.html. // Drop existing index - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX moz_basedomain")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1343,7 +1268,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Move the data over. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO new_moz_cookies (" "id, " "baseDomain, " @@ -1376,12 +1301,12 @@ nsCookieService::TryInitDB(bool aRecreateDB) NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop the old table - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE moz_cookies;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Rename new_moz_cookies to moz_cookies. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE new_moz_cookies RENAME TO moz_cookies;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1397,14 +1322,14 @@ nsCookieService::TryInitDB(bool aRecreateDB) case 8: { // Add the sameSite column to the table. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL( NS_LITERAL_CSTRING("ALTER TABLE moz_cookies ADD sameSite INTEGER")); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 9")); } // No more upgrades. Update the schema version. - rv = mDefaultDBState->dbConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); + rv = mDefaultDBState->syncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); MOZ_FALLTHROUGH; @@ -1420,7 +1345,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) // below, by verifying the columns we care about are all there. for now, // re-set the schema version in the db, in case the checks succeed (if // they don't, we're dropping the table anyway). - rv = mDefaultDBState->dbConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); + rv = mDefaultDBState->syncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } // fall through to downgrade check @@ -1436,7 +1361,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) { // check if all the expected columns exist nsCOMPtr stmt; - rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT " "id, " "baseDomain, " @@ -1456,7 +1381,7 @@ nsCookieService::TryInitDB(bool aRecreateDB) break; // our columns aren't there - drop the table! - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE moz_cookies")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); @@ -1467,6 +1392,118 @@ nsCookieService::TryInitDB(bool aRecreateDB) } } + // if we deleted a corrupt db, don't attempt to import - return now + if (aRecreateDB) { + return RESULT_OK; + } + + // check whether to import or just read in the db + if (tableExists) { + return Read(); + } + + nsCOMPtr runnable = + NS_NewRunnableFunction("TryInitDB.ImportCookies", [] { + NS_ENSURE_TRUE_VOID(gCookieService); + NS_ENSURE_TRUE_VOID(gCookieService->mDefaultDBState); + nsCOMPtr oldCookieFile; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(oldCookieFile)); + if (NS_FAILED(rv)) { + return; + } + + // Import cookies, and clean up the old file regardless of success or failure. + // Note that we have to switch out our DBState temporarily, in case we're in + // private browsing mode; otherwise ImportCookies() won't be happy. + DBState* initialState = gCookieService->mDBState; + gCookieService->mDBState = gCookieService->mDefaultDBState; + oldCookieFile->AppendNative(NS_LITERAL_CSTRING(OLD_COOKIE_FILE_NAME)); + gCookieService->ImportCookies(oldCookieFile); + oldCookieFile->Remove(false); + gCookieService->mDBState = initialState; + }); + + NS_DispatchToMainThread(runnable); + + return RESULT_OK; +} + +void +nsCookieService::InitDBConn() +{ + MOZ_ASSERT(NS_IsMainThread()); + + // We should skip InitDBConn if we close profile during initializing DBStates + // and then InitDBConn is called after we close the DBStates. + if (!mInitializedDBStates || mInitializedDBConn || !mDefaultDBState) { + return; + } + + if (!mAccumulatedWaitTelemetry) { + mAccumulatedWaitTelemetry = true; + Telemetry::Accumulate(Telemetry::MOZ_SQLITE_COOKIES_BLOCK_MAIN_THREAD_MS, 0); + } + for (uint32_t i = 0; i < mReadArray.Length(); ++i) { + CookieDomainTuple& tuple = mReadArray[i]; + RefPtr cookie = nsCookie::Create(tuple.cookie->name, + tuple.cookie->value, + tuple.cookie->host, + tuple.cookie->path, + tuple.cookie->expiry, + tuple.cookie->lastAccessed, + tuple.cookie->creationTime, + false, + tuple.cookie->isSecure, + tuple.cookie->isHttpOnly, + tuple.cookie->originAttributes, + tuple.cookie->sameSite); + + AddCookieToList(tuple.key, cookie, mDefaultDBState, nullptr, false); + } + + if (NS_FAILED(InitDBConnInternal())) { + COOKIE_LOGSTRING(LogLevel::Warning, ("InitDBConn(): retrying InitDBConnInternal()")); + CleanupCachedStatements(); + CleanupDefaultDBConnection(); + if (NS_FAILED(InitDBConnInternal())) { + COOKIE_LOGSTRING(LogLevel::Warning, + ("InitDBConn(): InitDBConnInternal() failed, closing connection")); + + // Game over, clean the connections. + CleanupCachedStatements(); + CleanupDefaultDBConnection(); + } + } + mInitializedDBConn = true; + + COOKIE_LOGSTRING(LogLevel::Debug, ("InitDBConn(): mInitializedDBConn = true")); + + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os && !mReadArray.IsEmpty()) { + os->NotifyObservers(nullptr, "cookie-db-read", nullptr); + mReadArray.Clear(); + } +} + +nsresult +nsCookieService::InitDBConnInternal() +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = mStorageService->OpenUnsharedDatabase(mDefaultDBState->cookieFile, + getter_AddRefs(mDefaultDBState->dbConn)); + NS_ENSURE_SUCCESS(rv, rv); + + // Set up our listeners. + mDefaultDBState->insertListener = new InsertCookieDBListener(mDefaultDBState); + mDefaultDBState->updateListener = new UpdateCookieDBListener(mDefaultDBState); + mDefaultDBState->removeListener = new RemoveCookieDBListener(mDefaultDBState); + mDefaultDBState->closeListener = new CloseCookieDBListener(mDefaultDBState); + + // Grow cookie db in 512KB increments + mDefaultDBState->dbConn->SetGrowthIncrement(512 * 1024, EmptyCString()); + // make operations on the table asynchronous, for performance mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "PRAGMA synchronous = OFF")); @@ -1508,44 +1545,19 @@ nsCookieService::TryInitDB(bool aRecreateDB) ":sameSite" ")"), getter_AddRefs(mDefaultDBState->stmtInsert)); - NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + NS_ENSURE_SUCCESS(rv, rv); rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_cookies " "WHERE name = :name AND host = :host AND path = :path AND originAttributes = :originAttributes"), getter_AddRefs(mDefaultDBState->stmtDelete)); - NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + NS_ENSURE_SUCCESS(rv, rv); rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "UPDATE moz_cookies SET lastAccessed = :lastAccessed " "WHERE name = :name AND host = :host AND path = :path AND originAttributes = :originAttributes"), getter_AddRefs(mDefaultDBState->stmtUpdate)); - NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - - // if we deleted a corrupt db, don't attempt to import - return now - if (aRecreateDB) - return RESULT_OK; - - // check whether to import or just read in the db - if (tableExists) - return Read(); - - nsCOMPtr oldCookieFile; - rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, - getter_AddRefs(oldCookieFile)); - if (NS_FAILED(rv)) return RESULT_OK; - - // Import cookies, and clean up the old file regardless of success or failure. - // Note that we have to switch out our DBState temporarily, in case we're in - // private browsing mode; otherwise ImportCookies() won't be happy. - DBState* initialState = mDBState; - mDBState = mDefaultDBState; - oldCookieFile->AppendNative(NS_LITERAL_CSTRING(OLD_COOKIE_FILE_NAME)); - ImportCookies(oldCookieFile); - oldCookieFile->Remove(false); - mDBState = initialState; - - return RESULT_OK; + return rv; } // Sets the schema version and creates the moz_cookies table. @@ -1575,7 +1587,7 @@ nsCookieService::CreateTableWorker(const char* aName) "sameSite INTEGER DEFAULT 0, " "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" ")"); - return mDefaultDBState->dbConn->ExecuteSimpleSQL(command); + return mDefaultDBState->syncConn->ExecuteSimpleSQL(command); } // Sets the schema version and creates the moz_cookies table. @@ -1583,7 +1595,7 @@ nsresult nsCookieService::CreateTable() { // Set the schema version, before creating the table. - nsresult rv = mDefaultDBState->dbConn->SetSchemaVersion( + nsresult rv = mDefaultDBState->syncConn->SetSchemaVersion( COOKIES_SCHEMA_VERSION); if (NS_FAILED(rv)) return rv; @@ -1597,7 +1609,7 @@ nsresult nsCookieService::CreateIndex() { // Create an index on baseDomain. - return mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + return mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " "originAttributes)")); } @@ -1607,14 +1619,14 @@ nsresult nsCookieService::CreateTableForSchemaVersion6() { // Set the schema version, before creating the table. - nsresult rv = mDefaultDBState->dbConn->SetSchemaVersion(6); + nsresult rv = mDefaultDBState->syncConn->SetSchemaVersion(6); if (NS_FAILED(rv)) return rv; // Create the table. // We default originAttributes to empty string: this is so if users revert to // an older Firefox version that doesn't know about this field, any cookies // set will still work once they upgrade back. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE moz_cookies (" "id INTEGER PRIMARY KEY, " "baseDomain TEXT, " @@ -1633,7 +1645,7 @@ nsCookieService::CreateTableForSchemaVersion6() if (NS_FAILED(rv)) return rv; // Create an index on baseDomain. - return mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + return mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " "originAttributes)")); } @@ -1643,13 +1655,13 @@ nsresult nsCookieService::CreateTableForSchemaVersion5() { // Set the schema version, before creating the table. - nsresult rv = mDefaultDBState->dbConn->SetSchemaVersion(5); + nsresult rv = mDefaultDBState->syncConn->SetSchemaVersion(5); if (NS_FAILED(rv)) return rv; // Create the table. We default appId/inBrowserElement to 0: this is so if // users revert to an older Firefox version that doesn't know about these // fields, any cookies set will still work once they upgrade back. - rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE moz_cookies (" "id INTEGER PRIMARY KEY, " "baseDomain TEXT, " @@ -1669,7 +1681,7 @@ nsCookieService::CreateTableForSchemaVersion5() if (NS_FAILED(rv)) return rv; // Create an index on baseDomain. - return mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + return mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " "appId, " "inBrowserElement)")); @@ -1678,6 +1690,13 @@ nsCookieService::CreateTableForSchemaVersion5() void nsCookieService::CloseDBStates() { + // return if we already closed + if (!mDBState) { + return; + } + + EnsureReadComplete(false); + // Null out our private and pointer DBStates regardless. mPrivateDBState = nullptr; mDBState = nullptr; @@ -1690,12 +1709,6 @@ nsCookieService::CloseDBStates() CleanupCachedStatements(); if (mDefaultDBState->dbConn) { - // Cancel any pending read. No further results will be received by our - // read listener. - if (mDefaultDBState->pendingRead) { - CancelAsyncRead(true); - } - // Asynchronously close the connection. We will null it below. mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener); } @@ -1703,6 +1716,7 @@ nsCookieService::CloseDBStates() CleanupDefaultDBConnection(); mDefaultDBState = nullptr; + mInitializedDBStates = false; } // Null out the statements. @@ -1735,11 +1749,12 @@ nsCookieService::CleanupDefaultDBConnection() // Manually null out our listeners. This is necessary because they hold a // strong ref to the DBState itself. They'll stay alive until whatever // statements are still executing complete. - mDefaultDBState->readListener = nullptr; mDefaultDBState->insertListener = nullptr; mDefaultDBState->updateListener = nullptr; mDefaultDBState->removeListener = nullptr; mDefaultDBState->closeListener = nullptr; + + mInitializedDBConn = false; } void @@ -1807,16 +1822,6 @@ nsCookieService::HandleCorruptDB(DBState* aDBState) // Move to 'closing' state. mDefaultDBState->corruptFlag = DBState::CLOSING_FOR_REBUILD; - // Cancel any pending read and close the database. If we do have an - // in-flight read we want to throw away all the results so far -- we have no - // idea how consistent the database is. Note that we may have already - // canceled the read but not emptied our readSet; do so now. - mDefaultDBState->readSet.Clear(); - if (mDefaultDBState->pendingRead) { - CancelAsyncRead(true); - mDefaultDBState->syncConn = nullptr; - } - CleanupCachedStatements(); mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener); CleanupDefaultDBConnection(); @@ -1867,61 +1872,80 @@ nsCookieService::RebuildCorruptDB(DBState* aDBState) COOKIE_LOGSTRING(LogLevel::Debug, ("RebuildCorruptDB(): creating new database")); - // The database has been closed, and we're ready to rebuild. Open a - // connection. - OpenDBResult result = TryInitDB(true); - if (result != RESULT_OK) { - // We're done. Reset our DB connection and statements, and notify of - // closure. - COOKIE_LOGSTRING(LogLevel::Warning, - ("RebuildCorruptDB(): TryInitDB() failed with result %u", result)); - CleanupCachedStatements(); - CleanupDefaultDBConnection(); - mDefaultDBState->corruptFlag = DBState::OK; - if (os) { - os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); - } - return; - } + nsCOMPtr runnable = + NS_NewRunnableFunction("RebuildCorruptDB.TryInitDB", [] { + NS_ENSURE_TRUE_VOID(gCookieService && gCookieService->mDefaultDBState); - // Notify observers that we're beginning the rebuild. - if (os) { - os->NotifyObservers(nullptr, "cookie-db-rebuilding", nullptr); - } + // The database has been closed, and we're ready to rebuild. Open a + // connection. + OpenDBResult result = gCookieService->TryInitDB(true); - // Enumerate the hash, and add cookies to the params array. - mozIStorageAsyncStatement* stmt = aDBState->stmtInsert; - nsCOMPtr paramsArray; - stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); - for (auto iter = aDBState->hostTable.Iter(); !iter.Done(); iter.Next()) { - nsCookieEntry* entry = iter.Get(); + nsCOMPtr innerRunnable = + NS_NewRunnableFunction("RebuildCorruptDB.TryInitDBComplete", [result] { + NS_ENSURE_TRUE_VOID(gCookieService && gCookieService->mDefaultDBState); - const nsCookieEntry::ArrayType& cookies = entry->GetCookies(); - for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { - nsCookie* cookie = cookies[i]; + nsCOMPtr os = mozilla::services::GetObserverService(); + if (result != RESULT_OK) { + // We're done. Reset our DB connection and statements, and notify of + // closure. + COOKIE_LOGSTRING(LogLevel::Warning, + ("RebuildCorruptDB(): TryInitDB() failed with result %u", result)); + gCookieService->CleanupCachedStatements(); + gCookieService->CleanupDefaultDBConnection(); + gCookieService->mDefaultDBState->corruptFlag = DBState::OK; + if (os) { + os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); + } + return; + } - if (!cookie->IsSession()) { - bindCookieParameters(paramsArray, nsCookieKey(entry), cookie); - } - } - } + // Notify observers that we're beginning the rebuild. + if (os) { + os->NotifyObservers(nullptr, "cookie-db-rebuilding", nullptr); + } - // Make sure we've got something to write. If we don't, we're done. - uint32_t length; - paramsArray->GetLength(&length); - if (length == 0) { - COOKIE_LOGSTRING(LogLevel::Debug, - ("RebuildCorruptDB(): nothing to write, rebuild complete")); - mDefaultDBState->corruptFlag = DBState::OK; - return; - } + gCookieService->InitDBConn(); - // Execute the statement. If any errors crop up, we won't try again. - DebugOnly rv = stmt->BindParameters(paramsArray); - NS_ASSERT_SUCCESS(rv); - nsCOMPtr handle; - rv = stmt->ExecuteAsync(aDBState->insertListener, getter_AddRefs(handle)); - NS_ASSERT_SUCCESS(rv); + // Enumerate the hash, and add cookies to the params array. + mozIStorageAsyncStatement* stmt = gCookieService->mDefaultDBState->stmtInsert; + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (auto iter = gCookieService->mDefaultDBState->hostTable.Iter(); + !iter.Done(); + iter.Next()) { + nsCookieEntry* entry = iter.Get(); + + const nsCookieEntry::ArrayType& cookies = entry->GetCookies(); + for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + nsCookie* cookie = cookies[i]; + + if (!cookie->IsSession()) { + bindCookieParameters(paramsArray, nsCookieKey(entry), cookie); + } + } + } + + // Make sure we've got something to write. If we don't, we're done. + uint32_t length; + paramsArray->GetLength(&length); + if (length == 0) { + COOKIE_LOGSTRING(LogLevel::Debug, + ("RebuildCorruptDB(): nothing to write, rebuild complete")); + gCookieService->mDefaultDBState->corruptFlag = DBState::OK; + return; + } + + // Execute the statement. If any errors crop up, we won't try again. + DebugOnly rv = stmt->BindParameters(paramsArray); + NS_ASSERT_SUCCESS(rv); + nsCOMPtr handle; + rv = stmt->ExecuteAsync(gCookieService->mDefaultDBState->insertListener, + getter_AddRefs(handle)); + NS_ASSERT_SUCCESS(rv); + }); + NS_DispatchToMainThread(innerRunnable); + }); + mThread->Dispatch(runnable, NS_DISPATCH_NORMAL); } nsCookieService::~nsCookieService() @@ -1931,6 +1955,9 @@ nsCookieService::~nsCookieService() UnregisterWeakMemoryReporter(this); gCookieService = nullptr; + if (mThread) { + mThread->Shutdown(); + } } NS_IMETHODIMP @@ -2124,6 +2151,8 @@ nsCookieService::SetCookieStringInternal(nsIURI *aHostURI, return; } + EnsureReadComplete(true); + AutoRestore savePrevDBState(mDBState); mDBState = (aOriginAttrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState; @@ -2308,6 +2337,13 @@ NS_IMETHODIMP nsCookieService::RunInTransaction(nsICookieTransactionCallback* aCallback) { NS_ENSURE_ARG(aCallback); + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + return NS_ERROR_NOT_AVAILABLE; + } + + EnsureReadComplete(true); + if (NS_WARN_IF(!mDefaultDBState->dbConn)) { return NS_ERROR_NOT_AVAILABLE; } @@ -2364,18 +2400,14 @@ nsCookieService::RemoveAll() return NS_ERROR_NOT_AVAILABLE; } + EnsureReadComplete(true); + RemoveAllFromMemory(); // clear the cookie file if (mDBState->dbConn) { NS_ASSERTION(mDBState == mDefaultDBState, "not in default DB state"); - // Cancel any pending read. No further results will be received by our - // read listener. - if (mDefaultDBState->pendingRead) { - CancelAsyncRead(true); - } - nsCOMPtr stmt; nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_cookies"), getter_AddRefs(stmt)); @@ -2404,7 +2436,7 @@ nsCookieService::GetEnumerator(nsISimpleEnumerator **aEnumerator) return NS_ERROR_NOT_AVAILABLE; } - EnsureReadComplete(); + EnsureReadComplete(true); nsCOMArray cookieList(mDBState->cookieCount); for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) { @@ -2425,7 +2457,7 @@ nsCookieService::GetSessionEnumerator(nsISimpleEnumerator **aEnumerator) return NS_ERROR_NOT_AVAILABLE; } - EnsureReadComplete(); + EnsureReadComplete(true); nsCOMArray cookieList(mDBState->cookieCount); for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) { @@ -2531,6 +2563,8 @@ nsCookieService::AddNative(const nsACString &aHost, return NS_ERROR_NOT_AVAILABLE; } + EnsureReadComplete(true); + AutoRestore savePrevDBState(mDBState); mDBState = (aOriginAttributes->mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState; @@ -2577,6 +2611,8 @@ nsCookieService::Remove(const nsACString& aHost, const OriginAttributes& aAttrs, return NS_ERROR_NOT_AVAILABLE; } + EnsureReadComplete(true); + AutoRestore savePrevDBState(mDBState); mDBState = (aAttrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState; @@ -2670,70 +2706,10 @@ nsCookieService::RemoveNative(const nsACString &aHost, * private file I/O functions ******************************************************************************/ -// Begin an asynchronous read from the database. -OpenDBResult -nsCookieService::Read() -{ - // Set up a statement for the read. Note that our query specifies that - // 'baseDomain' not be nullptr -- see below for why. - nsCOMPtr stmtRead; - nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( - "SELECT " - "name, " - "value, " - "host, " - "path, " - "expiry, " - "lastAccessed, " - "creationTime, " - "isSecure, " - "isHttpOnly, " - "baseDomain, " - "originAttributes, " - "sameSite " - "FROM moz_cookies " - "WHERE baseDomain NOTNULL"), getter_AddRefs(stmtRead)); - NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - - // Set up a statement to delete any rows with a nullptr 'baseDomain' - // column. This takes care of any cookies set by browsers that don't - // understand the 'baseDomain' column, where the database schema version - // is from one that does. (This would occur when downgrading.) - nsCOMPtr stmtDeleteNull; - rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( - "DELETE FROM moz_cookies WHERE baseDomain ISNULL"), - getter_AddRefs(stmtDeleteNull)); - NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - - // Start a new connection for sync reads, to reduce contention with the - // background thread. We need to do this before we kick off write statements, - // since they can lock the database and prevent connections from being opened. - rv = mStorageService->OpenUnsharedDatabase(mDefaultDBState->cookieFile, - getter_AddRefs(mDefaultDBState->syncConn)); - NS_ENSURE_SUCCESS(rv, RESULT_RETRY); - - // Init our readSet hash and execute the statements. Note that, after this - // point, we cannot fail without altering the cleanup code in InitDBStates() - // to handle closing of the now-asynchronous connection. - mDefaultDBState->hostArray.SetCapacity(kMaxNumberOfCookies); - - mDefaultDBState->readListener = new ReadCookieDBListener(mDefaultDBState); - rv = stmtRead->ExecuteAsync(mDefaultDBState->readListener, - getter_AddRefs(mDefaultDBState->pendingRead)); - NS_ASSERT_SUCCESS(rv); - - nsCOMPtr handle; - rv = stmtDeleteNull->ExecuteAsync(mDefaultDBState->removeListener, - getter_AddRefs(handle)); - NS_ASSERT_SUCCESS(rv); - - return RESULT_OK; -} - // Extract data from a single result row and create an nsCookie. -// This is templated since 'T' is different for sync vs async results. -template nsCookie* -nsCookieService::GetCookieFromRow(T &aRow, const OriginAttributes& aOriginAttributes) +mozilla::UniquePtr +nsCookieService::GetCookieFromRow(mozIStorageStatement *aRow, + const OriginAttributes &aOriginAttributes) { // Skip reading 'baseDomain' -- up to the caller. nsCString name, value, host, path; @@ -2753,213 +2729,58 @@ nsCookieService::GetCookieFromRow(T &aRow, const OriginAttributes& aOriginAttrib bool isHttpOnly = 0 != aRow->AsInt32(IDX_HTTPONLY); int32_t sameSite = aRow->AsInt32(IDX_SAME_SITE); - // Create a new nsCookie and assign the data. - return nsCookie::Create(name, value, host, path, - expiry, - lastAccessed, - creationTime, - false, - isSecure, - isHttpOnly, - aOriginAttributes, - sameSite); + // Create a new constCookie and assign the data. + return mozilla::MakeUnique(name, + value, + host, + path, + expiry, + lastAccessed, + creationTime, + isSecure, + isHttpOnly, + aOriginAttributes, + sameSite); } void -nsCookieService::AsyncReadComplete() +nsCookieService::EnsureReadComplete(bool aInitDBConn) { - // We may be in the private browsing DB state, with a pending read on the - // default DB state. (This would occur if we started up in private browsing - // mode.) As long as we do all our operations on the default state, we're OK. - NS_ASSERTION(mDefaultDBState, "no default DBState"); - NS_ASSERTION(mDefaultDBState->pendingRead, "no pending read"); - NS_ASSERTION(mDefaultDBState->readListener, "no read listener"); + MOZ_ASSERT(NS_IsMainThread()); - mozStorageTransaction transaction(mDefaultDBState->dbConn, false); - // Merge the data read on the background thread with the data synchronously - // read on the main thread. Note that transactions on the cookie table may - // have occurred on the main thread since, making the background data stale. - for (uint32_t i = 0; i < mDefaultDBState->hostArray.Length(); ++i) { - const CookieDomainTuple &tuple = mDefaultDBState->hostArray[i]; + if (!mInitializedDBStates) { + TimeStamp startBlockTime = TimeStamp::Now(); + MonitorAutoLock lock(mMonitor); - // Tiebreak: if the given base domain has already been read in, ignore - // the background data. Note that readSet may contain domains that were - // queried but found not to be in the db -- that's harmless. - if (mDefaultDBState->readSet.GetEntry(tuple.key)) - continue; - - AddCookieToList(tuple.key, tuple.cookie, mDefaultDBState, nullptr, false); - } - DebugOnly rv = transaction.Commit(); - MOZ_ASSERT(NS_SUCCEEDED(rv)); - - mDefaultDBState->stmtReadDomain = nullptr; - mDefaultDBState->pendingRead = nullptr; - mDefaultDBState->readListener = nullptr; - - // Close sync connection asynchronously: if we let destructor close, it may - // cause an expensive fsync operation on the main-thread. - if (mDefaultDBState->syncConn) { - mDefaultDBState->syncConn->AsyncClose(nullptr); - mDefaultDBState->syncConn = nullptr; - } - mDefaultDBState->hostArray.Clear(); - mDefaultDBState->readSet.Clear(); - - COOKIE_LOGSTRING(LogLevel::Debug, ("Read(): %" PRIu32 " cookies read", - mDefaultDBState->cookieCount)); - - nsCOMPtr os = mozilla::services::GetObserverService(); - if (os) { - os->NotifyObservers(nullptr, "cookie-db-read", nullptr); - } -} - -void -nsCookieService::CancelAsyncRead(bool aPurgeReadSet) -{ - // We may be in the private browsing DB state, with a pending read on the - // default DB state. (This would occur if we started up in private browsing - // mode.) As long as we do all our operations on the default state, we're OK. - NS_ASSERTION(mDefaultDBState, "no default DBState"); - NS_ASSERTION(mDefaultDBState->pendingRead, "no pending read"); - NS_ASSERTION(mDefaultDBState->readListener, "no read listener"); - - // Cancel the pending read, kill the read listener, and empty the array - // of data already read in on the background thread. - mDefaultDBState->readListener->Cancel(); - DebugOnly rv = mDefaultDBState->pendingRead->Cancel(); - NS_ASSERT_SUCCESS(rv); - - mDefaultDBState->stmtReadDomain = nullptr; - mDefaultDBState->pendingRead = nullptr; - mDefaultDBState->readListener = nullptr; - mDefaultDBState->hostArray.Clear(); - - // Only clear the 'readSet' table if we no longer need to know what set of - // data is already accounted for. - if (aPurgeReadSet) - mDefaultDBState->readSet.Clear(); -} - -void -nsCookieService::EnsureReadDomain(const nsCookieKey &aKey) -{ - NS_ASSERTION(!mDBState->dbConn || mDBState == mDefaultDBState, - "not in default db state"); - - // Fast path 1: nothing to read, or we've already finished reading. - if (MOZ_LIKELY(!mDBState->dbConn || !mDefaultDBState->pendingRead)) - return; - - // Fast path 2: already read in this particular domain. - if (MOZ_LIKELY(mDefaultDBState->readSet.GetEntry(aKey))) - return; - - // Read in the data synchronously. - // see IDX_NAME, etc. for parameter indexes - nsresult rv; - if (!mDefaultDBState->stmtReadDomain) { - // Cache the statement, since it's likely to be used again. - rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( - "SELECT " - "name, " - "value, " - "host, " - "path, " - "expiry, " - "lastAccessed, " - "creationTime, " - "isSecure, " - "isHttpOnly, " - "baseDomain, " - "originAttributes, " - "sameSite " - "FROM moz_cookies " - "WHERE baseDomain = :baseDomain " - " AND originAttributes = :originAttributes"), - getter_AddRefs(mDefaultDBState->stmtReadDomain)); - - if (NS_FAILED(rv)) { - // Recreate the database. - COOKIE_LOGSTRING(LogLevel::Debug, - ("EnsureReadDomain(): corruption detected when creating statement " - "with rv 0x%" PRIx32, static_cast(rv))); - HandleCorruptDB(mDefaultDBState); - return; + while (!mInitializedDBStates) { + mMonitor.Wait(); } + Telemetry::AccumulateTimeDelta(Telemetry::MOZ_SQLITE_COOKIES_BLOCK_MAIN_THREAD_MS, + startBlockTime); + mAccumulatedWaitTelemetry = true; } - - NS_ASSERTION(mDefaultDBState->syncConn, "should have a sync db connection"); - - mozStorageStatementScoper scoper(mDefaultDBState->stmtReadDomain); - - rv = mDefaultDBState->stmtReadDomain->BindUTF8StringByName( - NS_LITERAL_CSTRING("baseDomain"), aKey.mBaseDomain); - NS_ASSERT_SUCCESS(rv); - - nsAutoCString suffix; - aKey.mOriginAttributes.CreateSuffix(suffix); - rv = mDefaultDBState->stmtReadDomain->BindUTF8StringByName( - NS_LITERAL_CSTRING("originAttributes"), suffix); - NS_ASSERT_SUCCESS(rv); - - bool hasResult; - nsCString name, value, host, path; - AutoTArray, kMaxCookiesPerHost> array; - while (true) { - rv = mDefaultDBState->stmtReadDomain->ExecuteStep(&hasResult); - if (NS_FAILED(rv)) { - // Recreate the database. - COOKIE_LOGSTRING(LogLevel::Debug, - ("EnsureReadDomain(): corruption detected when reading result " - "with rv 0x%" PRIx32, static_cast(rv))); - HandleCorruptDB(mDefaultDBState); - return; - } - - if (!hasResult) - break; - - array.AppendElement(GetCookieFromRow(mDefaultDBState->stmtReadDomain, - aKey.mOriginAttributes)); + if (!mInitializedDBConn && aInitDBConn && mDefaultDBState) { + InitDBConn(); } - - mozStorageTransaction transaction(mDefaultDBState->dbConn, false); - // Add the cookies to the table in a single operation. This makes sure that - // either all the cookies get added, or in the case of corruption, none. - for (uint32_t i = 0; i < array.Length(); ++i) { - AddCookieToList(aKey, array[i], mDefaultDBState, nullptr, false); - } - rv = transaction.Commit(); - MOZ_ASSERT(NS_SUCCEEDED(rv)); - - // Add it to the hashset of read entries, so we don't read it again. - mDefaultDBState->readSet.PutEntry(aKey); - - COOKIE_LOGSTRING(LogLevel::Debug, - ("EnsureReadDomain(): %zu cookies read for base domain %s, " - " originAttributes = %s", array.Length(), aKey.mBaseDomain.get(), - suffix.get())); } -void -nsCookieService::EnsureReadComplete() +OpenDBResult +nsCookieService::Read() { - NS_ASSERTION(!mDBState->dbConn || mDBState == mDefaultDBState, - "not in default db state"); + MOZ_ASSERT(NS_GetCurrentThread() == mThread); - // Fast path 1: nothing to read, or we've already finished reading. - if (MOZ_LIKELY(!mDBState->dbConn || !mDefaultDBState->pendingRead)) - return; - - // Cancel the pending read, so we don't get any more results. - CancelAsyncRead(false); + // Set up a statement to delete any rows with a nullptr 'baseDomain' + // column. This takes care of any cookies set by browsers that don't + // understand the 'baseDomain' column, where the database schema version + // is from one that does. (This would occur when downgrading.) + nsresult rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DELETE FROM moz_cookies WHERE baseDomain ISNULL")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Read in the data synchronously. // see IDX_NAME, etc. for parameter indexes nsCOMPtr stmt; - nsresult rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( + rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT " "name, " "value, " @@ -2976,27 +2797,20 @@ nsCookieService::EnsureReadComplete() "FROM moz_cookies " "WHERE baseDomain NOTNULL"), getter_AddRefs(stmt)); - if (NS_FAILED(rv)) { - // Recreate the database. - COOKIE_LOGSTRING(LogLevel::Debug, - ("EnsureReadComplete(): corruption detected when creating statement " - "with rv 0x%" PRIx32, static_cast(rv))); - HandleCorruptDB(mDefaultDBState); - return; + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + if (NS_WARN_IF(!mReadArray.IsEmpty())) { + mReadArray.Clear(); } + mReadArray.SetCapacity(kMaxNumberOfCookies); nsCString baseDomain, name, value, host, path; bool hasResult; - nsTArray array(kMaxNumberOfCookies); while (true) { rv = stmt->ExecuteStep(&hasResult); - if (NS_FAILED(rv)) { - // Recreate the database. - COOKIE_LOGSTRING(LogLevel::Debug, - ("EnsureReadComplete(): corruption detected when reading result " - "with rv 0x%" PRIx32, static_cast(rv))); - HandleCorruptDB(mDefaultDBState); - return; + if (NS_WARN_IF(NS_FAILED(rv))) { + mReadArray.Clear(); + return RESULT_RETRY; } if (!hasResult) @@ -3013,30 +2827,16 @@ nsCookieService::EnsureReadComplete() Unused << attrs.PopulateFromSuffix(suffix); nsCookieKey key(baseDomain, attrs); - if (mDefaultDBState->readSet.GetEntry(key)) - continue; - - CookieDomainTuple* tuple = array.AppendElement(); + CookieDomainTuple* tuple = mReadArray.AppendElement(); tuple->key = key; tuple->cookie = GetCookieFromRow(stmt, attrs); } - mozStorageTransaction transaction(mDefaultDBState->dbConn, false); - // Add the cookies to the table in a single operation. This makes sure that - // either all the cookies get added, or in the case of corruption, none. - for (uint32_t i = 0; i < array.Length(); ++i) { - CookieDomainTuple& tuple = array[i]; - AddCookieToList(tuple.key, tuple.cookie, mDefaultDBState, nullptr, - false); - } - rv = transaction.Commit(); - MOZ_ASSERT(NS_SUCCEEDED(rv)); - mDefaultDBState->syncConn = nullptr; - mDefaultDBState->readSet.Clear(); - COOKIE_LOGSTRING(LogLevel::Debug, - ("EnsureReadComplete(): %zu cookies read", array.Length())); + COOKIE_LOGSTRING(LogLevel::Debug, ("Read(): %zu cookies read", mReadArray.Length())); + + return RESULT_OK; } NS_IMETHODIMP @@ -3047,6 +2847,8 @@ nsCookieService::ImportCookies(nsIFile *aCookieFile) return NS_ERROR_NOT_AVAILABLE; } + EnsureReadComplete(true); + // Make sure we're in the default DB state. We don't want people importing // cookies into a private browsing session! if (mDBState != mDefaultDBState) { @@ -3062,9 +2864,6 @@ nsCookieService::ImportCookies(nsIFile *aCookieFile) nsCOMPtr lineInputStream = do_QueryInterface(fileInputStream, &rv); if (NS_FAILED(rv)) return rv; - // First, ensure we've read in everything from the database, if we have one. - EnsureReadComplete(); - static const char kTrue[] = "TRUE"; nsAutoCString buffer, baseDomain; @@ -3283,6 +3082,8 @@ nsCookieService::GetCookiesForURI(nsIURI *aHostURI, return; } + EnsureReadComplete(true); + AutoRestore savePrevDBState(mDBState); mDBState = (aOriginAttrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState; @@ -3338,7 +3139,6 @@ nsCookieService::GetCookiesForURI(nsIURI *aHostURI, bool stale = false; nsCookieKey key(baseDomain, aOriginAttrs); - EnsureReadDomain(key); // perform the hash lookup nsCookieEntry *entry = mDBState->hostTable.GetEntry(key); @@ -3673,6 +3473,9 @@ nsCookieService::AddInternal(const nsCookieKey &aKey, const char *aCookieHeader, bool aFromHttp) { + MOZ_ASSERT(mInitializedDBStates); + MOZ_ASSERT(mInitializedDBConn); + int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC; nsListIter exactIter; @@ -4576,7 +4379,6 @@ already_AddRefed nsCookieService::PurgeCookies(int64_t aCurrentTimeInUsec) { NS_ASSERTION(mDBState->hostTable.Count() > 0, "table is empty"); - EnsureReadComplete(); uint32_t initialCookieCount = mDBState->cookieCount; COOKIE_LOGSTRING(LogLevel::Debug, @@ -4726,6 +4528,8 @@ nsCookieService::CookieExistsNative(nsICookie2* aCookie, return NS_ERROR_NOT_AVAILABLE; } + EnsureReadComplete(true); + AutoRestore savePrevDBState(mDBState); mDBState = (aOriginAttributes->mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState; @@ -4867,6 +4671,8 @@ nsCookieService::CountCookiesFromHost(const nsACString &aHost, return NS_ERROR_NOT_AVAILABLE; } + EnsureReadComplete(true); + // first, normalize the hostname, and fail if it contains illegal characters. nsAutoCString host(aHost); nsresult rv = NormalizeHost(host); @@ -4877,7 +4683,6 @@ nsCookieService::CountCookiesFromHost(const nsACString &aHost, NS_ENSURE_SUCCESS(rv, rv); nsCookieKey key = DEFAULT_APP_KEY(baseDomain); - EnsureReadDomain(key); // Return a count of all cookies, including expired. nsCookieEntry *entry = mDBState->hostTable.GetEntry(key); @@ -4901,6 +4706,8 @@ nsCookieService::GetCookiesFromHost(const nsACString &aHost, return NS_ERROR_NOT_AVAILABLE; } + EnsureReadComplete(true); + // first, normalize the hostname, and fail if it contains illegal characters. nsAutoCString host(aHost); nsresult rv = NormalizeHost(host); @@ -4923,7 +4730,6 @@ nsCookieService::GetCookiesFromHost(const nsACString &aHost, mDBState = (attrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState; nsCookieKey key = nsCookieKey(baseDomain, attrs); - EnsureReadDomain(key); nsCookieEntry *entry = mDBState->hostTable.GetEntry(key); if (!entry) @@ -4969,6 +4775,7 @@ nsCookieService::GetCookiesWithOriginAttributes( NS_WARNING("No DBState! Profile already closed?"); return NS_ERROR_NOT_AVAILABLE; } + EnsureReadComplete(true); AutoRestore savePrevDBState(mDBState); mDBState = (aPattern.mPrivateBrowsingId.WasPassed() && @@ -5028,6 +4835,8 @@ nsCookieService::RemoveCookiesWithOriginAttributes( return NS_ERROR_NOT_AVAILABLE; } + EnsureReadComplete(true); + AutoRestore savePrevDBState(mDBState); mDBState = (aPattern.mPrivateBrowsingId.WasPassed() && aPattern.mPrivateBrowsingId.Value() > 0) ? mPrivateDBState : mDefaultDBState; @@ -5076,8 +4885,6 @@ bool nsCookieService::FindSecureCookie(const nsCookieKey &aKey, nsCookie *aCookie) { - EnsureReadDomain(aKey); - nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey); if (!entry) return false; @@ -5113,7 +4920,9 @@ nsCookieService::FindCookie(const nsCookieKey &aKey, const nsCString& aPath, nsListIter &aIter) { - EnsureReadDomain(aKey); + // Should |EnsureReadComplete| before. + MOZ_ASSERT(mInitializedDBStates); + MOZ_ASSERT(mInitializedDBConn); nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey); if (!entry) diff --git a/netwerk/cookie/nsCookieService.h b/netwerk/cookie/nsCookieService.h index a74f3c4d4fd0..6f356e384222 100644 --- a/netwerk/cookie/nsCookieService.h +++ b/netwerk/cookie/nsCookieService.h @@ -29,9 +29,13 @@ #include "mozIStorageFunction.h" #include "nsIVariant.h" #include "nsIFile.h" +#include "mozilla/Atomics.h" #include "mozilla/BasePrincipal.h" #include "mozilla/MemoryReporting.h" #include "mozilla/Maybe.h" +#include "mozilla/Monitor.h" +#include "mozilla/UniquePtr.h" + using mozilla::OriginAttributes; @@ -43,6 +47,7 @@ class nsIObserverService; class nsIURI; class nsIChannel; class nsIArray; +class nsIThread; class mozIStorageService; class mozIThirdPartyUtil; class ReadCookieDBListener; @@ -88,13 +93,52 @@ class nsCookieEntry : public nsCookieKey ArrayType mCookies; }; +// struct for a constant cookie for threadsafe +struct ConstCookie +{ + ConstCookie(const nsCString& aName, + const nsCString& aValue, + const nsCString& aHost, + const nsCString& aPath, + int64_t aExpiry, + int64_t aLastAccessed, + int64_t aCreationTime, + bool aIsSecure, + bool aIsHttpOnly, + const OriginAttributes &aOriginAttributes, + int32_t aSameSite) + : name(aName) + , value(aValue) + , host(aHost) + , path(aPath) + , expiry(aExpiry) + , lastAccessed(aLastAccessed) + , creationTime(aCreationTime) + , isSecure(aIsSecure) + , isHttpOnly(aIsHttpOnly) + , originAttributes(aOriginAttributes) + , sameSite(aSameSite) + { + } + + const nsCString name; + const nsCString value; + const nsCString host; + const nsCString path; + const int64_t expiry; + const int64_t lastAccessed; + const int64_t creationTime; + const bool isSecure; + const bool isHttpOnly; + const OriginAttributes originAttributes; + const int32_t sameSite; +}; + // encapsulates a (key, nsCookie) tuple for temporary storage purposes. struct CookieDomainTuple { nsCookieKey key; - RefPtr cookie; - - size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + mozilla::UniquePtr cookie; }; // encapsulates in-memory and on-disk DB states, so we can @@ -137,17 +181,9 @@ public: // while the background read is taking place. nsCOMPtr syncConn; nsCOMPtr stmtReadDomain; - nsCOMPtr pendingRead; // The asynchronous read listener. This is a weak ref (storage has ownership) // since it may need to outlive the DBState's database connection. ReadCookieDBListener* readListener; - // An array of (baseDomain, cookie) tuples representing data read in - // asynchronously. This is merged into hostTable once read is complete. - nsTArray hostArray; - // A hashset of baseDomains read in synchronously, while the async read is - // in flight. This is used to keep track of which data in hostArray is stale - // when the time comes to merge. - nsTHashtable readSet; // DB completion handlers. nsCOMPtr insertListener; @@ -242,6 +278,8 @@ class nsCookieService final : public nsICookieService void PrefChanged(nsIPrefBranch *aPrefBranch); void InitDBStates(); OpenDBResult TryInitDB(bool aDeleteExistingDB); + void InitDBConn(); + nsresult InitDBConnInternal(); nsresult CreateTableWorker(const char* aName); nsresult CreateIndex(); nsresult CreateTable(); @@ -254,11 +292,8 @@ class nsCookieService final : public nsICookieService void HandleCorruptDB(DBState* aDBState); void RebuildCorruptDB(DBState* aDBState); OpenDBResult Read(); - template nsCookie* GetCookieFromRow(T &aRow, const OriginAttributes& aOriginAttributes); - void AsyncReadComplete(); - void CancelAsyncRead(bool aPurgeReadSet); - void EnsureReadDomain(const nsCookieKey &aKey); - void EnsureReadComplete(); + mozilla::UniquePtr GetCookieFromRow(mozIStorageStatement *aRow, const OriginAttributes &aOriginAttributes); + void EnsureReadComplete(bool aInitDBConn); nsresult NormalizeHost(nsCString &aHost); nsresult GetCookieStringCommon(nsIURI *aHostURI, nsIChannel *aChannel, bool aHttpBound, char** aCookie); void GetCookieStringInternal(nsIURI *aHostURI, bool aIsForeign, bool aHttpBound, const OriginAttributes& aOriginAttrs, nsCString &aCookie); @@ -326,6 +361,14 @@ class nsCookieService final : public nsICookieService uint16_t mMaxCookiesPerHost; int64_t mCookiePurgeAge; + // thread + nsCOMPtr mThread; + mozilla::Monitor mMonitor; + mozilla::Atomic mInitializedDBStates; + mozilla::Atomic mInitializedDBConn; + bool mAccumulatedWaitTelemetry; + nsTArray mReadArray; + // friends! friend class DBListenerErrorHandler; friend class ReadCookieDBListener; diff --git a/netwerk/cookie/test/unit/test_bug1321912.js b/netwerk/cookie/test/unit/test_bug1321912.js index c20d8d080c9f..2335c35c6f82 100644 --- a/netwerk/cookie/test/unit/test_bug1321912.js +++ b/netwerk/cookie/test/unit/test_bug1321912.js @@ -50,9 +50,9 @@ conn.executeSimpleSQL("INSERT INTO moz_cookies(" + now + ", " + now + ", " + now + ", 1, 1)"); // Now start the cookie service, and then check the fields in the table. - -const cs = Cc["@mozilla.org/cookieService;1"]. - getService(Ci.nsICookieService); +// Get sessionEnumerator to wait for the initialization in cookie thread +const enumerator = Cc["@mozilla.org/cookieService;1"]. + getService(Ci.nsICookieManager).sessionEnumerator; do_check_true(conn.schemaVersion, 8); let stmt = conn.createStatement("SELECT sql FROM sqlite_master " + diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index caa352a5c584..3ccae1e8546f 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -4404,6 +4404,16 @@ "n_buckets": 10, "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***" }, + "MOZ_SQLITE_COOKIES_BLOCK_MAIN_THREAD_MS": { + "record_in_processes": ["main"], + "expires_in_version": "never", + "kind": "exponential", + "alert_emails": ["necko@mozilla.com", "junior@mozilla.com"], + "bug_numbers": [870460], + "high": 3000, + "n_buckets": 10, + "description": "Time spent on blocking main thread by startup cookie database read (ms)" + }, "MOZ_SQLITE_COOKIES_READ_MAIN_THREAD_MS": { "record_in_processes": ["main", "content"], "expires_in_version": "40",