From 3bbd328396745d0dd6c5585935040082a2c41e3e Mon Sep 17 00:00:00 2001 From: Julian Andres Klode Date: Tue, 2 Jan 2018 22:15:50 +0100 Subject: Add rapid "happy eyeballs" connection fallback (RFC 8305) Try establishing connections in alternating address families in rapid intervals of 250 ms, adding more connections to the wait list until one succeeds (RFC 8305, happy eyeballs 2). It is important that WaitAndCheckErrors() waits until it has a successful connection, a time out, or all connections failed - otherwise the timing between tries might be wrong, and the final long wait might exit early because one connection failed without trying the others. Timing wise, this only works correctly on Linux, as select() counts down there. But we rely on that in some other places too, so this is not the time to fix that. Timeouts are only reported in the final long wait - the short inner waits are expected to time out more often, and multiple times, we do not want to report them. Closes: #668948 LP: #1308200 Gbp-Dch: paragraph --- doc/examples/configure-index | 3 ++ methods/connect.cc | 126 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 103 insertions(+), 26 deletions(-) diff --git a/doc/examples/configure-index b/doc/examples/configure-index index a765fbe42..153637ebc 100644 --- a/doc/examples/configure-index +++ b/doc/examples/configure-index @@ -257,6 +257,7 @@ Acquire Proxy "http://127.0.0.1:3128"; Proxy::http.us.debian.org "DIRECT"; // Specific per-host setting Timeout "120"; + ConnectionAttemptDelayMsec "250"; Pipeline-Depth "5"; AllowRedirect "true"; @@ -285,6 +286,7 @@ Acquire AllowRedirect "true"; Timeout "120"; + ConnectionAttemptDelayMsec "250"; AllowRedirect "true"; // Cache Control. Note these do not work with Squid 2.0.2 @@ -312,6 +314,7 @@ Acquire }; Timeout "120"; + ConnectionAttemptDelayMsec "250"; /* Passive mode control, proxy, non-proxy and per-host. Pasv mode is preferred if possible */ diff --git a/methods/connect.cc b/methods/connect.cc index 160491d96..334a1d3f3 100644 --- a/methods/connect.cc +++ b/methods/connect.cc @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -35,6 +36,7 @@ #include #include #include +#include #include #include "aptmethod.h" @@ -146,14 +148,19 @@ struct Connection std::stringstream ss; ioprintf(ss, _("[IP: %s %s]"), Name, Service); Owner->SetIP(ss.str()); + Owner->Status(_("Connected to %s (%s)"), Host.c_str(), Name); + _error->Discard(); + Owner->SetFailReason(""); + LastUsed = Addr; return std::move(Fd); } - ResultState DoConnect(unsigned long TimeOut); + ResultState DoConnect(); + ResultState CheckError(); }; -ResultState Connection::DoConnect(unsigned long TimeOut) +ResultState Connection::DoConnect() { getnameinfo(Addr->ai_addr,Addr->ai_addrlen, Name,sizeof(Name),Service,sizeof(Service), @@ -183,19 +190,7 @@ ResultState Connection::DoConnect(unsigned long TimeOut) return ResultState::TRANSIENT_ERROR; } - /* This implements a timeout for connect by opening the connection - nonblocking */ - if (WaitFd(Fd->Fd(), true, TimeOut) == false) - { - bad_addr.insert(bad_addr.begin(), std::string(Name)); - Owner->SetFailReason("Timeout"); - _error->Error(_("Could not connect to %s:%s (%s), " - "connection timed out"), - Host.c_str(), Service, Name); - return ResultState::TRANSIENT_ERROR; - } - - return CheckError(); + return ResultState::SUCCESSFUL; } ResultState Connection::CheckError() @@ -271,6 +266,82 @@ static std::vector OrderAddresses(struct addrinfo *CurHost) return std::move(allAddrs); } /*}}}*/ +// Check for errors and report them /*{{{*/ +static ResultState WaitAndCheckErrors(std::list &Conns, std::unique_ptr &Fd, long TimeoutMsec, bool ReportTimeout) +{ + // The last error detected + ResultState Result = ResultState::TRANSIENT_ERROR; + + struct timeval tv = { + // Split our millisecond timeout into seconds and microseconds + .tv_sec = TimeoutMsec / 1000, + .tv_usec = (TimeoutMsec % 1000) * 1000, + }; + + // We will return once we have no more connections, a time out, or + // a success. + while (!Conns.empty()) + { + fd_set Set; + int nfds = -1; + + FD_ZERO(&Set); + + for (auto &Conn : Conns) + { + int fd = Conn.Fd->Fd(); + FD_SET(fd, &Set); + nfds = std::max(nfds, fd); + } + + { + int Res; + do + { + Res = select(nfds + 1, 0, &Set, 0, (TimeoutMsec != 0 ? &tv : 0)); + } while (Res < 0 && errno == EINTR); + + if (Res == 0) + { + if (ReportTimeout) + { + for (auto &Conn : Conns) + { + Conn.Owner->SetFailReason("Timeout"); + _error->Error(_("Could not connect to %s:%s (%s), " + "connection timed out"), + Conn.Host.c_str(), Conn.Service, Conn.Name); + } + } + return ResultState::TRANSIENT_ERROR; + } + } + + // iterate over connections, remove failed ones, and return if + // there was a successful one. + for (auto ConnI = Conns.begin(); ConnI != Conns.end();) + { + if (!FD_ISSET(ConnI->Fd->Fd(), &Set)) + { + ConnI++; + continue; + } + + Result = ConnI->CheckError(); + if (Result == ResultState::SUCCESSFUL) + { + Fd = ConnI->Take(); + return Result; + } + + // Connection failed. Erase it and continue to next position + ConnI = Conns.erase(ConnI); + } + } + + return Result; +} + /*}}}*/ // Connect to a given Hostname /*{{{*/ static ResultState ConnectToHostname(std::string const &Host, int const Port, const char *const Service, int DefPort, std::unique_ptr &Fd, @@ -375,19 +446,22 @@ static ResultState ConnectToHostname(std::string const &Host, int const Port, // When we have an IP rotation stay with the last IP. auto Addresses = OrderAddresses(LastUsed != nullptr ? LastUsed : LastHostAddr); + std::list Conns; - for (auto CurHost : Addresses) + for (auto Addr : Addresses) { - _error->Discard(); - Connection Conn(CurHost, Host, Owner); - auto const result = Conn.DoConnect(TimeOut); - if (result == ResultState::SUCCESSFUL) - { - Fd = Conn.Take(); - LastUsed = CurHost; - return result; - } - } + Connection Conn(Addr, Host, Owner); + if (Conn.DoConnect() != ResultState::SUCCESSFUL) + continue; + + Conns.push_back(std::move(Conn)); + + if (WaitAndCheckErrors(Conns, Fd, Owner->ConfigFindI("ConnectionAttemptDelayMsec", 250), false) == ResultState::SUCCESSFUL) + return ResultState::SUCCESSFUL; + } + + if (WaitAndCheckErrors(Conns, Fd, TimeOut * 1000, true) == ResultState::SUCCESSFUL) + return ResultState::SUCCESSFUL; if (_error->PendingError() == true) return ResultState::FATAL_ERROR; -- cgit v1.2.3