| /* |
| * Copyright (C) 2008, 2009, 2010 Apple Inc. All Rights Reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "StorageAreaSync.h" |
| |
| #include "StorageAreaImpl.h" |
| #include "StorageSyncManager.h" |
| #include "StorageTracker.h" |
| #include <WebCore/SQLiteDatabaseTracker.h> |
| #include <WebCore/SQLiteStatement.h> |
| #include <WebCore/SQLiteTransaction.h> |
| #include <WebCore/SuddenTermination.h> |
| #include <wtf/FileSystem.h> |
| #include <wtf/MainThread.h> |
| |
| using namespace WebCore; |
| |
| namespace WebKit { |
| |
| // If the StorageArea undergoes rapid changes, don't sync each change to disk. |
| // Instead, queue up a batch of items to sync and actually do the sync at the following interval. |
| static const Seconds StorageSyncInterval { 1_s }; |
| |
| // A sane limit on how many items we'll schedule to sync all at once. This makes it |
| // much harder to starve the rest of LocalStorage and the OS's IO subsystem in general. |
| static const int MaxiumItemsToSync = 100; |
| |
| inline StorageAreaSync::StorageAreaSync(RefPtr<StorageSyncManager>&& storageSyncManager, Ref<StorageAreaImpl>&& storageArea, const String& databaseIdentifier) |
| : m_syncTimer(*this, &StorageAreaSync::syncTimerFired) |
| , m_itemsCleared(false) |
| , m_finalSyncScheduled(false) |
| , m_storageArea(WTFMove(storageArea)) |
| , m_syncManager(WTFMove(storageSyncManager)) |
| , m_databaseIdentifier(databaseIdentifier.isolatedCopy()) |
| , m_clearItemsWhileSyncing(false) |
| , m_syncScheduled(false) |
| , m_syncInProgress(false) |
| , m_databaseOpenFailed(false) |
| , m_syncCloseDatabase(false) |
| , m_importComplete(false) |
| { |
| ASSERT(isMainThread()); |
| ASSERT(m_storageArea); |
| ASSERT(m_syncManager); |
| |
| // FIXME: If it can't import, then the default WebKit behavior should be that of private browsing, |
| // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894 |
| RefPtr<StorageAreaSync> protector(this); |
| m_syncManager->dispatch([protector] { |
| protector->performImport(); |
| }); |
| } |
| |
| Ref<StorageAreaSync> StorageAreaSync::create(RefPtr<StorageSyncManager>&& storageSyncManager, Ref<StorageAreaImpl>&& storageArea, const String& databaseIdentifier) |
| { |
| return adoptRef(*new StorageAreaSync(WTFMove(storageSyncManager), WTFMove(storageArea), databaseIdentifier)); |
| } |
| |
| StorageAreaSync::~StorageAreaSync() |
| { |
| ASSERT(isMainThread()); |
| ASSERT(!m_syncTimer.isActive()); |
| ASSERT(m_finalSyncScheduled); |
| } |
| |
| void StorageAreaSync::scheduleFinalSync() |
| { |
| ASSERT(isMainThread()); |
| // FIXME: We do this to avoid races, but it'd be better to make things safe without blocking. |
| blockUntilImportComplete(); |
| m_storageArea = nullptr; // This is done in blockUntilImportComplete() but this is here as a form of documentation that we must be absolutely sure the ref count cycle is broken. |
| |
| if (m_syncTimer.isActive()) |
| m_syncTimer.stop(); |
| else { |
| // The following is balanced by the call to enableSuddenTermination in the |
| // syncTimerFired function. |
| disableSuddenTermination(); |
| } |
| // FIXME: This is synchronous. We should do it on the background process, but |
| // we should do it safely. |
| m_finalSyncScheduled = true; |
| syncTimerFired(); |
| |
| RefPtr<StorageAreaSync> protector(this); |
| m_syncManager->dispatch([protector] { |
| protector->deleteEmptyDatabase(); |
| }); |
| } |
| |
| void StorageAreaSync::scheduleItemForSync(const String& key, const String& value) |
| { |
| ASSERT(isMainThread()); |
| ASSERT(!m_finalSyncScheduled); |
| |
| m_changedItems.set(key, value); |
| if (!m_syncTimer.isActive()) { |
| m_syncTimer.startOneShot(StorageSyncInterval); |
| |
| // The following is balanced by the call to enableSuddenTermination in the |
| // syncTimerFired function. |
| disableSuddenTermination(); |
| } |
| } |
| |
| void StorageAreaSync::scheduleClear() |
| { |
| ASSERT(isMainThread()); |
| ASSERT(!m_finalSyncScheduled); |
| |
| m_changedItems.clear(); |
| m_itemsCleared = true; |
| if (!m_syncTimer.isActive()) { |
| m_syncTimer.startOneShot(StorageSyncInterval); |
| |
| // The following is balanced by the call to enableSuddenTermination in the |
| // syncTimerFired function. |
| disableSuddenTermination(); |
| } |
| } |
| |
| void StorageAreaSync::scheduleCloseDatabase() |
| { |
| ASSERT(isMainThread()); |
| ASSERT(!m_finalSyncScheduled); |
| |
| if (!m_database.isOpen()) |
| return; |
| |
| m_syncCloseDatabase = true; |
| |
| if (!m_syncTimer.isActive()) { |
| m_syncTimer.startOneShot(StorageSyncInterval); |
| |
| // The following is balanced by the call to enableSuddenTermination in the |
| // syncTimerFired function. |
| disableSuddenTermination(); |
| } |
| } |
| |
| void StorageAreaSync::syncTimerFired() |
| { |
| ASSERT(isMainThread()); |
| |
| bool partialSync = false; |
| { |
| Locker locker { m_syncLock }; |
| |
| // Do not schedule another sync if we're still trying to complete the |
| // previous one. But, if we're shutting down, schedule it anyway. |
| if (m_syncInProgress && !m_finalSyncScheduled) { |
| ASSERT(!m_syncTimer.isActive()); |
| m_syncTimer.startOneShot(StorageSyncInterval); |
| return; |
| } |
| |
| if (m_itemsCleared) { |
| m_itemsPendingSync.clear(); |
| m_clearItemsWhileSyncing = true; |
| m_itemsCleared = false; |
| } |
| |
| HashMap<String, String>::iterator changed_it = m_changedItems.begin(); |
| HashMap<String, String>::iterator changed_end = m_changedItems.end(); |
| for (int count = 0; changed_it != changed_end; ++count, ++changed_it) { |
| if (count >= MaxiumItemsToSync && !m_finalSyncScheduled) { |
| partialSync = true; |
| break; |
| } |
| m_itemsPendingSync.set(changed_it->key.isolatedCopy(), changed_it->value.isolatedCopy()); |
| } |
| |
| if (partialSync) { |
| // We can't do the fast path of simply clearing all items, so we'll need to manually |
| // remove them one by one. Done under lock since m_itemsPendingSync is modified by |
| // the background thread. |
| HashMap<String, String>::iterator pending_it = m_itemsPendingSync.begin(); |
| HashMap<String, String>::iterator pending_end = m_itemsPendingSync.end(); |
| for (; pending_it != pending_end; ++pending_it) |
| m_changedItems.remove(pending_it->key); |
| } |
| |
| if (!m_syncScheduled) { |
| m_syncScheduled = true; |
| |
| // The following is balanced by the call to enableSuddenTermination in the |
| // performSync function. |
| disableSuddenTermination(); |
| |
| RefPtr<StorageAreaSync> protector(this); |
| m_syncManager->dispatch([protector] { |
| protector->performSync(); |
| }); |
| } |
| } |
| |
| if (partialSync) { |
| // If we didn't finish syncing, then we need to finish the job later. |
| ASSERT(!m_syncTimer.isActive()); |
| m_syncTimer.startOneShot(StorageSyncInterval); |
| } else { |
| // The following is balanced by the calls to disableSuddenTermination in the |
| // scheduleItemForSync, scheduleClear, and scheduleFinalSync functions. |
| enableSuddenTermination(); |
| |
| m_changedItems.clear(); |
| } |
| } |
| |
| void StorageAreaSync::openDatabase(OpenDatabaseParamType openingStrategy) |
| { |
| ASSERT(!isMainThread()); |
| ASSERT(!m_database.isOpen()); |
| ASSERT(!m_databaseOpenFailed); |
| |
| SQLiteTransactionInProgressAutoCounter transactionCounter; |
| |
| String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier); |
| |
| if (!FileSystem::fileExists(databaseFilename) && openingStrategy == SkipIfNonExistent) |
| return; |
| |
| if (databaseFilename.isEmpty()) { |
| LOG_ERROR("Filename for local storage database is empty - cannot open for persistent storage"); |
| markImported(); |
| m_databaseOpenFailed = true; |
| return; |
| } |
| |
| // A StorageTracker thread may have been scheduled to delete the db we're |
| // reopening, so cancel possible deletion. |
| StorageTracker::tracker().cancelDeletingOrigin(m_databaseIdentifier); |
| |
| if (!m_database.open(databaseFilename)) { |
| LOG_ERROR("Failed to open database file %s for local storage", databaseFilename.utf8().data()); |
| markImported(); |
| m_databaseOpenFailed = true; |
| return; |
| } |
| |
| migrateItemTableIfNeeded(); |
| |
| if (!m_database.executeCommand("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)"_s)) { |
| LOG_ERROR("Failed to create table ItemTable for local storage"); |
| markImported(); |
| m_databaseOpenFailed = true; |
| return; |
| } |
| |
| StorageTracker::tracker().setOriginDetails(m_databaseIdentifier, databaseFilename); |
| } |
| |
| void StorageAreaSync::migrateItemTableIfNeeded() |
| { |
| if (!m_database.tableExists("ItemTable")) |
| return; |
| |
| { |
| auto query = m_database.prepareStatement("SELECT value FROM ItemTable LIMIT 1"_s); |
| // this query isn't ever executed. |
| if (query && query->isColumnDeclaredAsBlob(0)) |
| return; |
| } |
| |
| // alter table for backward compliance, change the value type from TEXT to BLOB. |
| static const ASCIILiteral commands[] = { |
| "DROP TABLE IF EXISTS ItemTable2"_s, |
| "CREATE TABLE ItemTable2 (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)"_s, |
| "INSERT INTO ItemTable2 SELECT * from ItemTable"_s, |
| "DROP TABLE ItemTable"_s, |
| "ALTER TABLE ItemTable2 RENAME TO ItemTable"_s, |
| ASCIILiteral::null(), |
| }; |
| |
| SQLiteTransaction transaction(m_database, false); |
| transaction.begin(); |
| for (size_t i = 0; commands[i]; ++i) { |
| if (!m_database.executeCommand(commands[i])) { |
| LOG_ERROR("Failed to migrate table ItemTable for local storage when executing: %s", commands[i].characters()); |
| transaction.rollback(); |
| |
| // finally it will try to keep a backup of ItemTable for the future restoration. |
| // NOTICE: this will essentially DELETE the current database, but that's better |
| // than continually hitting this case and never being able to use the local storage. |
| // if this is ever hit, it's definitely a bug. |
| ASSERT_NOT_REACHED(); |
| if (!m_database.executeCommand("ALTER TABLE ItemTable RENAME TO Backup_ItemTable"_s)) |
| LOG_ERROR("Failed to save ItemTable after migration job failed."); |
| |
| return; |
| } |
| } |
| transaction.commit(); |
| } |
| |
| void StorageAreaSync::performImport() |
| { |
| ASSERT(!isMainThread()); |
| ASSERT(!m_database.isOpen()); |
| |
| openDatabase(SkipIfNonExistent); |
| if (!m_database.isOpen()) { |
| markImported(); |
| return; |
| } |
| |
| auto query = m_database.prepareStatement("SELECT key, value FROM ItemTable"_s); |
| if (!query) { |
| LOG_ERROR("Unable to select items from ItemTable for local storage"); |
| markImported(); |
| return; |
| } |
| |
| HashMap<String, String> itemMap; |
| |
| int result = query->step(); |
| while (result == SQLITE_ROW) { |
| itemMap.set(query->columnText(0), query->columnBlobAsString(1)); |
| result = query->step(); |
| } |
| |
| if (result != SQLITE_DONE) { |
| LOG_ERROR("Error reading items from ItemTable for local storage"); |
| markImported(); |
| return; |
| } |
| |
| m_storageArea->importItems(WTFMove(itemMap)); |
| |
| markImported(); |
| } |
| |
| void StorageAreaSync::markImported() |
| { |
| Locker locker { m_importLock }; |
| m_importComplete = true; |
| m_importCondition.notifyOne(); |
| } |
| |
| // FIXME: In the future, we should allow use of StorageAreas while it's importing (when safe to do so). |
| // Blocking everything until the import is complete is by far the simplest and safest thing to do, but |
| // there is certainly room for safe optimization: Key/length will never be able to make use of such an |
| // optimization (since the order of iteration can change as items are being added). Get can return any |
| // item currently in the map. Get/remove can work whether or not it's in the map, but we'll need a list |
| // of items the import should not overwrite. Clear can also work, but it'll need to kill the import |
| // job first. |
| void StorageAreaSync::blockUntilImportComplete() |
| { |
| ASSERT(isMainThread()); |
| |
| // Fast path. We set m_storageArea to 0 only after m_importComplete being true. |
| if (!m_storageArea) |
| return; |
| |
| Locker locker { m_importLock }; |
| while (!m_importComplete) |
| m_importCondition.wait(m_importLock); |
| m_storageArea = nullptr; |
| } |
| |
| void StorageAreaSync::sync(bool clearItems, const HashMap<String, String>& items) |
| { |
| ASSERT(!isMainThread()); |
| |
| if (items.isEmpty() && !clearItems && !m_syncCloseDatabase) |
| return; |
| if (m_databaseOpenFailed) |
| return; |
| |
| if (!m_database.isOpen() && m_syncCloseDatabase) { |
| m_syncCloseDatabase = false; |
| return; |
| } |
| |
| if (!m_database.isOpen()) |
| openDatabase(CreateIfNonExistent); |
| if (!m_database.isOpen()) |
| return; |
| |
| // Closing this db because it is about to be deleted by StorageTracker. |
| // The delete will be cancelled if StorageAreaSync needs to reopen the db |
| // to write new items created after the request to delete the db. |
| if (m_syncCloseDatabase) { |
| m_syncCloseDatabase = false; |
| m_database.close(); |
| return; |
| } |
| |
| SQLiteTransactionInProgressAutoCounter transactionCounter; |
| |
| // If the clear flag is set, then we clear all items out before we write any new ones in. |
| if (clearItems) { |
| auto clear = m_database.prepareStatement("DELETE FROM ItemTable"_s); |
| if (!clear) { |
| LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database"); |
| return; |
| } |
| |
| int result = clear->step(); |
| if (result != SQLITE_DONE) { |
| LOG_ERROR("Failed to clear all items in the local storage database - %i", result); |
| return; |
| } |
| } |
| |
| auto insert = m_database.prepareStatement("INSERT INTO ItemTable VALUES (?, ?)"_s); |
| if (!insert) { |
| LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database"); |
| return; |
| } |
| |
| auto remove = m_database.prepareStatement("DELETE FROM ItemTable WHERE key=?"_s); |
| if (!remove) { |
| LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database"); |
| return; |
| } |
| |
| HashMap<String, String>::const_iterator end = items.end(); |
| |
| SQLiteTransaction transaction(m_database); |
| transaction.begin(); |
| for (HashMap<String, String>::const_iterator it = items.begin(); it != end; ++it) { |
| // Based on the null-ness of the second argument, decide whether this is an insert or a delete. |
| auto& query = it->value.isNull() ? remove : insert; |
| |
| query->bindText(1, it->key); |
| |
| // If the second argument is non-null, we're doing an insert, so bind it as the value. |
| if (!it->value.isNull()) |
| query->bindBlob(2, it->value); |
| |
| int result = query->step(); |
| if (result != SQLITE_DONE) { |
| LOG_ERROR("Failed to update item in the local storage database - %i", result); |
| break; |
| } |
| |
| query->reset(); |
| } |
| transaction.commit(); |
| } |
| |
| void StorageAreaSync::performSync() |
| { |
| ASSERT(!isMainThread()); |
| |
| bool clearItems; |
| HashMap<String, String> items; |
| { |
| Locker locker { m_syncLock }; |
| |
| ASSERT(m_syncScheduled); |
| |
| clearItems = m_clearItemsWhileSyncing; |
| m_itemsPendingSync.swap(items); |
| |
| m_clearItemsWhileSyncing = false; |
| m_syncScheduled = false; |
| m_syncInProgress = true; |
| } |
| |
| sync(clearItems, items); |
| |
| { |
| Locker locker { m_syncLock }; |
| m_syncInProgress = false; |
| } |
| |
| // The following is balanced by the call to disableSuddenTermination in the |
| // syncTimerFired function. |
| enableSuddenTermination(); |
| } |
| |
| void StorageAreaSync::deleteEmptyDatabase() |
| { |
| ASSERT(!isMainThread()); |
| if (!m_database.isOpen()) |
| return; |
| |
| auto count = [&] { |
| auto query = m_database.prepareStatement("SELECT COUNT(*) FROM ItemTable"_s); |
| if (!query) { |
| LOG_ERROR("Unable to count number of rows in ItemTable for local storage"); |
| return -1; |
| } |
| |
| int result = query->step(); |
| if (result != SQLITE_ROW) { |
| LOG_ERROR("No results when counting number of rows in ItemTable for local storage"); |
| return -1; |
| } |
| |
| return query->columnInt(0); |
| }(); |
| if (count) |
| return; |
| |
| m_database.close(); |
| if (StorageTracker::tracker().isActive()) { |
| callOnMainThread([databaseIdentifier = m_databaseIdentifier.isolatedCopy()] { |
| StorageTracker::tracker().deleteOriginWithIdentifier(databaseIdentifier); |
| }); |
| } else { |
| String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier); |
| if (!FileSystem::deleteFile(databaseFilename)) |
| LOG_ERROR("Failed to delete database file %s\n", databaseFilename.utf8().data()); |
| } |
| } |
| |
| void StorageAreaSync::scheduleSync() |
| { |
| syncTimerFired(); |
| } |
| |
| } // namespace WebCore |