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