blob: 9acbdc104ef435fe5671e490e2b9a5701ef14533 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/preloading/prefetch/prefetch_canary_checker.h"
#include <optional>
#include "base/memory/raw_ptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_renderer_host.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "net/base/address_list.h"
#include "net/base/host_port_pair.h"
#include "net/base/net_errors.h"
#include "net/dns/public/resolve_error_info.h"
#include "services/network/public/mojom/host_resolver.mojom.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "services/network/test/test_network_context.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif
namespace content {
namespace {
const base::TimeDelta kCacheRevalidateAfter = base::Days(1);
class FakeNetworkContext : public network::TestNetworkContext {
public:
explicit FakeNetworkContext(
mojo::PendingReceiver<network::mojom::NetworkContext> receiver)
: receiver_(this, std::move(receiver)) {}
void ResolveHost(
network::mojom::HostResolverHostPtr host,
const net::NetworkAnonymizationKey& network_anonymization_key,
network::mojom::ResolveHostParametersPtr optional_parameters,
mojo::PendingRemote<network::mojom::ResolveHostClient> response_client)
override {
net::HostPortPair host_port_pair =
host->is_host_port_pair()
? host->get_host_port_pair()
: net::HostPortPair(host->get_scheme_host_port().host(),
host->get_scheme_host_port().port());
EXPECT_TRUE(pending_requests_.find(host_port_pair) ==
pending_requests_.end());
auto request = std::make_unique<ResolveHostRequest>(
this, host_port_pair, std::move(response_client),
std::move(optional_parameters->control_handle));
pending_requests_.emplace(host_port_pair, std::move(request));
num_requests_made_++;
}
void MakeDNSResolveSuccess(const GURL& url) {
const net::IPEndPoint kFakeIPAddress{
net::IPEndPoint(net::IPAddress::IPv4Localhost(), /*port=*/1234)};
auto it = pending_requests_.find(net::HostPortPair::FromURL(url));
// Make sure a request has actually been made.
EXPECT_TRUE(it != pending_requests_.end());
it->second->OnComplete(net::OK, net::AddressList(kFakeIPAddress),
/*alternative_endpoints=*/{});
pending_requests_.erase(it);
}
void MakeDNSResolveError(const GURL& url, net::Error err) {
MakeDNSResolveError(net::HostPortPair::FromURL(url), err);
}
void MakeDNSResolveError(const net::HostPortPair& host, net::Error err) {
auto it = pending_requests_.find(host);
// Make sure a request has actually been made.
EXPECT_TRUE(it != pending_requests_.end());
it->second->OnComplete(err, /*resolved_addresses=*/{},
/*alternative_endpoints=*/{});
pending_requests_.erase(it);
}
size_t NumPendingRequests() { return pending_requests_.size(); }
size_t NumRequestsMade() { return num_requests_made_; }
private:
class ResolveHostRequest : public network::mojom::ResolveHostHandle {
public:
ResolveHostRequest(
FakeNetworkContext* network_context,
net::HostPortPair host,
mojo::PendingRemote<network::mojom::ResolveHostClient> response_client,
mojo::PendingReceiver<network::mojom::ResolveHostHandle> control_handle)
: network_context_(network_context),
host_(host),
response_client_(std::move(response_client)) {
control_handle_receiver_.Bind(std::move(control_handle));
}
// ResolveHostHandle override.
void Cancel(int error) override {
network_context_->MakeDNSResolveError(host_,
static_cast<net::Error>(error));
}
void OnComplete(net::Error err,
net::AddressList resolved_addresses,
net::HostResolverEndpointResults alternative_endpoints) {
response_client_->OnComplete(err, net::ResolveErrorInfo(),
resolved_addresses, alternative_endpoints);
}
private:
raw_ptr<FakeNetworkContext> network_context_;
net::HostPortPair host_;
mojo::Receiver<network::mojom::ResolveHostHandle> control_handle_receiver_{
this};
mojo::Remote<network::mojom::ResolveHostClient> response_client_;
};
mojo::Receiver<network::mojom::NetworkContext> receiver_;
std::map<net::HostPortPair, std::unique_ptr<ResolveHostRequest>>
pending_requests_;
size_t num_requests_made_ = 0;
};
class PrefetchCanaryCheckerTest : public RenderViewHostTestHarness {
public:
PrefetchCanaryCheckerTest()
: RenderViewHostTestHarness(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
void SetUp() override {
SetNetworkConnectionTrackerForTesting(nullptr);
RenderViewHostTestHarness::SetUp();
mojo::PendingRemote<network::mojom::NetworkContext> network_context_remote;
network_context_ = std::make_unique<FakeNetworkContext>(
network_context_remote.InitWithNewPipeAndPassReceiver());
browser_context()
->GetDefaultStoragePartition()
->SetNetworkContextForTesting(std::move(network_context_remote));
}
void TearDown() override {
network_context_.reset();
RenderViewHostTestHarness::TearDown();
}
std::unique_ptr<PrefetchCanaryChecker> MakeChecker(const GURL& url) {
PrefetchCanaryChecker::RetryPolicy retry_policy;
return MakeCheckerWithRetries(url, retry_policy,
base::TimeDelta::FiniteMax());
}
std::unique_ptr<PrefetchCanaryChecker> MakeCheckerWithRetries(
const GURL& url,
PrefetchCanaryChecker::RetryPolicy retry_policy,
base::TimeDelta timeout) {
return std::make_unique<PrefetchCanaryChecker>(
browser_context(), PrefetchCanaryChecker::CheckType::kDNS, url,
retry_policy, timeout, kCacheRevalidateAfter);
}
void RunUntilIdle() { task_environment()->RunUntilIdle(); }
FakeNetworkContext* NetworkContext() { return network_context_.get(); }
private:
std::unique_ptr<FakeNetworkContext> network_context_;
};
TEST_F(PrefetchCanaryCheckerTest, OK) {
base::HistogramTester histogram_tester;
GURL probe_url("https://probe-url.com");
std::unique_ptr<PrefetchCanaryChecker> checker = MakeChecker(probe_url);
std::optional<bool> result = checker->CanaryCheckSuccessful();
EXPECT_EQ(result, std::nullopt);
// Make sure a cache miss was logged.
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.CacheLookupResult.DNS", 2, 1);
RunUntilIdle();
NetworkContext()->MakeDNSResolveSuccess(probe_url);
RunUntilIdle();
result = checker->CanaryCheckSuccessful();
EXPECT_TRUE(result.value());
EXPECT_FALSE(checker->IsActive());
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.FinalState.DNS", true, 1);
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.NumAttemptsBeforeSuccess.DNS", 1, 1);
histogram_tester.ExpectBucketCount(
"PrefetchProxy.CanaryChecker.CacheLookupResult.DNS", 0, 1);
}
TEST_F(PrefetchCanaryCheckerTest, MultipleStart) {
base::HistogramTester histogram_tester;
GURL probe_url("https://probe-url.com");
std::unique_ptr<PrefetchCanaryChecker> checker = MakeChecker(probe_url);
// Make sure calling RunChecksIfNeeded multiple times only results in one
// pending DNS lookup.
checker->RunChecksIfNeeded();
checker->RunChecksIfNeeded();
RunUntilIdle();
// Resolve a single DNS lookup.
NetworkContext()->MakeDNSResolveSuccess(probe_url);
// Make sure only one lookup was made.
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
// Allow the checker to process and cache the response.
RunUntilIdle();
std::optional<bool> result = checker->CanaryCheckSuccessful();
EXPECT_TRUE(result.value());
EXPECT_FALSE(checker->IsActive());
}
TEST_F(PrefetchCanaryCheckerTest, CacheHit) {
GURL probe_url("https://probe-url.com");
std::unique_ptr<PrefetchCanaryChecker> checker = MakeChecker(probe_url);
std::optional<bool> result = checker->CanaryCheckSuccessful();
EXPECT_EQ(result, std::nullopt);
RunUntilIdle();
NetworkContext()->MakeDNSResolveSuccess(probe_url);
// Allow the checker to process and cache the response.
RunUntilIdle();
// Make sure that future calls don't cause DNS lookups since there should
// already be a cached result.
result = checker->CanaryCheckSuccessful();
RunUntilIdle();
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
EXPECT_TRUE(result.value());
EXPECT_FALSE(checker->IsActive());
}
// TODO(crbug.com/40828450): Re-enable; flaky.
TEST_F(PrefetchCanaryCheckerTest, DISABLED_NetworkConnectionShardsCache) {
network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
network::mojom::ConnectionType::CONNECTION_3G);
RunUntilIdle();
GURL probe_url("https://probe-url.com");
std::unique_ptr<PrefetchCanaryChecker> checker = MakeChecker(probe_url);
checker->RunChecksIfNeeded();
RunUntilIdle();
NetworkContext()->MakeDNSResolveSuccess(probe_url);
RunUntilIdle();
std::optional<bool> result = checker->CanaryCheckSuccessful();
// Make sure result is cached.
EXPECT_TRUE(result.has_value());
// Changing the network to 4G should reuse the cache.
network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
network::mojom::ConnectionType::CONNECTION_4G);
RunUntilIdle();
result = checker->CanaryCheckSuccessful();
EXPECT_TRUE(result.has_value());
// Changing the network to wifi should result in a cache miss and a new check.
network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
network::mojom::ConnectionType::CONNECTION_WIFI);
RunUntilIdle();
result = checker->CanaryCheckSuccessful();
EXPECT_EQ(result, std::nullopt);
// Finish the check and make sure the result is cached.
RunUntilIdle();
NetworkContext()->MakeDNSResolveSuccess(probe_url);
RunUntilIdle();
result = checker->CanaryCheckSuccessful();
EXPECT_TRUE(result.value());
}
TEST_F(PrefetchCanaryCheckerTest, NetError) {
base::HistogramTester histogram_tester;
GURL probe_url("https://probe-url.com");
std::unique_ptr<PrefetchCanaryChecker> checker = MakeChecker(probe_url);
RunUntilIdle();
checker->RunChecksIfNeeded();
RunUntilIdle();
NetworkContext()->MakeDNSResolveError(probe_url, net::ERR_FAILED);
RunUntilIdle();
std::optional<bool> result = checker->CanaryCheckSuccessful();
EXPECT_FALSE(result.value());
EXPECT_FALSE(checker->IsActive());
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.FinalState.DNS", false, 1);
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.NetError.DNS", std::abs(net::ERR_FAILED), 1);
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.CacheLookupResult.DNS", 1, 1);
}
TEST_F(PrefetchCanaryCheckerTest, TimeUntilSuccess) {
base::HistogramTester histogram_tester;
GURL probe_url("https://probe-url.com");
std::unique_ptr<PrefetchCanaryChecker> checker = MakeChecker(probe_url);
checker->RunChecksIfNeeded();
RunUntilIdle();
task_environment()->FastForwardBy(base::Milliseconds(11000));
NetworkContext()->MakeDNSResolveSuccess(probe_url);
RunUntilIdle();
EXPECT_TRUE(checker->CanaryCheckSuccessful().value());
EXPECT_FALSE(checker->IsActive());
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.TimeUntilSuccess.DNS", 11000, 1);
histogram_tester.ExpectTotalCount(
"PrefetchProxy.CanaryChecker.TimeUntilFailure.DNS", 0);
}
TEST_F(PrefetchCanaryCheckerTest, TimeUntilFailure) {
base::HistogramTester histogram_tester;
GURL probe_url("https://probe-url.com");
std::unique_ptr<PrefetchCanaryChecker> checker = MakeChecker(probe_url);
checker->RunChecksIfNeeded();
RunUntilIdle();
task_environment()->FastForwardBy(base::Milliseconds(11000));
NetworkContext()->MakeDNSResolveError(probe_url, net::ERR_FAILED);
RunUntilIdle();
EXPECT_FALSE(checker->CanaryCheckSuccessful().value());
EXPECT_FALSE(checker->IsActive());
histogram_tester.ExpectTotalCount(
"PrefetchProxy.CanaryChecker.TimeUntilSuccess.DNS", 0);
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.TimeUntilFailure.DNS", 11000, 1);
}
TEST_F(PrefetchCanaryCheckerTest, Retries) {
base::HistogramTester histogram_tester;
GURL probe_url("https://probe-url.com");
PrefetchCanaryChecker::RetryPolicy retry_policy;
retry_policy.max_retries = 2;
retry_policy.backoff_policy = {
.num_errors_to_ignore = 0,
.initial_delay_ms = 1000,
.multiply_factor = 2,
.jitter_factor = 0.0,
// No maximum backoff.
.maximum_backoff_ms = -1,
.entry_lifetime_ms = -1,
.always_use_initial_delay = false,
};
base::TimeDelta timeout = base::Days(1);
std::unique_ptr<PrefetchCanaryChecker> checker =
MakeCheckerWithRetries(probe_url, retry_policy, timeout);
checker->RunChecksIfNeeded();
RunUntilIdle();
NetworkContext()->MakeDNSResolveError(probe_url, net::ERR_FAILED);
RunUntilIdle();
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
// Make sure the failure was not cached: we're not done with retries.
EXPECT_EQ(checker->CanaryCheckSuccessful(), std::nullopt);
task_environment()->FastForwardBy(base::Milliseconds(900));
// There should still be no retry attempted.
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
task_environment()->FastForwardBy(base::Milliseconds(100));
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 1u);
NetworkContext()->MakeDNSResolveError(probe_url, net::ERR_FAILED);
RunUntilIdle();
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
// Make sure the failure was not cached: we're not done with retries.
EXPECT_EQ(checker->CanaryCheckSuccessful(), std::nullopt);
// Exponential backoff: the next retry should go off in 2s.
task_environment()->FastForwardBy(base::Milliseconds(1900));
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
task_environment()->FastForwardBy(base::Milliseconds(100));
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 1u);
NetworkContext()->MakeDNSResolveError(probe_url, net::ERR_FAILED);
RunUntilIdle();
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
// Make sure the failure was cached: we're done with retries.
EXPECT_FALSE(checker->CanaryCheckSuccessful().value());
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.FinalState.DNS", false, 1);
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.NetError.DNS", std::abs(net::ERR_FAILED), 3);
}
TEST_F(PrefetchCanaryCheckerTest, Timeout) {
base::HistogramTester histogram_tester;
GURL probe_url("https://probe-url.com");
PrefetchCanaryChecker::RetryPolicy retry_policy;
retry_policy.max_retries = 2;
retry_policy.backoff_policy = {
.num_errors_to_ignore = 0,
.initial_delay_ms = 1000,
.multiply_factor = 2,
.jitter_factor = 0.0,
// No maximum backoff.
.maximum_backoff_ms = -1,
.entry_lifetime_ms = -1,
.always_use_initial_delay = false,
};
base::TimeDelta timeout = base::Milliseconds(1500);
std::unique_ptr<PrefetchCanaryChecker> checker =
MakeCheckerWithRetries(probe_url, retry_policy, timeout);
checker->RunChecksIfNeeded();
task_environment()->FastForwardBy(base::Milliseconds(1400));
// Still one pending DNS lookup.
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 1u);
EXPECT_EQ(NetworkContext()->NumRequestsMade(), 1u);
task_environment()->FastForwardBy(base::Milliseconds(100));
// It's been 1500 ms. The first lookup should haved timed out. A new one
// will be sent in 1s since the initial backoff is 1s.
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
EXPECT_EQ(NetworkContext()->NumRequestsMade(), 1u);
task_environment()->FastForwardBy(base::Milliseconds(1000));
// The first retry should go out now.
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 1u);
EXPECT_EQ(NetworkContext()->NumRequestsMade(), 2u);
task_environment()->FastForwardBy(base::Milliseconds(1500));
// By now the first retry should have timed out. The exponential backoff will
// delay the next retry until 2s have passed. Make sure no new lookup has
// been triggered.
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
EXPECT_EQ(NetworkContext()->NumRequestsMade(), 2u);
task_environment()->FastForwardBy(base::Milliseconds(1900));
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 0u);
EXPECT_EQ(NetworkContext()->NumRequestsMade(), 2u);
task_environment()->FastForwardBy(base::Milliseconds(100));
// The second retry should go out now.
EXPECT_EQ(NetworkContext()->NumPendingRequests(), 1u);
EXPECT_EQ(NetworkContext()->NumRequestsMade(), 3u);
NetworkContext()->MakeDNSResolveSuccess(probe_url);
RunUntilIdle();
EXPECT_TRUE(checker->CanaryCheckSuccessful().value());
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.FinalState.DNS", true, 1);
histogram_tester.ExpectUniqueSample(
"PrefetchProxy.CanaryChecker.NumAttemptsBeforeSuccess.DNS", 3, 1);
}
TEST_F(PrefetchCanaryCheckerTest, CacheEntryAge) {
#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/434660312): Re-enable on macOS 26 once issues with
// unexpected test timeout failures are resolved.
if (base::mac::MacOSMajorVersion() == 26) {
GTEST_SKIP() << "Disabled on macOS Tahoe.";
}
#endif
base::HistogramTester histogram_tester;
GURL probe_url("https://probe-url.com");
std::unique_ptr<PrefetchCanaryChecker> checker = MakeChecker(probe_url);
checker->RunChecksIfNeeded();
RunUntilIdle();
NetworkContext()->MakeDNSResolveSuccess(probe_url);
RunUntilIdle();
EXPECT_TRUE(checker->CanaryCheckSuccessful().value());
task_environment()->FastForwardBy(base::Hours(24));
EXPECT_TRUE(checker->CanaryCheckSuccessful().value());
histogram_tester.ExpectBucketCount(
"PrefetchProxy.CanaryChecker.CacheEntryAge.DNS", 0, 1);
histogram_tester.ExpectBucketCount(
"PrefetchProxy.CanaryChecker.CacheEntryAge.DNS", 24, 1);
histogram_tester.ExpectTotalCount(
"PrefetchProxy.CanaryChecker.CacheEntryAge.DNS", 2);
}
} // namespace
} // namespace content