diff options
-rw-r--r-- | net/net.gyp | 3 | ||||
-rw-r--r-- | net/url_request/url_request_throttler_entry.cc | 36 | ||||
-rw-r--r-- | net/url_request/url_request_throttler_entry_interface.h | 2 | ||||
-rw-r--r-- | net/url_request/url_request_throttler_simulation_unittest.cc | 752 | ||||
-rw-r--r-- | net/url_request/url_request_throttler_test_support.cc | 63 | ||||
-rw-r--r-- | net/url_request/url_request_throttler_test_support.h | 62 | ||||
-rw-r--r-- | net/url_request/url_request_throttler_unittest.cc | 76 |
7 files changed, 906 insertions, 88 deletions
diff --git a/net/net.gyp b/net/net.gyp index 4c9603e..66ca4b8 100644 --- a/net/net.gyp +++ b/net/net.gyp @@ -978,6 +978,9 @@ 'tools/dump_cache/url_utilities_unittest.cc', 'udp/udp_socket_unittest.cc', 'url_request/url_request_job_factory_unittest.cc', + 'url_request/url_request_throttler_simulation_unittest.cc', + 'url_request/url_request_throttler_test_support.cc', + 'url_request/url_request_throttler_test_support.h', 'url_request/url_request_throttler_unittest.cc', 'url_request/url_request_unittest.cc', 'url_request/view_cache_helper_unittest.cc', diff --git a/net/url_request/url_request_throttler_entry.cc b/net/url_request/url_request_throttler_entry.cc index 45dc619..ca29fe0 100644 --- a/net/url_request/url_request_throttler_entry.cc +++ b/net/url_request/url_request_throttler_entry.cc @@ -60,16 +60,16 @@ class RejectedRequestParameters : public NetLog::EventParameters { release_after_ms_(release_after_ms) { } - virtual Value* ToValue() const {
- DictionaryValue* dict = new DictionaryValue();
- dict->SetString("url", url_id_);
- dict->SetInteger("num_failures", num_failures_);
- dict->SetInteger("release_after_ms", release_after_ms_);
- return dict;
- }
- - private:
- std::string url_id_;
+ virtual Value* ToValue() const { + DictionaryValue* dict = new DictionaryValue(); + dict->SetString("url", url_id_); + dict->SetInteger("num_failures", num_failures_); + dict->SetInteger("release_after_ms", release_after_ms_); + return dict; + } + + private: + std::string url_id_; int num_failures_; int release_after_ms_; }; @@ -83,15 +83,15 @@ class RetryAfterParameters : public NetLog::EventParameters { retry_after_ms_(retry_after_ms) { } - virtual Value* ToValue() const {
- DictionaryValue* dict = new DictionaryValue();
- dict->SetString("url", url_id_);
- dict->SetInteger("retry_after_ms", retry_after_ms_);
- return dict;
- }
+ virtual Value* ToValue() const { + DictionaryValue* dict = new DictionaryValue(); + dict->SetString("url", url_id_); + dict->SetInteger("retry_after_ms", retry_after_ms_); + return dict; + } - private:
- std::string url_id_;
+ private: + std::string url_id_; int retry_after_ms_; }; diff --git a/net/url_request/url_request_throttler_entry_interface.h b/net/url_request/url_request_throttler_entry_interface.h index a910e1a..86feb9f 100644 --- a/net/url_request/url_request_throttler_entry_interface.h +++ b/net/url_request/url_request_throttler_entry_interface.h @@ -6,6 +6,8 @@ #define NET_URL_REQUEST_URL_REQUEST_THROTTLER_ENTRY_INTERFACE_H_ #pragma once +#include <string> + #include "base/basictypes.h" #include "base/memory/ref_counted.h" #include "base/time.h" diff --git a/net/url_request/url_request_throttler_simulation_unittest.cc b/net/url_request/url_request_throttler_simulation_unittest.cc new file mode 100644 index 0000000..287b0a7 --- /dev/null +++ b/net/url_request/url_request_throttler_simulation_unittest.cc @@ -0,0 +1,752 @@ +// Copyright (c) 2011 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. + +// The tests in this file attempt to verify the following through simulation: +// a) That a server experiencing overload will actually benefit from the +// anti-DDoS throttling logic, i.e. that its traffic spike will subside +// and be distributed over a longer period of time; +// b) That "well-behaved" clients of a server under DDoS attack actually +// benefit from the anti-DDoS throttling logic; and +// c) That the approximate increase in "perceived downtime" introduced by +// anti-DDoS throttling for various different actual downtimes is what +// we expect it to be. + +#include <cmath> +#include <limits> +#include <vector> + +#include "base/environment.h" +#include "base/memory/scoped_vector.h" +#include "base/rand_util.h" +#include "base/time.h" +#include "net/url_request/url_request_throttler_manager.h" +#include "net/url_request/url_request_throttler_test_support.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::TimeDelta; +using base::TimeTicks; + +namespace net { +namespace { + +// Set this variable in your environment if you want to see verbose results +// of the simulation tests. +const char kShowSimulationVariableName[] = "SHOW_SIMULATION_RESULTS"; + +// Prints output only if a given environment variable is set. We use this +// to not print any output for human evaluation when the test is run without +// supervision. +void VerboseOut(const char* format, ...) { + static bool have_checked_environment = false; + static bool should_print = false; + if (!have_checked_environment) { + have_checked_environment = true; + scoped_ptr<base::Environment> env(base::Environment::Create()); + if (env->HasVar(kShowSimulationVariableName)) + should_print = true; + } + + if (should_print) { + va_list arglist; + va_start(arglist, format); + vprintf(format, arglist); + va_end(arglist); + } +} + +// A simple two-phase discrete time simulation. Actors are added in the order +// they should take action at every tick of the clock. Ticks of the clock +// are two-phase: +// - Phase 1 advances every actor's time to a new absolute time. +// - Phase 2 asks each actor to perform their action. +class DiscreteTimeSimulation { + public: + class Actor { + public: + virtual ~Actor() {} + virtual void AdvanceTime(const TimeTicks& absolute_time) = 0; + virtual void PerformAction() = 0; + }; + + DiscreteTimeSimulation() {} + + // Adds an |actor| to the simulation. The client of the simulation maintains + // ownership of |actor| and must ensure its lifetime exceeds that of the + // simulation. Actors should be added in the order you wish for them to + // act at each tick of the simulation. + void AddActor(Actor* actor) { + actors_.push_back(actor); + } + + // Runs the simulation for, pretending |time_between_ticks| passes from one + // tick to the next. The start time will be the current real time. The + // simulation will stop when the simulated duration is equal to or greater + // than |maximum_simulated_duration|. + void RunSimulation(const TimeDelta& maximum_simulated_duration, + const TimeDelta& time_between_ticks) { + TimeTicks start_time = TimeTicks(); + TimeTicks now = start_time; + while ((now - start_time) <= maximum_simulated_duration) { + for (std::vector<Actor*>::iterator it = actors_.begin(); + it != actors_.end(); + ++it) { + (*it)->AdvanceTime(now); + } + + for (std::vector<Actor*>::iterator it = actors_.begin(); + it != actors_.end(); + ++it) { + (*it)->PerformAction(); + } + + now += time_between_ticks; + } + } + + private: + std::vector<Actor*> actors_; + + DISALLOW_COPY_AND_ASSIGN(DiscreteTimeSimulation); +}; + +// Represents a web server in a simulation of a server under attack by +// a lot of clients. Must be added to the simulation's list of actors +// after all |Requester| objects. +class Server : public DiscreteTimeSimulation::Actor { + public: + Server(int max_queries_per_tick, + double request_drop_ratio) + : max_queries_per_tick_(max_queries_per_tick), + request_drop_ratio_(request_drop_ratio), + num_overloaded_ticks_remaining_(0), + num_current_tick_queries_(0), + num_overloaded_ticks_(0), + max_experienced_queries_per_tick_(0) { + } + + void SetDowntime(const TimeTicks& start_time, const TimeDelta& duration) { + start_downtime_ = start_time; + end_downtime_ = start_time + duration; + } + + virtual void AdvanceTime(const TimeTicks& absolute_time) OVERRIDE { + now_ = absolute_time; + } + + virtual void PerformAction() OVERRIDE { + // We are inserted at the end of the actor's list, so all Requester + // instances have already done their bit. + if (num_current_tick_queries_ > max_experienced_queries_per_tick_) + max_experienced_queries_per_tick_ = num_current_tick_queries_; + + if (num_current_tick_queries_ > max_queries_per_tick_) { + // We pretend the server fails for the next several ticks after it + // gets overloaded. + num_overloaded_ticks_remaining_ = 5; + ++num_overloaded_ticks_; + } else if (num_overloaded_ticks_remaining_ > 0) { + --num_overloaded_ticks_remaining_; + } + + requests_per_tick_.push_back(num_current_tick_queries_); + num_current_tick_queries_ = 0; + } + + // This is called by Requester. It returns the response code from + // the server. + int HandleRequest() { + ++num_current_tick_queries_; + if (!start_downtime_.is_null() && + start_downtime_ < now_ && now_ < end_downtime_) { + // TODO(joi): For the simulation measuring the increase in perceived + // downtime, it might be interesting to count separately the + // queries seen by the server (assuming a front-end reverse proxy + // is what actually serves up the 503s in this case) so that we could + // visualize the traffic spike seen by the server when it comes up, + // which would in many situations be ameliorated by the anti-DDoS + // throttling. + return 503; + } + + if ((num_overloaded_ticks_remaining_ > 0 || + num_current_tick_queries_ > max_queries_per_tick_) && + base::RandDouble() < request_drop_ratio_) { + return 503; + } + + return 200; + } + + int num_overloaded_ticks() const { + return num_overloaded_ticks_; + } + + int max_experienced_queries_per_tick() const { + return max_experienced_queries_per_tick_; + } + + std::string VisualizeASCII(int terminal_width) { + // Account for | characters we place at left of graph. + terminal_width -= 1; + + VerboseOut("Overloaded for %d of %d ticks.\n", + num_overloaded_ticks_, requests_per_tick_.size()); + VerboseOut("Got maximum of %d requests in a tick.\n\n", + max_experienced_queries_per_tick_); + + VerboseOut("Traffic graph:\n\n"); + + // Printing the graph like this is a bit overkill, but was very useful + // while developing the various simulations to see if they were testing + // the corner cases we want to simulate. + + // Find the smallest number of whole ticks we need to group into a + // column that will let all ticks fit into the column width we have. + int num_ticks = requests_per_tick_.size(); + double ticks_per_column_exact = + static_cast<double>(num_ticks) / static_cast<double>(terminal_width); + int ticks_per_column = std::ceil(ticks_per_column_exact); + DCHECK_GE(ticks_per_column * terminal_width, num_ticks); + + // Sum up the column values. + int num_columns = num_ticks / ticks_per_column; + if (num_ticks % ticks_per_column) + ++num_columns; + DCHECK_LE(num_columns, terminal_width); + scoped_array<int> columns(new int[num_columns]); + for (int tx = 0; tx < num_ticks; ++tx) { + int cx = tx / ticks_per_column; + if (tx % ticks_per_column == 0) + columns[cx] = 0; + columns[cx] += requests_per_tick_[tx]; + } + + // Find the lowest integer divisor that will let the column values + // be represented in a graph of maximum height 50. + int max_value = 0; + for (int cx = 0; cx < num_columns; ++cx) + max_value = std::max(max_value, columns[cx]); + const int kNumRows = 50; + double row_divisor_exact = max_value / static_cast<double>(kNumRows); + int row_divisor = std::ceil(row_divisor_exact); + DCHECK_GE(row_divisor * kNumRows, max_value); + + // To show the overload line, we calculate the appropriate value. + int overload_value = max_queries_per_tick_ * ticks_per_column; + + // When num_ticks is not a whole multiple of ticks_per_column, the last + // column includes fewer ticks than the others. In this case, don't + // print it so that we don't show an inconsistent value. + int num_printed_columns = num_columns; + if (num_ticks % ticks_per_column) + --num_printed_columns; + + // This is a top-to-bottom traversal of rows, left-to-right per row. + std::string output; + for (int rx = 0; rx < kNumRows; ++rx) { + int range_min = (kNumRows - rx) * row_divisor; + int range_max = range_min + row_divisor; + if (range_min == 0) + range_min = -1; // Make 0 values fit in the bottom range. + output.append("|"); + for (int cx = 0; cx < num_printed_columns; ++cx) { + char block = ' '; + // Show the overload line. + if (range_min < overload_value && overload_value <= range_max) + block = '-'; + + // Preferentially, show the graph line. + if (range_min < columns[cx] && columns[cx] <= range_max) + block = '#'; + + output.append(1, block); + } + output.append("\n"); + } + output.append("|"); + output.append(num_printed_columns, '='); + + return output; + } + + private: + TimeTicks now_; + TimeTicks start_downtime_; // Can be 0 to say "no downtime". + TimeTicks end_downtime_; + const int max_queries_per_tick_; + const double request_drop_ratio_; // Ratio of requests to 503 when failing. + int num_overloaded_ticks_remaining_; + int num_current_tick_queries_; + int num_overloaded_ticks_; + int max_experienced_queries_per_tick_; + std::vector<int> requests_per_tick_; + + DISALLOW_COPY_AND_ASSIGN(Server); +}; + +class TestingURLRequestThrottlerManager : public URLRequestThrottlerManager { + public: + TestingURLRequestThrottlerManager() : URLRequestThrottlerManager() { + } +}; + +// Mock throttler entry used by Requester class. +class MockURLRequestThrottlerEntry : public URLRequestThrottlerEntry { + public: + explicit MockURLRequestThrottlerEntry( + URLRequestThrottlerManager* manager) + : URLRequestThrottlerEntry(manager, ""), + mock_backoff_entry_(&backoff_policy_) { + } + virtual ~MockURLRequestThrottlerEntry() {} + + virtual const BackoffEntry* GetBackoffEntry() const OVERRIDE { + return &mock_backoff_entry_; + } + + virtual BackoffEntry* GetBackoffEntry() OVERRIDE { + return &mock_backoff_entry_; + } + + virtual TimeTicks ImplGetTimeNow() const OVERRIDE { + return fake_now_; + } + + void SetFakeNow(const TimeTicks& fake_time) { + fake_now_ = fake_time; + mock_backoff_entry_.set_fake_now(fake_time); + } + + TimeTicks fake_now() const { + return fake_now_; + } + + private: + TimeTicks fake_now_; + MockBackoffEntry mock_backoff_entry_; +}; + +// Registry of results for a class of |Requester| objects (e.g. attackers vs. +// regular clients). +class RequesterResults { +public: + RequesterResults() + : num_attempts_(0), num_successful_(0), num_failed_(0), num_blocked_(0) { + } + + void AddSuccess() { + ++num_attempts_; + ++num_successful_; + } + + void AddFailure() { + ++num_attempts_; + ++num_failed_; + } + + void AddBlocked() { + ++num_attempts_; + ++num_blocked_; + } + + int num_attempts() const { return num_attempts_; } + int num_successful() const { return num_successful_; } + int num_failed() const { return num_failed_; } + int num_blocked() const { return num_blocked_; } + + double GetBlockedRatio() { + DCHECK(num_attempts_); + return static_cast<double>(num_blocked_) / + static_cast<double>(num_attempts_); + } + + double GetSuccessRatio() { + DCHECK(num_attempts_); + return static_cast<double>(num_successful_) / + static_cast<double>(num_attempts_); + } + + void PrintResults(const char* class_description) { + if (num_attempts_ == 0) { + VerboseOut("No data for %s\n", class_description); + return; + } + + VerboseOut("Requester results for %s\n", class_description); + VerboseOut(" %d attempts\n", num_attempts_); + VerboseOut(" %d successes\n", num_successful_); + VerboseOut(" %d 5xx responses\n", num_failed_); + VerboseOut(" %d requests blocked\n", num_blocked_); + VerboseOut(" %.2f success ratio\n", GetSuccessRatio()); + VerboseOut(" %.2f blocked ratio\n", GetBlockedRatio()); + VerboseOut("\n"); + } + + private: + int num_attempts_; + int num_successful_; + int num_failed_; + int num_blocked_; +}; + +// Represents an Requester in a simulated DDoS situation, that periodically +// requests a specific resource. +class Requester : public DiscreteTimeSimulation::Actor { + public: + Requester(MockURLRequestThrottlerEntry* throttler_entry, + const TimeDelta& time_between_requests, + Server* server, + RequesterResults* results) + : throttler_entry_(throttler_entry), + time_between_requests_(time_between_requests), + last_attempt_was_failure_(false), + server_(server), + results_(results) { + DCHECK(server_); + } + + void AdvanceTime(const TimeTicks& absolute_time) OVERRIDE { + if (time_of_last_success_.is_null()) + time_of_last_success_ = absolute_time; + + throttler_entry_->SetFakeNow(absolute_time); + } + + void PerformAction() OVERRIDE { + TimeDelta effective_delay = time_between_requests_; + TimeDelta current_jitter = TimeDelta::FromMilliseconds( + request_jitter_.InMilliseconds() * base::RandDouble()); + if (base::RandInt(0, 1)) { + effective_delay -= current_jitter; + } else { + effective_delay += current_jitter; + } + + if (throttler_entry_->fake_now() - time_of_last_attempt_ > + effective_delay) { + if (!throttler_entry_->IsDuringExponentialBackoff()) { + int status_code = server_->HandleRequest(); + MockURLRequestThrottlerHeaderAdapter response_headers(status_code); + throttler_entry_->UpdateWithResponse("", &response_headers); + + if (status_code == 200) { + if (results_) + results_->AddSuccess(); + + if (last_attempt_was_failure_) { + last_downtime_duration_ = + throttler_entry_->fake_now() - time_of_last_success_; + } + + time_of_last_success_ = throttler_entry_->fake_now(); + last_attempt_was_failure_ = false; + } else { + if (results_) + results_->AddFailure(); + last_attempt_was_failure_ = true; + } + } else { + if (results_) + results_->AddBlocked(); + last_attempt_was_failure_ = true; + } + + time_of_last_attempt_ = throttler_entry_->fake_now(); + } + } + + // Adds a delay until the first request, equal to a uniformly distributed + // value between now and now + max_delay. + void SetStartupJitter(const TimeDelta& max_delay) { + int delay_ms = base::RandInt(0, max_delay.InMilliseconds()); + time_of_last_attempt_ = TimeTicks() + + TimeDelta::FromMilliseconds(delay_ms) - time_between_requests_; + } + + void SetRequestJitter(const TimeDelta& request_jitter) { + request_jitter_ = request_jitter; + } + + TimeDelta last_downtime_duration() const { return last_downtime_duration_; } + + private: + scoped_refptr<MockURLRequestThrottlerEntry> throttler_entry_; + const TimeDelta time_between_requests_; + TimeDelta request_jitter_; + TimeTicks time_of_last_attempt_; + TimeTicks time_of_last_success_; + bool last_attempt_was_failure_; + TimeDelta last_downtime_duration_; + Server* const server_; + RequesterResults* const results_; // May be NULL. + + DISALLOW_COPY_AND_ASSIGN(Requester); +}; + +void SimulateAttack(Server* server, + RequesterResults* attacker_results, + RequesterResults* client_results, + bool enable_throttling) { + const size_t kNumAttackers = 50; + const size_t kNumClients = 50; + DiscreteTimeSimulation simulation; + TestingURLRequestThrottlerManager manager; + ScopedVector<Requester> requesters; + for (size_t i = 0; i < kNumAttackers; ++i) { + // Use a tiny time_between_requests so the attackers will ping the + // server at every tick of the simulation. + scoped_refptr<MockURLRequestThrottlerEntry> throttler_entry( + new MockURLRequestThrottlerEntry(&manager)); + if (!enable_throttling) + throttler_entry->DisableBackoffThrottling(); + + Requester* attacker = new Requester(throttler_entry.get(), + TimeDelta::FromMilliseconds(1), + server, + attacker_results); + attacker->SetStartupJitter(TimeDelta::FromSeconds(120)); + requesters.push_back(attacker); + simulation.AddActor(attacker); + } + for (size_t i = 0; i < kNumClients; ++i) { + // Normal clients only make requests every 2 minutes, plus/minus 1 minute. + scoped_refptr<MockURLRequestThrottlerEntry> throttler_entry( + new MockURLRequestThrottlerEntry(&manager)); + if (!enable_throttling) + throttler_entry->DisableBackoffThrottling(); + + Requester* client = new Requester(throttler_entry.get(), + TimeDelta::FromMinutes(2), + server, + client_results); + client->SetStartupJitter(TimeDelta::FromSeconds(120)); + client->SetRequestJitter(TimeDelta::FromMinutes(1)); + requesters.push_back(client); + simulation.AddActor(client); + } + simulation.AddActor(server); + + simulation.RunSimulation(TimeDelta::FromMinutes(6), + TimeDelta::FromSeconds(1)); +} + +TEST(URLRequestThrottlerSimulation, HelpsInAttack) { + Server unprotected_server(30, 1.0); + RequesterResults unprotected_attacker_results; + RequesterResults unprotected_client_results; + Server protected_server(30, 1.0); + RequesterResults protected_attacker_results; + RequesterResults protected_client_results; + SimulateAttack(&unprotected_server, + &unprotected_attacker_results, + &unprotected_client_results, + false); + SimulateAttack(&protected_server, + &protected_attacker_results, + &protected_client_results, + true); + + // These assert that the DDoS protection actually benefits the + // server. Manual inspection of the traffic graphs will show this + // even more clearly. + EXPECT_GT(unprotected_server.num_overloaded_ticks(), + protected_server.num_overloaded_ticks()); + EXPECT_GT(unprotected_server.max_experienced_queries_per_tick(), + protected_server.max_experienced_queries_per_tick()); + + // These assert that the DDoS protection actually benefits non-malicious + // (and non-degenerate/accidentally DDoSing) users. + EXPECT_LT(protected_client_results.GetBlockedRatio(), + protected_attacker_results.GetBlockedRatio()); + EXPECT_GT(protected_client_results.GetSuccessRatio(), + unprotected_client_results.GetSuccessRatio()); + + // The rest is just for optional manual evaluation of the results; + // in particular the traffic pattern is interesting. + + VerboseOut("\nUnprotected server's results:\n\n"); + VerboseOut(unprotected_server.VisualizeASCII(132).c_str()); + VerboseOut("\n\n"); + VerboseOut("Protected server's results:\n\n"); + VerboseOut(protected_server.VisualizeASCII(132).c_str()); + VerboseOut("\n\n"); + + unprotected_attacker_results.PrintResults( + "attackers attacking unprotected server."); + unprotected_client_results.PrintResults( + "normal clients making requests to unprotected server."); + protected_attacker_results.PrintResults( + "attackers attacking protected server."); + protected_client_results.PrintResults( + "normal clients making requests to protected server."); +} + +// Returns the downtime perceived by the client, as a ratio of the +// actual downtime. +double SimulateDowntime(const TimeDelta& duration, + const TimeDelta& average_client_interval, + bool enable_throttling) { + TimeDelta time_between_ticks = duration / 200; + TimeTicks start_downtime = TimeTicks() + (duration / 2); + + // A server that never rejects requests, but will go down for maintenance. + Server server(std::numeric_limits<int>::max(), 1.0); + server.SetDowntime(start_downtime, duration); + + TestingURLRequestThrottlerManager manager; + scoped_refptr<MockURLRequestThrottlerEntry> throttler_entry( + new MockURLRequestThrottlerEntry(&manager)); + if (!enable_throttling) + throttler_entry->DisableBackoffThrottling(); + + Requester requester( + throttler_entry.get(), average_client_interval, &server, NULL); + requester.SetStartupJitter(duration / 3); + requester.SetRequestJitter(average_client_interval); + + DiscreteTimeSimulation simulation; + simulation.AddActor(&requester); + simulation.AddActor(&server); + + simulation.RunSimulation(duration * 2, time_between_ticks); + + return static_cast<double>( + requester.last_downtime_duration().InMilliseconds()) / + static_cast<double>(duration.InMilliseconds()); +} + +TEST(URLRequestThrottlerSimulation, PerceivedDowntimeRatio) { + struct Stats { + // Expected interval that we expect the ratio of downtime when anti-DDoS + // is enabled and downtime when anti-DDoS is not enabled to fall within. + // + // The expected interval depends on two things: The exponential back-off + // policy encoded in URLRequestThrottlerEntry, and the test or set of + // tests that the Stats object is tracking (e.g. a test where the client + // retries very rapidly on a very long downtime will tend to increase the + // number). + // + // To determine an appropriate new interval when parameters have changed, + // run the test a few times (you may have to Ctrl-C out of it after a few + // seconds) and choose an interval that the test converges quickly and + // reliably to. Then set the new interval, and run the test e.g. 20 times + // in succession to make sure it never takes an obscenely long time to + // converge to this interval. + double expected_min_increase; + double expected_max_increase; + + size_t num_runs; + double total_ratio_unprotected; + double total_ratio_protected; + + bool DidConverge(double* increase_ratio_out) { + double unprotected_ratio = total_ratio_unprotected / num_runs; + double protected_ratio = total_ratio_protected / num_runs; + double increase_ratio = protected_ratio / unprotected_ratio; + if (increase_ratio_out) + *increase_ratio_out = increase_ratio; + return expected_min_increase <= increase_ratio && + increase_ratio <= expected_max_increase; + } + + void ReportTrialResult(double increase_ratio) { + VerboseOut( + " Perceived downtime with throttling is %.4f times without.\n", + increase_ratio); + VerboseOut(" Test result after %d trials.\n", num_runs); + } + }; + + Stats global_stats = { 1.08, 1.15 }; + + struct Trial { + TimeDelta duration; + TimeDelta average_client_interval; + Stats stats; + + void PrintTrialDescription() { + double duration_minutes = + static_cast<double>(duration.InSeconds()) / 60.0; + double interval_minutes = + static_cast<double>(average_client_interval.InSeconds()) / 60.0; + VerboseOut("Trial with %.2f min downtime, avg. interval %.2f min.\n", + duration_minutes, interval_minutes); + } + }; + + // We don't set or check expected ratio intervals on individual + // experiments as this might make the test too fragile, but we + // print them out at the end for manual evaluation (we want to be + // able to make claims about the expected ratios depending on the + // type of behavior of the client and the downtime, e.g. the difference + // in behavior between a client making requests every few minutes vs. + // one that makes a request every 15 seconds). + Trial trials[] = { + { TimeDelta::FromSeconds(10), TimeDelta::FromSeconds(3) }, + { TimeDelta::FromSeconds(30), TimeDelta::FromSeconds(7) }, + { TimeDelta::FromMinutes(5), TimeDelta::FromSeconds(30) }, + { TimeDelta::FromMinutes(10), TimeDelta::FromSeconds(20) }, + { TimeDelta::FromMinutes(20), TimeDelta::FromSeconds(15) }, + { TimeDelta::FromMinutes(20), TimeDelta::FromSeconds(50) }, + { TimeDelta::FromMinutes(30), TimeDelta::FromMinutes(2) }, + { TimeDelta::FromMinutes(30), TimeDelta::FromMinutes(5) }, + { TimeDelta::FromMinutes(40), TimeDelta::FromMinutes(7) }, + { TimeDelta::FromMinutes(40), TimeDelta::FromMinutes(2) }, + { TimeDelta::FromMinutes(40), TimeDelta::FromSeconds(15) }, + { TimeDelta::FromMinutes(60), TimeDelta::FromMinutes(7) }, + { TimeDelta::FromMinutes(60), TimeDelta::FromMinutes(2) }, + { TimeDelta::FromMinutes(60), TimeDelta::FromSeconds(15) }, + { TimeDelta::FromMinutes(80), TimeDelta::FromMinutes(20) }, + { TimeDelta::FromMinutes(80), TimeDelta::FromMinutes(3) }, + { TimeDelta::FromMinutes(80), TimeDelta::FromSeconds(15) }, + + // Most brutal? + { TimeDelta::FromMinutes(45), TimeDelta::FromMilliseconds(500) }, + }; + + // If things don't converge by the time we've done 100K trials, then + // clearly one or more of the expected intervals are wrong. + while (global_stats.num_runs < 100000) { + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(trials); ++i) { + ++global_stats.num_runs; + ++trials[i].stats.num_runs; + double ratio_unprotected = SimulateDowntime( + trials[i].duration, trials[i].average_client_interval, false); + double ratio_protected = SimulateDowntime( + trials[i].duration, trials[i].average_client_interval, true); + global_stats.total_ratio_unprotected += ratio_unprotected; + global_stats.total_ratio_protected += ratio_protected; + trials[i].stats.total_ratio_unprotected += ratio_unprotected; + trials[i].stats.total_ratio_protected += ratio_protected; + } + + double increase_ratio; + if (global_stats.DidConverge(&increase_ratio)) + break; + + if (global_stats.num_runs > 200) { + VerboseOut("Test has not yet converged on expected interval.\n"); + global_stats.ReportTrialResult(increase_ratio); + } + } + + double average_increase_ratio; + EXPECT_TRUE(global_stats.DidConverge(&average_increase_ratio)); + + // Print individual trial results for optional manual evaluation. + double max_increase_ratio = 0.0; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(trials); ++i) { + double increase_ratio; + trials[i].stats.DidConverge(&increase_ratio); + max_increase_ratio = std::max(max_increase_ratio, increase_ratio); + trials[i].PrintTrialDescription(); + trials[i].stats.ReportTrialResult(increase_ratio); + } + + VerboseOut("Average increase ratio was %.4f\n", average_increase_ratio); + VerboseOut("Maximum increase ratio was %.4f\n", max_increase_ratio); +} + +} // namespace +} // namespace net diff --git a/net/url_request/url_request_throttler_test_support.cc b/net/url_request/url_request_throttler_test_support.cc new file mode 100644 index 0000000..1c418c2 --- /dev/null +++ b/net/url_request/url_request_throttler_test_support.cc @@ -0,0 +1,63 @@ +// Copyright (c) 2011 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 "net/url_request/url_request_throttler_test_support.h" + +#include "net/url_request/url_request_throttler_entry.h" + +namespace net { + +MockBackoffEntry::MockBackoffEntry(const BackoffEntry::Policy* const policy) + : BackoffEntry(policy) { +} + +MockBackoffEntry::~MockBackoffEntry() { +} + +base::TimeTicks MockBackoffEntry::ImplGetTimeNow() const { + return fake_now_; +} + +void MockBackoffEntry::set_fake_now(const base::TimeTicks& now) { + fake_now_ = now; +} + +MockURLRequestThrottlerHeaderAdapter::MockURLRequestThrottlerHeaderAdapter( + int response_code) + : fake_response_code_(response_code) { +} + +MockURLRequestThrottlerHeaderAdapter::MockURLRequestThrottlerHeaderAdapter( + const std::string& retry_value, + const std::string& opt_out_value, + int response_code) + : fake_retry_value_(retry_value), + fake_opt_out_value_(opt_out_value), + fake_response_code_(response_code) { +} + +MockURLRequestThrottlerHeaderAdapter::~MockURLRequestThrottlerHeaderAdapter() { +} + +std::string MockURLRequestThrottlerHeaderAdapter::GetNormalizedValue( + const std::string& key) const { + if (key == URLRequestThrottlerEntry::kRetryHeaderName && + !fake_retry_value_.empty()) { + return fake_retry_value_; + } + + if (key == + URLRequestThrottlerEntry::kExponentialThrottlingHeader && + !fake_opt_out_value_.empty()) { + return fake_opt_out_value_; + } + + return ""; +} + +int MockURLRequestThrottlerHeaderAdapter::GetResponseCode() const { + return fake_response_code_; +} + +} // namespace net diff --git a/net/url_request/url_request_throttler_test_support.h b/net/url_request/url_request_throttler_test_support.h new file mode 100644 index 0000000..0db36c0 --- /dev/null +++ b/net/url_request/url_request_throttler_test_support.h @@ -0,0 +1,62 @@ +// Copyright (c) 2011 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. + +#ifndef NET_URL_REQUEST_URL_REQUEST_THROTTLER_TEST_SUPPORT_H_ +#define NET_URL_REQUEST_URL_REQUEST_THROTTLER_TEST_SUPPORT_H_ +#pragma once + +#include <string> + +#include "base/time.h" +#include "net/base/backoff_entry.h" +#include "net/url_request/url_request_throttler_header_interface.h" + +namespace net { + +class MockBackoffEntry : public BackoffEntry { + public: + explicit MockBackoffEntry(const BackoffEntry::Policy* const policy); + virtual ~MockBackoffEntry(); + + // BackoffEntry overrides. + virtual base::TimeTicks ImplGetTimeNow() const OVERRIDE; + + void set_fake_now(const base::TimeTicks& now); + + private: + base::TimeTicks fake_now_; +}; + +// Mocks the URLRequestThrottlerHeaderInterface, allowing testing code to +// pass arbitrary fake headers to the throttling code. +class MockURLRequestThrottlerHeaderAdapter + : public URLRequestThrottlerHeaderInterface { + public: + // Constructs mock response headers with the given |response_code| and no + // custom response header fields. + explicit MockURLRequestThrottlerHeaderAdapter(int response_code); + + // Constructs mock response headers with the given |response_code| and + // with a custom-retry header value |retry_value| if it is non-empty, and + // a custom opt-out header value |opt_out_value| if it is non-empty. + MockURLRequestThrottlerHeaderAdapter(const std::string& retry_value, + const std::string& opt_out_value, + int response_code); + virtual ~MockURLRequestThrottlerHeaderAdapter(); + + // URLRequestThrottlerHeaderInterface overrides. + virtual std::string GetNormalizedValue(const std::string& key) const OVERRIDE; + virtual int GetResponseCode() const OVERRIDE; + + private: + std::string fake_retry_value_; + std::string fake_opt_out_value_; + int fake_response_code_; + + DISALLOW_COPY_AND_ASSIGN(MockURLRequestThrottlerHeaderAdapter); +}; + +} // namespace net + +#endif // NET_URL_REQUEST_URL_REQUEST_THROTTLER_TEST_SUPPORT_H_ diff --git a/net/url_request/url_request_throttler_unittest.cc b/net/url_request/url_request_throttler_unittest.cc index 354adb9..8e9734f 100644 --- a/net/url_request/url_request_throttler_unittest.cc +++ b/net/url_request/url_request_throttler_unittest.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "net/url_request/url_request_throttler_manager.h" + #include "base/memory/scoped_ptr.h" #include "base/metrics/histogram.h" #include "base/pickle.h" @@ -11,7 +13,7 @@ #include "net/base/test_completion_callback.h" #include "net/url_request/url_request_context.h" #include "net/url_request/url_request_throttler_header_interface.h" -#include "net/url_request/url_request_throttler_manager.h" +#include "net/url_request/url_request_throttler_test_support.h" #include "testing/gtest/include/gtest/gtest.h" using base::TimeDelta; @@ -24,30 +26,8 @@ namespace { using base::Histogram; using base::StatisticsRecorder; -class MockURLRequestThrottlerManager; - -class MockBackoffEntry : public BackoffEntry { - public: - explicit MockBackoffEntry(const BackoffEntry::Policy* const policy) - : BackoffEntry(policy), fake_now_(TimeTicks()) { - } - - virtual ~MockBackoffEntry() {} - - virtual TimeTicks ImplGetTimeNow() const OVERRIDE { - return fake_now_; - } - - void SetFakeNow(const TimeTicks& now) { - fake_now_ = now; - } - - private: - TimeTicks fake_now_; -}; - class MockURLRequestThrottlerEntry : public URLRequestThrottlerEntry { - public : + public: explicit MockURLRequestThrottlerEntry( net::URLRequestThrottlerManager* manager) : net::URLRequestThrottlerEntry(manager, ""), @@ -64,7 +44,7 @@ class MockURLRequestThrottlerEntry : public URLRequestThrottlerEntry { mock_backoff_entry_(&backoff_policy_) { InitPolicy(); - mock_backoff_entry_.SetFakeNow(fake_now); + mock_backoff_entry_.set_fake_now(fake_now); set_exponential_backoff_release_time(exponential_backoff_release_time); set_sliding_window_release_time(sliding_window_release_time); } @@ -89,7 +69,7 @@ class MockURLRequestThrottlerEntry : public URLRequestThrottlerEntry { void ResetToBlank(const TimeTicks& time_now) { fake_time_now_ = time_now; - mock_backoff_entry_.SetFakeNow(time_now); + mock_backoff_entry_.set_fake_now(time_now); GetBackoffEntry()->Reset(); GetBackoffEntry()->SetCustomReleaseTime(time_now); @@ -118,50 +98,6 @@ class MockURLRequestThrottlerEntry : public URLRequestThrottlerEntry { MockBackoffEntry mock_backoff_entry_; }; -class MockURLRequestThrottlerHeaderAdapter - : public URLRequestThrottlerHeaderInterface { - public: - MockURLRequestThrottlerHeaderAdapter() - : fake_retry_value_(""), - fake_opt_out_value_(""), - fake_response_code_(0) { - } - - explicit MockURLRequestThrottlerHeaderAdapter(int response_code) - : fake_retry_value_(""), - fake_opt_out_value_(""), - fake_response_code_(response_code) { - } - - MockURLRequestThrottlerHeaderAdapter(const std::string& retry_value, - const std::string& opt_out_value, - int response_code) - : fake_retry_value_(retry_value), - fake_opt_out_value_(opt_out_value), - fake_response_code_(response_code) { - } - - virtual ~MockURLRequestThrottlerHeaderAdapter() {} - - virtual std::string GetNormalizedValue(const std::string& key) const { - if (key == MockURLRequestThrottlerEntry::kRetryHeaderName && - !fake_retry_value_.empty()) { - return fake_retry_value_; - } else if (key == - MockURLRequestThrottlerEntry::kExponentialThrottlingHeader && - !fake_opt_out_value_.empty()) { - return fake_opt_out_value_; - } - return ""; - } - - virtual int GetResponseCode() const { return fake_response_code_; } - - std::string fake_retry_value_; - std::string fake_opt_out_value_; - int fake_response_code_; -}; - class MockURLRequestThrottlerManager : public URLRequestThrottlerManager { public: MockURLRequestThrottlerManager() : create_entry_index_(0) {} |