diff options
author | Julian Andres Klode <jak@debian.org> | 2018-01-02 22:15:50 +0100 |
---|---|---|
committer | Julian Andres Klode <jak@debian.org> | 2018-01-03 15:31:36 +0100 |
commit | 3bbd328396745d0dd6c5585935040082a2c41e3e (patch) | |
tree | 6a3ddf1026c286cc528c04ae3bbb55ad653228d2 /methods | |
parent | 53bdec3ebea66153b320ee497871355eb526e0f2 (diff) |
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
Diffstat (limited to 'methods')
-rw-r--r-- | methods/connect.cc | 126 |
1 files changed, 100 insertions, 26 deletions
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 <gnutls/gnutls.h> #include <gnutls/x509.h> +#include <list> #include <set> #include <sstream> #include <string> @@ -35,6 +36,7 @@ #include <netdb.h> #include <arpa/inet.h> #include <netinet/in.h> +#include <sys/select.h> #include <sys/socket.h> #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<struct addrinfo *> OrderAddresses(struct addrinfo *CurHost) return std::move(allAddrs); } /*}}}*/ +// Check for errors and report them /*{{{*/ +static ResultState WaitAndCheckErrors(std::list<Connection> &Conns, std::unique_ptr<MethodFd> &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<MethodFd> &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<Connection> 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; |