Bug 1325081 - Add interface to delay the cache fetch in order to test network-cache racing of HTTP requests r=michal draft
authorValentin Gosu <valentin.gosu@gmail.com>
Wed, 15 Feb 2017 20:44:09 +0100
changeset 485282 a89470efe71442fb6af36e74ac582e232557f81d
parent 484601 c0807d6938c13e43add377d5838df7168a59971e
child 485283 f908b168eb76781e3f398efed9b111a0aca63c29
push id45694
push uservalentin.gosu@gmail.com
push dateThu, 16 Feb 2017 14:21:00 +0000
reviewersmichal
bugs1325081
milestone54.0a1
Bug 1325081 - Add interface to delay the cache fetch in order to test network-cache racing of HTTP requests r=michal MozReview-Commit-ID: 96fSzPw8FHi * * * [mq]: over1 MozReview-Commit-ID: 80RePXK1TbR
netwerk/protocol/http/moz.build
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/protocol/http/nsIRaceCacheWithNetwork.idl
netwerk/test/unit/test_race_cache_network.js
netwerk/test/unit/xpcshell.ini
--- a/netwerk/protocol/http/moz.build
+++ b/netwerk/protocol/http/moz.build
@@ -15,16 +15,17 @@ XPIDL_SOURCES += [
     'nsIHttpAuthManager.idl',
     'nsIHttpChannel.idl',
     'nsIHttpChannelAuthProvider.idl',
     'nsIHttpChannelChild.idl',
     'nsIHttpChannelInternal.idl',
     'nsIHttpEventSink.idl',
     'nsIHttpHeaderVisitor.idl',
     'nsIHttpProtocolHandler.idl',
+    'nsIRaceCacheWithNetwork.idl',
     'nsIWellKnownOpportunisticUtils.idl',
 ]
 
 XPIDL_MODULE = 'necko_http'
 
 EXPORTS += [
     'nsCORSListenerProxy.h',
     'nsHttp.h',
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -3647,17 +3647,28 @@ nsHttpChannel::OpenCacheEntry(bool isHtt
             DebugOnly<bool> exists;
             MOZ_ASSERT(NS_SUCCEEDED(cacheStorage->Exists(openURI, extension, &exists)) && exists,
                        "The entry must exist in the cache after we create it here");
         }
 
         mCacheOpenWithPriority = cacheEntryOpenFlags & nsICacheStorage::OPEN_PRIORITY;
         mCacheQueueSizeWhenOpen = CacheStorageService::CacheQueueSize(mCacheOpenWithPriority);
 
-        rv = cacheStorage->AsyncOpenURI(openURI, extension, cacheEntryOpenFlags, this);
+        if (!mCacheOpenDelay) {
+            rv = cacheStorage->AsyncOpenURI(openURI, extension, cacheEntryOpenFlags, this);
+        } else {
+            mCacheOpenRunnable = NS_NewRunnableFunction([openURI, extension, cacheEntryOpenFlags, cacheStorage, this] () -> void {
+                cacheStorage->AsyncOpenURI(openURI, extension, cacheEntryOpenFlags, this);
+            });
+
+            mCacheOpenTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+            // calls nsHttpChannel::Notify after `mCacheOpenDelay` milliseconds
+            mCacheOpenTimer->InitWithCallback(this, mCacheOpenDelay, nsITimer::TYPE_ONE_SHOT);
+
+        }
         NS_ENSURE_SUCCESS(rv, rv);
     }
 
     waitFlags.Keep(WAIT_FOR_CACHE_ENTRY);
 
 bypassCacheEntryOpen:
     if (!mApplicationCacheForWrite)
         return NS_OK;
@@ -5658,16 +5669,18 @@ NS_INTERFACE_MAP_BEGIN(nsHttpChannel)
     NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheContainer)
     NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheChannel)
     NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectCallback)
     NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableRequest)
     NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener)
     NS_INTERFACE_MAP_ENTRY(nsIDNSListener)
     NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
     NS_INTERFACE_MAP_ENTRY(nsICorsPreflightCallback)
+    NS_INTERFACE_MAP_ENTRY(nsIRaceCacheWithNetwork)
+    NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
     NS_INTERFACE_MAP_ENTRY(nsIHstsPrimingCallback)
     NS_INTERFACE_MAP_ENTRY(nsIChannelWithDivertableParentListener)
     // we have no macro that covers this case.
     if (aIID.Equals(NS_GET_IID(nsHttpChannel)) ) {
         AddRef();
         *aInstancePtr = this;
         return NS_OK;
     } else
@@ -8462,10 +8475,61 @@ nsHttpChannel::ReportNetVSCacheTelemetry
     // No significant difference was observed between different sizes for |onStartDiff|
     if (diskStorageSizeK < 256) {
         Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_SMALL_V2, onStopDiff);
     } else {
         Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_LARGE_V2, onStopDiff);
     }
 }
 
+NS_IMETHODIMP
+nsHttpChannel::Test_delayCacheEntryOpeningBy(int32_t aTimeout)
+{
+    mCacheOpenDelay = aTimeout;
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHttpChannel::Test_triggerDelayedOpenCacheEntry()
+{
+    nsresult rv;
+    if (!mCacheOpenDelay) {
+        // No delay was set.
+        return NS_ERROR_NOT_AVAILABLE;
+    }
+    if (!mCacheOpenRunnable) {
+        // There should be a runnable.
+        return NS_ERROR_FAILURE;
+    }
+    if (mCacheOpenTimer) {
+        rv = mCacheOpenTimer->Cancel();
+        if (NS_FAILED(rv)) {
+            return rv;
+        }
+        mCacheOpenTimer = nullptr;
+    }
+    mCacheOpenDelay = 0;
+    mCacheOpenRunnable->Run();
+    mCacheOpenRunnable = nullptr;
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHttpChannel::Test_triggerNetwork(int32_t aTimeout)
+{
+    return TryHSTSPriming();
+}
+
+NS_IMETHODIMP
+nsHttpChannel::Notify(nsITimer *aTimer)
+{
+    RefPtr<nsHttpChannel> self(this);
+    if (aTimer == mCacheOpenTimer) {
+        return Test_triggerDelayedOpenCacheEntry();
+    } else {
+        MOZ_CRASH("Unknown timer");
+    }
+
+    return NS_OK;
+}
+
 } // namespace net
 } // namespace mozilla
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -24,16 +24,17 @@
 #include "TimingStruct.h"
 #include "ADivertableParentChannel.h"
 #include "AutoClose.h"
 #include "nsIStreamListener.h"
 #include "nsISupportsPrimitives.h"
 #include "nsICorsPreflightCallback.h"
 #include "AlternateServices.h"
 #include "nsIHstsPrimingCallback.h"
+#include <nsIRaceCacheWithNetwork.h>
 
 class nsDNSPrefetch;
 class nsICancelable;
 class nsIHttpChannelAuthProvider;
 class nsInputStreamPump;
 class nsISSLStatus;
 
 namespace mozilla { namespace net {
@@ -72,16 +73,18 @@ class nsHttpChannel final : public HttpB
                           , public nsIAsyncVerifyRedirectCallback
                           , public nsIThreadRetargetableRequest
                           , public nsIThreadRetargetableStreamListener
                           , public nsIDNSListener
                           , public nsSupportsWeakReference
                           , public nsICorsPreflightCallback
                           , public nsIChannelWithDivertableParentListener
                           , public nsIHstsPrimingCallback
+                          , public nsIRaceCacheWithNetwork
+                          , public nsITimerCallback
 {
 public:
     NS_DECL_ISUPPORTS_INHERITED
     NS_DECL_NSIREQUESTOBSERVER
     NS_DECL_NSISTREAMLISTENER
     NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
     NS_DECL_NSICACHEINFOCHANNEL
     NS_DECL_NSICACHINGCHANNEL
@@ -92,16 +95,18 @@ public:
     NS_DECL_NSIAPPLICATIONCACHECONTAINER
     NS_DECL_NSIAPPLICATIONCACHECHANNEL
     NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK
     NS_DECL_NSIHSTSPRIMINGCALLBACK
     NS_DECL_NSITHREADRETARGETABLEREQUEST
     NS_DECL_NSIDNSLISTENER
     NS_DECL_NSICHANNELWITHDIVERTABLEPARENTLISTENER
     NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTTPCHANNEL_IID)
+    NS_DECL_NSIRACECACHEWITHNETWORK
+    NS_DECL_NSITIMERCALLBACK
 
     // nsIHttpAuthenticableChannel. We can't use
     // NS_DECL_NSIHTTPAUTHENTICABLECHANNEL because it duplicates cancel() and
     // others.
     NS_IMETHOD GetIsSSL(bool *aIsSSL) override;
     NS_IMETHOD GetProxyMethodIsConnect(bool *aProxyMethodIsConnect) override;
     NS_IMETHOD GetServerResponseHeader(nsACString & aServerResponseHeader) override;
     NS_IMETHOD GetProxyChallenges(nsACString & aChallenges) override;
@@ -606,16 +611,21 @@ private:
     // If non-null, warnings should be reported to this object.
     HttpChannelSecurityWarningReporter* mWarningReporter;
 
     RefPtr<ADivertableParentChannel> mParentChannel;
 
     // True if the channel is reading from cache.
     Atomic<bool> mIsReadingFromCache;
 
+    // These next members are only used in unit tests to delay the call to
+    // cache->AsyncOpenURI in order to race the cache with the network.
+    nsCOMPtr<nsITimer> mCacheOpenTimer;
+    nsCOMPtr<nsIRunnable> mCacheOpenRunnable;
+    uint32_t mCacheOpenDelay = 0;
 protected:
     virtual void DoNotifyListenerCleanup() override;
 
 private: // cache telemetry
     bool mDidReval;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsHttpChannel, NS_HTTPCHANNEL_IID)
new file mode 100644
--- /dev/null
+++ b/netwerk/protocol/http/nsIRaceCacheWithNetwork.idl
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * This holds methods used to race the cache with the network for a specific
+ * channel. This interface is was designed with nsHttpChannel in mind, and it's
+ * expected this will be the only class implementing it.
+ */
+[scriptable, builtinclass, uuid(4d963475-8b16-4c58-b804-8a23d49436c5)]
+interface nsIRaceCacheWithNetwork : nsISupports
+{
+
+  /****************************************************************************
+   * TEST ONLY: The following methods are for testing purposes only. Do not use
+   * them to do anything important in your code.
+   ****************************************************************************
+
+  /**
+   * Triggers network activity after given timeout. If timeout is 0, network
+   * activity is triggered immediately. If the cache.asyncOpenURI callbacks
+   * have already been called, the network activity may have already been triggered
+   * or the content may have already been delivered from the cache, so this
+   * operation will have no effect.
+   *
+   * @param timeout
+   *        - the delay in milliseconds until the network will be triggered.
+   */
+  void test_triggerNetwork(in long timeout);
+
+  /**
+   * Normally a HTTP channel would immediately call AsyncOpenURI leading to the
+   * cache storage to lookup the cache entry and return it. In order to
+   * simmulate real life conditions where fetching a cache entry takes a long
+   * time, we set a timer to delay the operation.
+   * Can only be called on the main thread.
+   *
+   * @param timeout
+   *        - the delay in milliseconds until the cache open will be triggered.
+   */
+  void test_delayCacheEntryOpeningBy(in long timeout);
+
+  /**
+   * Immediatelly triggers AsyncOpenURI if the timer hasn't fired.
+   * Can only be called on the main thread.
+   * This is only called in tests to reliably trigger the opening of the cache
+   * entry.
+   * @throws NS_ERROR_NOT_AVAILABLE if opening the cache wasn't delayed.
+   */
+  void test_triggerDelayedOpenCacheEntry();
+};
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_race_cache_network.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+var httpserver = new HttpServer();
+httpserver.start(-1);
+const PORT = httpserver.identity.primaryPort;
+
+function make_channel(url) {
+  return NetUtil.newChannel({
+    uri: url,
+    loadUsingSystemPrincipal: true
+  }).QueryInterface(Components.interfaces.nsIHttpChannel);
+}
+
+let gResponseBody = "blahblah";
+let g200Counter = 0;
+let g304Counter = 0;
+function test_handler(metadata, response) {
+  response.setHeader("Content-Type", "text/plain");
+  response.setHeader("Cache-Control", "no-cache");
+  response.setHeader("ETag", "test-etag1");
+
+  try {
+    var etag = metadata.getHeader("If-None-Match");
+  } catch(ex) {
+    var etag = "";
+  }
+
+  if (etag == "test-etag1") {
+    response.setStatusLine(metadata.httpVersion, 304, "Not Modified");
+    g304Counter++;
+  } else {
+    response.setStatusLine(metadata.httpVersion, 200, "OK");
+    response.bodyOutputStream.write(gResponseBody, gResponseBody.length);
+    g200Counter++;
+  }
+}
+
+let gResponseCounter = 0;
+function checkContent(request, buffer)
+{
+  do_check_eq(buffer, gResponseBody);
+  gResponseCounter++;
+  testGenerator.next();
+}
+
+function run_test() {
+  httpserver.registerPathHandler("/rcwn", test_handler);
+  testGenerator.next();
+  do_test_pending();
+}
+
+let testGenerator = testSteps();
+function *testSteps() {
+  // Initial request. Stores the response in the cache.
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  yield undefined;
+  equal(gResponseCounter, 1);
+  equal(g200Counter, 1, "check number of 200 responses");
+  equal(g304Counter, 0, "check number of 304 responses");
+
+  // Checks that response is returned from the cache, after a 304 response.
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  yield undefined;
+  equal(gResponseCounter, 2);
+  equal(g200Counter, 1, "check number of 200 responses");
+  equal(g304Counter, 1, "check number of 304 responses");
+
+  // Checks that delaying the response from the cache works.
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(1000);
+  let startTime = Date.now();
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  yield undefined;
+  greater(Date.now() - startTime, 1000, "Check that timer works properly");
+  equal(gResponseCounter, 3);
+  equal(g200Counter, 1, "check number of 200 responses");
+  equal(g304Counter, 2, "check number of 304 responses");
+
+  // Checks that we can trigger the cache open immediately, even if the cache delay is set very high.
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  do_timeout(500, function() {
+    channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
+  });
+  yield undefined;
+  equal(gResponseCounter, 4);
+  equal(g200Counter, 1, "check number of 200 responses");
+  equal(g304Counter, 3, "check number of 304 responses");
+
+  // Sets a high delay for the cache fetch, and triggers the network activity.
+  var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+  channel.asyncOpen2(new ChannelListener(checkContent, null));
+  // Trigger network after 500 ms.
+  channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(500);
+  yield undefined;
+  equal(gResponseCounter, 5);
+  equal(g200Counter, 2, "check number of 200 responses");
+  equal(g304Counter, 3, "check number of 304 responses");
+
+  httpserver.stop(do_test_finished);
+}
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -376,8 +376,9 @@ skip-if = os == "android"
 [test_cache-control_request.js]
 [test_bug1279246.js]
 [test_throttlequeue.js]
 [test_throttlechannel.js]
 [test_throttling.js]
 [test_separate_connections.js]
 [test_rusturl.js]
 [test_trackingProtection_annotateChannels.js]
+[test_race_cache_network.js]