 Chromium Code Reviews
 Chromium Code Reviews Issue 807503002:
  Implement throttling logic for fetching affiliation information.  (Closed) 
  Base URL: https://chromium.googlesource.com/chromium/src.git@aff_database
    
  
    Issue 807503002:
  Implement throttling logic for fetching affiliation information.  (Closed) 
  Base URL: https://chromium.googlesource.com/chromium/src.git@aff_database| Index: components/password_manager/core/browser/affiliation_fetch_throttler_unittest.cc | 
| diff --git a/components/password_manager/core/browser/affiliation_fetch_throttler_unittest.cc b/components/password_manager/core/browser/affiliation_fetch_throttler_unittest.cc | 
| new file mode 100644 | 
| index 0000000000000000000000000000000000000000..211de1f6be80f21a7131895c3de024b406acc368 | 
| --- /dev/null | 
| +++ b/components/password_manager/core/browser/affiliation_fetch_throttler_unittest.cc | 
| @@ -0,0 +1,402 @@ | 
| +// Copyright 2015 The Chromium Authors. All rights reserved. | 
| +// Use of this source code is governed by a BSD-style license that can be | 
| +// found in the LICENSE file. | 
| + | 
| +#include "components/password_manager/core/browser/affiliation_fetch_throttler.h" | 
| + | 
| +#include <cmath> | 
| + | 
| +#include "base/callback.h" | 
| +#include "base/macros.h" | 
| +#include "base/memory/ref_counted.h" | 
| +#include "base/memory/scoped_ptr.h" | 
| +#include "base/message_loop/message_loop.h" | 
| +#include "base/numerics/safe_math.h" | 
| +#include "base/run_loop.h" | 
| +#include "base/test/test_mock_time_task_runner.h" | 
| +#include "base/thread_task_runner_handle.h" | 
| +#include "base/time/tick_clock.h" | 
| +#include "base/time/time.h" | 
| +#include "components/password_manager/core/browser/affiliation_fetch_throttler_delegate.h" | 
| +#include "net/base/network_change_notifier.h" | 
| +#include "testing/gtest/include/gtest/gtest.h" | 
| + | 
| +namespace password_manager { | 
| +namespace { | 
| + | 
| +class MockAffiliationFetchThrottlerDelegate | 
| + : public AffiliationFetchThrottlerDelegate { | 
| + public: | 
| + MockAffiliationFetchThrottlerDelegate(scoped_ptr<base::TickClock> tick_clock) | 
| + : tick_clock_(tick_clock.Pass()), | 
| + emulated_return_value_(true), | 
| + release_count_(0u) {} | 
| + ~MockAffiliationFetchThrottlerDelegate() { EXPECT_EQ(0u, release_count_); } | 
| + | 
| + void set_emulated_return_value(bool value) { emulated_return_value_ = value; } | 
| + void reset_release_count() { release_count_ = 0u; } | 
| + size_t release_count() const { return release_count_; } | 
| + base::TimeTicks last_release_time() const { return last_release_time_; } | 
| + | 
| + // AffiliationFetchThrottlerDelegate: | 
| + bool OnCanSendNetworkRequest() override { | 
| + ++release_count_; | 
| + last_release_time_ = tick_clock_->NowTicks(); | 
| + return emulated_return_value_; | 
| + } | 
| + | 
| + private: | 
| + scoped_ptr<base::TickClock> tick_clock_; | 
| + bool emulated_return_value_; | 
| + size_t release_count_; | 
| + base::TimeTicks last_release_time_; | 
| + | 
| + DISALLOW_COPY_AND_ASSIGN(MockAffiliationFetchThrottlerDelegate); | 
| +}; | 
| + | 
| +class MockNetworkChangeNotifier : public net::NetworkChangeNotifier { | 
| 
mmenke
2015/01/26 18:49:13
This class isn't needed.  See NetworkChangeNotifie
 
engedy
2015/01/26 19:39:35
Done.
 | 
| + public: | 
| + MockNetworkChangeNotifier() : connection_type_(CONNECTION_UNKNOWN) {} | 
| + | 
| + void set_connection_type(ConnectionType connection_type) { | 
| + connection_type_ = connection_type; | 
| + NotifyObserversOfConnectionTypeChangeForTests(connection_type_); | 
| + } | 
| + | 
| + ConnectionType GetCurrentConnectionType() const override { | 
| + return connection_type_; | 
| + } | 
| + | 
| + private: | 
| + ConnectionType connection_type_; | 
| +}; | 
| + | 
| +// Rounds the provided time delta in milliseconds (potentially having fractional | 
| +// part) to the nearest microsecond and returns it. | 
| +base::TimeDelta RoundToNearestMicrosecond(double delta_ms) { | 
| + base::internal::CheckedNumeric<int64_t> delta_us( | 
| + delta_ms * base::Time::kMicrosecondsPerMillisecond + .5); | 
| + return base::TimeDelta::FromMicroseconds(delta_us.ValueOrDie()); | 
| +} | 
| + | 
| +} // namespace | 
| + | 
| +class AffiliationFetchThrottlerTest : public testing::Test { | 
| + public: | 
| + AffiliationFetchThrottlerTest() | 
| + : task_runner_(new base::TestMockTimeTaskRunner), | 
| + mock_delegate_(task_runner_->GetMockTickClock()) {} | 
| + ~AffiliationFetchThrottlerTest() override {} | 
| + | 
| + scoped_ptr<AffiliationFetchThrottler> CreateThrottler() { | 
| + return make_scoped_ptr(new AffiliationFetchThrottler( | 
| + &mock_delegate_, task_runner_, task_runner_->GetMockTickClock())); | 
| + } | 
| + | 
| + void SimulateHasNetworkConnectivity(bool has_connectivity) { | 
| + network_change_notifier_.set_connection_type( | 
| + has_connectivity ? net::NetworkChangeNotifier::CONNECTION_UNKNOWN | 
| + : net::NetworkChangeNotifier::CONNECTION_NONE); | 
| + base::RunLoop().RunUntilIdle(); | 
| + } | 
| + | 
| + // Asserts that OnCanSendNetworkRequest() will have been called once by the | 
| + // time no tasks remain in the task runner, with an expected delay of at least | 
| + // |min_delay_ms| but no more than |max_delay_ms|. When called, the mock will | 
| + // return |emulated_return_value|, a boolean value normally indicating whether | 
| + // or not a request was actually issued in response to the call. | 
| + void AssertReleaseInBetween(bool emulated_return_value, | 
| + double min_delay_ms, | 
| + double max_delay_ms) { | 
| + ASSERT_EQ(0u, mock_delegate_.release_count()); | 
| + base::TimeTicks ticks_at_start = task_runner_->GetCurrentMockTime(); | 
| + mock_delegate_.set_emulated_return_value(emulated_return_value); | 
| + task_runner_->FastForwardUntilNoTasksRemain(); | 
| + base::TimeDelta delay = mock_delegate_.last_release_time() - ticks_at_start; | 
| + ASSERT_EQ(1u, mock_delegate_.release_count()); | 
| + EXPECT_LE(RoundToNearestMicrosecond(min_delay_ms), delay); | 
| + EXPECT_GE(RoundToNearestMicrosecond(max_delay_ms), delay); | 
| 
mmenke
2015/01/26 18:49:13
Per comment, it may theoretically be possible for
 
engedy
2015/01/26 19:39:34
The reasons that it should never occur is because
 
mmenke
2015/01/30 19:50:41
Hrm...I thought I checked and it was truncated...M
 | 
| + mock_delegate_.reset_release_count(); | 
| + } | 
| + | 
| + // Runs the task runner for |ms| and asserts that OnCanSendNetworkRequest() | 
| + // will not have been called by the end of this period. | 
| + void AssertNoReleaseForMs(double ms) { | 
| + task_runner_->FastForwardBy(RoundToNearestMicrosecond(ms)); | 
| + ASSERT_EQ(0u, mock_delegate_.release_count()); | 
| + } | 
| + | 
| + // Runs the task runner until no tasks remain, and asserts that | 
| + // OnCanSendNetworkRequest() will not have been called. | 
| + void AssertNoReleaseUntilNoTasksRemain() { | 
| + task_runner_->FastForwardUntilNoTasksRemain(); | 
| + ASSERT_EQ(0u, mock_delegate_.release_count()); | 
| + } | 
| + | 
| + size_t GetPendingTaskCount() const { | 
| + return task_runner_->GetPendingTaskCount(); | 
| + } | 
| + | 
| + private: | 
| + // Needed because NetworkChangeNotifier uses ObserverList, which notifies | 
| + // observers on the MessageLoop that belongs to the thread from which they | 
| + // have registered. | 
| + base::MessageLoop message_loop_; | 
| + MockNetworkChangeNotifier network_change_notifier_; | 
| + scoped_refptr<base::TestMockTimeTaskRunner> task_runner_; | 
| + MockAffiliationFetchThrottlerDelegate mock_delegate_; | 
| + | 
| + DISALLOW_COPY_AND_ASSIGN(AffiliationFetchThrottlerTest); | 
| +}; | 
| + | 
| +TEST_F(AffiliationFetchThrottlerTest, SuccessfulRequests) { | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| + | 
| + // Signal while request is in flight should be ignored. | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + throttler->InformOfNetworkRequestComplete(true); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + | 
| + // Duplicate the second signal 3 times: still only 1 callback should arrive. | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| +} | 
| + | 
| +TEST_F(AffiliationFetchThrottlerTest, FailedRequests) { | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| + throttler->InformOfNetworkRequestComplete(false); | 
| + | 
| + // The request after the first failure should be delayed by |initial_delay_ms| | 
| + // spread out over Uniform(1 - |jitter_factor|, 1). | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + const auto& kP = AffiliationFetchThrottler::kBackoffPolicy; | 
| 
mmenke
2015/01/26 18:49:13
Per Google style guide, uncommon abbreviations sho
 
engedy
2015/01/26 19:39:35
Done.
 | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween( | 
| + true, kP.initial_delay_ms * (1 - kP.jitter_factor), kP.initial_delay_ms)); | 
| + throttler->InformOfNetworkRequestComplete(true); | 
| + | 
| + // After a successful request, the next one should be released immediately. | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| + throttler->InformOfNetworkRequestComplete(false); | 
| + | 
| + // In general, the request after the n-th failure should be delayed by | 
| + // |multiply_factor| ^ (n-1) * |initial_delay_ms|, | 
| + // spread out over Uniform(1 - |jitter_factor|, 1), up until | 
| + // |maximum_backoff_ms| | 
| + // is reached. | 
| + for (size_t num_failures = 1; num_failures < 100; ++num_failures) { | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + double max_delay_ms = | 
| + kP.initial_delay_ms * pow(kP.multiply_factor, num_failures - 1); | 
| + double min_delay_ms = max_delay_ms * (1 - kP.jitter_factor); | 
| + if (max_delay_ms > kP.maximum_backoff_ms) | 
| + max_delay_ms = kP.maximum_backoff_ms; | 
| + if (min_delay_ms > kP.maximum_backoff_ms) | 
| + min_delay_ms = kP.maximum_backoff_ms; | 
| + ASSERT_NO_FATAL_FAILURE( | 
| + AssertReleaseInBetween(true, min_delay_ms, max_delay_ms)); | 
| + throttler->InformOfNetworkRequestComplete(false); | 
| + } | 
| +} | 
| + | 
| +TEST_F(AffiliationFetchThrottlerTest, OnCanSendNetworkRequestReturnsFalse) { | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + // A need for a network request is signaled, but as OnCanSendNetworkRequest() | 
| + // is called, the implementation returns false to indicate that the request | 
| + // will not be needed after all. InformOfNetworkRequestComplete() must not be | 
| + // called in this case. | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(false, 0, 0)); | 
| + | 
| + // A subsequent signaling, however, should result in OnCanSendNetworkRequest() | 
| + // being called immediately. | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| +} | 
| + | 
| +TEST_F(AffiliationFetchThrottlerTest, GracePeriodAfterConnectivityIsRestored) { | 
| + SimulateHasNetworkConnectivity(false); | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + // After connectivity is restored, the first request should be delayed by the | 
| + // grace period, spread out over Uniform(1 - |jitter_factor|, 1). | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + | 
| + SimulateHasNetworkConnectivity(true); | 
| + const auto& kP = AffiliationFetchThrottler::kBackoffPolicy; | 
| + const int64_t& kGraceMs = | 
| + AffiliationFetchThrottler::kGracePeriodAfterReconnectMs; | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween( | 
| + true, kGraceMs * (1 - kP.jitter_factor), kGraceMs)); | 
| + throttler->InformOfNetworkRequestComplete(true); | 
| + | 
| + // The next request should not be delayed. | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| +} | 
| + | 
| +// Same as GracePeriodAfterConnectivityIsRestored, but the network comes back | 
| +// just before SignalNetworkRequestNeeded() is called. | 
| +TEST_F(AffiliationFetchThrottlerTest, GracePeriodAfterConnectivityIsRestored2) { | 
| + SimulateHasNetworkConnectivity(false); | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + SimulateHasNetworkConnectivity(true); | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + const auto& kP = AffiliationFetchThrottler::kBackoffPolicy; | 
| + const int64_t& kGraceMs = | 
| + AffiliationFetchThrottler::kGracePeriodAfterReconnectMs; | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween( | 
| + true, kGraceMs * (1 - kP.jitter_factor), kGraceMs)); | 
| + throttler->InformOfNetworkRequestComplete(true); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| +} | 
| + | 
| +// Same as GracePeriodAfterConnectivityIsRestored, but the network goes down | 
| +// just as SignalNetworkRequestNeeded() is constructed. | 
| +TEST_F(AffiliationFetchThrottlerTest, GracePeriodAfterConnectivityIsRestored3) { | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + SimulateHasNetworkConnectivity(false); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + | 
| + SimulateHasNetworkConnectivity(true); | 
| + const auto& kP = AffiliationFetchThrottler::kBackoffPolicy; | 
| + const int64_t& kGraceMs = | 
| + AffiliationFetchThrottler::kGracePeriodAfterReconnectMs; | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween( | 
| + true, kGraceMs * (1 - kP.jitter_factor), kGraceMs)); | 
| + throttler->InformOfNetworkRequestComplete(true); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| +} | 
| + | 
| +TEST_F(AffiliationFetchThrottlerTest, ConnectivityLostDuringBackoff) { | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| + throttler->InformOfNetworkRequestComplete(false); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + SimulateHasNetworkConnectivity(false); | 
| + | 
| + // Let the exponential backoff delay expire, and verify nothing happens. | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + | 
| + // Verify that the request is, however, sent after the normal grace period | 
| + // once connectivity is restored. | 
| + SimulateHasNetworkConnectivity(true); | 
| + const auto& kP = AffiliationFetchThrottler::kBackoffPolicy; | 
| + const int64_t& kGraceMs = | 
| + AffiliationFetchThrottler::kGracePeriodAfterReconnectMs; | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween( | 
| + true, kGraceMs * (1 - kP.jitter_factor), kGraceMs)); | 
| + throttler->InformOfNetworkRequestComplete(true); | 
| +} | 
| + | 
| +TEST_F(AffiliationFetchThrottlerTest, | 
| + ConnectivityLostAndRestoredDuringBackoff) { | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| + throttler->InformOfNetworkRequestComplete(false); | 
| + | 
| + // Simulate 10 flakes, and run for a total of 5 grace periods. This verifies | 
| + // that a flaky connection will not flood the task queue with lots of of tasks | 
| + // and also that release will not happen while the connection is flaky even | 
| + // once the first grace period has expired. | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + const auto& kP = AffiliationFetchThrottler::kBackoffPolicy; | 
| + const int64_t& kGraceMs = | 
| + AffiliationFetchThrottler::kGracePeriodAfterReconnectMs; | 
| + for (size_t t = 1; t <= 10; ++t) { | 
| + SimulateHasNetworkConnectivity(false); | 
| + AssertNoReleaseForMs(kGraceMs * (1 - kP.jitter_factor) / 2); | 
| + SimulateHasNetworkConnectivity(true); | 
| + } | 
| + EXPECT_EQ(1u, GetPendingTaskCount()); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween( | 
| + true, kGraceMs * (1 - kP.jitter_factor), kGraceMs)); | 
| +} | 
| + | 
| +TEST_F(AffiliationFetchThrottlerTest, ConnectivityLostDuringRequest) { | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| + | 
| + SimulateHasNetworkConnectivity(false); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + | 
| + throttler->InformOfNetworkRequestComplete(false); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + | 
| + SimulateHasNetworkConnectivity(true); | 
| + | 
| + // Verify that the next request is released after the grace period once | 
| + // connectivity is restored. | 
| + const auto& kP = AffiliationFetchThrottler::kBackoffPolicy; | 
| + const int64_t& kGraceMs = | 
| + AffiliationFetchThrottler::kGracePeriodAfterReconnectMs; | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween( | 
| + true, kGraceMs * (1 - kP.jitter_factor), kGraceMs)); | 
| + throttler->InformOfNetworkRequestComplete(true); | 
| +} | 
| + | 
| +TEST_F(AffiliationFetchThrottlerTest, | 
| + ConnectivityLostAndRestoredDuringRequest) { | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| + | 
| + SimulateHasNetworkConnectivity(false); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + SimulateHasNetworkConnectivity(true); | 
| + throttler->InformOfNetworkRequestComplete(true); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| + | 
| + // Even though the previous request succeeded, the next request should still | 
| + // be held back for the normal grace period after connection is restored. | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + const auto& kP = AffiliationFetchThrottler::kBackoffPolicy; | 
| + const int64_t& kGraceMs = | 
| + AffiliationFetchThrottler::kGracePeriodAfterReconnectMs; | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween( | 
| + true, kGraceMs * (1 - kP.jitter_factor), kGraceMs)); | 
| + throttler->InformOfNetworkRequestComplete(true); | 
| +} | 
| + | 
| +TEST_F(AffiliationFetchThrottlerTest, InstanceDestroyedWhileInBackoff) { | 
| + scoped_ptr<AffiliationFetchThrottler> throttler(CreateThrottler()); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + ASSERT_NO_FATAL_FAILURE(AssertReleaseInBetween(true, 0, 0)); | 
| + throttler->InformOfNetworkRequestComplete(false); | 
| + | 
| + throttler->SignalNetworkRequestNeeded(); | 
| + throttler.reset(); | 
| + EXPECT_EQ(1u, GetPendingTaskCount()); | 
| + AssertNoReleaseUntilNoTasksRemain(); | 
| +} | 
| + | 
| +} // namespace password_manager |