]> git.ipfire.org Git - thirdparty/squid.git/blobdiff - src/peer_select.cc
Docs: Copyright updates for 2018 (#114)
[thirdparty/squid.git] / src / peer_select.cc
index 0470784e8698df17cb916bebd97b3ca5e7e4721c..0ac9a090615c565a0386e4ca0a9d619003ed5b36 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 1996-2014 The Squid Software Foundation and contributors
+ * Copyright (C) 1996-2018 The Squid Software Foundation and contributors
  *
  * Squid software is distributed under GPLv2+ license and includes
  * contributions from numerous individuals and organizations.
 
 #include "squid.h"
 #include "acl/FilledChecklist.h"
+#include "base/InstanceId.h"
 #include "CachePeer.h"
 #include "carp.h"
 #include "client_side.h"
-#include "DnsLookupDetails.h"
+#include "dns/LookupDetails.h"
 #include "errorpage.h"
 #include "event.h"
 #include "FwdState.h"
 #include "globals.h"
 #include "hier_code.h"
 #include "htcp.h"
+#include "http/Stream.h"
 #include "HttpRequest.h"
 #include "icmp/net_db.h"
 #include "ICP.h"
 #include "Store.h"
 #include "URL.h"
 
+/**
+ * A CachePeer which has been selected as a possible destination.
+ * Listed as pointers here so as to prevent duplicates being added but will
+ * be converted to a set of IP address path options before handing back out
+ * to the caller.
+ *
+ * Certain connection flags and outgoing settings will also be looked up and
+ * set based on the received request and CachePeer settings before handing back.
+ */
+class FwdServer
+{
+    MEMPROXY_CLASS(FwdServer);
+
+public:
+    FwdServer(CachePeer *p, hier_code c) :
+        _peer(p),
+        code(c),
+        next(nullptr)
+    {}
+
+    CbcPointer<CachePeer> _peer;                /* NULL --> origin server */
+    hier_code code;
+    FwdServer *next;
+};
+
 static struct {
     int timeouts;
 } PeerStats;
@@ -45,32 +72,39 @@ static const char *DirectStr[] = {
     "DIRECT_YES"
 };
 
-static void peerSelectFoo(ps_state *);
-static void peerPingTimeout(void *data);
-static IRCB peerHandlePingReply;
-static void peerIcpParentMiss(CachePeer *, icp_common_t *, ps_state *);
-#if USE_HTCP
-static void peerHtcpParentMiss(CachePeer *, HtcpReplyData *, ps_state *);
-static void peerHandleHtcpReply(CachePeer *, peer_t, HtcpReplyData *, void *);
-#endif
-static int peerCheckNetdbDirect(ps_state * psstate);
-static void peerGetSomeNeighbor(ps_state *);
-static void peerGetSomeNeighborReplies(ps_state *);
-static void peerGetSomeDirect(ps_state *);
-static void peerGetSomeParent(ps_state *);
-static void peerGetAllParents(ps_state *);
-static void peerAddFwdServer(FwdServer **, CachePeer *, hier_code);
-static void peerSelectPinned(ps_state * ps);
-static void peerSelectDnsResults(const ipcache_addrs *ia, const DnsLookupDetails &details, void *data);
+/// a helper class to report a selected destination (for debugging)
+class PeerSelectionDumper
+{
+public:
+    PeerSelectionDumper(const PeerSelector * const aSelector, const CachePeer * const aPeer, const hier_code aCode):
+        selector(aSelector), peer(aPeer), code(aCode) {}
 
-CBDATA_CLASS_INIT(ps_state);
+    const PeerSelector * const selector; ///< selection parameters
+    const CachePeer * const peer; ///< successful selection info
+    const hier_code code; ///< selection algorithm
+};
 
-ps_state::~ps_state()
+CBDATA_CLASS_INIT(PeerSelector);
+
+/// prints PeerSelectionDumper (for debugging)
+static std::ostream &
+operator <<(std::ostream &os, const PeerSelectionDumper &fsd)
+{
+    os << hier_code_str[fsd.code];
+
+    if (fsd.peer)
+        os << '/' << fsd.peer->host;
+    else if (fsd.selector) // useful for DIRECT and gone PINNED destinations
+        os << '#' << fsd.selector->request->url.host();
+
+    return os;
+}
+
+PeerSelector::~PeerSelector()
 {
     while (servers) {
         FwdServer *next = servers->next;
-        cbdataReferenceDone(servers->_peer);
-        memFree(servers, MEM_FWD_SERVER);
+        delete servers;
         servers = next;
     }
 
@@ -78,13 +112,13 @@ ps_state::~ps_state()
         debugs(44, 3, entry->url());
 
         if (entry->ping_status == PING_WAITING)
-            eventDelete(peerPingTimeout, this);
+            eventDelete(HandlePingTimeout, this);
 
         entry->ping_status = PING_DONE;
     }
 
     if (acl_checklist) {
-        debugs(44, DBG_IMPORTANT, "calling aclChecklistFree() from ps_state destructor");
+        debugs(44, DBG_IMPORTANT, "BUG: peer selector gone while waiting for a slow ACL");
         delete acl_checklist;
     }
 
@@ -106,7 +140,7 @@ peerSelectIcpPing(HttpRequest * request, int direct, StoreEntry * entry)
     assert(entry);
     assert(entry->ping_status == PING_NONE);
     assert(direct != DIRECT_YES);
-    debugs(44, 3, "peerSelectIcpPing: " << entry->url()  );
+    debugs(44, 3, entry->url());
 
     if (!request->flags.hierarchical && direct != DIRECT_NO)
         return 0;
@@ -117,38 +151,29 @@ peerSelectIcpPing(HttpRequest * request, int direct, StoreEntry * entry)
 
     n = neighborsCount(request);
 
-    debugs(44, 3, "peerSelectIcpPing: counted " << n << " neighbors");
+    debugs(44, 3, "counted " << n << " neighbors");
 
     return n;
 }
 
-void
-peerSelect(Comm::ConnectionList * paths,
+static void
+peerSelect(PeerSelectionInitiator *initiator,
            HttpRequest * request,
            AccessLogEntry::Pointer const &al,
-           StoreEntry * entry,
-           PSC * callback,
-           void *callback_data)
+           StoreEntry * entry)
 {
-    ps_state *psstate;
-
     if (entry)
         debugs(44, 3, *entry << ' ' << entry->url());
     else
         debugs(44, 3, request->method);
 
-    psstate = new ps_state;
+    const auto selector = new PeerSelector(initiator);
 
-    psstate->request = request;
-    HTTPMSGLOCK(psstate->request);
-    psstate->al = al;
+    selector->request = request;
+    HTTPMSGLOCK(selector->request);
+    selector->al = al;
 
-    psstate->entry = entry;
-    psstate->paths = paths;
-
-    psstate->callback = callback;
-
-    psstate->callback_data = cbdataReference(callback_data);
+    selector->entry = entry;
 
 #if USE_CACHE_DIGESTS
 
@@ -156,24 +181,31 @@ peerSelect(Comm::ConnectionList * paths,
 
 #endif
 
-    if (psstate->entry)
-        psstate->entry->lock("peerSelect");
+    if (selector->entry)
+        selector->entry->lock("peerSelect");
 
-    peerSelectFoo(psstate);
+    selector->selectMore();
 }
 
-static void
-peerCheckNeverDirectDone(allow_t answer, void *data)
+void
+PeerSelectionInitiator::startSelectingDestinations(HttpRequest *request, const AccessLogEntry::Pointer &ale, StoreEntry *entry)
 {
-    ps_state *psstate = (ps_state *) data;
-    psstate->acl_checklist = NULL;
-    debugs(44, 3, "peerCheckNeverDirectDone: " << answer);
-    psstate->never_direct = answer;
+    subscribed = true;
+    peerSelect(this, request, ale, entry);
+    // and wait for noteDestination() and/or noteDestinationsEnd() calls
+}
+
+void
+PeerSelector::checkNeverDirectDone(const allow_t answer)
+{
+    acl_checklist = nullptr;
+    debugs(44, 3, answer);
+    never_direct = answer;
     switch (answer) {
     case ACCESS_ALLOWED:
         /** if never_direct says YES, do that. */
-        psstate->direct = DIRECT_NO;
-        debugs(44, 3, HERE << "direct = " << DirectStr[psstate->direct] << " (never_direct allow)");
+        direct = DIRECT_NO;
+        debugs(44, 3, "direct = " << DirectStr[direct] << " (never_direct allow)");
         break;
     case ACCESS_DENIED: // not relevant.
     case ACCESS_DUNNO:  // not relevant.
@@ -182,21 +214,26 @@ peerCheckNeverDirectDone(allow_t answer, void *data)
         debugs(44, DBG_IMPORTANT, "WARNING: never_direct resulted in " << answer << ". Username ACLs are not reliable here.");
         break;
     }
-    peerSelectFoo(psstate);
+    selectMore();
 }
 
-static void
-peerCheckAlwaysDirectDone(allow_t answer, void *data)
+void
+PeerSelector::CheckNeverDirectDone(allow_t answer, void *data)
+{
+    static_cast<PeerSelector*>(data)->checkNeverDirectDone(answer);
+}
+
+void
+PeerSelector::checkAlwaysDirectDone(const allow_t answer)
 {
-    ps_state *psstate = (ps_state *)data;
-    psstate->acl_checklist = NULL;
-    debugs(44, 3, "peerCheckAlwaysDirectDone: " << answer);
-    psstate->always_direct = answer;
+    acl_checklist = nullptr;
+    debugs(44, 3, answer);
+    always_direct = answer;
     switch (answer) {
     case ACCESS_ALLOWED:
         /** if always_direct says YES, do that. */
-        psstate->direct = DIRECT_YES;
-        debugs(44, 3, HERE << "direct = " << DirectStr[psstate->direct] << " (always_direct allow)");
+        direct = DIRECT_YES;
+        debugs(44, 3, "direct = " << DirectStr[direct] << " (always_direct allow)");
         break;
     case ACCESS_DENIED: // not relevant.
     case ACCESS_DUNNO:  // not relevant.
@@ -205,25 +242,42 @@ peerCheckAlwaysDirectDone(allow_t answer, void *data)
         debugs(44, DBG_IMPORTANT, "WARNING: always_direct resulted in " << answer << ". Username ACLs are not reliable here.");
         break;
     }
-    peerSelectFoo(psstate);
+    selectMore();
 }
 
 void
-peerSelectDnsPaths(ps_state *psstate)
+PeerSelector::CheckAlwaysDirectDone(allow_t answer, void *data)
 {
-    FwdServer *fs = psstate->servers;
+    static_cast<PeerSelector*>(data)->checkAlwaysDirectDone(answer);
+}
+
+/// \returns true (after destroying "this") if the peer initiator is gone
+/// \returns false (without side effects) otherwise
+bool
+PeerSelector::selectionAborted()
+{
+    if (interestedInitiator())
+        return false;
 
-    if (!cbdataReferenceValid(psstate->callback_data)) {
-        debugs(44, 3, "Aborting peer selection. Parent Job went away.");
-        delete psstate;
+    debugs(44, 3, "Aborting peer selection: Initiator gone or lost interest.");
+    delete this;
+    return true;
+}
+
+/// A single DNS resolution loop iteration: Converts selected FwdServer to IPs.
+void
+PeerSelector::resolveSelected()
+{
+    if (selectionAborted())
         return;
-    }
+
+    FwdServer *fs = servers;
 
     // Bug 3243: CVE 2009-0801
     // Bypass of browser same-origin access control in intercepted communication
     // To resolve this we must use only the original client destination when going DIRECT
     // on intercepted traffic which failed Host verification
-    const HttpRequest *req = psstate->request;
+    const HttpRequest *req = request;
     const bool isIntercepted = !req->flags.redirected &&
                                (req->flags.intercepted || req->flags.interceptTproxy);
     const bool useOriginalDst = Config.onoff.client_dst_passthru || !req->flags.hostVerified;
@@ -234,190 +288,160 @@ peerSelectDnsPaths(ps_state *psstate)
             // construct a "result" adding the ORIGINAL_DST to the set instead of DIRECT
             Comm::ConnectionPointer p = new Comm::Connection();
             p->remote = req->clientConnectionManager->clientConnection->local;
-            p->peerType = ORIGINAL_DST; // fs->code is DIRECT. This fixes the display.
-            p->setPeer(fs->_peer);
-
-            // check for a configured outgoing address for this destination...
-            getOutgoingAddress(psstate->request, p);
-            psstate->paths->push_back(p);
+            fs->code = ORIGINAL_DST; // fs->code is DIRECT. This fixes the display.
+            handlePath(p, *fs);
         }
 
         // clear the used fs and continue
-        psstate->servers = fs->next;
-        cbdataReferenceDone(fs->_peer);
-        memFree(fs, MEM_FWD_SERVER);
-        peerSelectDnsPaths(psstate);
+        servers = fs->next;
+        delete fs;
+        resolveSelected();
         return;
     }
 
     // convert the list of FwdServer destinations into destinations IP addresses
-    if (fs && psstate->paths->size() < (unsigned int)Config.forward_max_tries) {
+    if (fs && wantsMoreDestinations()) {
         // send the next one off for DNS lookup.
-        const char *host = fs->_peer ? fs->_peer->host : psstate->request->GetHost();
-        debugs(44, 2, "Find IP destination for: " << psstate->entry->url() << "' via " << host);
-        ipcache_nbgethostbyname(host, peerSelectDnsResults, psstate);
+        const char *host = fs->_peer.valid() ? fs->_peer->host : request->url.host();
+        debugs(44, 2, "Find IP destination for: " << url() << "' via " << host);
+        Dns::nbgethostbyname(host, this);
         return;
     }
 
     // Bug 3605: clear any extra listed FwdServer destinations, when the options exceeds max_foward_tries.
     // due to the allocation method of fs, we must deallocate each manually.
     // TODO: use a std::list so we can get the size and abort adding whenever the selection loops reach Config.forward_max_tries
-    if (fs && psstate->paths->size() >= (unsigned int)Config.forward_max_tries) {
+    if (fs) {
+        assert(fs == servers);
         while (fs) {
-            FwdServer *next = fs->next;
-            cbdataReferenceDone(fs->_peer);
-            memFree(fs, MEM_FWD_SERVER);
-            fs = next;
+            servers = fs->next;
+            delete fs;
+            fs = servers;
         }
     }
 
     // done with DNS lookups. pass back to caller
-    PSC *callback = psstate->callback;
-    psstate->callback = NULL;
-
-    debugs(44, 2, (psstate->paths->size()<1?"Failed to select source":"Found sources") << " for '" << psstate->url() << "'");
-    debugs(44, 2, "  always_direct = " << psstate->always_direct);
-    debugs(44, 2, "   never_direct = " << psstate->never_direct);
-    if (psstate->paths) {
-        for (size_t i = 0; i < psstate->paths->size(); ++i) {
-            if ((*psstate->paths)[i]->peerType == HIER_DIRECT)
-                debugs(44, 2, "         DIRECT = " << (*psstate->paths)[i]);
-            else if ((*psstate->paths)[i]->peerType == ORIGINAL_DST)
-                debugs(44, 2, "   ORIGINAL_DST = " << (*psstate->paths)[i]);
-            else if ((*psstate->paths)[i]->peerType == PINNED)
-                debugs(44, 2, "         PINNED = " << (*psstate->paths)[i]);
-            else
-                debugs(44, 2, "     cache_peer = " << (*psstate->paths)[i]);
-        }
-    }
-    debugs(44, 2, "       timedout = " << psstate->ping.timedout);
 
-    psstate->ping.stop = current_time;
-    psstate->request->hier.ping = psstate->ping;
+    debugs(44, 2, id << " found all " << foundPaths << " destinations for " << url());
+    debugs(44, 2, "  always_direct = " << always_direct);
+    debugs(44, 2, "   never_direct = " << never_direct);
+    debugs(44, 2, "       timedout = " << ping.timedout);
+
+    ping.stop = current_time;
+    request->hier.ping = ping; // final result
 
-    void *cbdata;
-    if (cbdataReferenceValidDone(psstate->callback_data, &cbdata)) {
-        callback(psstate->paths, psstate->lastError, cbdata);
-        psstate->lastError = NULL; // FwdState has taken control over the ErrorState object.
+    if (lastError && foundPaths) {
+        // nobody cares about errors if we found destinations despite them
+        debugs(44, 3, "forgetting the last error");
+        delete lastError;
+        lastError = nullptr;
     }
 
-    delete psstate;
+    if (const auto initiator = interestedInitiator())
+        initiator->noteDestinationsEnd(lastError);
+    lastError = nullptr; // initiator owns the ErrorState object now
+    delete this;
 }
 
-static void
-peerSelectDnsResults(const ipcache_addrs *ia, const DnsLookupDetails &details, void *data)
+void
+PeerSelector::noteLookup(const Dns::LookupDetails &details)
 {
-    ps_state *psstate = (ps_state *)data;
+    /* ignore lookup delays that occurred after the initiator moved on */
 
-    if (!cbdataReferenceValid(psstate->callback_data)) {
-        debugs(44, 3, "Aborting peer selection. Parent Job went away.");
-        delete psstate;
+    if (selectionAborted())
         return;
-    }
-
-    psstate->request->recordLookup(details);
-
-    FwdServer *fs = psstate->servers;
-    if (ia != NULL) {
 
-        assert(ia->cur < ia->count);
+    if (!wantsMoreDestinations())
+        return;
 
-        // loop over each result address, adding to the possible destinations.
-        int ip = ia->cur;
-        for (int n = 0; n < ia->count; ++n, ++ip) {
-            Comm::ConnectionPointer p;
+    request->recordLookup(details);
+}
 
-            if (ip >= ia->count) ip = 0; // looped back to zero.
+void
+PeerSelector::noteIp(const Ip::Address &ip)
+{
+    if (selectionAborted())
+        return;
 
-            // Enforce forward_max_tries configuration.
-            if (psstate->paths->size() >= (unsigned int)Config.forward_max_tries)
-                break;
+    if (!wantsMoreDestinations())
+        return;
 
-            // for TPROXY spoofing we must skip unusable addresses.
-            if (psstate->request->flags.spoofClientIp && !(fs->_peer && fs->_peer->options.no_tproxy) ) {
-                if (ia->in_addrs[ip].isIPv4() != psstate->request->client_addr.isIPv4()) {
-                    // we CAN'T spoof the address on this link. find another.
-                    continue;
-                }
-            }
+    const auto peer = servers->_peer.valid();
 
-            p = new Comm::Connection();
-            p->remote = ia->in_addrs[ip];
+    // for TPROXY spoofing, we must skip unusable addresses
+    if (request->flags.spoofClientIp && !(peer && peer->options.no_tproxy) ) {
+        if (ip.isIPv4() != request->client_addr.isIPv4())
+            return; // cannot spoof the client address on this link
+    }
 
-            // when IPv6 is disabled we cannot use it
-            if (!Ip::EnableIpv6 && p->remote.isIPv6()) {
-                const char *host = (fs->_peer ? fs->_peer->host : psstate->request->GetHost());
-                ipcacheMarkBadAddr(host, p->remote);
-                continue;
-            }
+    Comm::ConnectionPointer p = new Comm::Connection();
+    p->remote = ip;
+    p->remote.port(peer ? peer->http_port : request->url.port());
+    handlePath(p, *servers);
+}
 
-            if (fs->_peer)
-                p->remote.port(fs->_peer->http_port);
-            else
-                p->remote.port(psstate->request->port);
-            p->peerType = fs->code;
-            p->setPeer(fs->_peer);
+void
+PeerSelector::noteIps(const Dns::CachedIps *ia, const Dns::LookupDetails &details)
+{
+    if (selectionAborted())
+        return;
 
-            // check for a configured outgoing address for this destination...
-            getOutgoingAddress(psstate->request, p);
-            psstate->paths->push_back(p);
-        }
-    } else {
-        debugs(44, 3, HERE << "Unknown host: " << (fs->_peer ? fs->_peer->host : psstate->request->GetHost()));
+    FwdServer *fs = servers;
+    if (!ia) {
+        debugs(44, 3, "Unknown host: " << (fs->_peer.valid() ? fs->_peer->host : request->url.host()));
         // discard any previous error.
-        delete psstate->lastError;
-        psstate->lastError = NULL;
+        delete lastError;
+        lastError = NULL;
         if (fs->code == HIER_DIRECT) {
-            psstate->lastError = new ErrorState(ERR_DNS_FAIL, Http::scServiceUnavailable, psstate->request);
-            psstate->lastError->dnsError = details.error;
+            lastError = new ErrorState(ERR_DNS_FAIL, Http::scServiceUnavailable, request);
+            lastError->dnsError = details.error;
         }
     }
+    // else noteIp() calls have already processed all IPs in *ia
 
-    psstate->servers = fs->next;
-    cbdataReferenceDone(fs->_peer);
-    memFree(fs, MEM_FWD_SERVER);
+    servers = fs->next;
+    delete fs;
 
-    // see if more paths can be found
-    peerSelectDnsPaths(psstate);
+    // continue resolving selected peers
+    resolveSelected();
 }
 
-static int
-peerCheckNetdbDirect(ps_state * psstate)
+int
+PeerSelector::checkNetdbDirect()
 {
 #if USE_ICMP
     CachePeer *p;
     int myrtt;
     int myhops;
 
-    if (psstate->direct == DIRECT_NO)
+    if (direct == DIRECT_NO)
         return 0;
 
     /* base lookup on RTT and Hops if ICMP NetDB is enabled. */
 
-    myrtt = netdbHostRtt(psstate->request->GetHost());
-
-    debugs(44, 3, "peerCheckNetdbDirect: MY RTT = " << myrtt << " msec");
-    debugs(44, 3, "peerCheckNetdbDirect: minimum_direct_rtt = " << Config.minDirectRtt << " msec");
+    myrtt = netdbHostRtt(request->url.host());
+    debugs(44, 3, "MY RTT = " << myrtt << " msec");
+    debugs(44, 3, "minimum_direct_rtt = " << Config.minDirectRtt << " msec");
 
     if (myrtt && myrtt <= Config.minDirectRtt)
         return 1;
 
-    myhops = netdbHostHops(psstate->request->GetHost());
+    myhops = netdbHostHops(request->url.host());
 
-    debugs(44, 3, "peerCheckNetdbDirect: MY hops = " << myhops);
-    debugs(44, 3, "peerCheckNetdbDirect: minimum_direct_hops = " << Config.minDirectHops);
+    debugs(44, 3, "MY hops = " << myhops);
+    debugs(44, 3, "minimum_direct_hops = " << Config.minDirectHops);
 
     if (myhops && myhops <= Config.minDirectHops)
         return 1;
 
-    p = whichPeer(psstate->closest_parent_miss);
+    p = whichPeer(closest_parent_miss);
 
     if (p == NULL)
         return 0;
 
-    debugs(44, 3, "peerCheckNetdbDirect: closest_parent_miss RTT = " << psstate->ping.p_rtt << " msec");
+    debugs(44, 3, "closest_parent_miss RTT = " << ping.p_rtt << " msec");
 
-    if (myrtt && myrtt <= psstate->ping.p_rtt)
+    if (myrtt && myrtt <= ping.p_rtt)
         return 1;
 
 #endif /* USE_ICMP */
@@ -425,131 +449,119 @@ peerCheckNetdbDirect(ps_state * psstate)
     return 0;
 }
 
-static void
-peerSelectFoo(ps_state * ps)
+void
+PeerSelector::selectMore()
 {
-    if (!cbdataReferenceValid(ps->callback_data)) {
-        debugs(44, 3, "Aborting peer selection. Parent Job went away.");
-        delete ps;
+    if (selectionAborted())
         return;
-    }
 
-    StoreEntry *entry = ps->entry;
-    HttpRequest *request = ps->request;
-    debugs(44, 3, request->method << ' ' << request->GetHost());
+    debugs(44, 3, request->method << ' ' << request->url.host());
 
     /** If we don't know whether DIRECT is permitted ... */
-    if (ps->direct == DIRECT_UNKNOWN) {
-        if (ps->always_direct == ACCESS_DUNNO) {
-            debugs(44, 3, "peerSelectFoo: direct = " << DirectStr[ps->direct] << " (always_direct to be checked)");
+    if (direct == DIRECT_UNKNOWN) {
+        if (always_direct == ACCESS_DUNNO) {
+            debugs(44, 3, "direct = " << DirectStr[direct] << " (always_direct to be checked)");
             /** check always_direct; */
             ACLFilledChecklist *ch = new ACLFilledChecklist(Config.accessList.AlwaysDirect, request, NULL);
-            ch->al = ps->al;
-            ps->acl_checklist = ch;
-            ps->acl_checklist->nonBlockingCheck(peerCheckAlwaysDirectDone, ps);
+            ch->al = al;
+            acl_checklist = ch;
+            acl_checklist->nonBlockingCheck(CheckAlwaysDirectDone, this);
             return;
-        } else if (ps->never_direct == ACCESS_DUNNO) {
-            debugs(44, 3, "peerSelectFoo: direct = " << DirectStr[ps->direct] << " (never_direct to be checked)");
+        } else if (never_direct == ACCESS_DUNNO) {
+            debugs(44, 3, "direct = " << DirectStr[direct] << " (never_direct to be checked)");
             /** check never_direct; */
             ACLFilledChecklist *ch = new ACLFilledChecklist(Config.accessList.NeverDirect, request, NULL);
-            ch->al = ps->al;
-            ps->acl_checklist = ch;
-            ps->acl_checklist->nonBlockingCheck(peerCheckNeverDirectDone, ps);
+            ch->al = al;
+            acl_checklist = ch;
+            acl_checklist->nonBlockingCheck(CheckNeverDirectDone, this);
             return;
         } else if (request->flags.noDirect) {
             /** if we are accelerating, direct is not an option. */
-            ps->direct = DIRECT_NO;
-            debugs(44, 3, "peerSelectFoo: direct = " << DirectStr[ps->direct] << " (forced non-direct)");
+            direct = DIRECT_NO;
+            debugs(44, 3, "direct = " << DirectStr[direct] << " (forced non-direct)");
         } else if (request->flags.loopDetected) {
             /** if we are in a forwarding-loop, direct is not an option. */
-            ps->direct = DIRECT_YES;
-            debugs(44, 3, "peerSelectFoo: direct = " << DirectStr[ps->direct] << " (forwarding loop detected)");
-        } else if (peerCheckNetdbDirect(ps)) {
-            ps->direct = DIRECT_YES;
-            debugs(44, 3, "peerSelectFoo: direct = " << DirectStr[ps->direct] << " (checkNetdbDirect)");
+            direct = DIRECT_YES;
+            debugs(44, 3, "direct = " << DirectStr[direct] << " (forwarding loop detected)");
+        } else if (checkNetdbDirect()) {
+            direct = DIRECT_YES;
+            debugs(44, 3, "direct = " << DirectStr[direct] << " (checkNetdbDirect)");
         } else {
-            ps->direct = DIRECT_MAYBE;
-            debugs(44, 3, "peerSelectFoo: direct = " << DirectStr[ps->direct] << " (default)");
+            direct = DIRECT_MAYBE;
+            debugs(44, 3, "direct = " << DirectStr[direct] << " (default)");
         }
 
-        debugs(44, 3, "peerSelectFoo: direct = " << DirectStr[ps->direct]);
+        debugs(44, 3, "direct = " << DirectStr[direct]);
     }
 
     if (!entry || entry->ping_status == PING_NONE)
-        peerSelectPinned(ps);
+        selectPinned();
     if (entry == NULL) {
         (void) 0;
     } else if (entry->ping_status == PING_NONE) {
-        peerGetSomeNeighbor(ps);
+        selectSomeNeighbor();
 
         if (entry->ping_status == PING_WAITING)
             return;
     } else if (entry->ping_status == PING_WAITING) {
-        peerGetSomeNeighborReplies(ps);
+        selectSomeNeighborReplies();
         entry->ping_status = PING_DONE;
     }
 
-    switch (ps->direct) {
+    switch (direct) {
 
     case DIRECT_YES:
-        peerGetSomeDirect(ps);
+        selectSomeDirect();
         break;
 
     case DIRECT_NO:
-        peerGetSomeParent(ps);
-        peerGetAllParents(ps);
+        selectSomeParent();
+        selectAllParents();
         break;
 
     default:
 
         if (Config.onoff.prefer_direct)
-            peerGetSomeDirect(ps);
+            selectSomeDirect();
 
         if (request->flags.hierarchical || !Config.onoff.nonhierarchical_direct) {
-            peerGetSomeParent(ps);
-            peerGetAllParents(ps);
+            selectSomeParent();
+            selectAllParents();
         }
 
         if (!Config.onoff.prefer_direct)
-            peerGetSomeDirect(ps);
+            selectSomeDirect();
 
         break;
     }
 
-    // resolve the possible peers
-    peerSelectDnsPaths(ps);
+    // end peer selection; start resolving selected peers
+    resolveSelected();
 }
 
 bool peerAllowedToUse(const CachePeer * p, HttpRequest * request);
 
-/**
- * peerSelectPinned
- *
- * Selects a pinned connection.
- */
-static void
-peerSelectPinned(ps_state * ps)
+/// Selects a pinned connection if it exists, is valid, and is allowed.
+void
+PeerSelector::selectPinned()
 {
-    HttpRequest *request = ps->request;
+    // TODO: Avoid all repeated calls. Relying on PING_DONE is not enough.
     if (!request->pinnedConnection())
         return;
     CachePeer *pear = request->pinnedConnection()->pinnedPeer();
     if (Comm::IsConnOpen(request->pinnedConnection()->validatePinnedConnection(request, pear))) {
-        if (pear && peerAllowedToUse(pear, request)) {
-            peerAddFwdServer(&ps->servers, pear, PINNED);
-            if (ps->entry)
-                ps->entry->ping_status = PING_DONE;     /* Skip ICP */
-        } else if (!pear && ps->direct != DIRECT_NO) {
-            peerAddFwdServer(&ps->servers, NULL, PINNED);
-            if (ps->entry)
-                ps->entry->ping_status = PING_DONE;     /* Skip ICP */
+        const bool usePinned = pear ? peerAllowedToUse(pear, request) : (direct != DIRECT_NO);
+        if (usePinned) {
+            addSelection(pear, PINNED);
+            if (entry)
+                entry->ping_status = PING_DONE; // skip ICP
         }
     }
+    // If the pinned connection is prohibited (for this request) or gone, then
+    // the initiator must decide whether it is OK to open a new one instead.
 }
 
 /**
- * peerGetSomeNeighbor
- *
  * Selects a neighbor (parent or sibling) based on one of the
  * following methods:
  *      Cache Digests
@@ -557,23 +569,21 @@ peerSelectPinned(ps_state * ps)
  *      ICMP Netdb RTT estimates
  *      ICP/HTCP queries
  */
-static void
-peerGetSomeNeighbor(ps_state * ps)
+void
+PeerSelector::selectSomeNeighbor()
 {
-    StoreEntry *entry = ps->entry;
-    HttpRequest *request = ps->request;
     CachePeer *p;
     hier_code code = HIER_NONE;
     assert(entry->ping_status == PING_NONE);
 
-    if (ps->direct == DIRECT_YES) {
+    if (direct == DIRECT_YES) {
         entry->ping_status = PING_DONE;
         return;
     }
 
 #if USE_CACHE_DIGESTS
     if ((p = neighborsDigestSelect(request))) {
-        if (neighborType(p, request) == PEER_PARENT)
+        if (neighborType(p, request->url) == PEER_PARENT)
             code = CD_PARENT_HIT;
         else
             code = CD_SIBLING_HIT;
@@ -581,28 +591,28 @@ peerGetSomeNeighbor(ps_state * ps)
 #endif
         if ((p = netdbClosestParent(request))) {
             code = CLOSEST_PARENT;
-        } else if (peerSelectIcpPing(request, ps->direct, entry)) {
-            debugs(44, 3, "peerSelect: Doing ICP pings");
-            ps->ping.start = current_time;
-            ps->ping.n_sent = neighborsUdpPing(request,
-                                               entry,
-                                               peerHandlePingReply,
-                                               ps,
-                                               &ps->ping.n_replies_expected,
-                                               &ps->ping.timeout);
-
-            if (ps->ping.n_sent == 0)
+        } else if (peerSelectIcpPing(request, direct, entry)) {
+            debugs(44, 3, "Doing ICP pings");
+            ping.start = current_time;
+            ping.n_sent = neighborsUdpPing(request,
+                                           entry,
+                                           HandlePingReply,
+                                           this,
+                                           &ping.n_replies_expected,
+                                           &ping.timeout);
+
+            if (ping.n_sent == 0)
                 debugs(44, DBG_CRITICAL, "WARNING: neighborsUdpPing returned 0");
-            debugs(44, 3, "peerSelect: " << ps->ping.n_replies_expected <<
-                   " ICP replies expected, RTT " << ps->ping.timeout <<
+            debugs(44, 3, ping.n_replies_expected <<
+                   " ICP replies expected, RTT " << ping.timeout <<
                    " msec");
 
-            if (ps->ping.n_replies_expected > 0) {
+            if (ping.n_replies_expected > 0) {
                 entry->ping_status = PING_WAITING;
-                eventAdd("peerPingTimeout",
-                         peerPingTimeout,
-                         ps,
-                         0.001 * ps->ping.timeout,
+                eventAdd("PeerSelector::HandlePingTimeout",
+                         HandlePingTimeout,
+                         this,
+                         0.001 * ping.timeout,
                          0);
                 return;
             }
@@ -610,79 +620,65 @@ peerGetSomeNeighbor(ps_state * ps)
 
     if (code != HIER_NONE) {
         assert(p);
-        debugs(44, 3, "peerSelect: " << hier_code_str[code] << "/" << p->host);
-        peerAddFwdServer(&ps->servers, p, code);
+        addSelection(p, code);
     }
 
     entry->ping_status = PING_DONE;
 }
 
-/*
- * peerGetSomeNeighborReplies
- *
- * Selects a neighbor (parent or sibling) based on ICP/HTCP replies.
- */
-static void
-peerGetSomeNeighborReplies(ps_state * ps)
+/// Selects a neighbor (parent or sibling) based on ICP/HTCP replies.
+void
+PeerSelector::selectSomeNeighborReplies()
 {
-    HttpRequest *request = ps->request;
     CachePeer *p = NULL;
     hier_code code = HIER_NONE;
-    assert(ps->entry->ping_status == PING_WAITING);
-    assert(ps->direct != DIRECT_YES);
+    assert(entry->ping_status == PING_WAITING);
+    assert(direct != DIRECT_YES);
 
-    if (peerCheckNetdbDirect(ps)) {
+    if (checkNetdbDirect()) {
         code = CLOSEST_DIRECT;
-        debugs(44, 3, "peerSelect: " << hier_code_str[code] << "/" << request->GetHost());
-        peerAddFwdServer(&ps->servers, NULL, code);
+        addSelection(nullptr, code);
         return;
     }
 
-    if ((p = ps->hit)) {
-        code = ps->hit_type == PEER_PARENT ? PARENT_HIT : SIBLING_HIT;
+    if ((p = hit)) {
+        code = hit_type == PEER_PARENT ? PARENT_HIT : SIBLING_HIT;
     } else {
-        if (!ps->closest_parent_miss.isAnyAddr()) {
-            p = whichPeer(ps->closest_parent_miss);
+        if (!closest_parent_miss.isAnyAddr()) {
+            p = whichPeer(closest_parent_miss);
             code = CLOSEST_PARENT_MISS;
-        } else if (!ps->first_parent_miss.isAnyAddr()) {
-            p = whichPeer(ps->first_parent_miss);
+        } else if (!first_parent_miss.isAnyAddr()) {
+            p = whichPeer(first_parent_miss);
             code = FIRST_PARENT_MISS;
         }
     }
     if (p && code != HIER_NONE) {
-        debugs(44, 3, "peerSelect: " << hier_code_str[code] << "/" << p->host);
-        peerAddFwdServer(&ps->servers, p, code);
+        addSelection(p, code);
     }
 }
 
-/*
- * peerGetSomeDirect
- *
- * Simply adds a 'direct' entry to the FwdServers list if this
- * request can be forwarded directly to the origin server
- */
-static void
-peerGetSomeDirect(ps_state * ps)
+/// Adds a "direct" entry if the request can be forwarded to the origin server.
+void
+PeerSelector::selectSomeDirect()
 {
-    if (ps->direct == DIRECT_NO)
+    if (direct == DIRECT_NO)
         return;
 
     /* WAIS is not implemented natively */
-    if (ps->request->url.getScheme() == AnyP::PROTO_WAIS)
+    if (request->url.getScheme() == AnyP::PROTO_WAIS)
         return;
 
-    peerAddFwdServer(&ps->servers, NULL, HIER_DIRECT);
+    addSelection(nullptr, HIER_DIRECT);
 }
 
-static void
-peerGetSomeParent(ps_state * ps)
+void
+PeerSelector::selectSomeParent()
 {
     CachePeer *p;
-    HttpRequest *request = ps->request;
     hier_code code = HIER_NONE;
-    debugs(44, 3, request->method << ' ' << request->GetHost());
+    debugs(44, 3, request->method << ' ' << request->url.host());
 
-    if (ps->direct == DIRECT_YES)
+    if (direct == DIRECT_YES)
         return;
 
     if ((p = peerSourceHashSelectParent(request))) {
@@ -704,18 +700,15 @@ peerGetSomeParent(ps_state * ps)
     }
 
     if (code != HIER_NONE) {
-        debugs(44, 3, "peerSelect: " << hier_code_str[code] << "/" << p->host);
-        peerAddFwdServer(&ps->servers, p, code);
+        addSelection(p, code);
     }
 }
 
-/* Adds alive parents. Used as a last resort for never_direct.
- */
-static void
-peerGetAllParents(ps_state * ps)
+/// Adds alive parents. Used as a last resort for never_direct.
+void
+PeerSelector::selectAllParents()
 {
     CachePeer *p;
-    HttpRequest *request = ps->request;
     /* Add all alive parents */
 
     for (p = Config.peers; p; p = p->next) {
@@ -723,15 +716,13 @@ peerGetAllParents(ps_state * ps)
          * parents to a request so we have to dig some here..
          */
 
-        if (neighborType(p, request) != PEER_PARENT)
+        if (neighborType(p, request->url) != PEER_PARENT)
             continue;
 
         if (!peerHTTPOkay(p, request))
             continue;
 
-        debugs(15, 3, "peerGetAllParents: adding alive parent " << p->host);
-
-        peerAddFwdServer(&ps->servers, p, ANY_OLD_PARENT);
+        addSelection(p, ANY_OLD_PARENT);
     }
 
     /* XXX: should add dead parents here, but it is currently
@@ -740,41 +731,40 @@ peerGetAllParents(ps_state * ps)
      */
     /* Add default parent as a last resort */
     if ((p = getDefaultParent(request))) {
-        peerAddFwdServer(&ps->servers, p, DEFAULT_PARENT);
+        addSelection(p, DEFAULT_PARENT);
     }
 }
 
-static void
-peerPingTimeout(void *data)
+void
+PeerSelector::handlePingTimeout()
 {
-    ps_state *psstate = (ps_state *)data;
-    StoreEntry *entry = psstate->entry;
+    debugs(44, 3, url());
 
     if (entry)
-        debugs(44, 3, "peerPingTimeout: '" << entry->url() << "'" );
-
-    if (!cbdataReferenceValid(psstate->callback_data)) {
-        /* request aborted */
         entry->ping_status = PING_DONE;
-        cbdataReferenceDone(psstate->callback_data);
-        delete psstate;
+
+    if (selectionAborted())
         return;
-    }
 
     ++PeerStats.timeouts;
-    psstate->ping.timedout = 1;
-    peerSelectFoo(psstate);
+    ping.timedout = 1;
+    selectMore();
+}
+
+void
+PeerSelector::HandlePingTimeout(void *data)
+{
+    static_cast<PeerSelector*>(data)->handlePingTimeout();
 }
 
 void
 peerSelectInit(void)
 {
     memset(&PeerStats, '\0', sizeof(PeerStats));
-    memDataInit(MEM_FWD_SERVER, "FwdServer", sizeof(FwdServer), 0);
 }
 
-static void
-peerIcpParentMiss(CachePeer * p, icp_common_t * header, ps_state * ps)
+void
+PeerSelector::handleIcpParentMiss(CachePeer *p, icp_common_t *header)
 {
     int rtt;
 
@@ -785,11 +775,11 @@ peerIcpParentMiss(CachePeer * p, icp_common_t * header, ps_state * ps)
             int hops = (header->pad >> 16) & 0xFFFF;
 
             if (rtt > 0 && rtt < 0xFFFF)
-                netdbUpdatePeer(ps->request, p, rtt, hops);
+                netdbUpdatePeer(request->url, p, rtt, hops);
 
-            if (rtt && (ps->ping.p_rtt == 0 || rtt < ps->ping.p_rtt)) {
-                ps->closest_parent_miss = p->in_addr;
-                ps->ping.p_rtt = rtt;
+            if (rtt && (ping.p_rtt == 0 || rtt < ping.p_rtt)) {
+                closest_parent_miss = p->in_addr;
+                ping.p_rtt = rtt;
             }
         }
     }
@@ -800,81 +790,77 @@ peerIcpParentMiss(CachePeer * p, icp_common_t * header, ps_state * ps)
         return;
 
     /* set FIRST_MISS if there is no CLOSEST parent */
-    if (!ps->closest_parent_miss.isAnyAddr())
+    if (!closest_parent_miss.isAnyAddr())
         return;
 
-    rtt = (tvSubMsec(ps->ping.start, current_time) - p->basetime) / p->weight;
+    rtt = (tvSubMsec(ping.start, current_time) - p->basetime) / p->weight;
 
     if (rtt < 1)
         rtt = 1;
 
-    if (ps->first_parent_miss.isAnyAddr() || rtt < ps->ping.w_rtt) {
-        ps->first_parent_miss = p->in_addr;
-        ps->ping.w_rtt = rtt;
+    if (first_parent_miss.isAnyAddr() || rtt < ping.w_rtt) {
+        first_parent_miss = p->in_addr;
+        ping.w_rtt = rtt;
     }
 }
 
-static void
-peerHandleIcpReply(CachePeer * p, peer_t type, icp_common_t * header, void *data)
+void
+PeerSelector::handleIcpReply(CachePeer *p, const peer_t type, icp_common_t *header)
 {
-    ps_state *psstate = (ps_state *)data;
     icp_opcode op = header->getOpCode();
-    debugs(44, 3, "peerHandleIcpReply: " << icp_opcode_str[op] << " " << psstate->entry->url()  );
+    debugs(44, 3, icp_opcode_str[op] << ' ' << url());
 #if USE_CACHE_DIGESTS && 0
     /* do cd lookup to count false misses */
 
     if (p && request)
         peerNoteDigestLookup(request, p,
-                             peerDigestLookup(p, request, psstate->entry));
+                             peerDigestLookup(p, request, entry));
 
 #endif
 
-    ++ psstate->ping.n_recv;
+    ++ping.n_recv;
 
     if (op == ICP_MISS || op == ICP_DECHO) {
         if (type == PEER_PARENT)
-            peerIcpParentMiss(p, header, psstate);
+            handleIcpParentMiss(p, header);
     } else if (op == ICP_HIT) {
-        psstate->hit = p;
-        psstate->hit_type = type;
-        peerSelectFoo(psstate);
+        hit = p;
+        hit_type = type;
+        selectMore();
         return;
     }
 
-    if (psstate->ping.n_recv < psstate->ping.n_replies_expected)
+    if (ping.n_recv < ping.n_replies_expected)
         return;
 
-    peerSelectFoo(psstate);
+    selectMore();
 }
 
 #if USE_HTCP
-static void
-peerHandleHtcpReply(CachePeer * p, peer_t type, HtcpReplyData * htcp, void *data)
+void
+PeerSelector::handleHtcpReply(CachePeer *p, const peer_t type, HtcpReplyData *htcp)
 {
-    ps_state *psstate = (ps_state *)data;
-    debugs(44, 3, "peerHandleHtcpReply: " <<
-           (htcp->hit ? "HIT" : "MISS") << " " <<
-           psstate->entry->url()  );
-    ++ psstate->ping.n_recv;
+    debugs(44, 3, (htcp->hit ? "HIT" : "MISS") << ' ' << url());
+    ++ping.n_recv;
 
     if (htcp->hit) {
-        psstate->hit = p;
-        psstate->hit_type = type;
-        peerSelectFoo(psstate);
+        hit = p;
+        hit_type = type;
+        selectMore();
         return;
     }
 
     if (type == PEER_PARENT)
-        peerHtcpParentMiss(p, htcp, psstate);
+        handleHtcpParentMiss(p, htcp);
 
-    if (psstate->ping.n_recv < psstate->ping.n_replies_expected)
+    if (ping.n_recv < ping.n_replies_expected)
         return;
 
-    peerSelectFoo(psstate);
+    selectMore();
 }
 
-static void
-peerHtcpParentMiss(CachePeer * p, HtcpReplyData * htcp, ps_state * ps)
+void
+PeerSelector::handleHtcpParentMiss(CachePeer *p, HtcpReplyData *htcp)
 {
     int rtt;
 
@@ -883,11 +869,11 @@ peerHtcpParentMiss(CachePeer * p, HtcpReplyData * htcp, ps_state * ps)
         if (htcp->cto.rtt > 0) {
             rtt = (int) htcp->cto.rtt * 1000;
             int hops = (int) htcp->cto.hops * 1000;
-            netdbUpdatePeer(ps->request, p, rtt, hops);
+            netdbUpdatePeer(request->url, p, rtt, hops);
 
-            if (rtt && (ps->ping.p_rtt == 0 || rtt < ps->ping.p_rtt)) {
-                ps->closest_parent_miss = p->in_addr;
-                ps->ping.p_rtt = rtt;
+            if (rtt && (ping.p_rtt == 0 || rtt < ping.p_rtt)) {
+                closest_parent_miss = p->in_addr;
+                ping.p_rtt = rtt;
             }
         }
     }
@@ -898,96 +884,156 @@ peerHtcpParentMiss(CachePeer * p, HtcpReplyData * htcp, ps_state * ps)
         return;
 
     /* set FIRST_MISS if there is no CLOSEST parent */
-    if (!ps->closest_parent_miss.isAnyAddr())
+    if (!closest_parent_miss.isAnyAddr())
         return;
 
-    rtt = (tvSubMsec(ps->ping.start, current_time) - p->basetime) / p->weight;
+    rtt = (tvSubMsec(ping.start, current_time) - p->basetime) / p->weight;
 
     if (rtt < 1)
         rtt = 1;
 
-    if (ps->first_parent_miss.isAnyAddr() || rtt < ps->ping.w_rtt) {
-        ps->first_parent_miss = p->in_addr;
-        ps->ping.w_rtt = rtt;
+    if (first_parent_miss.isAnyAddr() || rtt < ping.w_rtt) {
+        first_parent_miss = p->in_addr;
+        ping.w_rtt = rtt;
     }
 }
 
 #endif
 
-static void
-peerHandlePingReply(CachePeer * p, peer_t type, AnyP::ProtocolType proto, void *pingdata, void *data)
+void
+PeerSelector::HandlePingReply(CachePeer * p, peer_t type, AnyP::ProtocolType proto, void *pingdata, void *data)
 {
     if (proto == AnyP::PROTO_ICP)
-        peerHandleIcpReply(p, type, (icp_common_t *)pingdata, data);
+        static_cast<PeerSelector*>(data)->handleIcpReply(p, type, static_cast<icp_common_t*>(pingdata));
 
 #if USE_HTCP
 
     else if (proto == AnyP::PROTO_HTCP)
-        peerHandleHtcpReply(p, type, (HtcpReplyData *)pingdata, data);
+        static_cast<PeerSelector*>(data)->handleHtcpReply(p, type, static_cast<HtcpReplyData*>(pingdata));
 
 #endif
 
     else
-        debugs(44, DBG_IMPORTANT, "peerHandlePingReply: unknown protocol " << proto);
+        debugs(44, DBG_IMPORTANT, "ERROR: ignoring an ICP reply with unknown protocol " << proto);
 }
 
-static void
-peerAddFwdServer(FwdServer ** FSVR, CachePeer * p, hier_code code)
-{
-    FwdServer *fs = (FwdServer *)memAllocate(MEM_FWD_SERVER);
-    debugs(44, 5, "peerAddFwdServer: adding " <<
-           (p ? p->host : "DIRECT")  << " " <<
-           hier_code_str[code]  );
-    fs->_peer = cbdataReference(p);
-    fs->code = code;
-
-    while (*FSVR)
-        FSVR = &(*FSVR)->next;
-
-    *FSVR = fs;
-}
-
-ps_state::ps_state() : request (NULL),
-        entry (NULL),
-        always_direct(Config.accessList.AlwaysDirect?ACCESS_DUNNO:ACCESS_DENIED),
-        never_direct(Config.accessList.NeverDirect?ACCESS_DUNNO:ACCESS_DENIED),
-        direct(DIRECT_UNKNOWN),
-        callback (NULL),
-        callback_data (NULL),
-        lastError(NULL),
-        servers (NULL),
-        first_parent_miss(),
-        closest_parent_miss(),
-        hit(NULL),
-        hit_type(PEER_NONE),
-        acl_checklist (NULL)
+void
+PeerSelector::addSelection(CachePeer *peer, const hier_code code)
+{
+    // Find the end of the servers list. Bail on a duplicate destination.
+    auto **serversTail = &servers;
+    while (const auto server = *serversTail) {
+        // There can be at most one PINNED destination.
+        // Non-PINNED destinations are uniquely identified by their CachePeer
+        // (even though a DIRECT destination might match a cache_peer address).
+        const bool duplicate = (server->code == PINNED) ?
+                               (code == PINNED) : (server->_peer == peer);
+        if (duplicate) {
+            debugs(44, 3, "skipping " << PeerSelectionDumper(this, peer, code) <<
+                   "; have " << PeerSelectionDumper(this, server->_peer.get(), server->code));
+            return;
+        }
+        serversTail = &server->next;
+    }
+
+    debugs(44, 3, "adding " << PeerSelectionDumper(this, peer, code));
+    *serversTail = new FwdServer(peer, code);
+}
+
+PeerSelector::PeerSelector(PeerSelectionInitiator *initiator):
+    request(nullptr),
+    entry (NULL),
+    always_direct(Config.accessList.AlwaysDirect?ACCESS_DUNNO:ACCESS_DENIED),
+    never_direct(Config.accessList.NeverDirect?ACCESS_DUNNO:ACCESS_DENIED),
+    direct(DIRECT_UNKNOWN),
+    lastError(NULL),
+    servers (NULL),
+    first_parent_miss(),
+    closest_parent_miss(),
+    hit(NULL),
+    hit_type(PEER_NONE),
+    acl_checklist (NULL),
+    initiator_(initiator)
 {
     ; // no local defaults.
 }
 
-const char *
-ps_state::url() const
+const SBuf
+PeerSelector::url() const
 {
     if (entry)
-        return entry->url();
+        return SBuf(entry->url());
 
     if (request)
-        return urlCanonical(request);
+        return request->effectiveRequestUri();
+
+    static const SBuf noUrl("[no URL]");
+    return noUrl;
+}
+
+/// \returns valid/interested peer initiator or nil
+PeerSelectionInitiator *
+PeerSelector::interestedInitiator()
+{
+    const auto initiator = initiator_.valid();
+
+    if (!initiator) {
+        debugs(44, 3, id << " initiator gone");
+        return nullptr;
+    }
+
+    if (!initiator->subscribed) {
+        debugs(44, 3, id << " initiator lost interest");
+        return nullptr;
+    }
+
+    debugs(44, 7, id);
+    return initiator;
+}
+
+bool
+PeerSelector::wantsMoreDestinations() const {
+    const auto maxCount = Config.forward_max_tries;
+    return maxCount >= 0 && foundPaths <
+           static_cast<std::make_unsigned<decltype(maxCount)>::type>(maxCount);
+}
 
-    return "[no URL]";
+void
+PeerSelector::handlePath(Comm::ConnectionPointer &path, FwdServer &fs)
+{
+    ++foundPaths;
+
+    path->peerType = fs.code;
+    path->setPeer(fs._peer.get());
+
+    // check for a configured outgoing address for this destination...
+    getOutgoingAddress(request, path);
+
+    request->hier.ping = ping; // may be updated later
+
+    debugs(44, 2, id << " found " << path << ", destination #" << foundPaths << " for " << url());
+    debugs(44, 2, "  always_direct = " << always_direct);
+    debugs(44, 2, "   never_direct = " << never_direct);
+    debugs(44, 2, "       timedout = " << ping.timedout);
+
+    if (const auto initiator = interestedInitiator())
+        initiator->noteDestination(path);
 }
 
+InstanceIdDefinitions(PeerSelector, "PeerSelector");
+
 ping_data::ping_data() :
-        n_sent(0),
-        n_recv(0),
-        n_replies_expected(0),
-        timeout(0),
-        timedout(0),
-        w_rtt(0),
-        p_rtt(0)
+    n_sent(0),
+    n_recv(0),
+    n_replies_expected(0),
+    timeout(0),
+    timedout(0),
+    w_rtt(0),
+    p_rtt(0)
 {
     start.tv_sec = 0;
     start.tv_usec = 0;
     stop.tv_sec = 0;
     stop.tv_usec = 0;
 }
+