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
--- 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]