]> git.ipfire.org Git - thirdparty/squid.git/blobdiff - src/comm/ConnOpener.cc
SourceFormat Enforcement
[thirdparty/squid.git] / src / comm / ConnOpener.cc
index 2429c8c7ab1df99396f40f84253dcc1981c04b80..a5e36609d038640a517e119f59073c0f768989d1 100644 (file)
@@ -1,31 +1,44 @@
 /*
- * DEBUG: section 05    Socket Connection Opener
+ * Copyright (C) 1996-2015 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
  */
 
+/* DEBUG: section 05    Socket Connection Opener */
+
 #include "squid.h"
-#include "comm/ConnOpener.h"
+#include "CachePeer.h"
+#include "comm.h"
 #include "comm/Connection.h"
+#include "comm/ConnOpener.h"
 #include "comm/Loops.h"
-#include "comm.h"
+#include "fd.h"
 #include "fde.h"
+#include "globals.h"
 #include "icmp/net_db.h"
+#include "ip/QosConfig.h"
+#include "ip/tools.h"
 #include "ipcache.h"
+#include "SquidConfig.h"
 #include "SquidTime.h"
 
-namespace Comm
-{
-CBDATA_CLASS_INIT(ConnOpener);
-};
+#include <cerrno>
+
+class CachePeer;
+
+CBDATA_NAMESPACED_CLASS_INIT(Comm, ConnOpener);
 
 Comm::ConnOpener::ConnOpener(Comm::ConnectionPointer &c, AsyncCall::Pointer &handler, time_t ctimeout) :
-        AsyncJob("Comm::ConnOpener"),
-        host_(NULL),
-        conn_(c),
-        callback_(handler),
-        totalTries_(0),
-        failRetries_(0),
-        connectTimeout_(ctimeout),
-        connectStart_(0)
+    AsyncJob("Comm::ConnOpener"),
+    host_(NULL),
+    temporaryFd_(-1),
+    conn_(c),
+    callback_(handler),
+    totalTries_(0),
+    failRetries_(0),
+    deadline_(squid_curtime + static_cast<time_t>(ctimeout))
 {}
 
 Comm::ConnOpener::~ConnOpener()
@@ -46,39 +59,26 @@ Comm::ConnOpener::doneAll() const
         return AsyncJob::doneAll();
     }
 
+    // otherwise, we must be waiting for something
+    Must(temporaryFd_ >= 0 || calls_.sleep_);
     return false;
 }
 
 void
 Comm::ConnOpener::swanSong()
 {
-    // cancel any event watchers
-    // done here to get the "swanSong" mention in cancel debugging.
-    if (calls_.earlyAbort_ != NULL) {
-        calls_.earlyAbort_->cancel("Comm::ConnOpener::swanSong");
-        calls_.earlyAbort_ = NULL;
-    }
-    if (calls_.timeout_ != NULL) {
-        calls_.timeout_->cancel("Comm::ConnOpener::swanSong");
-        calls_.timeout_ = NULL;
+    if (callback_ != NULL) {
+        // inform the still-waiting caller we are dying
+        sendAnswer(Comm::ERR_CONNECT, 0, "Comm::ConnOpener::swanSong");
     }
 
-    // rollback what we can from the job state
-    if (conn_ != NULL && conn_->isOpen()) {
-        // drop any handlers now to save a lot of cycles later
-        Comm::SetSelect(conn_->fd, COMM_SELECT_WRITE, NULL, NULL, 0);
-        commUnsetConnTimeout(conn_);
-        // it never reached fully open, so abort the FD
-        conn_->close();
-    }
+    // did we abort with a temporary FD assigned?
+    if (temporaryFd_ >= 0)
+        closeFd();
 
-    if (callback_ != NULL) {
-        if (callback_->canceled())
-            callback_ = NULL;
-        else
-            // inform the still-waiting caller we are dying
-            doneConnecting(COMM_ERR_CONNECT, 0);
-    }
+    // did we abort while waiting between retries?
+    if (calls_.sleep_)
+        cancelSleep();
 
     AsyncJob::swanSong();
 }
@@ -104,14 +104,13 @@ Comm::ConnOpener::getHost() const
 /**
  * Connection attempt are completed. One way or the other.
  * Pass the results back to the external handler.
- * NP: on connection errors the connection close() must be called first.
  */
 void
-Comm::ConnOpener::doneConnecting(comm_err_t status, int xerrno)
+Comm::ConnOpener::sendAnswer(Comm::Flag errFlag, int xerrno, const char *why)
 {
     // only mark the address good/bad AFTER connect is finished.
     if (host_ != NULL) {
-        if (xerrno == 0)
+        if (xerrno == 0) // XXX: should not we use errFlag instead?
             ipcacheMarkGoodAddr(host_, conn_->remote);
         else {
             ipcacheMarkBadAddr(host_, conn_->remote);
@@ -123,17 +122,106 @@ Comm::ConnOpener::doneConnecting(comm_err_t status, int xerrno)
     }
 
     if (callback_ != NULL) {
-        typedef CommConnectCbParams Params;
-        Params &params = GetCommParams<Params>(callback_);
-        params.conn = conn_;
-        params.flag = status;
-        params.xerrno = xerrno;
-        ScheduleCallHere(callback_);
+        // avoid scheduling cancelled callbacks, assuming they are common
+        // enough to make this extra check an optimization
+        if (callback_->canceled()) {
+            debugs(5, 4, conn_ << " not calling canceled " << *callback_ <<
+                   " [" << callback_->id << ']' );
+            // TODO save the pconn to the pconnPool ?
+        } else {
+            typedef CommConnectCbParams Params;
+            Params &params = GetCommParams<Params>(callback_);
+            params.conn = conn_;
+            params.flag = errFlag;
+            params.xerrno = xerrno;
+            ScheduleCallHere(callback_);
+        }
         callback_ = NULL;
     }
 
-    /* ensure cleared local state, we are done. */
-    conn_ = NULL;
+    // The job will stop without this call because nil callback_ makes
+    // doneAll() true, but this explicit call creates nicer debugging.
+    mustStop(why);
+}
+
+/// cleans up this job I/O state without closing temporaryFd
+/// required before closing temporaryFd or keeping it in conn_
+/// leaves FD bare so must only be called via closeFd() or keepFd()
+void
+Comm::ConnOpener::cleanFd()
+{
+    debugs(5, 4, HERE << conn_ << " closing temp FD " << temporaryFd_);
+
+    Must(temporaryFd_ >= 0);
+    fde &f = fd_table[temporaryFd_];
+
+    // Our write_handler was set without using Comm::Write API, so we cannot
+    // use a cancellable Pointer-free job callback and simply cancel it here.
+    if (f.write_handler) {
+
+        /* XXX: We are about to remove write_handler, which was responsible
+         * for deleting write_data, so we have to delete write_data
+         * ourselves. Comm currently calls SetSelect handlers synchronously
+         * so if write_handler is set, we know it has not been called yet.
+         * ConnOpener converts that sync call into an async one, but only
+         * after deleting ptr, so that is not a problem.
+         */
+
+        delete static_cast<Pointer*>(f.write_data);
+        f.write_data = NULL;
+        f.write_handler = NULL;
+    }
+    // Comm::DoSelect does not do this when calling and resetting write_handler
+    // (because it expects more writes to come?). We could mimic that
+    // optimization by resetting Comm "Select" state only when the FD is
+    // actually closed.
+    Comm::SetSelect(temporaryFd_, COMM_SELECT_WRITE, NULL, NULL, 0);
+
+    if (calls_.timeout_ != NULL) {
+        calls_.timeout_->cancel("Comm::ConnOpener::cleanFd");
+        calls_.timeout_ = NULL;
+    }
+    // Comm checkTimeouts() and commCloseAllSockets() do not clear .timeout
+    // when calling timeoutHandler (XXX fix them), so we clear unconditionally.
+    f.timeoutHandler = NULL;
+    f.timeout = 0;
+
+    if (calls_.earlyAbort_ != NULL) {
+        comm_remove_close_handler(temporaryFd_, calls_.earlyAbort_);
+        calls_.earlyAbort_ = NULL;
+    }
+}
+
+/// cleans I/O state and ends I/O for temporaryFd_
+void
+Comm::ConnOpener::closeFd()
+{
+    if (temporaryFd_ < 0)
+        return;
+
+    cleanFd();
+
+    // comm_close() below uses COMMIO_FD_WRITECB(fd)->active() to clear Comm
+    // "Select" state. It will not clear ours. XXX: It should always clear
+    // because a callback may have been active but was called before comm_close
+    // Update: we now do this in cleanFd()
+    // Comm::SetSelect(temporaryFd_, COMM_SELECT_WRITE, NULL, NULL, 0);
+
+    comm_close(temporaryFd_);
+    temporaryFd_ = -1;
+}
+
+/// cleans I/O state and moves temporaryFd_ to the conn_ for long-term use
+void
+Comm::ConnOpener::keepFd()
+{
+    Must(conn_ != NULL);
+    Must(temporaryFd_ >= 0);
+
+    cleanFd();
+
+    conn_->fd = temporaryFd_;
+    temporaryFd_ = -1;
 }
 
 void
@@ -141,45 +229,91 @@ Comm::ConnOpener::start()
 {
     Must(conn_ != NULL);
 
-    /* get a socket open ready for connecting with */
-    if (!conn_->isOpen()) {
-#if USE_IPV6
-        /* outbound sockets have no need to be protocol agnostic. */
-        if (conn_->remote.IsIPv4()) {
-            conn_->local.SetIPv4();
-        }
-#endif
-        conn_->fd = comm_openex(SOCK_STREAM, IPPROTO_TCP, conn_->local, conn_->flags, conn_->tos, conn_->nfmark, host_);
-        if (!conn_->isOpen()) {
-            doneConnecting(COMM_ERR_CONNECT, 0);
-            return;
-        }
+    /* outbound sockets have no need to be protocol agnostic. */
+    if (!(Ip::EnableIpv6&IPV6_SPECIAL_V4MAPPING) && conn_->remote.isIPv4()) {
+        conn_->local.setIPv4();
+    }
+
+    conn_->noteStart();
+    if (createFd())
+        doConnect();
+}
+
+/// called at the end of Comm::ConnOpener::DelayedConnectRetry event
+void
+Comm::ConnOpener::restart()
+{
+    debugs(5, 5, conn_ << " restarting after sleep");
+    calls_.sleep_ = false;
+
+    if (createFd())
+        doConnect();
+}
+
+/// Create a socket for the future connection or return false.
+/// If false is returned, done() is guaranteed to return true and end the job.
+bool
+Comm::ConnOpener::createFd()
+{
+    Must(temporaryFd_ < 0);
+
+    // our initators signal abort by cancelling their callbacks
+    if (callback_ == NULL || callback_->canceled())
+        return false;
+
+    temporaryFd_ = comm_openex(SOCK_STREAM, IPPROTO_TCP, conn_->local, conn_->flags, host_);
+    if (temporaryFd_ < 0) {
+        sendAnswer(Comm::ERR_CONNECT, 0, "Comm::ConnOpener::createFd");
+        return false;
     }
 
+    // Set TOS if needed.
+    if (conn_->tos &&
+            Ip::Qos::setSockTos(temporaryFd_, conn_->tos, conn_->remote.isIPv4() ? AF_INET : AF_INET6) < 0)
+        conn_->tos = 0;
+#if SO_MARK
+    if (conn_->nfmark &&
+            Ip::Qos::setSockNfmark(temporaryFd_, conn_->nfmark) < 0)
+        conn_->nfmark = 0;
+#endif
+
+    fd_table[temporaryFd_].tosToServer = conn_->tos;
+    fd_table[temporaryFd_].nfmarkToServer = conn_->nfmark;
+
     typedef CommCbMemFunT<Comm::ConnOpener, CommCloseCbParams> abortDialer;
     calls_.earlyAbort_ = JobCallback(5, 4, abortDialer, this, Comm::ConnOpener::earlyAbort);
-    comm_add_close_handler(conn_->fd, calls_.earlyAbort_);
+    comm_add_close_handler(temporaryFd_, calls_.earlyAbort_);
 
     typedef CommCbMemFunT<Comm::ConnOpener, CommTimeoutCbParams> timeoutDialer;
     calls_.timeout_ = JobCallback(5, 4, timeoutDialer, this, Comm::ConnOpener::timeout);
-    debugs(5, 3, HERE << conn_ << " timeout " << connectTimeout_);
-    commSetConnTimeout(conn_, connectTimeout_, calls_.timeout_);
-
-    connectStart_ = squid_curtime;
-    connect();
+    debugs(5, 3, conn_ << " will timeout in " << (deadline_ - squid_curtime));
+
+    // Update the fd_table directly because commSetConnTimeout() needs open conn_
+    assert(temporaryFd_ < Squid_MaxFD);
+    assert(fd_table[temporaryFd_].flags.open);
+    typedef CommTimeoutCbParams Params;
+    Params &params = GetCommParams<Params>(calls_.timeout_);
+    params.conn = conn_;
+    fd_table[temporaryFd_].timeoutHandler = calls_.timeout_;
+    fd_table[temporaryFd_].timeout = deadline_;
+
+    return true;
 }
 
 void
 Comm::ConnOpener::connected()
 {
+    Must(temporaryFd_ >= 0);
+    keepFd();
+
     /*
      * stats.conn_open is used to account for the number of
-     * connections that we have open to the peer, so we can limit
+     * connections that we have open to the CachePeer, so we can limit
      * based on the max-conn option.  We need to increment here,
      * even if the connection may fail.
      */
-    if (conn_->getPeer())
-        conn_->getPeer()->stats.conn_open++;
+    if (CachePeer *peer=(conn_->getPeer()))
+        ++peer->stats.conn_open;
 
     lookupLocalAddress();
 
@@ -188,70 +322,81 @@ Comm::ConnOpener::connected()
      *       Also, legacy code still depends on comm_local_port() with no access to Comm::Connection
      *       when those are done comm_local_port can become one of our member functions to do the below.
      */
-    fd_table[conn_->fd].flags.open = 1;
+    Must(fd_table[conn_->fd].flags.open);
     fd_table[conn_->fd].local_addr = conn_->local;
+
+    sendAnswer(Comm::OK, 0, "Comm::ConnOpener::connected");
 }
 
-/** Make an FD connection attempt.
- * Handles the case(s) when a partially setup connection gets closed early.
- */
+/// Make an FD connection attempt.
 void
-Comm::ConnOpener::connect()
+Comm::ConnOpener::doConnect()
 {
     Must(conn_ != NULL);
+    Must(temporaryFd_ >= 0);
 
-    // our parent Jobs signal abort by cancelling their callbacks.
-    if (callback_ == NULL || callback_->canceled())
-        return;
-
-    totalTries_++;
+    ++ totalTries_;
 
-    switch (comm_connect_addr(conn_->fd, conn_->remote) ) {
+    switch (comm_connect_addr(temporaryFd_, conn_->remote) ) {
 
-    case COMM_INPROGRESS:
-        // check for timeout FIRST.
-        if (squid_curtime - connectStart_ > connectTimeout_) {
-            debugs(5, 5, HERE << conn_ << ": * - ERR took too long already.");
-            calls_.earlyAbort_->cancel("Comm::ConnOpener::connect timed out");
-            calls_.earlyAbort_ = NULL;
-            conn_->close();
-            doneConnecting(COMM_TIMEOUT, errno);
-            return;
-        } else {
-            debugs(5, 5, HERE << conn_ << ": COMM_INPROGRESS");
-            Comm::SetSelect(conn_->fd, COMM_SELECT_WRITE, Comm::ConnOpener::InProgressConnectRetry, this, 0);
-        }
+    case Comm::INPROGRESS:
+        debugs(5, 5, HERE << conn_ << ": Comm::INPROGRESS");
+        Comm::SetSelect(temporaryFd_, COMM_SELECT_WRITE, Comm::ConnOpener::InProgressConnectRetry, new Pointer(this), 0);
         break;
 
-    case COMM_OK:
-        debugs(5, 5, HERE << conn_ << ": COMM_OK - connected");
+    case Comm::OK:
+        debugs(5, 5, HERE << conn_ << ": Comm::OK - connected");
         connected();
-        doneConnecting(COMM_OK, 0);
         break;
 
-    default:
-        failRetries_++;
-
-        // check for timeout FIRST.
-        if (squid_curtime - connectStart_ > connectTimeout_) {
-            debugs(5, 5, HERE << conn_ << ": * - ERR took too long to receive response.");
-            calls_.earlyAbort_->cancel("Comm::ConnOpener::connect timed out");
-            calls_.earlyAbort_ = NULL;
-            conn_->close();
-            doneConnecting(COMM_TIMEOUT, errno);
-        } else if (failRetries_ < Config.connect_retries) {
+    default: {
+        const int xerrno = errno;
+
+        ++failRetries_;
+        debugs(5, 7, conn_ << ": failure #" << failRetries_ << " <= " <<
+               Config.connect_retries << ": " << xstrerr(xerrno));
+
+        if (failRetries_ < Config.connect_retries) {
             debugs(5, 5, HERE << conn_ << ": * - try again");
-            eventAdd("Comm::ConnOpener::DelayedConnectRetry", Comm::ConnOpener::DelayedConnectRetry, this, 0.05, 0);
+            retrySleep();
             return;
         } else {
             // send ERROR back to the upper layer.
             debugs(5, 5, HERE << conn_ << ": * - ERR tried too many times already.");
-            calls_.earlyAbort_->cancel("Comm::ConnOpener::connect failed");
-            calls_.earlyAbort_ = NULL;
-            conn_->close();
-            doneConnecting(COMM_ERR_CONNECT, errno);
+            sendAnswer(Comm::ERR_CONNECT, xerrno, "Comm::ConnOpener::doConnect");
         }
     }
+    }
+}
+
+/// Close and wait a little before trying to open and connect again.
+void
+Comm::ConnOpener::retrySleep()
+{
+    Must(!calls_.sleep_);
+    closeFd();
+    calls_.sleep_ = true;
+    eventAdd("Comm::ConnOpener::DelayedConnectRetry",
+             Comm::ConnOpener::DelayedConnectRetry,
+             new Pointer(this), 0.05, 0, false);
+}
+
+/// cleans up this job sleep state
+void
+Comm::ConnOpener::cancelSleep()
+{
+    if (calls_.sleep_) {
+        // It would be nice to delete the sleep event, but it might be out of
+        // the event queue and in the async queue already, so (a) we do not know
+        // whether we can safely delete the call ptr here and (b) eventDelete()
+        // will assert if the event went async. Thus, we let the event run so
+        // that it deletes the call ptr [after this job is gone]. Note that we
+        // are called only when the job ends so this "hanging event" will do
+        // nothing but deleting the call ptr.  TODO: Revise eventDelete() API.
+        // eventDelete(Comm::ConnOpener::DelayedConnectRetry, calls_.sleep);
+        calls_.sleep_ = false;
+        debugs(5, 9, conn_ << " stops sleeping");
+    }
 }
 
 /**
@@ -262,16 +407,16 @@ void
 Comm::ConnOpener::lookupLocalAddress()
 {
     struct addrinfo *addr = NULL;
-    conn_->local.InitAddrInfo(addr);
+    Ip::Address::InitAddr(addr);
 
     if (getsockname(conn_->fd, addr->ai_addr, &(addr->ai_addrlen)) != 0) {
         debugs(50, DBG_IMPORTANT, "ERROR: Failed to retrieve TCP/UDP details for socket: " << conn_ << ": " << xstrerror());
-        conn_->local.FreeAddrInfo(addr);
+        Ip::Address::FreeAddr(addr);
         return;
     }
 
     conn_->local = *addr;
-    conn_->local.FreeAddrInfo(addr);
+    Ip::Address::FreeAddr(addr);
     debugs(5, 6, HERE << conn_);
 }
 
@@ -282,7 +427,9 @@ void
 Comm::ConnOpener::earlyAbort(const CommCloseCbParams &io)
 {
     debugs(5, 3, HERE << io.conn);
-    doneConnecting(COMM_ERR_CLOSING, io.xerrno); // NP: is closing or shutdown better?
+    calls_.earlyAbort_ = NULL;
+    // NP: is closing or shutdown better?
+    sendAnswer(Comm::ERR_CLOSING, io.xerrno, "Comm::ConnOpener::earlyAbort");
 }
 
 /**
@@ -292,37 +439,44 @@ Comm::ConnOpener::earlyAbort(const CommCloseCbParams &io)
 void
 Comm::ConnOpener::timeout(const CommTimeoutCbParams &)
 {
-    connect();
+    debugs(5, 5, HERE << conn_ << ": * - ERR took too long to receive response.");
+    calls_.timeout_ = NULL;
+    sendAnswer(Comm::TIMEOUT, ETIMEDOUT, "Comm::ConnOpener::timeout");
 }
 
-/* Legacy Wrapper for the retry event after COMM_INPROGRESS
- * XXX: As soon as Comm::SetSelect() accepts Async calls we can use a ConnOpener::connect call
+/* Legacy Wrapper for the retry event after Comm::INPROGRESS
+ * XXX: As soon as Comm::SetSelect() accepts Async calls we can use a ConnOpener::doConnect call
  */
 void
-Comm::ConnOpener::InProgressConnectRetry(int fd, void *data)
+Comm::ConnOpener::InProgressConnectRetry(int, void *data)
 {
-    ConnOpener *cs = static_cast<Comm::ConnOpener *>(data);
-    assert(cs);
-
-    // Ew. we are now outside the all AsyncJob protections.
-    // get back inside by scheduling another call...
-    typedef NullaryMemFunT<Comm::ConnOpener> Dialer;
-    AsyncCall::Pointer call = JobCallback(5, 4, Dialer, cs, Comm::ConnOpener::connect);
-    ScheduleCallHere(call);
+    Pointer *ptr = static_cast<Pointer*>(data);
+    assert(ptr);
+    if (ConnOpener *cs = ptr->valid()) {
+        // Ew. we are now outside the all AsyncJob protections.
+        // get back inside by scheduling another call...
+        typedef NullaryMemFunT<Comm::ConnOpener> Dialer;
+        AsyncCall::Pointer call = JobCallback(5, 4, Dialer, cs, Comm::ConnOpener::doConnect);
+        ScheduleCallHere(call);
+    }
+    delete ptr;
 }
 
 /* Legacy Wrapper for the retry event with small delay after errors.
- * XXX: As soon as eventAdd() accepts Async calls we can use a ConnOpener::connect call
+ * XXX: As soon as eventAdd() accepts Async calls we can use a ConnOpener::restart call
  */
 void
 Comm::ConnOpener::DelayedConnectRetry(void *data)
 {
-    ConnOpener *cs = static_cast<Comm::ConnOpener *>(data);
-    assert(cs);
-
-    // Ew. we are now outside the all AsyncJob protections.
-    // get back inside by scheduling another call...
-    typedef NullaryMemFunT<Comm::ConnOpener> Dialer;
-    AsyncCall::Pointer call = JobCallback(5, 4, Dialer, cs, Comm::ConnOpener::connect);
-    ScheduleCallHere(call);
+    Pointer *ptr = static_cast<Pointer*>(data);
+    assert(ptr);
+    if (ConnOpener *cs = ptr->valid()) {
+        // Ew. we are now outside the all AsyncJob protections.
+        // get back inside by scheduling another call...
+        typedef NullaryMemFunT<Comm::ConnOpener> Dialer;
+        AsyncCall::Pointer call = JobCallback(5, 4, Dialer, cs, Comm::ConnOpener::restart);
+        ScheduleCallHere(call);
+    }
+    delete ptr;
 }
+