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 48744 a1553643c43926ce0dfa5eb4f158d5a6bce7350a
parent 48743 4323514c66df2c7a4bec214a6d6031fd4010b1d5
child 48745 402e75c420302588681a551d971ef7d40b025e43
push idunknown
push userunknown
push dateunknown
reviewerssdwilsh
bugs572223
milestone2.0b3pre
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);