Home | History | Annotate | Download | only in storage
      1 /*
      2  * Copyright (C) 2008, 2009, 2010 Apple Inc. All Rights Reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions
      6  * are met:
      7  * 1. Redistributions of source code must retain the above copyright
      8  *    notice, this list of conditions and the following disclaimer.
      9  * 2. Redistributions in binary form must reproduce the above copyright
     10  *    notice, this list of conditions and the following disclaimer in the
     11  *    documentation and/or other materials provided with the distribution.
     12  *
     13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
     14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
     17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
     21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     24  */
     25 
     26 #include "config.h"
     27 #include "StorageAreaSync.h"
     28 
     29 #if ENABLE(DOM_STORAGE)
     30 
     31 #include "EventNames.h"
     32 #include "FileSystem.h"
     33 #include "HTMLElement.h"
     34 #include "SQLiteFileSystem.h"
     35 #include "SQLiteStatement.h"
     36 #include "SecurityOrigin.h"
     37 #include "StorageAreaImpl.h"
     38 #include "StorageSyncManager.h"
     39 #include "StorageTracker.h"
     40 #include "SuddenTermination.h"
     41 #include <wtf/text/CString.h>
     42 
     43 namespace WebCore {
     44 
     45 // If the StorageArea undergoes rapid changes, don't sync each change to disk.
     46 // Instead, queue up a batch of items to sync and actually do the sync at the following interval.
     47 static const double StorageSyncInterval = 1.0;
     48 
     49 // A sane limit on how many items we'll schedule to sync all at once.  This makes it
     50 // much harder to starve the rest of LocalStorage and the OS's IO subsystem in general.
     51 static const int MaxiumItemsToSync = 100;
     52 
     53 inline StorageAreaSync::StorageAreaSync(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier)
     54     : m_syncTimer(this, &StorageAreaSync::syncTimerFired)
     55     , m_itemsCleared(false)
     56     , m_finalSyncScheduled(false)
     57     , m_storageArea(storageArea)
     58     , m_syncManager(storageSyncManager)
     59     , m_databaseIdentifier(databaseIdentifier.crossThreadString())
     60     , m_clearItemsWhileSyncing(false)
     61     , m_syncScheduled(false)
     62     , m_syncInProgress(false)
     63     , m_databaseOpenFailed(false)
     64     , m_syncCloseDatabase(false)
     65     , m_importComplete(false)
     66 {
     67     ASSERT(isMainThread());
     68     ASSERT(m_storageArea);
     69     ASSERT(m_syncManager);
     70 }
     71 
     72 PassRefPtr<StorageAreaSync> StorageAreaSync::create(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier)
     73 {
     74     RefPtr<StorageAreaSync> area = adoptRef(new StorageAreaSync(storageSyncManager, storageArea, databaseIdentifier));
     75 
     76     // FIXME: If it can't import, then the default WebKit behavior should be that of private browsing,
     77     // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894
     78     if (!area->m_syncManager->scheduleImport(area.get()))
     79         area->m_importComplete = true;
     80 
     81     return area.release();
     82 }
     83 
     84 StorageAreaSync::~StorageAreaSync()
     85 {
     86     ASSERT(isMainThread());
     87     ASSERT(!m_syncTimer.isActive());
     88     ASSERT(m_finalSyncScheduled);
     89 }
     90 
     91 void StorageAreaSync::scheduleFinalSync()
     92 {
     93     ASSERT(isMainThread());
     94     // FIXME: We do this to avoid races, but it'd be better to make things safe without blocking.
     95     blockUntilImportComplete();
     96     m_storageArea = 0;  // 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.
     97 
     98     if (m_syncTimer.isActive())
     99         m_syncTimer.stop();
    100     else {
    101         // The following is balanced by the call to enableSuddenTermination in the
    102         // syncTimerFired function.
    103         disableSuddenTermination();
    104     }
    105     // FIXME: This is synchronous.  We should do it on the background process, but
    106     // we should do it safely.
    107     m_finalSyncScheduled = true;
    108     syncTimerFired(&m_syncTimer);
    109     m_syncManager->scheduleDeleteEmptyDatabase(this);
    110 }
    111 
    112 void StorageAreaSync::scheduleItemForSync(const String& key, const String& value)
    113 {
    114     ASSERT(isMainThread());
    115     ASSERT(!m_finalSyncScheduled);
    116 
    117     m_changedItems.set(key, value);
    118     if (!m_syncTimer.isActive()) {
    119         m_syncTimer.startOneShot(StorageSyncInterval);
    120 
    121         // The following is balanced by the call to enableSuddenTermination in the
    122         // syncTimerFired function.
    123         disableSuddenTermination();
    124     }
    125 }
    126 
    127 void StorageAreaSync::scheduleClear()
    128 {
    129     ASSERT(isMainThread());
    130     ASSERT(!m_finalSyncScheduled);
    131 
    132     m_changedItems.clear();
    133     m_itemsCleared = true;
    134     if (!m_syncTimer.isActive()) {
    135         m_syncTimer.startOneShot(StorageSyncInterval);
    136 
    137         // The following is balanced by the call to enableSuddenTermination in the
    138         // syncTimerFired function.
    139         disableSuddenTermination();
    140     }
    141 }
    142 
    143 void StorageAreaSync::scheduleCloseDatabase()
    144 {
    145     ASSERT(isMainThread());
    146     ASSERT(!m_finalSyncScheduled);
    147 
    148     if (!m_database.isOpen())
    149         return;
    150 
    151     m_syncCloseDatabase = true;
    152 
    153     if (!m_syncTimer.isActive()) {
    154         m_syncTimer.startOneShot(StorageSyncInterval);
    155 
    156         // The following is balanced by the call to enableSuddenTermination in the
    157         // syncTimerFired function.
    158         disableSuddenTermination();
    159     }
    160 }
    161 
    162 void StorageAreaSync::syncTimerFired(Timer<StorageAreaSync>*)
    163 {
    164     ASSERT(isMainThread());
    165 
    166     bool partialSync = false;
    167     {
    168         MutexLocker locker(m_syncLock);
    169 
    170         // Do not schedule another sync if we're still trying to complete the
    171         // previous one.  But, if we're shutting down, schedule it anyway.
    172         if (m_syncInProgress && !m_finalSyncScheduled) {
    173             ASSERT(!m_syncTimer.isActive());
    174             m_syncTimer.startOneShot(StorageSyncInterval);
    175             return;
    176         }
    177 
    178         if (m_itemsCleared) {
    179             m_itemsPendingSync.clear();
    180             m_clearItemsWhileSyncing = true;
    181             m_itemsCleared = false;
    182         }
    183 
    184         HashMap<String, String>::iterator changed_it = m_changedItems.begin();
    185         HashMap<String, String>::iterator changed_end = m_changedItems.end();
    186         for (int count = 0; changed_it != changed_end; ++count, ++changed_it) {
    187             if (count >= MaxiumItemsToSync && !m_finalSyncScheduled) {
    188                 partialSync = true;
    189                 break;
    190             }
    191             m_itemsPendingSync.set(changed_it->first.crossThreadString(), changed_it->second.crossThreadString());
    192         }
    193 
    194         if (partialSync) {
    195             // We can't do the fast path of simply clearing all items, so we'll need to manually
    196             // remove them one by one.  Done under lock since m_itemsPendingSync is modified by
    197             // the background thread.
    198             HashMap<String, String>::iterator pending_it = m_itemsPendingSync.begin();
    199             HashMap<String, String>::iterator pending_end = m_itemsPendingSync.end();
    200             for (; pending_it != pending_end; ++pending_it)
    201                 m_changedItems.remove(pending_it->first);
    202         }
    203 
    204         if (!m_syncScheduled) {
    205             m_syncScheduled = true;
    206 
    207             // The following is balanced by the call to enableSuddenTermination in the
    208             // performSync function.
    209             disableSuddenTermination();
    210 
    211             m_syncManager->scheduleSync(this);
    212         }
    213     }
    214 
    215     if (partialSync) {
    216         // If we didn't finish syncing, then we need to finish the job later.
    217         ASSERT(!m_syncTimer.isActive());
    218         m_syncTimer.startOneShot(StorageSyncInterval);
    219     } else {
    220         // The following is balanced by the calls to disableSuddenTermination in the
    221         // scheduleItemForSync, scheduleClear, and scheduleFinalSync functions.
    222         enableSuddenTermination();
    223 
    224         m_changedItems.clear();
    225     }
    226 }
    227 
    228 void StorageAreaSync::openDatabase(OpenDatabaseParamType openingStrategy)
    229 {
    230     ASSERT(!isMainThread());
    231     ASSERT(!m_database.isOpen());
    232     ASSERT(!m_databaseOpenFailed);
    233 
    234     String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier);
    235 
    236     if (!fileExists(databaseFilename) && openingStrategy == SkipIfNonExistent)
    237         return;
    238 
    239     if (databaseFilename.isEmpty()) {
    240         LOG_ERROR("Filename for local storage database is empty - cannot open for persistent storage");
    241         markImported();
    242         m_databaseOpenFailed = true;
    243         return;
    244     }
    245 
    246     // A StorageTracker thread may have been scheduled to delete the db we're
    247     // reopening, so cancel possible deletion.
    248     StorageTracker::tracker().cancelDeletingOrigin(m_databaseIdentifier);
    249 
    250     if (!m_database.open(databaseFilename)) {
    251         LOG_ERROR("Failed to open database file %s for local storage", databaseFilename.utf8().data());
    252         markImported();
    253         m_databaseOpenFailed = true;
    254         return;
    255     }
    256 
    257     if (!m_database.executeCommand("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value TEXT NOT NULL ON CONFLICT FAIL)")) {
    258         LOG_ERROR("Failed to create table ItemTable for local storage");
    259         markImported();
    260         m_databaseOpenFailed = true;
    261         return;
    262     }
    263 
    264     StorageTracker::tracker().setOriginDetails(m_databaseIdentifier, databaseFilename);
    265 }
    266 
    267 void StorageAreaSync::performImport()
    268 {
    269     ASSERT(!isMainThread());
    270     ASSERT(!m_database.isOpen());
    271 
    272     openDatabase(SkipIfNonExistent);
    273     if (!m_database.isOpen()) {
    274         markImported();
    275         return;
    276     }
    277 
    278     SQLiteStatement query(m_database, "SELECT key, value FROM ItemTable");
    279     if (query.prepare() != SQLResultOk) {
    280         LOG_ERROR("Unable to select items from ItemTable for local storage");
    281         markImported();
    282         return;
    283     }
    284 
    285     HashMap<String, String> itemMap;
    286 
    287     int result = query.step();
    288     while (result == SQLResultRow) {
    289         itemMap.set(query.getColumnText(0), query.getColumnText(1));
    290         result = query.step();
    291     }
    292 
    293     if (result != SQLResultDone) {
    294         LOG_ERROR("Error reading items from ItemTable for local storage");
    295         markImported();
    296         return;
    297     }
    298 
    299     HashMap<String, String>::iterator it = itemMap.begin();
    300     HashMap<String, String>::iterator end = itemMap.end();
    301 
    302     for (; it != end; ++it)
    303         m_storageArea->importItem(it->first, it->second);
    304 
    305     markImported();
    306 }
    307 
    308 void StorageAreaSync::markImported()
    309 {
    310     MutexLocker locker(m_importLock);
    311     m_importComplete = true;
    312     m_importCondition.signal();
    313 }
    314 
    315 // FIXME: In the future, we should allow use of StorageAreas while it's importing (when safe to do so).
    316 // Blocking everything until the import is complete is by far the simplest and safest thing to do, but
    317 // there is certainly room for safe optimization: Key/length will never be able to make use of such an
    318 // optimization (since the order of iteration can change as items are being added). Get can return any
    319 // item currently in the map. Get/remove can work whether or not it's in the map, but we'll need a list
    320 // of items the import should not overwrite. Clear can also work, but it'll need to kill the import
    321 // job first.
    322 void StorageAreaSync::blockUntilImportComplete()
    323 {
    324     ASSERT(isMainThread());
    325 
    326     // Fast path.  We set m_storageArea to 0 only after m_importComplete being true.
    327     if (!m_storageArea)
    328         return;
    329 
    330     MutexLocker locker(m_importLock);
    331     while (!m_importComplete)
    332         m_importCondition.wait(m_importLock);
    333     m_storageArea = 0;
    334 }
    335 
    336 void StorageAreaSync::sync(bool clearItems, const HashMap<String, String>& items)
    337 {
    338     ASSERT(!isMainThread());
    339 
    340     if (items.isEmpty() && !clearItems)
    341         return;
    342     if (m_databaseOpenFailed)
    343         return;
    344     if (!m_database.isOpen())
    345         openDatabase(CreateIfNonExistent);
    346     if (!m_database.isOpen())
    347         return;
    348 
    349     // Closing this db because it is about to be deleted by StorageTracker.
    350     // The delete will be cancelled if StorageAreaSync needs to reopen the db
    351     // to write new items created after the request to delete the db.
    352     if (m_syncCloseDatabase) {
    353         m_syncCloseDatabase = false;
    354         m_database.close();
    355         return;
    356     }
    357 
    358     // If the clear flag is set, then we clear all items out before we write any new ones in.
    359     if (clearItems) {
    360         SQLiteStatement clear(m_database, "DELETE FROM ItemTable");
    361         if (clear.prepare() != SQLResultOk) {
    362             LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database");
    363             return;
    364         }
    365 
    366         int result = clear.step();
    367         if (result != SQLResultDone) {
    368             LOG_ERROR("Failed to clear all items in the local storage database - %i", result);
    369             return;
    370         }
    371     }
    372 
    373     SQLiteStatement insert(m_database, "INSERT INTO ItemTable VALUES (?, ?)");
    374     if (insert.prepare() != SQLResultOk) {
    375         LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database");
    376         return;
    377     }
    378 
    379     SQLiteStatement remove(m_database, "DELETE FROM ItemTable WHERE key=?");
    380     if (remove.prepare() != SQLResultOk) {
    381         LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database");
    382         return;
    383     }
    384 
    385     HashMap<String, String>::const_iterator end = items.end();
    386 
    387     for (HashMap<String, String>::const_iterator it = items.begin(); it != end; ++it) {
    388         // Based on the null-ness of the second argument, decide whether this is an insert or a delete.
    389         SQLiteStatement& query = it->second.isNull() ? remove : insert;
    390 
    391         query.bindText(1, it->first);
    392 
    393         // If the second argument is non-null, we're doing an insert, so bind it as the value.
    394         if (!it->second.isNull())
    395             query.bindText(2, it->second);
    396 
    397         int result = query.step();
    398         if (result != SQLResultDone) {
    399             LOG_ERROR("Failed to update item in the local storage database - %i", result);
    400             break;
    401         }
    402 
    403         query.reset();
    404     }
    405 }
    406 
    407 void StorageAreaSync::performSync()
    408 {
    409     ASSERT(!isMainThread());
    410 
    411     bool clearItems;
    412     HashMap<String, String> items;
    413     {
    414         MutexLocker locker(m_syncLock);
    415 
    416         ASSERT(m_syncScheduled);
    417 
    418         clearItems = m_clearItemsWhileSyncing;
    419         m_itemsPendingSync.swap(items);
    420 
    421         m_clearItemsWhileSyncing = false;
    422         m_syncScheduled = false;
    423         m_syncInProgress = true;
    424     }
    425 
    426     sync(clearItems, items);
    427 
    428     {
    429         MutexLocker locker(m_syncLock);
    430         m_syncInProgress = false;
    431     }
    432 
    433     // The following is balanced by the call to disableSuddenTermination in the
    434     // syncTimerFired function.
    435     enableSuddenTermination();
    436 }
    437 
    438 void StorageAreaSync::deleteEmptyDatabase()
    439 {
    440     ASSERT(!isMainThread());
    441     if (!m_database.isOpen())
    442         return;
    443 
    444     SQLiteStatement query(m_database, "SELECT COUNT(*) FROM ItemTable");
    445     if (query.prepare() != SQLResultOk) {
    446         LOG_ERROR("Unable to count number of rows in ItemTable for local storage");
    447         return;
    448     }
    449 
    450     int result = query.step();
    451     if (result != SQLResultRow) {
    452         LOG_ERROR("No results when counting number of rows in ItemTable for local storage");
    453         return;
    454     }
    455 
    456     int count = query.getColumnInt(0);
    457     if (!count) {
    458         query.finalize();
    459         m_database.close();
    460         if (StorageTracker::tracker().isActive())
    461             StorageTracker::tracker().deleteOrigin(m_databaseIdentifier);
    462         else {
    463             String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier);
    464             if (!SQLiteFileSystem::deleteDatabaseFile(databaseFilename))
    465                 LOG_ERROR("Failed to delete database file %s\n", databaseFilename.utf8().data());
    466         }
    467     }
    468 }
    469 
    470 void StorageAreaSync::scheduleSync()
    471 {
    472     syncTimerFired(&m_syncTimer);
    473 }
    474 
    475 } // namespace WebCore
    476 
    477 #endif // ENABLE(DOM_STORAGE)
    478