mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 05:11:16 +00:00
99fe7a41c3
Depends on D96931 Differential Revision: https://phabricator.services.mozilla.com/D96932
1610 lines
48 KiB
C++
1610 lines
48 KiB
C++
/* -*- 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 "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: {
|
|
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);
|
|
|
|
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
|