Bug 432492: rate limit long-running safebrowsing updates, patch by Dave Camp <dcamp@mozilla.com>, r=tony, a=beltzner
authorgavin@gavinsharp.com
Wed, 07 May 2008 13:33:45 -0700
changeset 15023 84a37eec08f39866e5de14f9cf844dc3edf1d5a6
parent 15022 157a19a1a9f73115c87bde92359325dfce78ba0c
child 15024 312d85c9d4b0716f6b6b3adcd3832672692f9edb
push id25
push userjorendorff@mozilla.com
push dateFri, 09 May 2008 18:10:52 +0000
treeherdermozilla-central@b7dd3823dbdd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstony, beltzner
bugs432492
milestone1.9pre
Bug 432492: rate limit long-running safebrowsing updates, patch by Dave Camp <dcamp@mozilla.com>, r=tony, a=beltzner
toolkit/components/url-classifier/public/nsIUrlClassifierDBService.idl
toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp
toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.cpp
toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.h
--- a/toolkit/components/url-classifier/public/nsIUrlClassifierDBService.idl
+++ b/toolkit/components/url-classifier/public/nsIUrlClassifierDBService.idl
@@ -52,17 +52,17 @@ interface nsIUrlClassifierCallback : nsI
   void handleEvent(in ACString value);
 };
 
 /**
  * The nsIUrlClassifierUpdateObserver interface is implemented by
  * clients streaming updates to the url-classifier (usually
  * nsUrlClassifierStreamUpdater.
  */
-[scriptable, uuid(1c9bd1c2-f6fe-43a8-a2b9-48359eb4a9b1)]
+[scriptable, uuid(bbb33c65-e783-476c-8db0-6ddb91826c07)]
 interface nsIUrlClassifierUpdateObserver : nsISupports {
   /**
    * The update requested a new URL whose contents should be downloaded
    * and sent to the classifier as a new stream.
    *
    * @param url The url that was requested.
    * @param table The table name that this URL's contents will be associated
    *              with.  This should be passed back to beginStream().
@@ -78,18 +78,20 @@ interface nsIUrlClassifierUpdateObserver
    * MAC requests.
    */
   void rekeyRequested();
 
   /**
    * A stream update has completed.
    *
    * @param status The state of the update process.
+   * @param delay The amount of time the updater should wait to fetch the
+   *              next URL in ms.
    */
-  void streamFinished(in nsresult status);
+  void streamFinished(in nsresult status, in unsigned long delay);
 
   /* The update has encountered an error and should be cancelled */
   void updateError(in nsresult error);
 
   /**
    * The update has completed successfully.
    *
    * @param requestedTimeout The number of seconds that the caller should
--- a/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp
+++ b/toolkit/components/url-classifier/src/nsUrlClassifierDBService.cpp
@@ -149,16 +149,27 @@ static const PRLogModuleInfo *gUrlClassi
 #define GETHASH_TABLES_PREF     "urlclassifier.gethashtables"
 
 #define CONFIRM_AGE_PREF        "urlclassifier.confirm-age"
 #define CONFIRM_AGE_DEFAULT_SEC (45 * 60)
 
 #define UPDATE_CACHE_SIZE_PREF    "urlclassifier.updatecachemax"
 #define UPDATE_CACHE_SIZE_DEFAULT -1
 
+// Amount of time to spend updating before committing and delaying, in
+// seconds.  This is checked after each update stream, so the actual
+// time spent can be higher than this, depending on update stream size.
+#define UPDATE_WORKING_TIME         "urlclassifier.workingtime"
+#define UPDATE_WORKING_TIME_DEFAULT 5
+
+// The amount of time to delay after hitting UPDATE_WORKING_TIME, in
+// seconds.
+#define UPDATE_DELAY_TIME           "urlclassifier.updatetime"
+#define UPDATE_DELAY_TIME_DEFAULT   60
+
 #define PAGE_SIZE 4096
 
 class nsUrlClassifierDBServiceWorker;
 
 // Singleton instance.
 static nsUrlClassifierDBService* sUrlClassifierDBService;
 
 // Thread that we do the updates on.
@@ -167,16 +178,19 @@ static nsIThread* gDbBackgroundThread = 
 // Once we've committed to shutting down, don't do work in the background
 // thread.
 static PRBool gShuttingDownThread = PR_FALSE;
 
 static PRInt32 gFreshnessGuarantee = CONFIRM_AGE_DEFAULT_SEC;
 
 static PRInt32 gUpdateCacheSize = UPDATE_CACHE_SIZE_DEFAULT;
 
+static PRInt32 gWorkingTimeThreshold = UPDATE_WORKING_TIME_DEFAULT;
+static PRInt32 gDelayTime = UPDATE_DELAY_TIME_DEFAULT;
+
 static void
 SplitTables(const nsACString& str, nsTArray<nsCString>& tables)
 {
   tables.Clear();
 
   nsACString::const_iterator begin, iter, end;
   str.BeginReading(begin);
   str.EndReading(end);
@@ -1097,16 +1111,22 @@ private:
   // Expire a subtract chunk
   nsresult ExpireSub(PRUint32 tableId, PRUint32 chunkNum);
 
   // Handle line-oriented control information from a stream update
   nsresult ProcessResponseLines(PRBool* done);
   // Handle chunk data from a stream update
   nsresult ProcessChunk(PRBool* done);
 
+  // Sets up a transaction and begins counting update time.
+  nsresult SetupUpdate();
+
+  // Applies the current transaction and resets the update/working times.
+  nsresult ApplyUpdate();
+
   // Reset the in-progress update stream
   void ResetStream();
 
   // Reset the in-progress update
   void ResetUpdate();
 
   // take a lookup string (www.hostname.com/path/to/resource.html) and
   // expand it into the set of fragments that should be searched for in an
@@ -1212,16 +1232,20 @@ private:
   nsTArray<PRUint32> mCachedSubChunks;
 
   // The client key with which the data from the server will be MAC'ed.
   nsCString mUpdateClientKey;
 
   // The MAC stated by the server.
   nsCString mServerMAC;
 
+  // Start time of the current update interval.  This will be reset
+  // every time we apply the update.
+  PRIntervalTime mUpdateStartTime;
+
   nsCOMPtr<nsICryptoHMAC> mHMAC;
   // The number of noise entries to add to the set of lookup results.
   PRInt32 mGethashNoise;
 
   // Pending lookups are stored in a queue for processing.  The queue
   // is protected by mPendingLookupLock.
   PRLock* mPendingLookupLock;
 
@@ -1251,16 +1275,17 @@ nsUrlClassifierDBServiceWorker::nsUrlCla
   , mUpdateTableId(0)
   , mUpdateStatus(NS_OK)
   , mInStream(PR_FALSE)
   , mPrimaryStream(PR_FALSE)
   , mHaveCachedLists(PR_FALSE)
   , mCachedListsTable(PR_UINT32_MAX)
   , mHaveCachedAddChunks(PR_FALSE)
   , mHaveCachedSubChunks(PR_FALSE)
+  , mUpdateStartTime(0)
   , mGethashNoise(0)
   , mPendingLookupLock(nsnull)
 {
 }
 
 nsUrlClassifierDBServiceWorker::~nsUrlClassifierDBServiceWorker()
 {
   NS_ASSERTION(!mConnection,
@@ -2752,29 +2777,17 @@ nsUrlClassifierDBServiceWorker::BeginUpd
   }
 
   if (transaction) {
     NS_WARNING("Transaction already in progress in nsUrlClassifierDBServiceWorker::BeginUpdate.  Cancelling update.");
     mUpdateStatus = NS_ERROR_FAILURE;
     return rv;
   }
 
-  if (gUpdateCacheSize > 0) {
-    PRUint32 cachePages = gUpdateCacheSize / PAGE_SIZE;
-    nsCAutoString cacheSizePragma("PRAGMA cache_size=");
-    cacheSizePragma.AppendInt(cachePages);
-    rv = mConnection->ExecuteSimpleSQL(cacheSizePragma);
-    if (NS_FAILED(rv)) {
-      mUpdateStatus = rv;
-      return rv;
-    }
-    mGrewCache = PR_TRUE;
-  }
-
-  rv = mConnection->BeginTransaction();
+  rv = SetupUpdate();
   if (NS_FAILED(rv)) {
     mUpdateStatus = rv;
     return rv;
   }
 
   mUpdateObserver = observer;
 
   if (!clientKey.IsEmpty()) {
@@ -2796,20 +2809,26 @@ nsUrlClassifierDBServiceWorker::BeginStr
                                             const nsACString &serverMAC)
 {
   if (gShuttingDownThread)
     return NS_ERROR_NOT_INITIALIZED;
 
   NS_ENSURE_STATE(mUpdateObserver);
   NS_ENSURE_STATE(!mInStream);
 
+  // We may have committed the update in FinishStream, if so set it up
+  // again here.
+  nsresult rv = SetupUpdate();
+  if (NS_FAILED(rv)) {
+    mUpdateStatus = rv;
+    return rv;
+  }
+
   mInStream = PR_TRUE;
 
-  nsresult rv;
-
   // If we're expecting a MAC, create the nsICryptoHMAC component now.
   if (!mUpdateClientKey.IsEmpty()) {
     nsCOMPtr<nsIKeyObjectFactory> keyObjectFactory(do_GetService(
         "@mozilla.org/security/keyobjectfactory;1", &rv));
     if (NS_FAILED(rv)) {
       NS_WARNING("Failed to get nsIKeyObjectFactory service");
       mUpdateStatus = rv;
       return mUpdateStatus;
@@ -2936,54 +2955,113 @@ NS_IMETHODIMP
 nsUrlClassifierDBServiceWorker::FinishStream()
 {
   if (gShuttingDownThread)
     return NS_ERROR_NOT_INITIALIZED;
 
   NS_ENSURE_STATE(mInStream);
   NS_ENSURE_STATE(mUpdateObserver);
 
+  PRInt32 nextStreamDelay = 0;
+
   if (NS_SUCCEEDED(mUpdateStatus) && mHMAC) {
     nsCAutoString clientMAC;
     mHMAC->Finish(PR_TRUE, clientMAC);
 
     if (clientMAC != mServerMAC) {
       NS_WARNING("Invalid update MAC!");
       LOG(("Invalid update MAC: expected %s, got %s",
            mServerMAC.get(), clientMAC.get()));
       mUpdateStatus = NS_ERROR_FAILURE;
     }
+    PRIntervalTime updateTime = PR_IntervalNow() - mUpdateStartTime;
+    if (PR_IntervalToSeconds(updateTime) >=
+        static_cast<PRUint32>(gWorkingTimeThreshold)) {
+      // We've spent long enough working that we should commit what we
+      // have and hold off for a bit.
+      ApplyUpdate();
+
+      nextStreamDelay = gDelayTime * 1000;
+    }
   }
 
-  mUpdateObserver->StreamFinished(mUpdateStatus);
+  mUpdateObserver->StreamFinished(mUpdateStatus,
+                                  static_cast<PRUint32>(nextStreamDelay));
 
   ResetStream();
 
   return NS_OK;
 }
 
+nsresult
+nsUrlClassifierDBServiceWorker::SetupUpdate()
+{
+  LOG(("nsUrlClassifierDBServiceWorker::SetupUpdate"));
+  PRBool inProgress;
+  nsresult rv = mConnection->GetTransactionInProgress(&inProgress);
+  if (inProgress) {
+    return NS_OK;
+  }
+
+  mUpdateStartTime = PR_IntervalNow();
+
+  rv = mConnection->BeginTransaction();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (gUpdateCacheSize > 0) {
+    PRUint32 cachePages = gUpdateCacheSize / PAGE_SIZE;
+    nsCAutoString cacheSizePragma("PRAGMA cache_size=");
+    cacheSizePragma.AppendInt(cachePages);
+    rv = mConnection->ExecuteSimpleSQL(cacheSizePragma);
+    NS_ENSURE_SUCCESS(rv, rv);
+    mGrewCache = PR_TRUE;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::ApplyUpdate()
+{
+  LOG(("nsUrlClassifierDBServiceWorker::ApplyUpdate"));
+
+  if (NS_FAILED(mUpdateStatus)) {
+    mConnection->RollbackTransaction();
+  } else {
+    mUpdateStatus = FlushChunkLists();
+    if (NS_SUCCEEDED(mUpdateStatus)) {
+      mUpdateStatus = mConnection->CommitTransaction();
+    }
+  }
+
+  if (mGrewCache) {
+    // During the update we increased the page cache to bigger than we
+    // want to keep around.  At the moment, the only reliable way to make
+    // sure that the page cache is freed is to reopen the connection.
+    mGrewCache = PR_FALSE;
+    CloseDb();
+    OpenDb();
+  }
+
+  mUpdateStartTime = 0;
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 nsUrlClassifierDBServiceWorker::FinishUpdate()
 {
+  LOG(("nsUrlClassifierDBServiceWorker::FinishUpdate()"));
   if (gShuttingDownThread)
     return NS_ERROR_NOT_INITIALIZED;
 
   NS_ENSURE_STATE(!mInStream);
   NS_ENSURE_STATE(mUpdateObserver);
 
-  if (NS_SUCCEEDED(mUpdateStatus)) {
-    mUpdateStatus = FlushChunkLists();
-  }
-
-  nsCAutoString arg;
-  if (NS_SUCCEEDED(mUpdateStatus)) {
-    mUpdateStatus = mConnection->CommitTransaction();
-  } else {
-    mConnection->RollbackTransaction();
-  }
+  ApplyUpdate();
 
   if (NS_SUCCEEDED(mUpdateStatus)) {
     mUpdateObserver->UpdateSuccess(mUpdateWait);
   } else {
     mUpdateObserver->UpdateError(mUpdateStatus);
   }
 
   if (!mResetRequested) {
@@ -3006,23 +3084,16 @@ nsUrlClassifierDBServiceWorker::FinishUp
 
   ResetUpdate();
 
   // It's important that we only reset the database if the update was
   // successful, otherwise unauthenticated updates could cause a
   // database reset.
   if (NS_SUCCEEDED(mUpdateStatus) && resetRequested) {
     ResetDatabase();
-  } else if (mGrewCache) {
-    // During the update we increased the page cache to bigger than we
-    // want to keep around.  At the moment, the only reliable way to make
-    // sure that the page cache is freed is to reopen the connection.
-    mGrewCache = PR_FALSE;
-    CloseDb();
-    OpenDb();
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsUrlClassifierDBServiceWorker::ResetDatabase()
 {
@@ -3066,18 +3137,16 @@ nsUrlClassifierDBServiceWorker::CancelUp
 // Allows the main thread to delete the connection which may be in
 // a background thread.
 // XXX This could be turned into a single shutdown event so the logic
 // is simpler in nsUrlClassifierDBService::Shutdown.
 NS_IMETHODIMP
 nsUrlClassifierDBServiceWorker::CloseDb()
 {
   if (mConnection) {
-    CancelUpdate();
-
     mMainStore.Close();
     mPendingSubStore.Close();
 
     mGetChunkListsStatement = nsnull;
     mSetChunkListsStatement = nsnull;
 
     mGetTablesStatement = nsnull;
     mGetTableIdStatement = nsnull;
@@ -3663,16 +3732,24 @@ nsUrlClassifierDBService::Init()
     PRInt32 tmpint;
     rv = prefs->GetIntPref(CONFIRM_AGE_PREF, &tmpint);
     PR_AtomicSet(&gFreshnessGuarantee, NS_SUCCEEDED(rv) ? tmpint : CONFIRM_AGE_DEFAULT_SEC);
 
     prefs->AddObserver(CONFIRM_AGE_PREF, this, PR_FALSE);
 
     rv = prefs->GetIntPref(UPDATE_CACHE_SIZE_PREF, &tmpint);
     PR_AtomicSet(&gUpdateCacheSize, NS_SUCCEEDED(rv) ? tmpint : UPDATE_CACHE_SIZE_DEFAULT);
+
+    rv = prefs->GetIntPref(UPDATE_WORKING_TIME, &tmpint);
+    PR_AtomicSet(&gWorkingTimeThreshold,
+                 NS_SUCCEEDED(rv) ? tmpint : UPDATE_WORKING_TIME_DEFAULT);
+
+    rv = prefs->GetIntPref(UPDATE_DELAY_TIME, &tmpint);
+    PR_AtomicSet(&gDelayTime,
+                 NS_SUCCEEDED(rv) ? tmpint : UPDATE_DELAY_TIME_DEFAULT);
   }
 
   // Start the background thread.
   rv = NS_NewThread(&gDbBackgroundThread);
   if (NS_FAILED(rv))
     return rv;
 
   mWorker = new nsUrlClassifierDBServiceWorker();
@@ -3955,16 +4032,26 @@ nsUrlClassifierDBService::Observe(nsISup
     } else if (NS_LITERAL_STRING(CONFIRM_AGE_PREF).Equals(aData)) {
       PRInt32 tmpint;
       rv = prefs->GetIntPref(CONFIRM_AGE_PREF, &tmpint);
       PR_AtomicSet(&gFreshnessGuarantee, NS_SUCCEEDED(rv) ? tmpint : CONFIRM_AGE_DEFAULT_SEC);
     } else if (NS_LITERAL_STRING(UPDATE_CACHE_SIZE_PREF).Equals(aData)) {
       PRInt32 tmpint;
       rv = prefs->GetIntPref(UPDATE_CACHE_SIZE_PREF, &tmpint);
       PR_AtomicSet(&gUpdateCacheSize, NS_SUCCEEDED(rv) ? tmpint : UPDATE_CACHE_SIZE_DEFAULT);
+    } else if (NS_LITERAL_STRING(UPDATE_WORKING_TIME).Equals(aData)) {
+      PRInt32 tmpint;
+      rv = prefs->GetIntPref(UPDATE_WORKING_TIME, &tmpint);
+      PR_AtomicSet(&gWorkingTimeThreshold,
+                   NS_SUCCEEDED(rv) ? tmpint : UPDATE_WORKING_TIME_DEFAULT);
+    } else if (NS_LITERAL_STRING(UPDATE_DELAY_TIME).Equals(aData)) {
+      PRInt32 tmpint;
+      rv = prefs->GetIntPref(UPDATE_DELAY_TIME, &tmpint);
+      PR_AtomicSet(&gDelayTime,
+                 NS_SUCCEEDED(rv) ? tmpint : UPDATE_DELAY_TIME_DEFAULT);
     }
   } else if (!strcmp(aTopic, "profile-before-change") ||
              !strcmp(aTopic, "xpcom-shutdown-threads")) {
     Shutdown();
   } else {
     return NS_ERROR_UNEXPECTED;
   }
 
@@ -3988,16 +4075,18 @@ nsUrlClassifierDBService::Shutdown()
     prefs->RemoveObserver(CHECK_PHISHING_PREF, this);
     prefs->RemoveObserver(GETHASH_TABLES_PREF, this);
     prefs->RemoveObserver(CONFIRM_AGE_PREF, this);
   }
 
   nsresult rv;
   // First close the db connection.
   if (mWorker) {
+    rv = mWorkerProxy->CancelUpdate();
+    NS_ASSERTION(NS_SUCCEEDED(rv), "failed to post cancel udpate event");
     rv = mWorkerProxy->CloseDb();
     NS_ASSERTION(NS_SUCCEEDED(rv), "failed to post close db event");
   }
 
   mWorkerProxy = nsnull;
 
   LOG(("joining background thread"));
 
--- a/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.cpp
+++ b/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.cpp
@@ -70,25 +70,26 @@ nsUrlClassifierStreamUpdater::nsUrlClass
 {
 #if defined(PR_LOGGING)
   if (!gUrlClassifierStreamUpdaterLog)
     gUrlClassifierStreamUpdaterLog = PR_NewLogModule("UrlClassifierStreamUpdater");
 #endif
 
 }
 
-NS_IMPL_THREADSAFE_ISUPPORTS8(nsUrlClassifierStreamUpdater,
+NS_IMPL_THREADSAFE_ISUPPORTS9(nsUrlClassifierStreamUpdater,
                               nsIUrlClassifierStreamUpdater,
                               nsIUrlClassifierUpdateObserver,
                               nsIRequestObserver,
                               nsIStreamListener,
                               nsIObserver,
                               nsIBadCertListener2,
                               nsISSLErrorListener,
-                              nsIInterfaceRequestor)
+                              nsIInterfaceRequestor,
+                              nsITimerCallback)
 
 /**
  * Clear out the update.
  */
 void
 nsUrlClassifierStreamUpdater::DownloadDone()
 {
   LOG(("nsUrlClassifierStreamUpdater::DownloadDone [this=%p]", this));
@@ -266,39 +267,64 @@ nsUrlClassifierStreamUpdater::RekeyReque
     do_GetService("@mozilla.org/observer-service;1", &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return observerService->NotifyObservers(static_cast<nsIUrlClassifierStreamUpdater*>(this),
                                           "url-classifier-rekey-requested",
                                           nsnull);
 }
 
-NS_IMETHODIMP
-nsUrlClassifierStreamUpdater::StreamFinished(nsresult status)
+nsresult
+nsUrlClassifierStreamUpdater::FetchNext()
 {
-  nsresult rv;
+  if (mPendingUpdates.Length() == 0) {
+    return NS_OK;
+  }
+
+  PendingUpdate &update = mPendingUpdates[0];
+  LOG(("Fetching update url: %s\n", update.mUrl.get()));
+  nsresult rv = FetchUpdate(update.mUrl, EmptyCString(),
+                            update.mTable, update.mServerMAC);
+  if (NS_FAILED(rv)) {
+    LOG(("Error fetching update url: %s\n", update.mUrl.get()));
+    // We can commit the urls that we've applied so far.  This is
+    // probably a transient server problem, so trigger backoff.
+    mDownloadErrorCallback->HandleEvent(EmptyCString());
+    mDownloadError = PR_TRUE;
+    mDBService->FinishUpdate();
+    return rv;
+  }
+
+  mPendingUpdates.RemoveElementAt(0);
 
-  // Pop off a pending URL and update it.
-  if (NS_SUCCEEDED(status) && mPendingUpdates.Length() > 0) {
-    PendingUpdate &update = mPendingUpdates[0];
-    rv = FetchUpdate(update.mUrl, EmptyCString(),
-                     update.mTable, update.mServerMAC);
-    if (NS_FAILED(rv)) {
-      LOG(("Error fetching update url: %s\n", update.mUrl.get()));
-      // We can commit the urls that we've applied so far.  This is
-      // probably a transient server problem, so trigger backoff.
-      mDownloadErrorCallback->HandleEvent(EmptyCString());
-      mDownloadError = PR_TRUE;
-      mDBService->FinishUpdate();
-      return rv;
-    }
+  return NS_OK;
+}
 
-    mPendingUpdates.RemoveElementAt(0);
-  } else {
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::StreamFinished(nsresult status,
+                                             PRUint32 requestedDelay)
+{
+  LOG(("nsUrlClassifierStreamUpdater::StreamFinished [%x, %d]", status, requestedDelay));
+  if (NS_FAILED(status) || mPendingUpdates.Length() == 0) {
+    // We're done.
     mDBService->FinishUpdate();
+    return NS_OK;
+  }
+
+  // Wait the requested amount of time before starting a new stream.
+  nsresult rv;
+  mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+  if (NS_SUCCEEDED(rv)) {
+    rv = mTimer->InitWithCallback(this, requestedDelay,
+                                  nsITimer::TYPE_ONE_SHOT);
+  }
+
+  if (NS_FAILED(rv)) {
+    NS_WARNING("Unable to initialize timer, fetching next safebrowsing item immediately");
+    return FetchNext();
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsUrlClassifierStreamUpdater::UpdateSuccess(PRUint32 requestedTimeout)
 {
@@ -495,16 +521,20 @@ nsUrlClassifierStreamUpdater::Observe(ns
     if (mIsUpdating && mChannel) {
       LOG(("Cancel download"));
       nsresult rv;
       rv = mChannel->Cancel(NS_ERROR_ABORT);
       NS_ENSURE_SUCCESS(rv, rv);
       mIsUpdating = PR_FALSE;
       mChannel = nsnull;
     }
+    if (mTimer) {
+      mTimer->Cancel();
+      mTimer = nsnull;
+    }
   }
   return NS_OK;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // nsIBadCertListener2 implementation
 
 NS_IMETHODIMP
@@ -533,8 +563,25 @@ nsUrlClassifierStreamUpdater::NotifySSLE
 ///////////////////////////////////////////////////////////////////////////////
 // nsIInterfaceRequestor implementation
 
 NS_IMETHODIMP
 nsUrlClassifierStreamUpdater::GetInterface(const nsIID & eventSinkIID, void* *_retval)
 {
   return QueryInterface(eventSinkIID, _retval);
 }
+
+
+///////////////////////////////////////////////////////////////////////////////
+// nsITimerCallback implementation
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::Notify(nsITimer *timer)
+{
+  LOG(("nsUrlClassifierStreamUpdater::Notify [%p]", this));
+
+  mTimer = nsnull;
+
+  // Start the update process up again.
+  FetchNext();
+
+  return NS_OK;
+}
+
--- a/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.h
+++ b/toolkit/components/url-classifier/src/nsUrlClassifierStreamUpdater.h
@@ -44,40 +44,43 @@
 #include "nsCOMPtr.h"
 #include "nsIObserver.h"
 #include "nsIUrlClassifierStreamUpdater.h"
 #include "nsIStreamListener.h"
 #include "nsNetUtil.h"
 #include "nsTArray.h"
 #include "nsIBadCertListener2.h"
 #include "nsISSLErrorListener.h"
+#include "nsITimer.h"
 
 // Forward declare pointers
 class nsIURI;
 
 class nsUrlClassifierStreamUpdater : public nsIUrlClassifierStreamUpdater,
                                      public nsIUrlClassifierUpdateObserver,
                                      public nsIStreamListener,
                                      public nsIObserver,
                                      public nsIBadCertListener2,
                                      public nsISSLErrorListener,
-                                     public nsIInterfaceRequestor
+                                     public nsIInterfaceRequestor,
+                                     public nsITimerCallback
 {
 public:
   nsUrlClassifierStreamUpdater();
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIURLCLASSIFIERSTREAMUPDATER
   NS_DECL_NSIURLCLASSIFIERUPDATEOBSERVER
   NS_DECL_NSIINTERFACEREQUESTOR
   NS_DECL_NSIREQUESTOBSERVER
   NS_DECL_NSISTREAMLISTENER
   NS_DECL_NSIBADCERTLISTENER2
   NS_DECL_NSISSLERRORLISTENER
   NS_DECL_NSIOBSERVER
+  NS_DECL_NSITIMERCALLBACK
 
 private:
   // No subclassing
   ~nsUrlClassifierStreamUpdater() {}
 
   // When the dbservice sends an UpdateComplete or UpdateFailure, we call this
   // to reset the stream updater.
   void DownloadDone();
@@ -91,25 +94,28 @@ private:
                        const nsACString &aRequestBody,
                        const nsACString &aTable,
                        const nsACString &aServerMAC);
   nsresult FetchUpdate(const nsACString &aURI,
                        const nsACString &aRequestBody,
                        const nsACString &aTable,
                        const nsACString &aServerMAC);
 
+  nsresult FetchNext();
+
   PRBool mIsUpdating;
   PRBool mInitialized;
   PRBool mDownloadError;
   PRBool mBeganStream;
   nsCOMPtr<nsIURI> mUpdateUrl;
   nsCString mStreamTable;
   nsCString mServerMAC;
   nsCOMPtr<nsIChannel> mChannel;
   nsCOMPtr<nsIUrlClassifierDBService> mDBService;
+  nsCOMPtr<nsITimer> mTimer;
 
   struct PendingUpdate {
     nsCString mUrl;
     nsCString mTable;
     nsCString mServerMAC;
   };
   nsTArray<PendingUpdate> mPendingUpdates;