/*
- * Copyright (C) 1996-2018 The Squid Software Foundation and contributors
+ * Copyright (C) 1996-2023 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/AsyncCbdataCalls.h"
#include "base/InstanceId.h"
+#include "base/TypeTraits.h"
#include "CachePeer.h"
+#include "CachePeers.h"
#include "carp.h"
#include "client_side.h"
#include "dns/LookupDetails.h"
#include "peer_userhash.h"
#include "PeerSelectState.h"
#include "SquidConfig.h"
-#include "SquidTime.h"
#include "Store.h"
-#include "URL.h"
+#include "time/gadgets.h"
/**
* A CachePeer which has been selected as a possible destination.
os << hier_code_str[fsd.code];
if (fsd.peer)
- os << '/' << fsd.peer->host;
+ os << '/' << *fsd.peer;
else if (fsd.selector) // useful for DIRECT and gone PINNED destinations
os << '#' << fsd.selector->request->url.host();
return os;
}
+/// An ICP ping timeout service.
+/// Protects event.cc (which is designed to handle a few unrelated timeouts)
+/// from exposure to thousands of ping-related timeouts on busy proxies.
+class PeerSelectorPingMonitor
+{
+public:
+ /// registers the given selector to be notified about the IPC ping timeout
+ void monitor(PeerSelector *);
+
+ /// removes a PeerSelector from the waiting list
+ void forget(PeerSelector *);
+
+ /// \returns a (nil) registration of a non-waiting peer selector
+ WaitingPeerSelectorPosition npos() { return selectors.end(); }
+
+private:
+ static void NoteWaitOver(void *monitor);
+
+ void startWaiting();
+ void abortWaiting();
+ void noteWaitOver();
+
+ WaitingPeerSelectors selectors; ///< \see WaitingPeerSelectors
+};
+
+/// monitors all PeerSelector ICP ping timeouts
+static PeerSelectorPingMonitor &
+PingMonitor()
+{
+ static const auto Instance = new PeerSelectorPingMonitor();
+ return *Instance;
+}
+
+/* PeerSelectorPingMonitor */
+
+/// PeerSelectorPingMonitor::noteWaitOver() wrapper
+void
+PeerSelectorPingMonitor::NoteWaitOver(void *raw)
+{
+ assert(raw);
+ static_cast<PeerSelectorPingMonitor*>(raw)->noteWaitOver();
+}
+
+/// schedules a single event to represent all waiting selectors
+void
+PeerSelectorPingMonitor::startWaiting()
+{
+ assert(!selectors.empty());
+ const auto interval = tvSubDsec(current_time, selectors.begin()->first);
+ eventAdd("PeerSelectorPingMonitor::NoteWaitOver", &PeerSelectorPingMonitor::NoteWaitOver, this, interval, 0, false);
+}
+
+/// undoes an earlier startWaiting() call
+void
+PeerSelectorPingMonitor::abortWaiting()
+{
+ // our event may be already in the AsyncCallQueue but that is OK:
+ // such queued calls cannot accumulate, and we ignore any stale ones
+ eventDelete(&PeerSelectorPingMonitor::NoteWaitOver, nullptr);
+}
+
+/// calls back all ready PeerSelectors and continues to wait for others
+void
+PeerSelectorPingMonitor::noteWaitOver()
+{
+ while (!selectors.empty() && current_time >= selectors.begin()->first) {
+ const auto selector = selectors.begin()->second;
+ CallBack(selector->al, [selector,this] {
+ selector->ping.monitorRegistration = npos();
+ AsyncCall::Pointer callback = asyncCall(44, 4, "PeerSelector::HandlePingTimeout",
+ cbdataDialer(PeerSelector::HandlePingTimeout, selector));
+ ScheduleCallHere(callback);
+ });
+ selectors.erase(selectors.begin());
+ }
+
+ if (!selectors.empty()) {
+ // Since abortWaiting() is unreliable, we may have been awakened by a
+ // stale event A after event B has been scheduled. Now we are going to
+ // schedule event C. Prevent event accumulation by deleting B (if any).
+ abortWaiting();
+
+ startWaiting();
+ }
+}
+
+void
+PeerSelectorPingMonitor::monitor(PeerSelector *selector)
+{
+ assert(selector);
+
+ const auto deadline = selector->ping.deadline();
+ const auto position = selectors.emplace(deadline, selector);
+ selector->ping.monitorRegistration = position;
+
+ if (position == selectors.begin()) {
+ if (selectors.size() > 1)
+ abortWaiting(); // remove the previously scheduled earlier event
+ startWaiting();
+ } // else the already scheduled event is still the earliest one
+}
+
+void
+PeerSelectorPingMonitor::forget(PeerSelector *selector)
+{
+ assert(selector);
+
+ if (selector->ping.monitorRegistration == npos())
+ return; // already forgotten
+
+ const auto wasFirst = selector->ping.monitorRegistration == selectors.begin();
+ selectors.erase(selector->ping.monitorRegistration);
+ selector->ping.monitorRegistration = npos();
+
+ if (wasFirst) {
+ // do not reschedule if there are still elements with the same deadline
+ if (!selectors.empty() && selectors.begin()->first == selector->ping.deadline())
+ return;
+ abortWaiting();
+ if (!selectors.empty())
+ startWaiting();
+ } // else do nothing since the old scheduled event is still the earliest one
+}
+
+/* PeerSelector */
+
PeerSelector::~PeerSelector()
{
while (servers) {
servers = next;
}
+ cancelPingTimeoutMonitoring();
+
if (entry) {
debugs(44, 3, entry->url());
-
- if (entry->ping_status == PING_WAITING)
- eventDelete(HandlePingTimeout, this);
-
entry->ping_status = PING_DONE;
}
if (acl_checklist) {
- debugs(44, DBG_IMPORTANT, "BUG: peer selector gone while waiting for a slow ACL");
+ debugs(44, DBG_IMPORTANT, "ERROR: Squid BUG: peer selector gone while waiting for a slow ACL");
delete acl_checklist;
}
if (entry) {
assert(entry->ping_status != PING_WAITING);
entry->unlock("peerSelect");
- entry = NULL;
+ entry = nullptr;
}
delete lastError;
}
+void
+PeerSelector::startPingWaiting()
+{
+ assert(entry);
+ assert(entry->ping_status != PING_WAITING);
+ PingMonitor().monitor(this);
+ entry->ping_status = PING_WAITING;
+}
+
+void
+PeerSelector::cancelPingTimeoutMonitoring()
+{
+ PingMonitor().forget(this);
+}
+
static int
peerSelectIcpPing(PeerSelector *ps, int direct, StoreEntry * entry)
{
}
void
-PeerSelector::checkNeverDirectDone(const allow_t answer)
+PeerSelector::checkNeverDirectDone(const Acl::Answer answer)
{
acl_checklist = nullptr;
debugs(44, 3, answer);
}
void
-PeerSelector::CheckNeverDirectDone(allow_t answer, void *data)
+PeerSelector::CheckNeverDirectDone(Acl::Answer answer, void *data)
{
static_cast<PeerSelector*>(data)->checkNeverDirectDone(answer);
}
void
-PeerSelector::checkAlwaysDirectDone(const allow_t answer)
+PeerSelector::checkAlwaysDirectDone(const Acl::Answer answer)
{
acl_checklist = nullptr;
debugs(44, 3, answer);
}
void
-PeerSelector::CheckAlwaysDirectDone(allow_t answer, void *data)
+PeerSelector::CheckAlwaysDirectDone(Acl::Answer answer, void *data)
{
static_cast<PeerSelector*>(data)->checkAlwaysDirectDone(answer);
}
return;
}
+ if (fs && fs->code == PINNED) {
+ // Nil path signals a PINNED destination selection. Our initiator should
+ // borrow and use clientConnectionManager's pinned connection object
+ // (regardless of that connection destination).
+ handlePath(nullptr, *fs);
+ servers = fs->next;
+ delete fs;
+ resolveSelected();
+ return;
+ }
+
// convert the list of FwdServer destinations into destinations IP addresses
if (fs && wantsMoreDestinations()) {
// send the next one off for DNS lookup.
Comm::ConnectionPointer p = new Comm::Connection();
p->remote = ip;
- p->remote.port(peer ? peer->http_port : request->url.port());
+ // XXX: We return a (non-peer) destination with a zero port if the selection
+ // initiator supplied a request target without a port. If there are no valid
+ // use cases for this behavior, stop _selecting_ such destinations.
+ p->remote.port(peer ? peer->http_port : request->url.port().value_or(0));
handlePath(p, *servers);
}
debugs(44, 3, "Unknown host: " << (fs->_peer.valid() ? fs->_peer->host : request->url.host()));
// discard any previous error.
delete lastError;
- lastError = NULL;
+ lastError = nullptr;
if (fs->code == HIER_DIRECT) {
- lastError = new ErrorState(ERR_DNS_FAIL, Http::scServiceUnavailable, request);
+ lastError = new ErrorState(ERR_DNS_FAIL, Http::scServiceUnavailable, request, al);
lastError->dnsError = details.error;
}
}
p = whichPeer(closest_parent_miss);
- if (p == NULL)
+ if (p == nullptr)
return 0;
debugs(44, 3, "closest_parent_miss RTT = " << ping.p_rtt << " msec");
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);
+ ACLFilledChecklist *ch = new ACLFilledChecklist(Config.accessList.AlwaysDirect, request, nullptr);
ch->al = al;
acl_checklist = ch;
acl_checklist->syncAle(request, nullptr);
} 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);
+ ACLFilledChecklist *ch = new ACLFilledChecklist(Config.accessList.NeverDirect, request, nullptr);
ch->al = al;
acl_checklist = ch;
acl_checklist->syncAle(request, nullptr);
if (!entry || entry->ping_status == PING_NONE)
selectPinned();
- if (entry == NULL) {
+ if (entry == nullptr) {
(void) 0;
} else if (entry->ping_status == PING_NONE) {
selectSomeNeighbor();
return;
} else if (entry->ping_status == PING_WAITING) {
selectSomeNeighborReplies();
+ cancelPingTimeoutMonitoring();
entry->ping_status = PING_DONE;
}
// 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))) {
- const bool usePinned = pear ? peerAllowedToUse(pear, this) : (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
+
+ const auto peer = request->pinnedConnection()->pinnedPeer();
+ const auto usePinned = peer ? peerAllowedToUse(peer, this) : (direct != DIRECT_NO);
+ // If the pinned connection is prohibited (for this request) then
// the initiator must decide whether it is OK to open a new one instead.
+ request->pinnedConnection()->pinning.peerAccessDenied = !usePinned;
+
+ addSelection(peer, PINNED);
+ if (entry)
+ entry->ping_status = PING_DONE; // skip ICP
}
/**
this,
&ping.n_replies_expected,
&ping.timeout);
+ // TODO: Refactor neighborsUdpPing() to guarantee positive timeouts.
+ if (ping.timeout < 0)
+ ping.timeout = 0;
if (ping.n_sent == 0)
debugs(44, DBG_CRITICAL, "WARNING: neighborsUdpPing returned 0");
" msec");
if (ping.n_replies_expected > 0) {
- entry->ping_status = PING_WAITING;
- eventAdd("PeerSelector::HandlePingTimeout",
- HandlePingTimeout,
- this,
- 0.001 * ping.timeout,
- 0);
+ startPingWaiting();
return;
}
}
void
PeerSelector::selectSomeNeighborReplies()
{
- CachePeer *p = NULL;
+ CachePeer *p = nullptr;
hier_code code = HIER_NONE;
assert(entry->ping_status == PING_WAITING);
assert(direct != DIRECT_YES);
void
PeerSelector::selectAllParents()
{
- CachePeer *p;
/* Add all alive parents */
- for (p = Config.peers; p; p = p->next) {
+ for (const auto &peer: CurrentCachePeers()) {
+ const auto p = peer.get();
/* XXX: neighbors.c lacks a public interface for enumerating
* parents to a request so we have to dig some here..
*/
* simply are not configured to handle the request.
*/
/* Add default parent as a last resort */
- if ((p = getDefaultParent(this))) {
+ if (const auto p = getDefaultParent(this)) {
addSelection(p, DEFAULT_PARENT);
}
}
{
debugs(44, 3, url());
- if (entry)
- entry->ping_status = PING_DONE;
+ // do nothing if ping reply came while handlePingTimeout() was queued
+ if (!entry || entry->ping_status != PING_WAITING)
+ return;
+
+ entry->ping_status = PING_DONE;
if (selectionAborted())
return;
}
void
-PeerSelector::HandlePingTimeout(void *data)
+PeerSelector::HandlePingTimeout(PeerSelector *selector)
{
- static_cast<PeerSelector*>(data)->handlePingTimeout();
+ selector->handlePingTimeout();
}
void
}
}
}
+#else
+ (void)header;
#endif /* USE_ICMP */
/* if closest-only is set, then don't allow FIRST_PARENT_MISS */
}
}
}
+#else
+ (void)htcp;
#endif /* USE_ICMP */
/* if closest-only is set, then don't allow FIRST_PARENT_MISS */
// 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).
+ // TODO: We may still add duplicates because the same peer could have
+ // been removed from `servers` already (and given to the requestor).
const bool duplicate = (server->code == PINNED) ?
(code == PINNED) : (server->_peer == peer);
if (duplicate) {
PeerSelector::PeerSelector(PeerSelectionInitiator *initiator):
request(nullptr),
- entry (NULL),
+ entry (nullptr),
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),
+ lastError(nullptr),
+ servers (nullptr),
first_parent_miss(),
closest_parent_miss(),
- hit(NULL),
+ hit(nullptr),
hit_type(PEER_NONE),
- acl_checklist (NULL),
+ acl_checklist (nullptr),
initiator_(initiator)
{
; // no local defaults.
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 maxCount >= 0 && foundPaths < static_cast<size_t>(maxCount);
}
void
-PeerSelector::handlePath(Comm::ConnectionPointer &path, FwdServer &fs)
+PeerSelector::handlePath(const Comm::ConnectionPointer &path, FwdServer &fs)
{
++foundPaths;
- path->peerType = fs.code;
- path->setPeer(fs._peer.get());
+ if (path) {
+ path->peerType = fs.code;
+ path->setPeer(fs._peer.get());
- // check for a configured outgoing address for this destination...
- getOutgoingAddress(request, path);
+ // check for a configured outgoing address for this destination...
+ getOutgoingAddress(request, path);
+ debugs(44, 2, id << " found " << path << ", destination #" << foundPaths << " for " << url());
+ } else
+ debugs(44, 2, id << " found pinned, destination #" << foundPaths << " for " << url());
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);
timeout(0),
timedout(0),
w_rtt(0),
- p_rtt(0)
+ p_rtt(0),
+ monitorRegistration(PingMonitor().npos())
{
start.tv_sec = 0;
start.tv_usec = 0;
stop.tv_usec = 0;
}
+timeval
+ping_data::deadline() const
+{
+ timeval timeInterval;
+ timeInterval.tv_sec = timeout / 1000;
+ timeInterval.tv_usec = (timeout % 1000) * 1000;
+
+ timeval result;
+ tvAdd(result, start, timeInterval);
+ return result;
+}
+