Skip to content

Commit

Permalink
Fix and refactor HttpServerPropertiesImpl's alternative services brok…
Browse files Browse the repository at this point in the history
…enness expiration behavior.

One of HttpServerPropertiesImpl's responsibilities is to store HTTP alternative services that have failed and keeps track of their time-until-retry (i.e. when their brokenness expires). The broken alt-svcs and their respective expiration times are kept in a list ordered by expiration time. When the expiration task runs, it drops items from the front of the list until the expiration time of the head alt-svc is no longer in the past. Turns out this list was kept in insertion order as opposed to expiration-sorted order. This change keeps this queue in expiration-sorted order, as expected.
The additional logic added to implement the above change led to a refactor of HttpServerPropertiesImpl: its broken alt-svc logic is put in its own class, BrokenAlternativeServices.

BUG=724302

Review-Url: https://codereview.chromium.org/2898983006
Cr-Commit-Position: refs/heads/master@{#476475}
  • Loading branch information
wangyix authored and Commit Bot committed Jun 1, 2017
1 parent 3f7a5c7 commit 64ccc57
Show file tree
Hide file tree
Showing 7 changed files with 849 additions and 149 deletions.
3 changes: 3 additions & 0 deletions net/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,8 @@ component("net") {
"http/bidirectional_stream_impl.h",
"http/bidirectional_stream_request_info.cc",
"http/bidirectional_stream_request_info.h",
"http/broken_alternative_services.cc",
"http/broken_alternative_services.h",
"http/des.cc",
"http/des.h",
"http/failing_http_transaction_factory.cc",
Expand Down Expand Up @@ -4436,6 +4438,7 @@ test("net_unittests") {
"ftp/ftp_network_transaction_unittest.cc",
"ftp/ftp_util_unittest.cc",
"http/bidirectional_stream_unittest.cc",
"http/broken_alternative_services_unittest.cc",
"http/des_unittest.cc",
"http/http_auth_cache_unittest.cc",
"http/http_auth_challenge_tokenizer_unittest.cc",
Expand Down
181 changes: 181 additions & 0 deletions net/http/broken_alternative_services.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright (c) 2017 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/http/broken_alternative_services.h"

#include "base/memory/singleton.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "net/http/http_server_properties_impl.h"

namespace net {

namespace {

// Initial delay for broken alternative services.
const uint64_t kBrokenAlternativeProtocolDelaySecs = 300;
// Subsequent failures result in exponential (base 2) backoff.
// Limit binary shift to limit delay to approximately 2 days.
const int kBrokenDelayMaxShift = 9;

base::TimeDelta ComputeBrokenAlternativeServiceExpirationDelay(
int broken_count) {
DCHECK_GE(broken_count, 0);
if (broken_count > kBrokenDelayMaxShift)
broken_count = kBrokenDelayMaxShift;
return base::TimeDelta::FromSeconds(kBrokenAlternativeProtocolDelaySecs) *
(1 << broken_count);
}

} // namespace

BrokenAlternativeServices::BrokenAlternativeServices(Delegate* delegate,
base::TickClock* clock)
: delegate_(delegate),
clock_(clock),
recently_broken_alternative_services_(
RecentlyBrokenAlternativeServices::NO_AUTO_EVICT),
weak_ptr_factory_(this) {
DCHECK(delegate_);
DCHECK(clock_);
}

BrokenAlternativeServices::~BrokenAlternativeServices() {}

void BrokenAlternativeServices::MarkAlternativeServiceBroken(
const AlternativeService& alternative_service) {
// Empty host means use host of origin, callers are supposed to substitute.
DCHECK(!alternative_service.host.empty());
DCHECK_NE(kProtoUnknown, alternative_service.protocol);

auto it = recently_broken_alternative_services_.Get(alternative_service);
int broken_count = 0;
if (it == recently_broken_alternative_services_.end()) {
recently_broken_alternative_services_.Put(alternative_service, 1);
} else {
broken_count = it->second++;
}
base::TimeTicks expiration =
clock_->NowTicks() +
ComputeBrokenAlternativeServiceExpirationDelay(broken_count);
// Return if alternative service is already in expiration queue.
BrokenAlternativeServiceList::iterator list_it;
if (!AddToBrokenAlternativeServiceListAndMap(alternative_service, expiration,
&list_it)) {
return;
}

// If this is now the first entry in the list (i.e. |alternative_service| is
// the next alt svc to expire), schedule an expiration task for it.
if (list_it == broken_alternative_service_list_.begin()) {
ScheduleBrokenAlternateProtocolMappingsExpiration();
}
}

void BrokenAlternativeServices::MarkAlternativeServiceRecentlyBroken(
const AlternativeService& alternative_service) {
DCHECK_NE(kProtoUnknown, alternative_service.protocol);
if (recently_broken_alternative_services_.Get(alternative_service) ==
recently_broken_alternative_services_.end()) {
recently_broken_alternative_services_.Put(alternative_service, 1);
}
}

bool BrokenAlternativeServices::IsAlternativeServiceBroken(
const AlternativeService& alternative_service) const {
// Empty host means use host of origin, callers are supposed to substitute.
DCHECK(!alternative_service.host.empty());
return broken_alternative_service_map_.find(alternative_service) !=
broken_alternative_service_map_.end();
}

bool BrokenAlternativeServices::WasAlternativeServiceRecentlyBroken(
const AlternativeService& alternative_service) {
return recently_broken_alternative_services_.Get(alternative_service) !=
recently_broken_alternative_services_.end();
}

void BrokenAlternativeServices::ConfirmAlternativeService(
const AlternativeService& alternative_service) {
DCHECK_NE(kProtoUnknown, alternative_service.protocol);

// Remove |alternative_service| from |alternative_service_list_| and
// |alternative_service_map_|.
auto map_it = broken_alternative_service_map_.find(alternative_service);
if (map_it != broken_alternative_service_map_.end()) {
broken_alternative_service_list_.erase(map_it->second);
broken_alternative_service_map_.erase(map_it);
}

auto it = recently_broken_alternative_services_.Get(alternative_service);
if (it != recently_broken_alternative_services_.end()) {
recently_broken_alternative_services_.Erase(it);
}
}

bool BrokenAlternativeServices::AddToBrokenAlternativeServiceListAndMap(
const AlternativeService& alternative_service,
base::TimeTicks expiration,
BrokenAlternativeServiceList::iterator* it) {
DCHECK(it);

auto map_it = broken_alternative_service_map_.find(alternative_service);
if (map_it != broken_alternative_service_map_.end())
return false;

// Iterate from end of |broken_alternative_service_list_| to find where to
// insert it to keep the list sorted by expiration time.
auto list_it = broken_alternative_service_list_.end();
while (list_it != broken_alternative_service_list_.begin()) {
--list_it;
if (list_it->expiration <= expiration) {
++list_it;
break;
}
}

// Insert |alternative_service| into the list and the map
list_it = broken_alternative_service_list_.insert(
list_it, BrokenAltSvcExpireInfo(alternative_service, expiration));
broken_alternative_service_map_.insert(
std::make_pair(alternative_service, list_it));

*it = list_it;
return true;
}

void BrokenAlternativeServices::ExpireBrokenAlternateProtocolMappings() {
base::TimeTicks now = clock_->NowTicks();

while (!broken_alternative_service_list_.empty()) {
auto it = broken_alternative_service_list_.begin();
if (now < it->expiration) {
break;
}

delegate_->OnExpireBrokenAlternativeService(it->alternative_service);

broken_alternative_service_map_.erase(it->alternative_service);
broken_alternative_service_list_.erase(it);
}

if (!broken_alternative_service_list_.empty())
ScheduleBrokenAlternateProtocolMappingsExpiration();
}

void BrokenAlternativeServices ::
ScheduleBrokenAlternateProtocolMappingsExpiration() {
DCHECK(!broken_alternative_service_list_.empty());
base::TimeTicks now = clock_->NowTicks();
base::TimeTicks when = broken_alternative_service_list_.front().expiration;
base::TimeDelta delay = when > now ? when - now : base::TimeDelta();
expiration_timer_.Stop();
expiration_timer_.Start(
FROM_HERE, delay,
base::Bind(
&BrokenAlternativeServices ::ExpireBrokenAlternateProtocolMappings,
weak_ptr_factory_.GetWeakPtr()));
}

} // namespace net
145 changes: 145 additions & 0 deletions net/http/broken_alternative_services.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) 2017 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_HTTP_BROKEN_ALTERNATIVE_SERVICES_H_
#define NET_HTTP_BROKEN_ALTERNATIVE_SERVICES_H_

#include <list>
#include <unordered_map>

#include "base/memory/weak_ptr.h"
#include "base/timer/timer.h"
#include "net/http/http_server_properties.h"

namespace base {
class TickClock;
}

namespace net {

struct AlternativeServiceHash {
size_t operator()(const net::AlternativeService& entry) const {
return entry.protocol ^ std::hash<std::string>()(entry.host) ^ entry.port;
}
};

// This class tracks HTTP alternative services that have been marked as broken.
// The brokenness of an alt-svc will expire after some time according to an
// exponential back-off formula: each time an alt-svc is marked broken, the
// expiration delay will be some constant multiple of its previous expiration
// delay. This prevents broken alt-svcs from being retried too often by the
// network stack.
class NET_EXPORT_PRIVATE BrokenAlternativeServices {
public:
// Delegate to be used by owner so it can be notified when the brokenness of
// an AlternativeService expires.
class NET_EXPORT Delegate {
public:
// Called when a broken alternative service's expiration time is reached.
virtual void OnExpireBrokenAlternativeService(
const AlternativeService& expired_alternative_service) = 0;
virtual ~Delegate() {}
};

// |delegate| will be notified when a broken alternative service expires. It
// must not be null.
// |clock| is used for setting expiration times and scheduling the
// expiration of broken alternative services. It must not be null.
// |delegate| and |clock| are both unowned and must outlive this.
BrokenAlternativeServices(Delegate* delegate, base::TickClock* clock);

BrokenAlternativeServices(const BrokenAlternativeServices&) = delete;
void operator=(const BrokenAlternativeServices&) = delete;

~BrokenAlternativeServices();

// Marks |alternative_service| as broken until after some expiration delay
// (determined by how many times it's been marked broken before). Being broken
// will cause IsAlternativeServiceBroken(alternative_service) to return true
// until the expiration time is reached, or until
// ConfirmAlternativeService(alternative_service) is called.
void MarkAlternativeServiceBroken(
const AlternativeService& alternative_service);

// Marks |alternative_service| as recently broken. Being recently broken will
// cause WasAlternativeServiceRecentlyBroken(alternative_service) to return
// true until ConfirmAlternativeService(alternative_service) is called.
void MarkAlternativeServiceRecentlyBroken(
const AlternativeService& alternative_service);

// Returns true if MarkAlternativeServiceBroken(alternative_service) has been
// called, the expiration time has not been reached, and
// ConfirmAlternativeService(alternative_service) has not been called
// afterwards.
bool IsAlternativeServiceBroken(
const AlternativeService& alternative_service) const;

// Returns true if MarkAlternativeServiceRecentlyBroken(alternative_service)
// or MarkAlternativeServiceBroken(alternative_service) has been called and
// ConfirmAlternativeService(alternative_service) has not been called
// afterwards (even if brokenness of |alternative_service| has expired).
bool WasAlternativeServiceRecentlyBroken(
const AlternativeService& alternative_service);

// Marks |alternative_service| as not broken and not recently broken.
void ConfirmAlternativeService(const AlternativeService& alternative_service);

private:
// TODO (wangyix): modify HttpServerPropertiesImpl unit tests so this
// friendness is no longer required.
friend class HttpServerPropertiesImplPeer;

// A pair containing a broken AlternativeService and the expiration time of
// its brokenness.
struct BrokenAltSvcExpireInfo {
BrokenAltSvcExpireInfo(const AlternativeService& alt_svc,
base::TimeTicks expire)
: alternative_service(alt_svc), expiration(expire) {}

AlternativeService alternative_service;
base::TimeTicks expiration;
};

typedef std::list<BrokenAltSvcExpireInfo> BrokenAlternativeServiceList;

typedef std::unordered_map<AlternativeService,
BrokenAlternativeServiceList::iterator,
AlternativeServiceHash>
BrokenAlternativeServiceMap;

// Inserts |alternative_service| and its |expiration| time into
// |broken_alternative_service_list_| and |broken_alternative_service_map_|.
// |it| is the position in |broken_alternative_service_list_| where it was
// inserted.
bool AddToBrokenAlternativeServiceListAndMap(
const AlternativeService& alternative_service,
base::TimeTicks expiration,
BrokenAlternativeServiceList::iterator* it);

void ExpireBrokenAlternateProtocolMappings();
void ScheduleBrokenAlternateProtocolMappingsExpiration();

Delegate* delegate_; // Unowned
base::TickClock* clock_; // Unowned

// List of <broken alt svc, expiration time> pairs sorted by expiration time.
BrokenAlternativeServiceList broken_alternative_service_list_;
// A map from broken alt-svcs to their iterator pointing to that alt-svc's
// position in |broken_alternative_service_list_|.
BrokenAlternativeServiceMap broken_alternative_service_map_;

// Maps broken alternative services to how many times they've been marked
// broken.
RecentlyBrokenAlternativeServices recently_broken_alternative_services_;

// Used for scheduling the task that expires the brokenness of alternative
// services.
base::OneShotTimer expiration_timer_;

base::WeakPtrFactory<BrokenAlternativeServices> weak_ptr_factory_;
};

} // namespace net

#endif // NET_HTTP_BROKEN_ALTERNATIVE_SERVICES_H_
Loading

0 comments on commit 64ccc57

Please sign in to comment.