diff options
Diffstat (limited to 'dom/storage/StorageDBThread.cpp')
-rw-r--r-- | dom/storage/StorageDBThread.cpp | 1636 |
1 files changed, 1636 insertions, 0 deletions
diff --git a/dom/storage/StorageDBThread.cpp b/dom/storage/StorageDBThread.cpp new file mode 100644 index 0000000000..0820d47faa --- /dev/null +++ b/dom/storage/StorageDBThread.cpp @@ -0,0 +1,1636 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "StorageDBThread.h" +#include "StorageDBUpdater.h" +#include "StorageUtils.h" +#include "LocalStorageCache.h" +#include "LocalStorageManager.h" + +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozStorageCID.h" +#include "mozStorageHelper.h" +#include "mozIStorageService.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageValueArray.h" +#include "mozIStorageFunction.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsIObserverService.h" +#include "nsThread.h" +#include "nsThreadManager.h" +#include "nsVariant.h" +#include "mozilla/EventQueue.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/ThreadEventQueue.h" +#include "mozilla/Services.h" +#include "mozilla/Tokenizer.h" +#include "GeckoProfiler.h" + +// How long we collect write oprerations +// before they are flushed to the database +// In milliseconds. +#define FLUSHING_INTERVAL_MS 5000 + +// Write Ahead Log's maximum size is 512KB +#define MAX_WAL_SIZE_BYTES 512 * 1024 + +// Current version of the database schema +#define CURRENT_SCHEMA_VERSION 2 + +namespace mozilla { +namespace dom { + +using namespace StorageUtils; + +namespace { // anon + +StorageDBThread* sStorageThread[2] = {nullptr, nullptr}; + +// False until we shut the storage thread down. +bool sStorageThreadDown[2] = {false, false}; + +} // namespace + +// XXX Fix me! +#if 0 +StorageDBBridge::StorageDBBridge() +{ +} +#endif + +class StorageDBThread::InitHelper final : public Runnable { + nsCOMPtr<nsIEventTarget> mOwningThread; + mozilla::Mutex mMutex; + mozilla::CondVar mCondVar; + nsString mProfilePath; + nsresult mMainThreadResultCode; + bool mWaiting; + + public: + InitHelper() + : Runnable("dom::StorageDBThread::InitHelper"), + mOwningThread(GetCurrentEventTarget()), + mMutex("InitHelper::mMutex"), + mCondVar(mMutex, "InitHelper::mCondVar"), + mMainThreadResultCode(NS_OK), + mWaiting(true) {} + + // Because of the `sync Preload` IPC, we need to be able to synchronously + // initialize, which includes consulting and initializing + // some main-thread-only APIs. Bug 1386441 discusses improving this situation. + nsresult SyncDispatchAndReturnProfilePath(nsAString& aProfilePath); + + private: + ~InitHelper() override = default; + + nsresult RunOnMainThread(); + + NS_DECL_NSIRUNNABLE +}; + +class StorageDBThread::NoteBackgroundThreadRunnable final : public Runnable { + // Expected to be only 0 or 1. + const uint32_t mPrivateBrowsingId; + nsCOMPtr<nsIEventTarget> mOwningThread; + + public: + explicit NoteBackgroundThreadRunnable(const uint32_t aPrivateBrowsingId) + : Runnable("dom::StorageDBThread::NoteBackgroundThreadRunnable"), + mPrivateBrowsingId(aPrivateBrowsingId), + mOwningThread(GetCurrentEventTarget()) {} + + private: + ~NoteBackgroundThreadRunnable() override = default; + + NS_DECL_NSIRUNNABLE +}; + +StorageDBThread::StorageDBThread(const uint32_t aPrivateBrowsingId) + : mThread(nullptr), + mThreadObserver(new ThreadObserver()), + mStopIOThread(false), + mWALModeEnabled(false), + mDBReady(false), + mStatus(NS_OK), + mWorkerStatements(mWorkerConnection), + mReaderStatements(mReaderConnection), + mFlushImmediately(false), + mPrivateBrowsingId(aPrivateBrowsingId), + mPriorityCounter(0) { + MOZ_ASSERT(aPrivateBrowsingId <= 1); +} + +// static +StorageDBThread* StorageDBThread::Get(const uint32_t aPrivateBrowsingId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPrivateBrowsingId <= 1); + + return sStorageThread[aPrivateBrowsingId]; +} + +// static +StorageDBThread* StorageDBThread::GetOrCreate( + const nsString& aProfilePath, const uint32_t aPrivateBrowsingId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPrivateBrowsingId <= 1); + + StorageDBThread*& storageThread = sStorageThread[aPrivateBrowsingId]; + if (storageThread || sStorageThreadDown[aPrivateBrowsingId]) { + // When sStorageThreadDown is at true, sStorageThread is null. + // Checking sStorageThreadDown flag here prevents reinitialization of + // the storage thread after shutdown. + return storageThread; + } + + auto newStorageThread = MakeUnique<StorageDBThread>(aPrivateBrowsingId); + + nsresult rv = newStorageThread->Init(aProfilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + storageThread = newStorageThread.release(); + + return storageThread; +} + +// static +nsresult StorageDBThread::GetProfilePath(nsString& aProfilePath) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + // Need to determine location on the main thread, since + // NS_GetSpecialDirectory accesses the atom table that can + // only be accessed on the main thread. + nsCOMPtr<nsIFile> profileDir; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profileDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = profileDir->GetPath(aProfilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // This service has to be started on the main thread currently. + nsCOMPtr<mozIStorageService> ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult StorageDBThread::Init(const nsString& aProfilePath) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mPrivateBrowsingId == 0) { + nsresult rv; + + nsString profilePath; + if (aProfilePath.IsEmpty()) { + RefPtr<InitHelper> helper = new InitHelper(); + + rv = helper->SyncDispatchAndReturnProfilePath(profilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + profilePath = aProfilePath; + } + + mDatabaseFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mDatabaseFile->InitWithPath(profilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mDatabaseFile->Append(u"webappsstore.sqlite"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Need to keep the lock to avoid setting mThread later then + // the thread body executes. + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + + mThread = PR_CreateThread(PR_USER_THREAD, &StorageDBThread::ThreadFunc, this, + PR_PRIORITY_LOW, PR_GLOBAL_THREAD, + PR_JOINABLE_THREAD, 262144); + if (!mThread) { + return NS_ERROR_OUT_OF_MEMORY; + } + + RefPtr<NoteBackgroundThreadRunnable> runnable = + new NoteBackgroundThreadRunnable(mPrivateBrowsingId); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); + + return NS_OK; +} + +nsresult StorageDBThread::Shutdown() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!mThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + Telemetry::AutoTimer<Telemetry::LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS> timer; + + { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + + // After we stop, no other operations can be accepted + mFlushImmediately = true; + mStopIOThread = true; + monitor.Notify(); + } + + PR_JoinThread(mThread); + mThread = nullptr; + + return mStatus; +} + +void StorageDBThread::SyncPreload(LocalStorageCacheBridge* aCache, + bool aForceSync) { + AUTO_PROFILER_LABEL("StorageDBThread::SyncPreload", OTHER); + if (!aForceSync && aCache->LoadedCount()) { + // Preload already started for this cache, just wait for it to finish. + // LoadWait will exit after LoadDone on the cache has been called. + SetHigherPriority(); + aCache->LoadWait(); + SetDefaultPriority(); + return; + } + + // Bypass sync load when an update is pending in the queue to write, we would + // get incosistent data in the cache. Also don't allow sync main-thread + // preload when DB open and init is still pending on the background thread. + if (mDBReady && mWALModeEnabled) { + bool pendingTasks; + { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + pendingTasks = mPendingTasks.IsOriginUpdatePending( + aCache->OriginSuffix(), aCache->OriginNoSuffix()) || + mPendingTasks.IsOriginClearPending( + aCache->OriginSuffix(), aCache->OriginNoSuffix()); + } + + if (!pendingTasks) { + // WAL is enabled, thus do the load synchronously on the main thread. + DBOperation preload(DBOperation::opPreload, aCache); + preload.PerformAndFinalize(this); + return; + } + } + + // Need to go asynchronously since WAL is not allowed or scheduled updates + // need to be flushed first. + // Schedule preload for this cache as the first operation. + nsresult rv = + InsertDBOp(new DBOperation(DBOperation::opPreloadUrgent, aCache)); + + // LoadWait exits after LoadDone of the cache has been called. + if (NS_SUCCEEDED(rv)) { + aCache->LoadWait(); + } +} + +void StorageDBThread::AsyncFlush() { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + mFlushImmediately = true; + monitor.Notify(); +} + +bool StorageDBThread::ShouldPreloadOrigin(const nsACString& aOrigin) { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + return mOriginsHavingData.Contains(aOrigin); +} + +void StorageDBThread::GetOriginsHavingData(nsTArray<nsCString>* aOrigins) { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + for (auto iter = mOriginsHavingData.Iter(); !iter.Done(); iter.Next()) { + aOrigins->AppendElement(iter.Get()->GetKey()); + } +} + +nsresult StorageDBThread::InsertDBOp(StorageDBThread::DBOperation* aOperation) { + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + + // Sentinel to don't forget to delete the operation when we exit early. + UniquePtr<StorageDBThread::DBOperation> opScope(aOperation); + + if (NS_FAILED(mStatus)) { + MonitorAutoUnlock unlock(mThreadObserver->GetMonitor()); + aOperation->Finalize(mStatus); + return mStatus; + } + + if (mStopIOThread) { + // Thread use after shutdown demanded. + MOZ_ASSERT(false); + return NS_ERROR_NOT_INITIALIZED; + } + + switch (aOperation->Type()) { + case DBOperation::opPreload: + case DBOperation::opPreloadUrgent: + if (mPendingTasks.IsOriginUpdatePending(aOperation->OriginSuffix(), + aOperation->OriginNoSuffix())) { + // If there is a pending update operation for the scope first do the + // flush before we preload the cache. This may happen in an extremely + // rare case when a child process throws away its cache before flush on + // the parent has finished. If we would preloaded the cache as a + // priority operation before the pending flush, we would have got an + // inconsistent cache content. + mFlushImmediately = true; + } else if (mPendingTasks.IsOriginClearPending( + aOperation->OriginSuffix(), + aOperation->OriginNoSuffix())) { + // The scope is scheduled to be cleared, so just quickly load as empty. + // We need to do this to prevent load of the DB data before the scope + // has actually been cleared from the database. Preloads are processed + // immediately before update and clear operations on the database that + // are flushed periodically in batches. + MonitorAutoUnlock unlock(mThreadObserver->GetMonitor()); + aOperation->Finalize(NS_OK); + return NS_OK; + } + [[fallthrough]]; + + case DBOperation::opGetUsage: + if (aOperation->Type() == DBOperation::opPreloadUrgent) { + SetHigherPriority(); // Dropped back after urgent preload execution + mPreloads.InsertElementAt(0, aOperation); + } else { + mPreloads.AppendElement(aOperation); + } + + // DB operation adopted, don't delete it. + Unused << opScope.release(); + + // Immediately start executing this. + monitor.Notify(); + break; + + default: + // Update operations are first collected, coalesced and then flushed + // after a short time. + mPendingTasks.Add(aOperation); + + // DB operation adopted, don't delete it. + Unused << opScope.release(); + + ScheduleFlush(); + break; + } + + return NS_OK; +} + +void StorageDBThread::SetHigherPriority() { + ++mPriorityCounter; + PR_SetThreadPriority(mThread, PR_PRIORITY_URGENT); +} + +void StorageDBThread::SetDefaultPriority() { + if (--mPriorityCounter <= 0) { + PR_SetThreadPriority(mThread, PR_PRIORITY_LOW); + } +} + +void StorageDBThread::ThreadFunc(void* aArg) { + { + auto queue = MakeRefPtr<ThreadEventQueue>(MakeUnique<EventQueue>()); + Unused << nsThreadManager::get().CreateCurrentThread( + queue, nsThread::NOT_MAIN_THREAD); + } + + AUTO_PROFILER_REGISTER_THREAD("localStorage DB"); + NS_SetCurrentThreadName("localStorage DB"); + mozilla::IOInterposer::RegisterCurrentThread(); + + StorageDBThread* thread = static_cast<StorageDBThread*>(aArg); + thread->ThreadFunc(); + mozilla::IOInterposer::UnregisterCurrentThread(); +} + +void StorageDBThread::ThreadFunc() { + nsresult rv = InitDatabase(); + + MonitorAutoLock lockMonitor(mThreadObserver->GetMonitor()); + + if (NS_FAILED(rv)) { + mStatus = rv; + mStopIOThread = true; + return; + } + + // Create an nsIThread for the current PRThread, so we can observe runnables + // dispatched to it. + nsCOMPtr<nsIThread> thread = NS_GetCurrentThread(); + nsCOMPtr<nsIThreadInternal> threadInternal = do_QueryInterface(thread); + MOZ_ASSERT(threadInternal); // Should always succeed. + threadInternal->SetObserver(mThreadObserver); + + while (MOZ_LIKELY(!mStopIOThread || mPreloads.Length() || + mPendingTasks.HasTasks() || + mThreadObserver->HasPendingEvents())) { + // Process xpcom events first. + while (MOZ_UNLIKELY(mThreadObserver->HasPendingEvents())) { + mThreadObserver->ClearPendingEvents(); + MonitorAutoUnlock unlock(mThreadObserver->GetMonitor()); + bool processedEvent; + do { + rv = thread->ProcessNextEvent(false, &processedEvent); + } while (NS_SUCCEEDED(rv) && processedEvent); + } + + TimeDuration timeUntilFlush = TimeUntilFlush(); + if (MOZ_UNLIKELY(timeUntilFlush.IsZero())) { + // Flush time is up or flush has been forced, do it now. + UnscheduleFlush(); + if (mPendingTasks.Prepare()) { + { + MonitorAutoUnlock unlockMonitor(mThreadObserver->GetMonitor()); + rv = mPendingTasks.Execute(this); + } + + if (!mPendingTasks.Finalize(rv)) { + mStatus = rv; + NS_WARNING("localStorage DB access broken"); + } + } + NotifyFlushCompletion(); + } else if (MOZ_LIKELY(mPreloads.Length())) { + UniquePtr<DBOperation> op(mPreloads[0]); + mPreloads.RemoveElementAt(0); + { + MonitorAutoUnlock unlockMonitor(mThreadObserver->GetMonitor()); + op->PerformAndFinalize(this); + } + + if (op->Type() == DBOperation::opPreloadUrgent) { + SetDefaultPriority(); // urgent preload unscheduled + } + } else if (MOZ_UNLIKELY(!mStopIOThread)) { + AUTO_PROFILER_LABEL("StorageDBThread::ThreadFunc::Wait", IDLE); + lockMonitor.Wait(timeUntilFlush); + } + } // thread loop + + mStatus = ShutdownDatabase(); + + if (threadInternal) { + threadInternal->SetObserver(nullptr); + } +} + +NS_IMPL_ISUPPORTS(StorageDBThread::ThreadObserver, nsIThreadObserver) + +NS_IMETHODIMP +StorageDBThread::ThreadObserver::OnDispatchedEvent() { + MonitorAutoLock lock(mMonitor); + mHasPendingEvents = true; + lock.Notify(); + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::ThreadObserver::OnProcessNextEvent(nsIThreadInternal* aThread, + bool mayWait) { + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::ThreadObserver::AfterProcessNextEvent( + nsIThreadInternal* aThread, bool eventWasProcessed) { + return NS_OK; +} + +nsresult StorageDBThread::OpenDatabaseConnection() { + nsresult rv; + + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageService> service = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (mPrivateBrowsingId == 0) { + MOZ_ASSERT(mDatabaseFile); + + rv = service->OpenUnsharedDatabase(mDatabaseFile, + getter_AddRefs(mWorkerConnection)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + // delete the db and try opening again + rv = mDatabaseFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + rv = service->OpenUnsharedDatabase(mDatabaseFile, + getter_AddRefs(mWorkerConnection)); + } + } else { + MOZ_ASSERT(mPrivateBrowsingId == 1); + + rv = service->OpenSpecialDatabase(kMozStorageMemoryStorageKey, + "lsprivatedb"_ns, + getter_AddRefs(mWorkerConnection)); + } + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult StorageDBThread::OpenAndUpdateDatabase() { + nsresult rv; + + // Here we are on the worker thread. This opens the worker connection. + MOZ_ASSERT(!NS_IsMainThread()); + + rv = OpenDatabaseConnection(); + NS_ENSURE_SUCCESS(rv, rv); + + // SQLite doesn't support WAL journals for in-memory databases. + if (mPrivateBrowsingId == 0) { + rv = TryJournalMode(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult StorageDBThread::InitDatabase() { + nsresult rv; + + // Here we are on the worker thread. This opens the worker connection. + MOZ_ASSERT(!NS_IsMainThread()); + + rv = OpenAndUpdateDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = StorageDBUpdater::Update(mWorkerConnection); + if (NS_FAILED(rv)) { + if (mPrivateBrowsingId == 0) { + // Update has failed, rather throw the database away and try + // opening and setting it up again. + rv = mWorkerConnection->Close(); + mWorkerConnection = nullptr; + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDatabaseFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = OpenAndUpdateDatabase(); + } + NS_ENSURE_SUCCESS(rv, rv); + } + + // Create a read-only clone + (void)mWorkerConnection->Clone(true, getter_AddRefs(mReaderConnection)); + NS_ENSURE_TRUE(mReaderConnection, NS_ERROR_FAILURE); + + // Database open and all initiation operation are done. Switching this flag + // to true allow main thread to read directly from the database. If we would + // allow this sooner, we would have opened a window where main thread read + // might operate on a totally broken and incosistent database. + mDBReady = true; + + // List scopes having any stored data + nsCOMPtr<mozIStorageStatement> stmt; + // Note: result of this select must match StorageManager::CreateOrigin() + rv = mWorkerConnection->CreateStatement( + nsLiteralCString("SELECT DISTINCT originAttributes || ':' || originKey " + "FROM webappsstore2"), + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + mozStorageStatementScoper scope(stmt); + + bool exists; + while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&exists)) && exists) { + nsAutoCString foundOrigin; + rv = stmt->GetUTF8String(0, foundOrigin); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock monitor(mThreadObserver->GetMonitor()); + mOriginsHavingData.PutEntry(foundOrigin); + } + + return NS_OK; +} + +nsresult StorageDBThread::SetJournalMode(bool aIsWal) { + nsresult rv; + + nsAutoCString stmtString(MOZ_STORAGE_UNIQUIFY_QUERY_STR + "PRAGMA journal_mode = "); + if (aIsWal) { + stmtString.AppendLiteral("wal"); + } else { + stmtString.AppendLiteral("truncate"); + } + + nsCOMPtr<mozIStorageStatement> stmt; + rv = mWorkerConnection->CreateStatement(stmtString, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + mozStorageStatementScoper scope(stmt); + + bool hasResult = false; + rv = stmt->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, rv); + if (!hasResult) { + return NS_ERROR_FAILURE; + } + + nsAutoCString journalMode; + rv = stmt->GetUTF8String(0, journalMode); + NS_ENSURE_SUCCESS(rv, rv); + if ((aIsWal && !journalMode.EqualsLiteral("wal")) || + (!aIsWal && !journalMode.EqualsLiteral("truncate"))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult StorageDBThread::TryJournalMode() { + nsresult rv; + + rv = SetJournalMode(true); + if (NS_FAILED(rv)) { + mWALModeEnabled = false; + + rv = SetJournalMode(false); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mWALModeEnabled = true; + + rv = ConfigureWALBehavior(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult StorageDBThread::ConfigureWALBehavior() { + // Get the DB's page size + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = mWorkerConnection->CreateStatement( + nsLiteralCString(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA page_size"), + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasResult = false; + rv = stmt->ExecuteStep(&hasResult); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_FAILURE); + + int32_t pageSize = 0; + rv = stmt->GetInt32(0, &pageSize); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && pageSize > 0, NS_ERROR_UNEXPECTED); + + // Set the threshold for auto-checkpointing the WAL. + // We don't want giant logs slowing down reads & shutdown. + int32_t thresholdInPages = + static_cast<int32_t>(MAX_WAL_SIZE_BYTES / pageSize); + nsAutoCString thresholdPragma("PRAGMA wal_autocheckpoint = "); + thresholdPragma.AppendInt(thresholdInPages); + rv = mWorkerConnection->ExecuteSimpleSQL(thresholdPragma); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the maximum WAL log size to reduce footprint on mobile (large empty + // WAL files will be truncated) + nsAutoCString journalSizePragma("PRAGMA journal_size_limit = "); + // bug 600307: mak recommends setting this to 3 times the auto-checkpoint + // threshold + journalSizePragma.AppendInt(MAX_WAL_SIZE_BYTES * 3); + rv = mWorkerConnection->ExecuteSimpleSQL(journalSizePragma); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult StorageDBThread::ShutdownDatabase() { + // Has to be called on the worker thread. + MOZ_ASSERT(!NS_IsMainThread()); + + nsresult rv = mStatus; + + mDBReady = false; + + // Finalize the cached statements. + mReaderStatements.FinalizeStatements(); + mWorkerStatements.FinalizeStatements(); + + if (mReaderConnection) { + // No need to sync access to mReaderConnection since the main thread + // is right now joining this thread, unable to execute any events. + mReaderConnection->Close(); + mReaderConnection = nullptr; + } + + if (mWorkerConnection) { + rv = mWorkerConnection->Close(); + mWorkerConnection = nullptr; + } + + return rv; +} + +void StorageDBThread::ScheduleFlush() { + if (mDirtyEpoch) { + return; // Already scheduled + } + + // Must be non-zero to indicate we are scheduled + mDirtyEpoch = TimeStamp::Now(); + + // Wake the monitor from indefinite sleep... + (mThreadObserver->GetMonitor()).Notify(); +} + +void StorageDBThread::UnscheduleFlush() { + // We are just about to do the flush, drop flags + mFlushImmediately = false; + mDirtyEpoch = TimeStamp(); +} + +TimeDuration StorageDBThread::TimeUntilFlush() { + if (mFlushImmediately) { + return 0; // Do it now regardless the timeout. + } + + if (!mDirtyEpoch) { + return TimeDuration::Forever(); // No pending task... + } + + TimeStamp now = TimeStamp::Now(); + TimeDuration age = now - mDirtyEpoch; + static const TimeDuration kMaxAge = + TimeDuration::FromMilliseconds(FLUSHING_INTERVAL_MS); + if (age > kMaxAge) { + return 0; // It is time. + } + + return kMaxAge - age; // Time left. This is used to sleep the monitor. +} + +void StorageDBThread::NotifyFlushCompletion() { +#ifdef DOM_STORAGE_TESTS + if (!NS_IsMainThread()) { + RefPtr<nsRunnableMethod<StorageDBThread, void, false>> event = + NewNonOwningRunnableMethod( + "dom::StorageDBThread::NotifyFlushCompletion", this, + &StorageDBThread::NotifyFlushCompletion); + NS_DispatchToMainThread(event); + return; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "domstorage-test-flushed", nullptr); + } +#endif +} + +// Helper SQL function classes + +namespace { + +class OriginAttrsPatternMatchSQLFunction final : public mozIStorageFunction { + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION + + explicit OriginAttrsPatternMatchSQLFunction( + OriginAttributesPattern const& aPattern) + : mPattern(aPattern) {} + + private: + OriginAttrsPatternMatchSQLFunction() = delete; + ~OriginAttrsPatternMatchSQLFunction() = default; + + OriginAttributesPattern mPattern; +}; + +NS_IMPL_ISUPPORTS(OriginAttrsPatternMatchSQLFunction, mozIStorageFunction) + +NS_IMETHODIMP +OriginAttrsPatternMatchSQLFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + nsresult rv; + + nsAutoCString suffix; + rv = aFunctionArguments->GetUTF8String(0, suffix); + NS_ENSURE_SUCCESS(rv, rv); + + OriginAttributes oa; + bool success = oa.PopulateFromSuffix(suffix); + NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); + bool result = mPattern.Matches(oa); + + RefPtr<nsVariant> outVar(new nsVariant()); + rv = outVar->SetAsBool(result); + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +} // namespace + +// StorageDBThread::DBOperation + +StorageDBThread::DBOperation::DBOperation(const OperationType aType, + LocalStorageCacheBridge* aCache, + const nsAString& aKey, + const nsAString& aValue) + : mType(aType), mCache(aCache), mKey(aKey), mValue(aValue) { + MOZ_ASSERT(mType == opPreload || mType == opPreloadUrgent || + mType == opAddItem || mType == opUpdateItem || + mType == opRemoveItem || mType == opClear || mType == opClearAll); + MOZ_COUNT_CTOR(StorageDBThread::DBOperation); +} + +StorageDBThread::DBOperation::DBOperation(const OperationType aType, + StorageUsageBridge* aUsage) + : mType(aType), mUsage(aUsage) { + MOZ_ASSERT(mType == opGetUsage); + MOZ_COUNT_CTOR(StorageDBThread::DBOperation); +} + +StorageDBThread::DBOperation::DBOperation(const OperationType aType, + const nsACString& aOriginNoSuffix) + : mType(aType), mCache(nullptr), mOrigin(aOriginNoSuffix) { + MOZ_ASSERT(mType == opClearMatchingOrigin); + MOZ_COUNT_CTOR(StorageDBThread::DBOperation); +} + +StorageDBThread::DBOperation::DBOperation( + const OperationType aType, const OriginAttributesPattern& aOriginNoSuffix) + : mType(aType), mCache(nullptr), mOriginPattern(aOriginNoSuffix) { + MOZ_ASSERT(mType == opClearMatchingOriginAttributes); + MOZ_COUNT_CTOR(StorageDBThread::DBOperation); +} + +StorageDBThread::DBOperation::~DBOperation() { + MOZ_COUNT_DTOR(StorageDBThread::DBOperation); +} + +const nsCString StorageDBThread::DBOperation::OriginNoSuffix() const { + if (mCache) { + return mCache->OriginNoSuffix(); + } + + return ""_ns; +} + +const nsCString StorageDBThread::DBOperation::OriginSuffix() const { + if (mCache) { + return mCache->OriginSuffix(); + } + + return ""_ns; +} + +const nsCString StorageDBThread::DBOperation::Origin() const { + if (mCache) { + return mCache->Origin(); + } + + return mOrigin; +} + +const nsCString StorageDBThread::DBOperation::Target() const { + switch (mType) { + case opAddItem: + case opUpdateItem: + case opRemoveItem: + return Origin() + "|"_ns + NS_ConvertUTF16toUTF8(mKey); + + default: + return Origin(); + } +} + +void StorageDBThread::DBOperation::PerformAndFinalize( + StorageDBThread* aThread) { + Finalize(Perform(aThread)); +} + +nsresult StorageDBThread::DBOperation::Perform(StorageDBThread* aThread) { + nsresult rv; + + switch (mType) { + case opPreload: + case opPreloadUrgent: { + // Already loaded? + if (mCache->Loaded()) { + break; + } + + StatementCache* statements; + if (MOZ_UNLIKELY(::mozilla::ipc::IsOnBackgroundThread())) { + statements = &aThread->mReaderStatements; + } else { + statements = &aThread->mWorkerStatements; + } + + // OFFSET is an optimization when we have to do a sync load + // and cache has already loaded some parts asynchronously. + // It skips keys we have already loaded. + nsCOMPtr<mozIStorageStatement> stmt = statements->GetCachedStatement( + "SELECT key, value FROM webappsstore2 " + "WHERE originAttributes = :originAttributes AND originKey = " + ":originKey " + "ORDER BY key LIMIT -1 OFFSET :offset"); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("originAttributes"_ns, + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName("originKey"_ns, mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindInt32ByName("offset"_ns, + static_cast<int32_t>(mCache->LoadedCount())); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&exists)) && exists) { + nsAutoString key; + rv = stmt->GetString(0, key); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString value; + rv = stmt->GetString(1, value); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mCache->LoadItem(key, value)) { + break; + } + } + // The loop condition's call to ExecuteStep() may have terminated because + // !NS_SUCCEEDED(), we need an early return to cover that case. This also + // covers success cases as well, but that's inductively safe. + NS_ENSURE_SUCCESS(rv, rv); + break; + } + + case opGetUsage: { + // Bug 1676410 fixed a regression caused by bug 1165214. However, it + // turns out that 100% correct checking of the eTLD+1 usage is not + // possible to recover easily, see bug 1683299. +#if 0 + // This is how it should be done, but due to other problems like lack + // of usage synchronization between content processes, we temporarily + // disabled the matching using "%". + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "SELECT SUM(LENGTH(key) + LENGTH(value)) FROM webappsstore2 " + "WHERE (originAttributes || ':' || originKey) LIKE :usageOrigin " + "ESCAPE '\\'"); + NS_ENSURE_STATE(stmt); + + mozStorageStatementScoper scope(stmt); + + // The database schema is built around cleverly reversing domain names + // (the "originKey") so that we can efficiently group usage by eTLD+1. + // "foo.example.org" has an eTLD+1 of ".example.org". They reverse to + // "gro.elpmaxe.oof" and "gro.elpmaxe." respectively, noting that the + // reversed eTLD+1 is a prefix of its reversed sub-domain. To this end, + // we can calculate all of the usage for an eTLD+1 by summing up all the + // rows which have the reversed eTLD+1 as a prefix. In SQL we can + // accomplish this using LIKE which provides for case-insensitive + // matching with "_" as a single-character wildcard match and "%" any + // sequence of zero or more characters. So by suffixing the reversed + // eTLD+1 and using "%" we get our case-insensitive (domain names are + // case-insensitive) matching. Note that although legal domain names + // don't include "_" or "%", file origins can include them, so we need + // to escape our OriginScope for correctness. + nsAutoCString originScopeEscaped; + rv = stmt->EscapeUTF8StringForLIKE(mUsage->OriginScope(), '\\', + originScopeEscaped); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->BindUTF8StringByName("usageOrigin"_ns, + originScopeEscaped + "%"_ns); + NS_ENSURE_SUCCESS(rv, rv); +#else + // This is the code before bug 1676410 and bug 1676973. The returned + // usage will be zero in most of the cases, but due to lack of usage + // synchronization between content processes we have to live with this + // semi-broken behaviour because it causes less harm than the matching + // using "%". + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "SELECT SUM(LENGTH(key) + LENGTH(value)) FROM webappsstore2 " + "WHERE (originAttributes || ':' || originKey) LIKE :usageOrigin"); + NS_ENSURE_STATE(stmt); + + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("usageOrigin"_ns, mUsage->OriginScope()); + NS_ENSURE_SUCCESS(rv, rv); +#endif + + bool exists; + rv = stmt->ExecuteStep(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t usage = 0; + if (exists) { + rv = stmt->GetInt64(0, &usage); + NS_ENSURE_SUCCESS(rv, rv); + } + + mUsage->LoadUsage(usage); + break; + } + + case opAddItem: + case opUpdateItem: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "INSERT OR REPLACE INTO webappsstore2 (originAttributes, " + "originKey, scope, key, value) " + "VALUES (:originAttributes, :originKey, :scope, :key, :value) "); + NS_ENSURE_STATE(stmt); + + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("originAttributes"_ns, + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName("originKey"_ns, mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + // Filling the 'scope' column just for downgrade compatibility reasons + rv = stmt->BindUTF8StringByName( + "scope"_ns, + Scheme0Scope(mCache->OriginSuffix(), mCache->OriginNoSuffix())); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName("key"_ns, mKey); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName("value"_ns, mValue); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock monitor(aThread->mThreadObserver->GetMonitor()); + aThread->mOriginsHavingData.PutEntry(Origin()); + break; + } + + case opRemoveItem: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2 " + "WHERE originAttributes = :originAttributes AND originKey = " + ":originKey " + "AND key = :key "); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("originAttributes"_ns, + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName("originKey"_ns, mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindStringByName("key"_ns, mKey); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + break; + } + + case opClear: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2 " + "WHERE originAttributes = :originAttributes AND originKey = " + ":originKey"); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("originAttributes"_ns, + mCache->OriginSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindUTF8StringByName("originKey"_ns, mCache->OriginNoSuffix()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock monitor(aThread->mThreadObserver->GetMonitor()); + aThread->mOriginsHavingData.RemoveEntry(Origin()); + break; + } + + case opClearAll: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2"); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock monitor(aThread->mThreadObserver->GetMonitor()); + aThread->mOriginsHavingData.Clear(); + break; + } + + case opClearMatchingOrigin: { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2" + " WHERE originKey GLOB :scope"); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scope(stmt); + + rv = stmt->BindUTF8StringByName("scope"_ns, mOrigin + "*"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + // No need to selectively clear mOriginsHavingData here. That hashtable + // only prevents preload for scopes with no data. Leaving a false record + // in it has a negligible effect on performance. + break; + } + + case opClearMatchingOriginAttributes: { + MOZ_ASSERT(!NS_IsMainThread()); + + // Register the ORIGIN_ATTRS_PATTERN_MATCH function, initialized with the + // pattern + nsCOMPtr<mozIStorageFunction> patternMatchFunction( + new OriginAttrsPatternMatchSQLFunction(mOriginPattern)); + + rv = aThread->mWorkerConnection->CreateFunction( + "ORIGIN_ATTRS_PATTERN_MATCH"_ns, 1, patternMatchFunction); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIStorageStatement> stmt = + aThread->mWorkerStatements.GetCachedStatement( + "DELETE FROM webappsstore2" + " WHERE ORIGIN_ATTRS_PATTERN_MATCH(originAttributes)"); + + if (stmt) { + mozStorageStatementScoper scope(stmt); + rv = stmt->Execute(); + } else { + rv = NS_ERROR_UNEXPECTED; + } + + // Always remove the function + aThread->mWorkerConnection->RemoveFunction( + "ORIGIN_ATTRS_PATTERN_MATCH"_ns); + + NS_ENSURE_SUCCESS(rv, rv); + + // No need to selectively clear mOriginsHavingData here. That hashtable + // only prevents preload for scopes with no data. Leaving a false record + // in it has a negligible effect on performance. + break; + } + + default: + NS_ERROR("Unknown task type"); + break; + } + + return NS_OK; +} + +void StorageDBThread::DBOperation::Finalize(nsresult aRv) { + switch (mType) { + case opPreloadUrgent: + case opPreload: + if (NS_FAILED(aRv)) { + // When we are here, something failed when loading from the database. + // Notify that the storage is loaded to prevent deadlock of the main + // thread, even though it is actually empty or incomplete. + NS_WARNING("Failed to preload localStorage"); + } + + mCache->LoadDone(aRv); + break; + + case opGetUsage: + if (NS_FAILED(aRv)) { + mUsage->LoadUsage(0); + } + + break; + + default: + if (NS_FAILED(aRv)) { + NS_WARNING( + "localStorage update/clear operation failed," + " data may not persist or clean up"); + } + + break; + } +} + +// StorageDBThread::PendingOperations + +StorageDBThread::PendingOperations::PendingOperations() + : mFlushFailureCount(0) {} + +bool StorageDBThread::PendingOperations::HasTasks() const { + return !!mUpdates.Count() || !!mClears.Count(); +} + +namespace { + +bool OriginPatternMatches(const nsACString& aOriginSuffix, + const OriginAttributesPattern& aPattern) { + OriginAttributes oa; + DebugOnly<bool> rv = oa.PopulateFromSuffix(aOriginSuffix); + MOZ_ASSERT(rv); + return aPattern.Matches(oa); +} + +} // namespace + +bool StorageDBThread::PendingOperations::CheckForCoalesceOpportunity( + DBOperation* aNewOp, DBOperation::OperationType aPendingType, + DBOperation::OperationType aNewType) { + if (aNewOp->Type() != aNewType) { + return false; + } + + StorageDBThread::DBOperation* pendingTask; + if (!mUpdates.Get(aNewOp->Target(), &pendingTask)) { + return false; + } + + if (pendingTask->Type() != aPendingType) { + return false; + } + + return true; +} + +void StorageDBThread::PendingOperations::Add( + StorageDBThread::DBOperation* aOperation) { + // Optimize: when a key to remove has never been written to disk + // just bypass this operation. A key is new when an operation scheduled + // to write it to the database is of type opAddItem. + if (CheckForCoalesceOpportunity(aOperation, DBOperation::opAddItem, + DBOperation::opRemoveItem)) { + mUpdates.Remove(aOperation->Target()); + delete aOperation; + return; + } + + // Optimize: when changing a key that is new and has never been + // written to disk, keep type of the operation to store it at opAddItem. + // This allows optimization to just forget adding a new key when + // it is removed from the storage before flush. + if (CheckForCoalesceOpportunity(aOperation, DBOperation::opAddItem, + DBOperation::opUpdateItem)) { + aOperation->mType = DBOperation::opAddItem; + } + + // Optimize: to prevent lose of remove operation on a key when doing + // remove/set/remove on a previously existing key we have to change + // opAddItem to opUpdateItem on the new operation when there is opRemoveItem + // pending for the key. + if (CheckForCoalesceOpportunity(aOperation, DBOperation::opRemoveItem, + DBOperation::opAddItem)) { + aOperation->mType = DBOperation::opUpdateItem; + } + + switch (aOperation->Type()) { + // Operations on single keys + + case DBOperation::opAddItem: + case DBOperation::opUpdateItem: + case DBOperation::opRemoveItem: + // Override any existing operation for the target (=scope+key). + mUpdates.Put(aOperation->Target(), aOperation); + break; + + // Clear operations + + case DBOperation::opClear: + case DBOperation::opClearMatchingOrigin: + case DBOperation::opClearMatchingOriginAttributes: + // Drop all update (insert/remove) operations for equivavelent or matching + // scope. We do this as an optimization as well as a must based on the + // logic, if we would not delete the update tasks, changes would have been + // stored to the database after clear operations have been executed. + for (auto iter = mUpdates.Iter(); !iter.Done(); iter.Next()) { + const auto& pendingTask = iter.Data(); + + if (aOperation->Type() == DBOperation::opClear && + (pendingTask->OriginNoSuffix() != aOperation->OriginNoSuffix() || + pendingTask->OriginSuffix() != aOperation->OriginSuffix())) { + continue; + } + + if (aOperation->Type() == DBOperation::opClearMatchingOrigin && + !StringBeginsWith(pendingTask->OriginNoSuffix(), + aOperation->Origin())) { + continue; + } + + if (aOperation->Type() == + DBOperation::opClearMatchingOriginAttributes && + !OriginPatternMatches(pendingTask->OriginSuffix(), + aOperation->OriginPattern())) { + continue; + } + + iter.Remove(); + } + + mClears.Put(aOperation->Target(), aOperation); + break; + + case DBOperation::opClearAll: + // Drop simply everything, this is a super-operation. + mUpdates.Clear(); + mClears.Clear(); + mClears.Put(aOperation->Target(), aOperation); + break; + + default: + MOZ_ASSERT(false); + break; + } +} + +bool StorageDBThread::PendingOperations::Prepare() { + // Called under the lock + + // First collect clear operations and then updates, we can + // do this since whenever a clear operation for a scope is + // scheduled, we drop all updates matching that scope. So, + // all scope-related update operations we have here now were + // scheduled after the clear operations. + for (auto iter = mClears.Iter(); !iter.Done(); iter.Next()) { + mExecList.AppendElement(std::move(iter.Data())); + } + mClears.Clear(); + + for (auto iter = mUpdates.Iter(); !iter.Done(); iter.Next()) { + mExecList.AppendElement(std::move(iter.Data())); + } + mUpdates.Clear(); + + return !!mExecList.Length(); +} + +nsresult StorageDBThread::PendingOperations::Execute(StorageDBThread* aThread) { + // Called outside the lock + + mozStorageTransaction transaction(aThread->mWorkerConnection, false); + + nsresult rv; + + for (uint32_t i = 0; i < mExecList.Length(); ++i) { + const auto& task = mExecList[i]; + rv = task->Perform(aThread); + if (NS_FAILED(rv)) { + return rv; + } + } + + rv = transaction.Commit(); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +bool StorageDBThread::PendingOperations::Finalize(nsresult aRv) { + // Called under the lock + + // The list is kept on a failure to retry it + if (NS_FAILED(aRv)) { + // XXX Followup: we may try to reopen the database and flush these + // pending tasks, however testing showed that even though I/O is actually + // broken some amount of operations is left in sqlite+system buffers and + // seems like successfully flushed to disk. + // Tested by removing a flash card and disconnecting from network while + // using a network drive on Windows system. + NS_WARNING("Flush operation on localStorage database failed"); + + ++mFlushFailureCount; + + return mFlushFailureCount >= 5; + } + + mFlushFailureCount = 0; + mExecList.Clear(); + return true; +} + +namespace { + +bool FindPendingClearForOrigin( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix, + StorageDBThread::DBOperation* aPendingOperation) { + if (aPendingOperation->Type() == StorageDBThread::DBOperation::opClearAll) { + return true; + } + + if (aPendingOperation->Type() == StorageDBThread::DBOperation::opClear && + aOriginNoSuffix == aPendingOperation->OriginNoSuffix() && + aOriginSuffix == aPendingOperation->OriginSuffix()) { + return true; + } + + if (aPendingOperation->Type() == + StorageDBThread::DBOperation::opClearMatchingOrigin && + StringBeginsWith(aOriginNoSuffix, aPendingOperation->Origin())) { + return true; + } + + if (aPendingOperation->Type() == + StorageDBThread::DBOperation::opClearMatchingOriginAttributes && + OriginPatternMatches(aOriginSuffix, aPendingOperation->OriginPattern())) { + return true; + } + + return false; +} + +} // namespace + +bool StorageDBThread::PendingOperations::IsOriginClearPending( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) const { + // Called under the lock + + for (auto iter = mClears.ConstIter(); !iter.Done(); iter.Next()) { + if (FindPendingClearForOrigin(aOriginSuffix, aOriginNoSuffix, + iter.UserData())) { + return true; + } + } + + for (uint32_t i = 0; i < mExecList.Length(); ++i) { + if (FindPendingClearForOrigin(aOriginSuffix, aOriginNoSuffix, + mExecList[i].get())) { + return true; + } + } + + return false; +} + +namespace { + +bool FindPendingUpdateForOrigin( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix, + StorageDBThread::DBOperation* aPendingOperation) { + if ((aPendingOperation->Type() == StorageDBThread::DBOperation::opAddItem || + aPendingOperation->Type() == + StorageDBThread::DBOperation::opUpdateItem || + aPendingOperation->Type() == + StorageDBThread::DBOperation::opRemoveItem) && + aOriginNoSuffix == aPendingOperation->OriginNoSuffix() && + aOriginSuffix == aPendingOperation->OriginSuffix()) { + return true; + } + + return false; +} + +} // namespace + +bool StorageDBThread::PendingOperations::IsOriginUpdatePending( + const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix) const { + // Called under the lock + + for (auto iter = mUpdates.ConstIter(); !iter.Done(); iter.Next()) { + if (FindPendingUpdateForOrigin(aOriginSuffix, aOriginNoSuffix, + iter.UserData())) { + return true; + } + } + + for (uint32_t i = 0; i < mExecList.Length(); ++i) { + if (FindPendingUpdateForOrigin(aOriginSuffix, aOriginNoSuffix, + mExecList[i].get())) { + return true; + } + } + + return false; +} + +nsresult StorageDBThread::InitHelper::SyncDispatchAndReturnProfilePath( + nsAString& aProfilePath) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + mozilla::MutexAutoLock autolock(mMutex); + while (mWaiting) { + mCondVar.Wait(); + } + + if (NS_WARN_IF(NS_FAILED(mMainThreadResultCode))) { + return mMainThreadResultCode; + } + + aProfilePath = mProfilePath; + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::InitHelper::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = GetProfilePath(mProfilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + mMainThreadResultCode = rv; + } + + mozilla::MutexAutoLock lock(mMutex); + MOZ_ASSERT(mWaiting); + + mWaiting = false; + mCondVar.Notify(); + + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::NoteBackgroundThreadRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + StorageObserver* observer = StorageObserver::Self(); + MOZ_ASSERT(observer); + + observer->NoteBackgroundThread(mPrivateBrowsingId, mOwningThread); + + return NS_OK; +} + +NS_IMETHODIMP +StorageDBThread::ShutdownRunnable::Run() { + if (NS_IsMainThread()) { + mDone = true; + + return NS_OK; + } + + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + StorageDBThread*& storageThread = sStorageThread[mPrivateBrowsingId]; + if (storageThread) { + sStorageThreadDown[mPrivateBrowsingId] = true; + + storageThread->Shutdown(); + + delete storageThread; + storageThread = nullptr; + } + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + return NS_OK; +} + +} // namespace dom +} // namespace mozilla |