--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -123,16 +123,21 @@ static uint64_t gNumIntercepted = 0;
(loadFlags & (nsIRequest::LOAD_BYPASS_CACHE | \
nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE))
#define RECOVER_FROM_CACHE_FILE_ERROR(result) \
((result) == NS_ERROR_FILE_NOT_FOUND || \
(result) == NS_ERROR_FILE_CORRUPTED || \
(result) == NS_ERROR_OUT_OF_MEMORY)
+#define WRONG_RACING_RESPONSE_SOURCE(req) \
+ (mRacingNetAndCache && \
+ (((mFirstResponseSource == RESPONSE_FROM_CACHE) && (req != mCachePump)) || \
+ ((mFirstResponseSource == RESPONSE_FROM_NETWORK) && (req != mTransactionPump))))
+
static NS_DEFINE_CID(kStreamListenerTeeCID, NS_STREAMLISTENERTEE_CID);
static NS_DEFINE_CID(kStreamTransportServiceCID,
NS_STREAMTRANSPORTSERVICE_CID);
enum CacheDisposition {
kCacheHit = 1,
kCacheHitViaReval = 2,
kCacheMissedViaReval = 3,
@@ -275,16 +280,18 @@ nsHttpChannel::nsHttpChannel()
, mHasAutoRedirectVetoNotifier(0)
, mPinCacheContent(0)
, mIsCorsPreflightDone(0)
, mStronglyFramed(false)
, mPushedStream(nullptr)
, mLocalBlocklist(false)
, mWarningReporter(nullptr)
, mIsReadingFromCache(false)
+ , mOnCacheAvailableCalled(false)
+ , mRacingNetAndCache(false)
, mDidReval(false)
{
LOG(("Creating nsHttpChannel [this=%p]\n", this));
mChannelCreationTime = PR_Now();
mChannelCreationTimestamp = TimeStamp::Now();
}
nsHttpChannel::~nsHttpChannel()
@@ -417,16 +424,24 @@ nsHttpChannel::Connect()
// open a cache entry for this channel...
rv = OpenCacheEntry(isHttps);
// do not continue if asyncOpenCacheEntry is in progress
if (AwaitingCacheCallbacks()) {
LOG(("nsHttpChannel::Connect %p AwaitingCacheCallbacks forces async\n", this));
MOZ_ASSERT(NS_SUCCEEDED(rv), "Unexpected state");
+
+ if (mNetworkTriggered && mWaitingForProxy) {
+ // Someone has called TriggerNetwork(), meaning we are racing the
+ // network with the cache.
+ mWaitingForProxy = false;
+ return TryHSTSPriming();
+ }
+
return NS_OK;
}
if (NS_FAILED(rv)) {
LOG(("OpenCacheEntry failed [rv=%x]\n", rv));
// if this channel is only allowed to pull from the cache, then
// we must fail if we were unable to open a cache entry.
if (mLoadFlags & LOAD_ONLY_FROM_CACHE) {
@@ -435,17 +450,17 @@ nsHttpChannel::Connect()
if (!mFallbackChannel && !mFallbackKey.IsEmpty()) {
return AsyncCall(&nsHttpChannel::HandleAsyncFallback);
}
return NS_ERROR_DOCUMENT_NOT_CACHED;
}
// otherwise, let's just proceed without using the cache.
}
- return TryHSTSPriming();
+ return TriggerNetwork(0);
}
nsresult
nsHttpChannel::TryHSTSPriming()
{
if (mLoadInfo) {
// HSTS priming requires the LoadInfo provided with AsyncOpen2
bool requireHSTSPriming =
@@ -3745,16 +3760,22 @@ NS_IMETHODIMP
nsHttpChannel::OnCacheEntryCheck(nsICacheEntry* entry, nsIApplicationCache* appCache,
uint32_t* aResult)
{
nsresult rv = NS_OK;
LOG(("nsHttpChannel::OnCacheEntryCheck enter [channel=%p entry=%p]",
this, entry));
+ if (mRacingNetAndCache && mFirstResponseSource == RESPONSE_FROM_NETWORK) {
+ LOG(("Not using cached response because we've already got one from the network\n"));
+ *aResult = ENTRY_NOT_WANTED;
+ return NS_OK;
+ }
+
nsAutoCString cacheControlRequestHeader;
mRequestHead.GetHeader(nsHttp::Cache_Control, cacheControlRequestHeader);
CacheControlParser cacheControlRequest(cacheControlRequestHeader);
if (cacheControlRequest.NoStore()) {
LOG(("Not using cached response based on no-store request cache directive\n"));
*aResult = ENTRY_NOT_WANTED;
return NS_OK;
@@ -4182,18 +4203,19 @@ nsHttpChannel::OnCacheEntryCheck(nsICach
mCachedContentIsValid = false;
}
}
if (mDidReval)
*aResult = ENTRY_NEEDS_REVALIDATION;
else if (wantCompleteEntry)
*aResult = RECHECK_AFTER_WRITE_FINISHED;
- else
+ else {
*aResult = ENTRY_WANTED;
+ }
if (mCachedContentIsValid) {
entry->MaybeMarkValid();
}
LOG(("nsHTTPChannel::OnCacheEntryCheck exit [this=%p doValidation=%d result=%d]\n",
this, doValidation, *aResult));
return rv;
@@ -4201,16 +4223,17 @@ nsHttpChannel::OnCacheEntryCheck(nsICach
NS_IMETHODIMP
nsHttpChannel::OnCacheEntryAvailable(nsICacheEntry *entry,
bool aNew,
nsIApplicationCache* aAppCache,
nsresult status)
{
MOZ_ASSERT(NS_IsMainThread());
+ mOnCacheAvailableCalled = true;
nsresult rv;
LOG(("nsHttpChannel::OnCacheEntryAvailable [this=%p entry=%p "
"new=%d appcache=%p status=%x mAppCache=%p mAppCacheForWrite=%p]\n",
this, entry, aNew, aAppCache, status,
mApplicationCache.get(), mApplicationCacheForWrite.get()));
@@ -4272,17 +4295,21 @@ nsHttpChannel::OnCacheEntryAvailableInte
return rv;
}
// We may be waiting for more callbacks...
if (AwaitingCacheCallbacks()) {
return NS_OK;
}
- return TryHSTSPriming();
+ if (mCachedContentIsValid && mNetworkTriggered) {
+ ReadFromCache(true);
+ }
+
+ return TriggerNetwork(0);
}
nsresult
nsHttpChannel::OnNormalCacheEntryAvailable(nsICacheEntry *aEntry,
bool aNew,
nsresult aEntryStatus)
{
mCacheEntriesToWaitFor &= ~WAIT_FOR_CACHE_ENTRY;
@@ -4724,16 +4751,17 @@ nsHttpChannel::OpenCacheInputStream(nsIC
// Actually process the cached response that we started to handle in CheckCache
// and/or StartBufferingCachedEntity.
nsresult
nsHttpChannel::ReadFromCache(bool alreadyMarkedValid)
{
NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_FAILURE);
NS_ENSURE_TRUE(mCachedContentIsValid, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(!mCachePump, NS_OK); // already opened
LOG(("nsHttpChannel::ReadFromCache [this=%p] "
"Using cached copy of: %s\n", this, mSpec.get()));
if (mCachedResponseHead)
mResponseHead = Move(mCachedResponseHead);
UpdateInhibitPersistentCachingFlag();
@@ -4790,17 +4818,16 @@ nsHttpChannel::ReadFromCache(bool alread
MOZ_ASSERT(mCacheInputStream);
if (!mCacheInputStream) {
NS_ERROR("mCacheInputStream is null but we're expecting to "
"be able to read from it.");
return NS_ERROR_UNEXPECTED;
}
-
nsCOMPtr<nsIInputStream> inputStream = mCacheInputStream.forget();
rv = nsInputStreamPump::Create(getter_AddRefs(mCachePump), inputStream,
int64_t(-1), int64_t(-1), 0, 0, true);
if (NS_FAILED(rv)) {
inputStream->Close();
return rv;
}
@@ -5704,30 +5731,36 @@ nsHttpChannel::Cancel(nsresult status)
}
if (mWaitingForRedirectCallback) {
LOG(("channel canceled during wait for redirect callback"));
}
mCanceled = true;
mStatus = status;
if (mProxyRequest)
mProxyRequest->Cancel(status);
- if (mTransaction)
- gHttpHandler->CancelTransaction(mTransaction, status);
- if (mTransactionPump)
- mTransactionPump->Cancel(status);
+ CancelNetworkRequest(status);
mCacheInputStream.CloseAndRelease();
if (mCachePump)
mCachePump->Cancel(status);
if (mAuthProvider)
mAuthProvider->Cancel(status);
if (mPreflightChannel)
mPreflightChannel->Cancel(status);
return NS_OK;
}
+void
+nsHttpChannel::CancelNetworkRequest(nsresult aStatus)
+{
+ if (mTransaction)
+ gHttpHandler->CancelTransaction(mTransaction, aStatus);
+ if (mTransactionPump)
+ mTransactionPump->Cancel(aStatus);
+}
+
NS_IMETHODIMP
nsHttpChannel::Suspend()
{
nsresult rv = SuspendInternal();
nsresult rvParentChannel = NS_OK;
if (mParentChannel) {
rvParentChannel = mParentChannel->SuspendMessageDiversion();
@@ -5773,16 +5806,17 @@ nsHttpChannel::AsyncOpen(nsIStreamListen
MOZ_ASSERT(!mLoadInfo ||
mLoadInfo->GetSecurityMode() == 0 ||
mLoadInfo->GetInitialSecurityCheckDone() ||
(mLoadInfo->GetSecurityMode() == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL &&
nsContentUtils::IsSystemPrincipal(mLoadInfo->LoadingPrincipal())),
"security flags in loadInfo but asyncOpen2() not called");
LOG(("nsHttpChannel::AsyncOpen [this=%p]\n", this));
+
#ifdef MOZ_TASK_TRACER
{
uint64_t sourceEventId, parentTaskId;
tasktracer::SourceEventType sourceEventType;
GetCurTraceInfo(&sourceEventId, &parentTaskId, &sourceEventType);
nsCOMPtr<nsIURI> uri;
GetURI(getter_AddRefs(uri));
nsAutoCString urispec;
@@ -6518,29 +6552,50 @@ nsHttpChannel::GetRequestMethod(nsACStri
NS_IMETHODIMP
nsHttpChannel::OnStartRequest(nsIRequest *request, nsISupports *ctxt)
{
nsresult rv;
PROFILER_LABEL("nsHttpChannel", "OnStartRequest",
js::ProfileEntry::Category::NETWORK);
- if (!(mCanceled || NS_FAILED(mStatus))) {
+ if (!(mCanceled || NS_FAILED(mStatus)) && !WRONG_RACING_RESPONSE_SOURCE(request)) {
// capture the request's status, so our consumers will know ASAP of any
// connection failures, etc - bug 93581
request->GetStatus(&mStatus);
}
LOG(("nsHttpChannel::OnStartRequest [this=%p request=%p status=%x]\n",
this, request, mStatus));
+ if (mRacingNetAndCache) {
+ LOG((" racingNetAndCache - mFirstResponseSource:%d fromCache:%d fromNet:%d\n",
+ mFirstResponseSource, request == mCachePump, request == mTransactionPump));
+ if (mFirstResponseSource == RESPONSE_PENDING &&
+ request == mTransactionPump) {
+ LOG((" First response from network\n"));
+ mFirstResponseSource = RESPONSE_FROM_NETWORK;
+ } else if (mFirstResponseSource == RESPONSE_PENDING &&
+ request == mCachePump) {
+ LOG((" First response from cache\n"));
+ mFirstResponseSource = RESPONSE_FROM_CACHE;
+
+ // XXX: Consider cancelling H2 transactions or H1 transactions
+ // that are not keep-alive.
+ } else if (WRONG_RACING_RESPONSE_SOURCE(request)) {
+ LOG((" Early return when racing. This response not needed."));
+ return NS_OK;
+ }
+ }
+
// Make sure things are what we expect them to be...
MOZ_ASSERT(request == mCachePump || request == mTransactionPump,
"Unexpected request");
- MOZ_ASSERT(!(mTransactionPump && mCachePump) || mCachedContentIsPartial,
+
+ MOZ_ASSERT(mRacingNetAndCache || !(mTransactionPump && mCachePump) || mCachedContentIsPartial,
"If we have both pumps, the cache content must be partial");
mAfterOnStartRequestBegun = true;
mOnStartRequestTimestamp = TimeStamp::Now();
if (!mSecurityInfo && !mCachePump && mTransaction) {
// grab the security info from the connection object; the transaction
// is guaranteed to own a reference to the connection.
@@ -6667,19 +6722,25 @@ NS_IMETHODIMP
nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt, nsresult status)
{
PROFILER_LABEL("nsHttpChannel", "OnStopRequest",
js::ProfileEntry::Category::NETWORK);
LOG(("nsHttpChannel::OnStopRequest [this=%p request=%p status=%x]\n",
this, request, status));
+ LOG(("OnStopRequest %p requestFromCache: %d mFirstResponseSource: %d\n", this, request == mCachePump, mFirstResponseSource));
+
MOZ_ASSERT(NS_IsMainThread(),
"OnStopRequest should only be called from the main thread");
+ if (WRONG_RACING_RESPONSE_SOURCE(request)) {
+ return NS_OK;
+ }
+
if (NS_FAILED(status)) {
ProcessSecurityReport(status);
}
// If this load failed because of a security error, it may be because we
// are in a captive portal - trigger an async check to make sure.
int32_t nsprError = -1 * NS_ERROR_GET_CODE(status);
if (mozilla::psm::IsNSSErrorCode(nsprError)) {
@@ -6827,18 +6888,22 @@ nsHttpChannel::OnStopRequest(nsIRequest
mListener->OnStartRequest(this, mListenerContext);
mOnStartRequestCalled = true;
} else {
NS_WARNING("OnStartRequest skipped because of null listener");
}
}
// if this transaction has been replaced, then bail.
- if (mTransactionReplaced)
+ if (mTransactionReplaced) {
+ LOG(("Transaction replaced\n"));
+ // This was just the network check for a 304 response.
+ mFirstResponseSource = RESPONSE_PENDING;
return NS_OK;
+ }
if (mUpgradeProtocolCallback && stickyConn &&
mResponseHead && mResponseHead->Status() == 101) {
gHttpHandler->ConnMgr()->CompleteUpgrade(stickyConn,
mUpgradeProtocolCallback);
}
}
@@ -6976,26 +7041,29 @@ nsHttpChannel::OnDataAvailable(nsIReques
uint64_t offset, uint32_t count)
{
PROFILER_LABEL("nsHttpChannel", "OnDataAvailable",
js::ProfileEntry::Category::NETWORK);
LOG(("nsHttpChannel::OnDataAvailable [this=%p request=%p offset=%llu count=%u]\n",
this, request, offset, count));
+ LOG(("OnDataAvailable %p requestFromCache: %d mFirstResponseSource: %d\n", this, request == mCachePump, mFirstResponseSource));
+
// don't send out OnDataAvailable notifications if we've been canceled.
if (mCanceled)
return mStatus;
MOZ_ASSERT(mResponseHead, "No response head in ODA!!");
MOZ_ASSERT(!(mCachedContentIsPartial && (request == mTransactionPump)),
"transaction pump not suspended");
- if (mAuthRetryPending || (request == mTransactionPump && mTransactionReplaced)) {
+ if (mAuthRetryPending || WRONG_RACING_RESPONSE_SOURCE(request) ||
+ (request == mTransactionPump && mTransactionReplaced)) {
uint32_t n;
return input->ReadSegments(NS_DiscardSegment, nullptr, count, &n);
}
mIsReadingFromCache = (request == mCachePump);
if (mListener) {
//
@@ -7217,21 +7285,27 @@ nsHttpChannel::OnTransportStatus(nsITran
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsHttpChannel::IsFromCache(bool *value)
{
if (!mIsPending)
return NS_ERROR_NOT_AVAILABLE;
- // return false if reading a partial cache entry; the data isn't entirely
- // from the cache!
-
- *value = (mCachePump || (mLoadFlags & LOAD_ONLY_IF_MODIFIED)) &&
- mCachedContentIsValid && !mCachedContentIsPartial;
+ if (!mRacingNetAndCache) {
+ // return false if reading a partial cache entry; the data isn't
+ // entirely from the cache!
+ *value = (mCachePump || (mLoadFlags & LOAD_ONLY_IF_MODIFIED)) &&
+ mCachedContentIsValid && !mCachedContentIsPartial;
+ return NS_OK;
+ }
+
+ // If we are racing network and cache (or skipping the cache)
+ // we just return the first response source.
+ *value = mFirstResponseSource == RESPONSE_FROM_CACHE;
return NS_OK;
}
NS_IMETHODIMP
nsHttpChannel::GetCacheTokenExpirationTime(uint32_t *_retval)
{
NS_ENSURE_ARG_POINTER(_retval);
@@ -8478,57 +8552,105 @@ nsHttpChannel::ReportNetVSCacheTelemetry
} else {
Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_LARGE_V2, onStopDiff);
}
}
NS_IMETHODIMP
nsHttpChannel::Test_delayCacheEntryOpeningBy(int32_t aTimeout)
{
+ MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
mCacheOpenDelay = aTimeout;
return NS_OK;
}
NS_IMETHODIMP
nsHttpChannel::Test_triggerDelayedOpenCacheEntry()
{
+ MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
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;
}
+ nsCOMPtr<nsIRunnable> runnable;
+ mCacheOpenRunnable.swap(runnable);
mCacheOpenDelay = 0;
- mCacheOpenRunnable->Run();
- mCacheOpenRunnable = nullptr;
+ runnable->Run();
+
+ return NS_OK;
+}
+
+nsresult
+nsHttpChannel::TriggerNetwork(int32_t aTimeout)
+{
+ MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
+ // If a network request has already gone out, there is no point in
+ // doing this again.
+ if (mNetworkTriggered) {
+ return NS_OK;
+ }
+
+ if (!aTimeout) {
+ mNetworkTriggered = true;
+ if (!mOnCacheAvailableCalled) {
+ // If the network was triggered before onCacheEntryAvailable was
+ // called, we are either racing network and cache, or the load is
+ // bypassing the cache.
+ mRacingNetAndCache = true;
+ }
+ if (mNetworkTriggerTimer) {
+ mNetworkTriggerTimer->Cancel();
+ mNetworkTriggerTimer = nullptr;
+ }
+
+ // If we are waiting for a proxy request, that means we can't trigger
+ // the next step just yet. We need for mConnectionInfo to be non-null
+ // before we call TryHSTSPriming. OnProxyAvailable will trigger
+ // BeginConnect, and Connect will call TryHSTSPriming even if it's
+ // for the cache callbacks.
+ if (mProxyRequest) {
+ mWaitingForProxy = true;
+ return NS_OK;
+ }
+
+ return TryHSTSPriming();
+ }
+
+ mNetworkTriggerTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+ mNetworkTriggerTimer->InitWithCallback(this, aTimeout, nsITimer::TYPE_ONE_SHOT);
return NS_OK;
}
NS_IMETHODIMP
nsHttpChannel::Test_triggerNetwork(int32_t aTimeout)
{
- return TryHSTSPriming();
+ MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
+ return TriggerNetwork(aTimeout);
}
NS_IMETHODIMP
nsHttpChannel::Notify(nsITimer *aTimer)
{
RefPtr<nsHttpChannel> self(this);
if (aTimer == mCacheOpenTimer) {
return Test_triggerDelayedOpenCacheEntry();
+ } else if (aTimer == mNetworkTriggerTimer) {
+ return TriggerNetwork(0);
} else {
MOZ_CRASH("Unknown timer");
}
return NS_OK;
}
} // namespace net
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -24,17 +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>
+#include "nsIRaceCacheWithNetwork.h"
class nsDNSPrefetch;
class nsICancelable;
class nsIHttpChannelAuthProvider;
class nsInputStreamPump;
class nsISSLStatus;
namespace mozilla { namespace net {
@@ -616,16 +616,39 @@ private:
// 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;
+
+ // We need to remember which is the source of the response we are using.
+ enum {
+ RESPONSE_PENDING, // response is pending
+ RESPONSE_FROM_CACHE, // response coming from cache. no network.
+ RESPONSE_FROM_NETWORK, // response coming from the network
+ } mFirstResponseSource = RESPONSE_PENDING;
+
+ nsresult TriggerNetwork(int32_t aTimeout);
+ void CancelNetworkRequest(nsresult aStatus);
+ // Timer used to delay the network request, or to trigger the network
+ // request if retrieving the cache entry takes too long.
+ nsCOMPtr<nsITimer> mNetworkTriggerTimer;
+ // Is true if the network request has been triggered.
+ bool mNetworkTriggered = false;
+ bool mWaitingForProxy = false;
+ // Is true if the onCacheEntryAvailable callback has been called.
+ Atomic<bool> mOnCacheAvailableCalled;
+ // Will be true if the onCacheEntryAvailable callback is not called by the
+ // time we send the network request. This could also be true when we are
+ // bypassing the cache.
+ Atomic<bool> mRacingNetAndCache;
+
protected:
virtual void DoNotifyListenerCleanup() override;
private: // cache telemetry
bool mDidReval;
};
NS_DEFINE_STATIC_IID_ACCESSOR(nsHttpChannel, NS_HTTPCHANNEL_IID)
--- a/netwerk/test/unit/head_channels.js
+++ b/netwerk/test/unit/head_channels.js
@@ -47,16 +47,17 @@ const SUSPEND_DELAY = 3000;
*
* Note that it also requires a valid content length on the channel and
* is thus not fully generic.
*/
function ChannelListener(closure, ctx, flags) {
this._closure = closure;
this._closurectx = ctx;
this._flags = flags;
+ this._isFromCache = false;
}
ChannelListener.prototype = {
_closure: null,
_closurectx: null,
_buffer: "",
_got_onstartrequest: false,
_got_onstoprequest: false,
_contentLen: -1,
@@ -72,16 +73,20 @@ ChannelListener.prototype = {
onStartRequest: function(request, context) {
try {
if (this._got_onstartrequest)
do_throw("Got second onStartRequest event!");
this._got_onstartrequest = true;
this._lastEvent = Date.now();
+ try {
+ this._isFromCache = request.QueryInterface(Ci.nsICachingChannel).isFromCache();
+ } catch (e) {}
+
request.QueryInterface(Components.interfaces.nsIChannel);
try {
this._contentLen = request.contentLength;
}
catch (ex) {
if (!(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL)))
do_throw("Could not get contentLength");
}
@@ -102,17 +107,16 @@ ChannelListener.prototype = {
do_throw("Response is from the cache (CL_NOT_FROM_CACHE)");
}
}
if (this._flags & CL_SUSPEND) {
request.suspend();
do_timeout(SUSPEND_DELAY, function() { request.resume(); });
}
-
} catch (ex) {
do_throw("Error in onStartRequest: " + ex);
}
},
onDataAvailable: function(request, context, stream, offset, count) {
try {
let current = Date.now();
@@ -162,17 +166,17 @@ ChannelListener.prototype = {
if (!(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL)) &&
!(this._flags & CL_EXPECT_GZIP) &&
this._contentLen != -1)
do_check_eq(this._buffer.length, this._contentLen)
} catch (ex) {
do_throw("Error in onStopRequest: " + ex);
}
try {
- this._closure(request, this._buffer, this._closurectx);
+ this._closure(request, this._buffer, this._closurectx, this._isFromCache);
} catch (ex) {
do_throw("Error in closure function: " + ex);
}
}
};
var ES_ABORT_REDIRECT = 0x01;
--- a/netwerk/test/unit/test_race_cache_network.js
+++ b/netwerk/test/unit/test_race_cache_network.js
@@ -37,26 +37,43 @@ function test_handler(metadata, response
g304Counter++;
} else {
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.bodyOutputStream.write(gResponseBody, gResponseBody.length);
g200Counter++;
}
}
+function cached_handler(metadata, response) {
+ response.setHeader("Content-Type", "text/plain");
+ response.setHeader("Cache-Control", "Cache-Control: max-age=3600");
+ response.setHeader("ETag", "test-etag1");
+
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(gResponseBody, gResponseBody.length);
+
+ g200Counter++;
+}
+
let gResponseCounter = 0;
-function checkContent(request, buffer)
+let gIsFromCache = 0;
+function checkContent(request, buffer, context, isFromCache)
{
do_check_eq(buffer, gResponseBody);
gResponseCounter++;
- testGenerator.next();
+ if (isFromCache) {
+ gIsFromCache++;
+ }
+ do_execute_soon(() => { testGenerator.next(); });
}
function run_test() {
+ do_get_profile();
httpserver.registerPathHandler("/rcwn", test_handler);
+ httpserver.registerPathHandler("/rcwn_cached", cached_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");
@@ -71,42 +88,137 @@ function *testSteps() {
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);
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(200);
let startTime = Date.now();
channel.asyncOpen2(new ChannelListener(checkContent, null));
yield undefined;
- greater(Date.now() - startTime, 1000, "Check that timer works properly");
+ greater(Date.now() - startTime, 200, "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() {
+ do_timeout(50, 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);
+ // Trigger network after 50 ms.
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
yield undefined;
equal(gResponseCounter, 5);
equal(g200Counter, 2, "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.
+ // While the network response is produced, we trigger the cache fetch.
+ // Because the network response was the first
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen2(new ChannelListener(checkContent, null));
+ do_timeout(50, function() {
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(0);
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
+ });
+ yield undefined;
+ equal(gResponseCounter, 6);
+ equal(g200Counter, 3, "check number of 200 responses");
+ equal(g304Counter, 3, "check number of 304 responses");
+
+ // Triggers cache open before triggering network.
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn");
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen2(new ChannelListener(checkContent, null));
+ do_timeout(50, function() {
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(0);
+ });
+ yield undefined;
+ equal(gResponseCounter, 7);
+ equal(g200Counter, 3, "check number of 200 responses");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Load the cached handler so we don't need to revalidate
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel.asyncOpen2(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gResponseCounter, 8);
+ equal(g200Counter, 4, "check number of 200 responses");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Make sure response is loaded from the cache, not the network
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel.asyncOpen2(new ChannelListener(checkContent, null));
+ yield undefined;
+ equal(gResponseCounter, 9);
+ equal(g200Counter, 4, "check number of 200 responses");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Cache times out, so we trigger the network
+ gIsFromCache = 0;
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen2(new ChannelListener(checkContent, null));
+ // trigger network after 50 ms
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(50);
+ yield undefined;
+ equal(gResponseCounter, 10);
+ equal(gIsFromCache, 0, "should be from the network");
+ equal(g200Counter, 5, "check number of 200 responses");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Cache callback comes back right after network is triggered.
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen2(new ChannelListener(checkContent, null));
+ do_timeout(50, function() {
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(0);
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
+ });
+ yield undefined;
+ equal(gResponseCounter, 11);
+ do_print("IsFromCache: " + gIsFromCache + "\n");
+ do_print("Number of 200 responses: " + g200Counter + "\n");
+ equal(g304Counter, 4, "check number of 304 responses");
+
+ // Set an increasingly high timeout to trigger opening the cache entry
+ // This way we ensure that some of the entries we will get from the network,
+ // and some we will get from the cache.
+ gIsFromCache = 0;
+ for (var i = 0; i < 50; i++) {
+ var channel = make_channel("http://localhost:" + PORT + "/rcwn_cached");
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_delayCacheEntryOpeningBy(100000);
+ channel.asyncOpen2(new ChannelListener(checkContent, null));
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerNetwork(10);
+ // This may be racy. The delay was chosen because the distribution of net-cache
+ // results was around 25-25 on my machine.
+ do_timeout(i*100, function() {
+ try {
+ channel.QueryInterface(Components.interfaces.nsIRaceCacheWithNetwork).test_triggerDelayedOpenCacheEntry();
+ } catch (e) {}
+
+ });
+
+ yield undefined;
+ }
+
+ greater(gIsFromCache, 0, "Some of the responses should be from the cache");
+ less(gIsFromCache, 50, "Some of the responses should be from the net");
+
httpserver.stop(do_test_finished);
}