Bug 572223 - too much cookies.sqlite io. Part 3: sync read per domain. r=sdwilsh
authorDan Witte <dwitte@mozilla.com>
Thu, 29 Jul 2010 12:17:42 -0700
changeset 48372 66e85e6fcf8719b46e8e4c5a9ade01d9b399f30e
parent 48371 deacdfe550e51ee824865e7eee51bf23bc07b55a
child 48373 06be5719ad0b1570d07f6a97a199c7e21066e467
push idunknown
push userunknown
push dateunknown
reviewerssdwilsh
bugs572223
milestone2.0b3pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 572223 - too much cookies.sqlite io. Part 3: sync read per domain. r=sdwilsh
netwerk/cookie/nsCookieService.cpp
netwerk/cookie/nsCookieService.h
storage/src/mozStorageStatement.cpp
--- a/netwerk/cookie/nsCookieService.cpp
+++ b/netwerk/cookie/nsCookieService.cpp
@@ -82,16 +82,18 @@
 
 using namespace mozilla::net;
 
 /******************************************************************************
  * nsCookieService impl:
  * useful types & constants
  ******************************************************************************/
 
+static nsCookieService *gCookieService;
+
 // XXX_hack. See bug 178993.
 // This is a hack to hide HttpOnly cookies from older browsers
 static const char kHttpOnlyPrefix[] = "#HttpOnly_";
 
 static const char kCookieFileName[] = "cookies.sqlite";
 #define COOKIES_SCHEMA_VERSION 3
 
 static const PRInt64 kCookieStaleThreshold = 60 * PR_USEC_PER_SEC; // 1 minute in microseconds
@@ -367,16 +369,17 @@ class DBListenerErrorHandler : public mo
 protected:
   virtual const char *GetOpType() = 0;
 
 public:
   NS_DECL_ISUPPORTS
 
   NS_IMETHOD HandleError(mozIStorageError* aError)
   {
+    // XXX Ignore corruption handling for now. See bug 547031.
 #ifdef PR_LOGGING
     PRInt32 result = -1;
     aError->GetResult(&result);
     nsCAutoString message;
     aError->GetMessage(message);
     COOKIE_LOGSTRING(PR_LOG_WARNING,
                      ("Error %d occurred while performing a %s operation.  Message: `%s`\n",
                       result, GetOpType(), message.get()));
@@ -456,36 +459,37 @@ public:
  ******************************************************************************/
 class ReadCookieDBListener :  public DBListenerErrorHandler
 {
 protected:
   virtual const char *GetOpType() { return "READ"; }
   bool mCanceled;
 
 public:
-  ReadCookieDBListener() : mCanceled(PR_FALSE) { }
+  ReadCookieDBListener() : mCanceled(false) { }
 
   void Cancel() { mCanceled = true; }
 
   NS_IMETHOD HandleResult(mozIStorageResultSet *aResult)
   {
     nsresult rv;
     nsCOMPtr<mozIStorageRow> row;
     nsTArray<CookieDomainTuple> &cookieArray =
-      nsCookieService::gCookieService->mDefaultDBState.hostArray;
+      gCookieService->mDefaultDBState.hostArray;
 
     while (1) {
       rv = aResult->GetNextRow(getter_AddRefs(row));
       NS_ASSERT_SUCCESS(rv);
 
       if (!row)
         break;
 
       CookieDomainTuple *tuple = cookieArray.AppendElement();
-      nsCookieService::gCookieService->ReadRow(row, *tuple);
+      row->GetUTF8String(9, tuple->baseDomain);
+      tuple->cookie = gCookieService->GetCookieFromRow(row);
     }
 
     return NS_OK;
   }
   NS_IMETHOD HandleCompletion(PRUint16 aReason)
   {
     // Process the completion of the read operation. If we have been canceled,
     // we cannot assume that the cookieservice still has an open connection
@@ -497,17 +501,17 @@ public:
     if (mCanceled) {
       // We may receive a REASON_FINISHED after being canceled;
       // tweak the reason accordingly.
       aReason = mozIStorageStatementCallback::REASON_CANCELED;
     }
 
     switch (aReason) {
     case mozIStorageStatementCallback::REASON_FINISHED:
-      nsCookieService::gCookieService->ReadComplete();
+      gCookieService->AsyncReadComplete();
       break;
     case mozIStorageStatementCallback::REASON_CANCELED:
       // Nothing more to do here. The partially read data has already been
       // thrown away.
       COOKIE_LOGSTRING(PR_LOG_DEBUG, ("Read canceled"));
       break;
     case mozIStorageStatementCallback::REASON_ERROR:
       // Nothing more to do here. DBListenerErrorHandler::HandleError()
@@ -545,18 +549,16 @@ public:
 
 NS_IMPL_ISUPPORTS1(CloseCookieDBListener, mozIStorageCompletionCallback)
 
 /******************************************************************************
  * nsCookieService impl:
  * singleton instance ctor/dtor methods
  ******************************************************************************/
 
-static nsCookieService *gCookieService;
-
 nsICookieService*
 nsCookieService::GetXPCOMSingleton()
 {
 #ifdef MOZ_IPC
   if (IsNeckoChild())
     return CookieServiceChild::GetSingleton();
 #endif
 
@@ -719,18 +721,19 @@ nsCookieService::TryInitDB(PRBool aDelet
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   nsCOMPtr<mozIStorageService> storage = do_GetService("@mozilla.org/storage/service;1");
   if (!storage)
     return NS_ERROR_UNEXPECTED;
 
   // open a connection to the cookie database, and only cache our connection
-  // and statements upon success.
-  rv = storage->OpenUnsharedDatabase(cookieFile, getter_AddRefs(mDBState->dbConn));
+  // and statements upon success. The connection is opened shared such that
+  // the main and background threads can operate on the db concurrently.
+  rv = storage->OpenDatabase(cookieFile, getter_AddRefs(mDBState->dbConn));
   NS_ENSURE_SUCCESS(rv, rv);
 
   PRBool tableExists = PR_FALSE;
   mDBState->dbConn->TableExists(NS_LITERAL_CSTRING("moz_cookies"), &tableExists);
   if (!tableExists) {
       rv = CreateTable();
       NS_ENSURE_SUCCESS(rv, rv);
 
@@ -963,27 +966,18 @@ nsCookieService::CloseDB()
   // finalize our statements and close the db connection for the default state.
   // since we own these objects, nulling the pointers is sufficient here.
   mDefaultDBState.stmtInsert = nsnull;
   mDefaultDBState.stmtDelete = nsnull;
   mDefaultDBState.stmtUpdate = nsnull;
   if (mDefaultDBState.dbConn) {
     // Cancel any pending read. No further results will be received by our
     // read listener.
-    if (mDefaultDBState.pendingRead) {
-      NS_ASSERTION(mDefaultDBState.readListener, "no read listener");
-
-      mDefaultDBState.readListener->Cancel();
-      nsresult rv = mDefaultDBState.pendingRead->Cancel();
-      NS_ASSERT_SUCCESS(rv);
-
-      mDefaultDBState.pendingRead = nsnull;
-      mDefaultDBState.readListener = nsnull;
-      mDefaultDBState.hostArray.Clear();
-    }
+    if (mDefaultDBState.pendingRead)
+      CancelAsyncRead(PR_TRUE);
 
     mDefaultDBState.dbConn->AsyncClose(mCloseListener);
     mDefaultDBState.dbConn = nsnull;
   }
 }
 
 nsCookieService::~nsCookieService()
 {
@@ -1306,16 +1300,18 @@ COMArrayCallback(nsCookieEntry *aEntry,
 }
 
 NS_IMETHODIMP
 nsCookieService::GetEnumerator(nsISimpleEnumerator **aEnumerator)
 {
   nsCOMArray<nsICookie> cookieList(mDBState->cookieCount);
   nsGetEnumeratorData data(&cookieList, PR_Now() / PR_USEC_PER_SEC);
 
+  EnsureReadComplete();
+
   mDBState->hostTable.EnumerateEntries(COMArrayCallback, &data);
 
   return NS_NewArrayEnumerator(aEnumerator, cookieList);
 }
 
 NS_IMETHODIMP
 nsCookieService::Add(const nsACString &aHost,
                      const nsACString &aPath,
@@ -1426,62 +1422,238 @@ nsCookieService::Read()
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsRefPtr<ReadCookieDBListener> readListener = new ReadCookieDBListener;
   rv = stmt->ExecuteAsync(readListener,
                           getter_AddRefs(mDefaultDBState.pendingRead));
   NS_ASSERT_SUCCESS(rv);
 
   mDefaultDBState.readListener = readListener;
+  if (!mDefaultDBState.readSet.IsInitialized())
+    mDefaultDBState.readSet.Init();
+
   return NS_OK;
 }
 
-void
-nsCookieService::ReadRow(mozIStorageRow *aRow, CookieDomainTuple &aTuple)
+// Extract data from a single result row and create an nsCookie.
+// This is templated since 'T' is different for sync vs async results.
+template<class T> nsCookie*
+nsCookieService::GetCookieFromRow(T &aRow)
 {
+  // Skip reading 'baseDomain' -- up to the caller.
   PRInt64 creationID = aRow->AsInt64(0);
 
   nsCString name, value, host, path;
-  aRow->GetUTF8String(1, name);
-  aRow->GetUTF8String(2, value);
-  aRow->GetUTF8String(3, host);
-  aRow->GetUTF8String(4, path);
+  nsresult rv = aRow->GetUTF8String(1, name);
+  NS_ASSERT_SUCCESS(rv);
+  rv = aRow->GetUTF8String(2, value);
+  NS_ASSERT_SUCCESS(rv);
+  rv = aRow->GetUTF8String(3, host);
+  NS_ASSERT_SUCCESS(rv);
+  rv = aRow->GetUTF8String(4, path);
+  NS_ASSERT_SUCCESS(rv);
 
   PRInt64 expiry = aRow->AsInt64(5);
   PRInt64 lastAccessed = aRow->AsInt64(6);
   PRBool isSecure = 0 != aRow->AsInt32(7);
   PRBool isHttpOnly = 0 != aRow->AsInt32(8);
 
-  aRow->GetUTF8String(9, aTuple.baseDomain);
-
-  // Create a new nsCookie and assign the data.
-  aTuple.cookie =
-    nsCookie::Create(name, value, host, path,
-                     expiry,
-                     lastAccessed,
-                     creationID,
-                     PR_FALSE,
-                     isSecure,
-                     isHttpOnly);
+  // create a new nsCookie and assign the data.
+  return nsCookie::Create(name, value, host, path,
+                          expiry,
+                          lastAccessed,
+                          creationID,
+                          PR_FALSE,
+                          isSecure,
+                          isHttpOnly);
+}
+
+void
+nsCookieService::AsyncReadComplete()
+{
+  NS_ASSERTION(mDBState == &mDefaultDBState, "not in default db state");
+  NS_ASSERTION(mDBState->pendingRead, "no pending read");
+  NS_ASSERTION(mDBState->readListener, "no read listener");
+
+  // Merge the data read on the background thread with the data synchronously
+  // read on the main thread. Note that transactions on the cookie table may
+  // have occurred on the main thread since, making the background data stale.
+  for (PRUint32 i = 0; i < mDefaultDBState.hostArray.Length(); ++i) {
+    const CookieDomainTuple &tuple = mDefaultDBState.hostArray[i];
+
+    // Tiebreak: if the given base domain has already been read in, ignore
+    // the background data. Note that readSet may contain domains that were
+    // queried but found not to be in the db -- that's harmless.
+    if (mDefaultDBState.readSet.GetEntry(tuple.baseDomain))
+      continue;
+
+    AddCookieToList(tuple.baseDomain, tuple.cookie, NULL, PR_FALSE);
+  }
+
+  mDefaultDBState.stmtReadDomain = nsnull;
+  mDefaultDBState.pendingRead = nsnull;
+  mDefaultDBState.readListener = nsnull;
+  mDefaultDBState.hostArray.Clear();
+  mDefaultDBState.readSet.Clear();
+
+  mObserverService->NotifyObservers(nsnull, "cookie-db-read", nsnull);
+
+  COOKIE_LOGSTRING(PR_LOG_DEBUG, ("Read(): %ld cookies read",
+                                  mDefaultDBState.cookieCount));
+}
+
+void
+nsCookieService::CancelAsyncRead(PRBool aPurgeReadSet)
+{
+  NS_ASSERTION(mDBState == &mDefaultDBState, "not in default db state");
+  NS_ASSERTION(mDBState->pendingRead, "no pending read");
+  NS_ASSERTION(mDBState->readListener, "no read listener");
+
+  // Cancel the pending read, kill the read listener, and empty the array
+  // of data already read in on the background thread.
+  mDefaultDBState.readListener->Cancel();
+  nsresult rv = mDefaultDBState.pendingRead->Cancel();
+  NS_ASSERT_SUCCESS(rv);
+
+  mDefaultDBState.stmtReadDomain = nsnull;
+  mDefaultDBState.pendingRead = nsnull;
+  mDefaultDBState.readListener = nsnull;
+  mDefaultDBState.hostArray.Clear();
+
+  // Only clear the 'readSet' table if we no longer need to know what set of
+  // data is already accounted for.
+  if (aPurgeReadSet)
+    mDefaultDBState.readSet.Clear();
 }
 
 void
-nsCookieService::ReadComplete()
+nsCookieService::EnsureReadDomain(const nsCString &aBaseDomain)
 {
-  for (PRUint32 i = 0; i < mDefaultDBState.hostArray.Length(); ++i) {
-    const CookieDomainTuple &tuple = mDefaultDBState.hostArray[i];
-    AddCookieToList(tuple.baseDomain, tuple.cookie, NULL, PR_FALSE);
+  NS_ASSERTION(!mDBState->dbConn || mDBState == &mDefaultDBState,
+    "not in default db state");
+
+  // Fast path 1: nothing to read, or we've already finished reading.
+  if (NS_LIKELY(!mDBState->dbConn || !mDefaultDBState.pendingRead))
+    return;
+
+  // Fast path 2: already read in this particular domain.
+  if (NS_LIKELY(mDefaultDBState.readSet.GetEntry(aBaseDomain)))
+    return;
+
+  // Read in the data synchronously.
+  nsresult rv;
+  if (!mDefaultDBState.stmtReadDomain) {
+    // Cache the statement, since it's likely to be used again.
+    rv = mDefaultDBState.dbConn->CreateStatement(NS_LITERAL_CSTRING(
+      "SELECT "
+        "id, "
+        "name, "
+        "value, "
+        "host, "
+        "path, "
+        "expiry, "
+        "lastAccessed, "
+        "isSecure, "
+        "isHttpOnly "
+      "FROM moz_cookies "
+      "WHERE baseDomain = ?1"),
+      getter_AddRefs(mDefaultDBState.stmtReadDomain));
+
+    // XXX Ignore corruption for now. See bug 547031.
+    if (NS_FAILED(rv)) return;
+  }
+
+  mozStorageStatementScoper scoper(mDefaultDBState.stmtReadDomain);
+
+  rv = mDefaultDBState.stmtReadDomain->BindUTF8StringByIndex(0, aBaseDomain);
+  NS_ASSERT_SUCCESS(rv);
+
+  PRBool hasResult;
+  PRUint32 readCount = 0;
+  nsCString name, value, host, path;
+  while (1) {
+    rv = mDefaultDBState.stmtReadDomain->ExecuteStep(&hasResult);
+    // XXX Ignore corruption for now. See bug 547031.
+    if (NS_FAILED(rv)) return;
+
+    if (!hasResult)
+      break;
+
+    nsCookie* newCookie = GetCookieFromRow(mDefaultDBState.stmtReadDomain);
+    AddCookieToList(aBaseDomain, newCookie, NULL, PR_FALSE);
+    ++readCount;
   }
 
-  mDefaultDBState.hostArray.Clear();
-  mDefaultDBState.pendingRead = nsnull;
-  mDefaultDBState.readListener = nsnull;
-
-  COOKIE_LOGSTRING(PR_LOG_DEBUG, ("Read(): %ld cookies read",
-                                  mDefaultDBState.cookieCount));
+  // Add it to the hashset of read entries, so we don't read it again.
+  mDefaultDBState.readSet.PutEntry(aBaseDomain);
+
+  COOKIE_LOGSTRING(PR_LOG_DEBUG,
+    ("EnsureReadDomain(): %ld cookies read for base domain %s",
+     readCount, aBaseDomain.get()));
+}
+
+void
+nsCookieService::EnsureReadComplete()
+{
+  NS_ASSERTION(!mDBState->dbConn || mDBState == &mDefaultDBState,
+    "not in default db state");
+
+  // Fast path 1: nothing to read, or we've already finished reading.
+  if (NS_LIKELY(!mDBState->dbConn || !mDefaultDBState.pendingRead))
+    return;
+
+  // Cancel the pending read, so we don't get any more results.
+  CancelAsyncRead(PR_FALSE);
+
+  // Read in the data synchronously.
+  nsCOMPtr<mozIStorageStatement> stmt;
+  nsresult rv = mDefaultDBState.dbConn->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT "
+      "id, "
+      "name, "
+      "value, "
+      "host, "
+      "path, "
+      "expiry, "
+      "lastAccessed, "
+      "isSecure, "
+      "isHttpOnly, "
+      "baseDomain "
+    "FROM moz_cookies"), getter_AddRefs(stmt));
+
+  // XXX Ignore corruption for now. See bug 547031.
+  if (NS_FAILED(rv)) return;
+
+  nsCString baseDomain, name, value, host, path;
+  PRBool hasResult;
+  PRUint32 readCount = 0;
+  while (1) {
+    rv = stmt->ExecuteStep(&hasResult);
+    // XXX Ignore corruption for now. See bug 547031.
+    if (NS_FAILED(rv)) return;
+
+    if (!hasResult)
+      break;
+
+    // Make sure we haven't already read the data.
+    stmt->GetUTF8String(9, baseDomain);
+    if (mDefaultDBState.readSet.GetEntry(baseDomain))
+      continue;
+
+    nsCookie* newCookie = GetCookieFromRow(stmt);
+    AddCookieToList(baseDomain, newCookie, NULL, PR_FALSE);
+    ++readCount;
+  }
+
+  mDefaultDBState.readSet.Clear();
+
+  mObserverService->NotifyObservers(nsnull, "cookie-db-read", nsnull);
+
+  COOKIE_LOGSTRING(PR_LOG_DEBUG,
+    ("EnsureReadComplete(): %ld cookies read", readCount));
 }
 
 NS_IMETHODIMP
 nsCookieService::ImportCookies(nsIFile *aCookieFile)
 {
   nsresult rv;
   nsCOMPtr<nsIInputStream> fileInputStream;
   rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), aCookieFile);
@@ -1525,16 +1697,19 @@ nsCookieService::ImportCookies(nsIFile *
    * in a comment, so they don't expose HttpOnly cookies to JS.
    *
    * The format for HttpOnly cookies is
    *
    * #HttpOnly_host \t isDomain \t path \t secure \t expires \t name \t cookie
    *
    */
 
+  // First, ensure we've read in everything from the database, if we have one.
+  EnsureReadComplete();
+
   // We will likely be adding a bunch of cookies to the DB, so we use async
   // batching with storage to make this super fast.
   nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
   if (originalCookieCount == 0 && mDBState->dbConn) {
     mDBState->stmtInsert->NewBindingParamsArray(getter_AddRefs(paramsArray));
   }
 
   while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) {
@@ -1721,16 +1896,18 @@ nsCookieService::GetCookieInternal(nsIUR
   }
 
   nsCookie *cookie;
   nsAutoTArray<nsCookie*, 8> foundCookieList;
   PRInt64 currentTimeInUsec = PR_Now();
   PRInt64 currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
   PRBool stale = PR_FALSE;
 
+  EnsureReadDomain(baseDomain);
+
   // perform the hash lookup
   nsCookieEntry *entry = mDBState->hostTable.GetEntry(baseDomain);
   if (!entry)
     return;
 
   // iterate the cookies!
   const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
   for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
@@ -2780,16 +2957,18 @@ nsCookieService::PurgeCookies(PRInt64 aC
   // Create a params array to batch the removals. This is OK here because
   // all the removals are in order, and there are no interleaved additions.
   mozIStorageStatement *stmt = mDBState->stmtDelete;
   nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
   if (mDBState->dbConn) {
     stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
   }
 
+  EnsureReadComplete();
+
   nsPurgeData data(aCurrentTimeInUsec / PR_USEC_PER_SEC,
     aCurrentTimeInUsec - mCookiePurgeAge, purgeList, removedList, paramsArray);
   mDBState->hostTable.EnumerateEntries(purgeCookiesCallback, &data);
 
 #ifdef PR_LOGGING
   PRUint32 postExpiryCookieCount = mDBState->cookieCount;
 #endif
 
@@ -2872,16 +3051,18 @@ nsCookieService::CookieExists(nsICookie2
 }
 
 // count the number of cookies in a base domain, and simultaneously find the
 // oldest cookie in that domain.
 PRUint32
 nsCookieService::CountCookiesFromHostInternal(const nsCString   &aBaseDomain,
                                               nsEnumerationData &aData)
 {
+  EnsureReadDomain(aBaseDomain);
+
   nsCookieEntry *entry = mDBState->hostTable.GetEntry(aBaseDomain);
   if (!entry)
     return 0;
 
   PRUint32 countFromHost = 0;
   const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
   for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
     nsCookie *cookie = cookies[i];
@@ -2935,16 +3116,18 @@ nsCookieService::GetCookiesFromHost(cons
 
   nsCAutoString baseDomain;
   rv = GetBaseDomainFromHost(host, baseDomain);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMArray<nsICookie> cookieList(mMaxCookiesPerHost);
   PRInt64 currentTime = PR_Now() / PR_USEC_PER_SEC;
 
+  EnsureReadDomain(baseDomain);
+
   nsCookieEntry *entry = mDBState->hostTable.GetEntry(baseDomain);
   if (!entry)
     return NS_NewEmptyEnumerator(aEnumerator);
 
   const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
   for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
     nsCookie *cookie = cookies[i];
 
@@ -2960,16 +3143,18 @@ nsCookieService::GetCookiesFromHost(cons
 PRBool
 nsCookieService::FindCookie(const nsCString      &aBaseDomain,
                             const nsAFlatCString &aHost,
                             const nsAFlatCString &aName,
                             const nsAFlatCString &aPath,
                             nsListIter           &aIter,
                             PRInt64               aCurrentTime)
 {
+  EnsureReadDomain(aBaseDomain);
+
   nsCookieEntry *entry = mDBState->hostTable.GetEntry(aBaseDomain);
   if (!entry)
     return PR_FALSE;
 
   const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
   for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
     nsCookie *cookie = cookies[i];
 
--- a/netwerk/cookie/nsCookieService.h
+++ b/netwerk/cookie/nsCookieService.h
@@ -153,19 +153,30 @@ struct DBState
   nsTHashtable<nsCookieEntry>     hostTable;
   PRUint32                        cookieCount;
   PRInt64                         cookieOldestTime;
   nsCOMPtr<mozIStorageConnection> dbConn;
   nsCOMPtr<mozIStorageStatement>  stmtInsert;
   nsCOMPtr<mozIStorageStatement>  stmtDelete;
   nsCOMPtr<mozIStorageStatement>  stmtUpdate;
 
+  // Various parts representing asynchronous read state. These are useful
+  // while the background read is taking place.
+  nsCOMPtr<mozIStorageStatement>        stmtReadDomain;
   nsCOMPtr<mozIStoragePendingStatement> pendingRead;
-  ReadCookieDBListener*                 readListener; // weak ref
+  // The asynchronous read listener. This is a weak ref (storage has ownership)
+  // since it may need to outlive the DBState's database connection.
+  ReadCookieDBListener*                 readListener;
+  // An array of (baseDomain, cookie) tuples representing data read in
+  // asynchronously. This is merged into hostTable once read is complete.
   nsTArray<CookieDomainTuple>           hostArray;
+  // A hashset of baseDomains read in synchronously, while the async read is
+  // in flight. This is used to keep track of which data in hostArray is stale
+  // when the time comes to merge.
+  nsTHashtable<nsCStringHashKey>        readSet;
 };
 
 // these constants represent a decision about a cookie based on user prefs.
 enum CookieStatus
 {
   STATUS_ACCEPTED,
   STATUS_ACCEPT_SESSION,
   STATUS_REJECTED,
@@ -201,18 +212,21 @@ class nsCookieService : public nsICookie
 
   protected:
     void                          PrefChanged(nsIPrefBranch *aPrefBranch);
     nsresult                      InitDB();
     nsresult                      TryInitDB(PRBool aDeleteExistingDB);
     nsresult                      CreateTable();
     void                          CloseDB();
     nsresult                      Read();
-    void                          ReadRow(mozIStorageRow *aRow, CookieDomainTuple &aCookeTuple);
-    void                          ReadComplete();
+    template<class T> nsCookie*   GetCookieFromRow(T &aRow);
+    void                          AsyncReadComplete();
+    void                          CancelAsyncRead(PRBool aPurgeReadSet);
+    void                          EnsureReadDomain(const nsCString &aBaseDomain);
+    void                          EnsureReadComplete();
     nsresult                      NormalizeHost(nsCString &aHost);
     nsresult                      GetBaseDomain(nsIURI *aHostURI, nsCString &aBaseDomain, PRBool &aRequireHostMatch);
     nsresult                      GetBaseDomainFromHost(const nsACString &aHost, nsCString &aBaseDomain);
     void                          GetCookieInternal(nsIURI *aHostURI, nsIURI *aOriginatingURI, PRBool aHttpBound, nsCString &aCookie);
     void                          SetCookieStringInternal(nsIURI *aHostURI, nsIURI *aOriginatingURI, const nsCString &aCookieHeader, const nsCString &aServerTime, PRBool aFromHttp);
     PRBool                        SetCookieInternal(nsIURI *aHostURI, const nsCString& aBaseDomain, PRBool aRequireHostMatch, CookieStatus aStatus, nsDependentCString &aCookieHeader, PRInt64 aServerTime, PRBool aFromHttp);
     void                          AddInternal(const nsCString& aBaseDomain, nsCookie *aCookie, PRInt64 aCurrentTimeInUsec, nsIURI *aHostURI, const char *aCookieHeader, PRBool aFromHttp);
     void                          RemoveCookieFromList(const nsListIter &aIter, mozIStorageBindingParamsArray *aParamsArray = NULL);
--- a/storage/src/mozStorageStatement.cpp
+++ b/storage/src/mozStorageStatement.cpp
@@ -778,17 +778,17 @@ NS_IMETHODIMP
 Statement::GetUTF8String(PRUint32 aIndex,
                          nsACString &_value)
 {
   // Get type of Index will check aIndex for us, so we don't have to.
   PRInt32 type;
   nsresult rv = GetTypeOfIndex(aIndex, &type);
   NS_ENSURE_SUCCESS(rv, rv);
   if (type == mozIStorageStatement::VALUE_TYPE_NULL) {
-    // NULL columns should have IsVod set to distinguis them from an empty
+    // NULL columns should have IsVoid set to distinguish them from the empty
     // string.
     _value.Truncate(0);
     _value.SetIsVoid(PR_TRUE);
   }
   else {
     const char *value =
       reinterpret_cast<const char *>(::sqlite3_column_text(mDBStatement,
                                                            aIndex));
@@ -801,17 +801,17 @@ NS_IMETHODIMP
 Statement::GetString(PRUint32 aIndex,
                      nsAString &_value)
 {
   // Get type of Index will check aIndex for us, so we don't have to.
   PRInt32 type;
   nsresult rv = GetTypeOfIndex(aIndex, &type);
   NS_ENSURE_SUCCESS(rv, rv);
   if (type == mozIStorageStatement::VALUE_TYPE_NULL) {
-    // NULL columns should have IsVod set to distinguis them from an empty
+    // NULL columns should have IsVoid set to distinguish them from the empty
     // string.
     _value.Truncate(0);
     _value.SetIsVoid(PR_TRUE);
   } else {
     const PRUnichar *value =
       static_cast<const PRUnichar *>(::sqlite3_column_text16(mDBStatement,
                                                              aIndex));
     _value.Assign(value, ::sqlite3_column_bytes16(mDBStatement, aIndex) / 2);