Add PrefService integration to PrefetchProxyOriginDecider

Persists origin retry-after data to disk. Serialization of a base::Time
into a DictionaryValue was tricky, suggestions welcome.

Bug: 1146210
Change-Id: I57e356c44d014076fcfe3895d82cd7bd0fbdbdcf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2531273
Reviewed-by: Christian Dullweber <[email protected]>
Reviewed-by: Dominic Battré <[email protected]>
Reviewed-by: Ryan Sturm <[email protected]>
Commit-Queue: Robert Ogden <[email protected]>
Cr-Commit-Position: refs/heads/master@{#827318}
diff --git a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc
index 0af5e944..0d9e110f 100644
--- a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc
+++ b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc
@@ -60,6 +60,9 @@
 #include "chrome/browser/permissions/adaptive_quiet_notification_permission_ui_enabler.h"
 #include "chrome/browser/permissions/permission_decision_auto_blocker_factory.h"
 #include "chrome/browser/prefetch/no_state_prefetch/prerender_manager_factory.h"
+#include "chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.h"
+#include "chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_service.h"
+#include "chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_service_factory.h"
 #include "chrome/browser/prefetch/search_prefetch/search_prefetch_service.h"
 #include "chrome/browser/prefetch/search_prefetch/search_prefetch_service_factory.h"
 #include "chrome/browser/previews/previews_service.h"
@@ -582,6 +585,12 @@
     if (lite_video_keyed_service)
       lite_video_keyed_service->ClearData(delete_begin_, delete_end_);
 
+    PrefetchProxyService* prefetch_proxy_service =
+        PrefetchProxyServiceFactory::GetForProfile(profile_);
+    if (prefetch_proxy_service) {
+      prefetch_proxy_service->origin_decider()->OnBrowsingDataCleared();
+    }
+
 #if defined(OS_ANDROID)
     OomInterventionDecider* oom_intervention_decider =
         OomInterventionDecider::GetForBrowserContext(profile_);
diff --git a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.cc b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.cc
index 075ad15a..688617c 100644
--- a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.cc
+++ b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.cc
@@ -4,21 +4,51 @@
 
 #include "chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.h"
 
-#include "base/time/default_clock.h"
+#include <memory>
+#include <vector>
+
+#include "base/util/values/values_util.h"
 #include "chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_params.h"
 #include "chrome/browser/profiles/profile.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/pref_service.h"
 
-PrefetchProxyOriginDecider::PrefetchProxyOriginDecider()
-    : clock_(base::DefaultClock::GetInstance()) {}
+namespace {
+// This pref contains a dictionary value whose keys are string representations
+// of a url::Origin and values are a base::Time. The recorded base::Time is the
+// time at which prefetch requests to the corresponding origin can resume, (any
+// base::Time that is in the past can be removed). Entries to the dictionary are
+// created when a prefetch request gets a 503 response with Retry-After header.
+const char kRetryAfterPrefPath[] =
+    "chrome.prefetch_proxy.origin_decider.retry_after";
+}  // namespace
+
+// static
+void PrefetchProxyOriginDecider::RegisterPrefs(PrefRegistrySimple* registry) {
+  // Some loss in this pref (especially following a browser crash) is well
+  // tolerated and helps ensure the pref service isn't slammed.
+  registry->RegisterDictionaryPref(kRetryAfterPrefPath,
+                                   PrefRegistry::LOSSY_PREF);
+}
+
+PrefetchProxyOriginDecider::PrefetchProxyOriginDecider(
+    PrefService* pref_service,
+    base::Clock* clock)
+    : pref_service_(pref_service), clock_(clock) {
+  DCHECK(pref_service);
+  DCHECK(clock);
+
+  LoadFromPrefs();
+  if (ClearPastEntries()) {
+    SaveToPrefs();
+  }
+}
 
 PrefetchProxyOriginDecider::~PrefetchProxyOriginDecider() = default;
 
-void PrefetchProxyOriginDecider::SetClockForTesting(const base::Clock* clock) {
-  clock_ = clock;
-}
-
 void PrefetchProxyOriginDecider::OnBrowsingDataCleared() {
   origin_retry_afters_.clear();
+  SaveToPrefs();
 }
 
 bool PrefetchProxyOriginDecider::IsOriginOutsideRetryAfterWindow(
@@ -48,4 +78,62 @@
 
   origin_retry_afters_.emplace(url::Origin::Create(url),
                                clock_->Now() + retry_after);
+  SaveToPrefs();
+}
+
+void PrefetchProxyOriginDecider::LoadFromPrefs() {
+  origin_retry_afters_.clear();
+
+  const base::DictionaryValue* dictionary =
+      pref_service_->GetDictionary(kRetryAfterPrefPath);
+  DCHECK(dictionary);
+
+  for (const auto& element : *dictionary) {
+    GURL url_origin(element.first);
+    if (!url_origin.is_valid()) {
+      // This may happen in the case of corrupted prefs, or otherwise. Handle
+      // gracefully.
+      NOTREACHED();
+      continue;
+    }
+
+    base::Optional<base::Time> retry_after = util::ValueToTime(*element.second);
+    if (!retry_after) {
+      // This may happen in the case of corrupted prefs, or otherwise. Handle
+      // gracefully.
+      NOTREACHED();
+      continue;
+    }
+
+    url::Origin origin = url::Origin::Create(url_origin);
+    origin_retry_afters_.emplace(origin, retry_after.value());
+  }
+}
+
+void PrefetchProxyOriginDecider::SaveToPrefs() const {
+  base::DictionaryValue dictionary;
+  for (const auto& element : origin_retry_afters_) {
+    std::string key = element.first.GetURL().spec();
+    base::Value value = util::TimeToValue(element.second);
+    dictionary.SetKey(std::move(key), std::move(value));
+  }
+  pref_service_->Set(kRetryAfterPrefPath, dictionary);
+}
+
+bool PrefetchProxyOriginDecider::ClearPastEntries() {
+  std::vector<url::Origin> to_remove;
+  for (const auto& entry : origin_retry_afters_) {
+    if (entry.second < clock_->Now()) {
+      to_remove.push_back(entry.first);
+    }
+  }
+
+  if (to_remove.empty()) {
+    return false;
+  }
+
+  for (const auto& rm : to_remove) {
+    origin_retry_afters_.erase(rm);
+  }
+  return true;
 }
diff --git a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.h b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.h
index 54e79eb..e554601 100644
--- a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.h
+++ b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.h
@@ -8,17 +8,27 @@
 #include <map>
 
 #include "base/time/clock.h"
+#include "base/time/default_clock.h"
 #include "base/time/time.h"
 #include "url/gurl.h"
 #include "url/origin.h"
 
+class PrefService;
+class PrefRegistrySimple;
+
 // A browser-scoped class that maintains persistent logic for when origins
 // should not be prefetched.
 class PrefetchProxyOriginDecider {
  public:
-  PrefetchProxyOriginDecider();
+  explicit PrefetchProxyOriginDecider(
+      PrefService* pref_service,
+      base::Clock* clock = base::DefaultClock::GetInstance());
   ~PrefetchProxyOriginDecider();
 
+  // Registers prefs. Should only be used for storing in a device-local registry
+  // (non-syncing).
+  static void RegisterPrefs(PrefRegistrySimple* registry);
+
   // This should be called anytime browsing data is cleared by the user so that
   // the persistent data store can be cleared as well.
   void OnBrowsingDataCleared();
@@ -32,9 +42,23 @@
   // a maximum value provided by experiment params.
   void ReportOriginRetryAfter(const GURL& url, base::TimeDelta retry_after);
 
-  void SetClockForTesting(const base::Clock* clock);
+  PrefetchProxyOriginDecider(const PrefetchProxyOriginDecider&) = delete;
+  PrefetchProxyOriginDecider& operator=(const PrefetchProxyOriginDecider&) =
+      delete;
 
  private:
+  // These methods serialize and deserialize |origin_retry_afters_| to
+  // |pref_service_| in a dictionary value.
+  void LoadFromPrefs();
+  void SaveToPrefs() const;
+
+  // Erases any expired entries in |origin_retry_afters_|, returning true iff
+  // any entries were removed.
+  bool ClearPastEntries();
+
+  // Not owned.
+  PrefService* pref_service_;
+
   const base::Clock* clock_;
 
   // Maps origins to their last known retry_after time.
diff --git a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider_unittest.cc b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider_unittest.cc
index a085d64..6ed9ca2 100644
--- a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider_unittest.cc
+++ b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider_unittest.cc
@@ -7,58 +7,76 @@
 #include "base/test/scoped_feature_list.h"
 #include "base/test/simple_test_clock.h"
 #include "chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_features.h"
+#include "components/prefs/testing_pref_service.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
-TEST(PrefetchProxyOriginDeciderTest, DefaultEligible) {
-  PrefetchProxyOriginDecider decider;
-  EXPECT_TRUE(decider.IsOriginOutsideRetryAfterWindow(GURL("http://foo.com")));
+class PrefetchProxyOriginDeciderTest : public testing::Test {
+ public:
+  void SetUp() override {
+    PrefetchProxyOriginDecider::RegisterPrefs(pref_service_.registry());
+  }
+
+  std::unique_ptr<PrefetchProxyOriginDecider> NewDecider() {
+    return std::make_unique<PrefetchProxyOriginDecider>(&pref_service_,
+                                                        &clock_);
+  }
+
+  base::SimpleTestClock* clock() { return &clock_; }
+
+ private:
+  TestingPrefServiceSimple pref_service_;
+  base::SimpleTestClock clock_;
+};
+
+TEST_F(PrefetchProxyOriginDeciderTest, DefaultEligible) {
+  auto decider = NewDecider();
+  EXPECT_TRUE(decider->IsOriginOutsideRetryAfterWindow(GURL("http://foo.com")));
 }
 
-TEST(PrefetchProxyOriginDeciderTest, NegativeIgnored) {
+TEST_F(PrefetchProxyOriginDeciderTest, NegativeIgnored) {
   GURL url("http://foo.com");
-  PrefetchProxyOriginDecider decider;
-  decider.ReportOriginRetryAfter(url, base::TimeDelta::FromSeconds(-1));
-  EXPECT_TRUE(decider.IsOriginOutsideRetryAfterWindow(url));
+
+  auto decider = NewDecider();
+  decider->ReportOriginRetryAfter(url, base::TimeDelta::FromSeconds(-1));
+  EXPECT_TRUE(decider->IsOriginOutsideRetryAfterWindow(url));
 }
 
-TEST(PrefetchProxyOriginDeciderTest, MaxCap) {
+TEST_F(PrefetchProxyOriginDeciderTest, MaxCap) {
   base::test::ScopedFeatureList scoped_feature_list;
   scoped_feature_list.InitAndEnableFeatureWithParameters(
       features::kIsolatePrerenders, {{"max_retry_after_duration_secs", "10"}});
 
   GURL url("http://foo.com");
-  PrefetchProxyOriginDecider decider;
-  base::SimpleTestClock clock;
-  decider.SetClockForTesting(&clock);
 
-  decider.ReportOriginRetryAfter(url, base::TimeDelta::FromSeconds(15));
-  EXPECT_FALSE(decider.IsOriginOutsideRetryAfterWindow(url));
+  auto decider = NewDecider();
 
-  clock.Advance(base::TimeDelta::FromSeconds(11));
-  EXPECT_TRUE(decider.IsOriginOutsideRetryAfterWindow(url));
+  decider->ReportOriginRetryAfter(url, base::TimeDelta::FromSeconds(15));
+  EXPECT_FALSE(decider->IsOriginOutsideRetryAfterWindow(url));
+
+  clock()->Advance(base::TimeDelta::FromSeconds(11));
+  EXPECT_TRUE(decider->IsOriginOutsideRetryAfterWindow(url));
 }
 
-TEST(PrefetchProxyOriginDeciderTest, WaitsForDelta) {
+TEST_F(PrefetchProxyOriginDeciderTest, WaitsForDelta) {
   GURL url("http://foo.com");
-  PrefetchProxyOriginDecider decider;
-  base::SimpleTestClock clock;
-  decider.SetClockForTesting(&clock);
 
-  decider.ReportOriginRetryAfter(url, base::TimeDelta::FromSeconds(15));
+  auto decider = NewDecider();
+
+  decider->ReportOriginRetryAfter(url, base::TimeDelta::FromSeconds(15));
 
   for (size_t i = 0; i <= 15; i++) {
-    EXPECT_FALSE(decider.IsOriginOutsideRetryAfterWindow(url));
-    clock.Advance(base::TimeDelta::FromSeconds(1));
+    EXPECT_FALSE(decider->IsOriginOutsideRetryAfterWindow(url));
+    clock()->Advance(base::TimeDelta::FromSeconds(1));
   }
 
-  EXPECT_TRUE(decider.IsOriginOutsideRetryAfterWindow(url));
+  EXPECT_TRUE(decider->IsOriginOutsideRetryAfterWindow(url));
 }
 
-TEST(PrefetchProxyOriginDeciderTest, ByOrigin) {
-  PrefetchProxyOriginDecider decider;
+TEST_F(PrefetchProxyOriginDeciderTest, ByOrigin) {
+  auto decider = NewDecider();
 
-  decider.ReportOriginRetryAfter(GURL("http://foo.com"),
-                                 base::TimeDelta::FromSeconds(1));
+  decider->ReportOriginRetryAfter(GURL("http://foo.com"),
+                                  base::TimeDelta::FromSeconds(1));
 
   // Any url for the origin should be ineligible.
   for (const GURL& url : {
@@ -67,7 +85,7 @@
            GURL("http://foo.com/?query=yes"),
        }) {
     SCOPED_TRACE(url);
-    EXPECT_FALSE(decider.IsOriginOutsideRetryAfterWindow(url));
+    EXPECT_FALSE(decider->IsOriginOutsideRetryAfterWindow(url));
   }
 
   // Other origins are eligible.
@@ -77,17 +95,40 @@
            GURL("http://test.com/"),
        }) {
     SCOPED_TRACE(url);
-    EXPECT_TRUE(decider.IsOriginOutsideRetryAfterWindow(url));
+    EXPECT_TRUE(decider->IsOriginOutsideRetryAfterWindow(url));
   }
 }
 
-TEST(PrefetchProxyOriginDeciderTest, Clear) {
+TEST_F(PrefetchProxyOriginDeciderTest, Clear) {
   GURL url("http://foo.com");
-  PrefetchProxyOriginDecider decider;
 
-  decider.ReportOriginRetryAfter(url, base::TimeDelta::FromSeconds(1));
-  EXPECT_FALSE(decider.IsOriginOutsideRetryAfterWindow(url));
+  auto decider = NewDecider();
 
-  decider.OnBrowsingDataCleared();
-  EXPECT_TRUE(decider.IsOriginOutsideRetryAfterWindow(url));
+  decider->ReportOriginRetryAfter(url, base::TimeDelta::FromSeconds(1));
+  EXPECT_FALSE(decider->IsOriginOutsideRetryAfterWindow(url));
+
+  decider->OnBrowsingDataCleared();
+  EXPECT_TRUE(decider->IsOriginOutsideRetryAfterWindow(url));
+}
+
+TEST_F(PrefetchProxyOriginDeciderTest, PersistentPrefs) {
+  {
+    auto decider = NewDecider();
+
+    decider->ReportOriginRetryAfter(GURL("http://expired.com"),
+                                    base::TimeDelta::FromSeconds(1));
+    decider->ReportOriginRetryAfter(GURL("http://foo.com"),
+                                    base::TimeDelta::FromSeconds(3));
+  }
+
+  clock()->Advance(base::TimeDelta::FromSeconds(2));
+
+  {
+    auto decider = NewDecider();
+
+    EXPECT_TRUE(
+        decider->IsOriginOutsideRetryAfterWindow(GURL("http://expired.com")));
+    EXPECT_FALSE(
+        decider->IsOriginOutsideRetryAfterWindow(GURL("http://foo.com")));
+  }
 }
diff --git a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_service.cc b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_service.cc
index 8b3e0c8..557d997 100644
--- a/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_service.cc
+++ b/chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_service.cc
@@ -22,7 +22,8 @@
     : profile_(profile),
       proxy_configurator_(std::make_unique<PrefetchProxyProxyConfigurator>()),
       origin_prober_(std::make_unique<PrefetchProxyOriginProber>(profile)),
-      origin_decider_(std::make_unique<PrefetchProxyOriginDecider>()) {}
+      origin_decider_(
+          std::make_unique<PrefetchProxyOriginDecider>(profile->GetPrefs())) {}
 
 PrefetchProxyService::~PrefetchProxyService() = default;
 
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index 7e31648..7f6e892 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -52,6 +52,7 @@
 #include "chrome/browser/permissions/quiet_notification_permission_ui_state.h"
 #include "chrome/browser/policy/developer_tools_policy_handler.h"
 #include "chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.h"
+#include "chrome/browser/prefetch/prefetch_proxy/prefetch_proxy_origin_decider.h"
 #include "chrome/browser/prefs/chrome_pref_service_factory.h"
 #include "chrome/browser/prefs/incognito_mode_prefs.h"
 #include "chrome/browser/prefs/origin_trial_prefs.h"
@@ -817,6 +818,7 @@
   policy::DeveloperToolsPolicyHandler::RegisterProfilePrefs(registry);
   policy::URLBlocklistManager::RegisterProfilePrefs(registry);
   PrefProxyConfigTrackerImpl::RegisterProfilePrefs(registry);
+  PrefetchProxyOriginDecider::RegisterPrefs(registry);
   PrefsTabHelper::RegisterProfilePrefs(registry, locale);
   PreviewsHTTPSNotificationInfoBarDecider::RegisterProfilePrefs(registry);
   Profile::RegisterProfilePrefs(registry);