]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: protocol: implement a "drain" function in protocol layers
authorWilly Tarreau <w@1wt.eu>
Mon, 10 Jun 2013 17:56:38 +0000 (19:56 +0200)
committerWilly Tarreau <w@1wt.eu>
Mon, 10 Jun 2013 18:33:23 +0000 (20:33 +0200)
Since commit cfd97c6f was merged into 1.5-dev14 (BUG/MEDIUM: checks:
prevent TIME_WAITs from appearing also on timeouts), some valid health
checks sometimes used to show some TCP resets. For example, this HTTP
health check sent to a local server :

  19:55:15.742818 IP 127.0.0.1.16568 > 127.0.0.1.8000: S 3355859679:3355859679(0) win 32792 <mss 16396,nop,nop,sackOK,nop,wscale 7>
  19:55:15.742841 IP 127.0.0.1.8000 > 127.0.0.1.16568: S 1060952566:1060952566(0) ack 3355859680 win 32792 <mss 16396,nop,nop,sackOK,nop,wscale 7>
  19:55:15.742863 IP 127.0.0.1.16568 > 127.0.0.1.8000: . ack 1 win 257
  19:55:15.745402 IP 127.0.0.1.16568 > 127.0.0.1.8000: P 1:23(22) ack 1 win 257
  19:55:15.745488 IP 127.0.0.1.8000 > 127.0.0.1.16568: FP 1:146(145) ack 23 win 257
  19:55:15.747109 IP 127.0.0.1.16568 > 127.0.0.1.8000: R 23:23(0) ack 147 win 257

After some discussion with Chris Huang-Leaver, it appeared clear that
what we want is to only send the RST when we have no other choice, which
means when the server has not closed. So we still keep SYN/SYN-ACK/RST
for pure TCP checks, but don't want to see an RST emitted as above when
the server has already sent the FIN.

The solution against this consists in implementing a "drain" function at
the protocol layer, which, when defined, causes as much as possible of
the input socket buffer to be flushed to make recv() return zero so that
we know that the server's FIN was received and ACKed. On Linux, we can make
use of MSG_TRUNC on TCP sockets, which has the benefit of draining everything
at once without even copying data. On other platforms, we read up to one
buffer of data before the close. If recv() manages to get the final zero,
we don't disable lingering. Same for hard errors. Otherwise we do.

In practice, on HTTP health checks we generally find that the close was
pending and is returned upon first recv() call. The network trace becomes
cleaner :

  19:55:23.650621 IP 127.0.0.1.16561 > 127.0.0.1.8000: S 3982804816:3982804816(0) win 32792 <mss 16396,nop,nop,sackOK,nop,wscale 7>
  19:55:23.650644 IP 127.0.0.1.8000 > 127.0.0.1.16561: S 4082139313:4082139313(0) ack 3982804817 win 32792 <mss 16396,nop,nop,sackOK,nop,wscale 7>
  19:55:23.650666 IP 127.0.0.1.16561 > 127.0.0.1.8000: . ack 1 win 257
  19:55:23.651615 IP 127.0.0.1.16561 > 127.0.0.1.8000: P 1:23(22) ack 1 win 257
  19:55:23.651696 IP 127.0.0.1.8000 > 127.0.0.1.16561: FP 1:146(145) ack 23 win 257
  19:55:23.652628 IP 127.0.0.1.16561 > 127.0.0.1.8000: F 23:23(0) ack 147 win 257
  19:55:23.652655 IP 127.0.0.1.8000 > 127.0.0.1.16561: . ack 24 win 257

This change should be backported to 1.4 which is where Chris encountered
this issue. The code is different, so probably the tcp_drain() function
will have to be put in the checks only.

include/common/compat.h
include/proto/proto_tcp.h
include/types/protocol.h
src/checks.c
src/proto_tcp.c
src/session.c
src/ssl_sock.c

index 0085a3aa1bc0f8f6c6f34768c647a4fccad3a98a..a0764b103a6c2d93e6471a323668c5a4ae37ed56 100644 (file)
 #define MSG_MORE       0
 #endif
 
+/* On Linux 2.4 and above, MSG_TRUNC can be used on TCP sockets to drop any
+ * pending data. Let's rely on NETFILTER to detect if this is supported.
+ */
+#ifdef NETFILTER
+#define MSG_TRUNC_CLEARS_INPUT
+#endif
+
 /* Maximum path length, OS-dependant */
 #ifndef MAXPATHLEN
 #define MAXPATHLEN 128
index 20ae2df71d7ef0919f660c397cfc3f0385fb85d5..07127648f6a48ef734d376cb937fdc3b1b2a6921 100644 (file)
@@ -34,6 +34,7 @@ int tcp_connect_server(struct connection *conn, int data, int delack);
 int tcp_connect_probe(struct connection *conn);
 int tcp_get_src(int fd, struct sockaddr *sa, socklen_t salen, int dir);
 int tcp_get_dst(int fd, struct sockaddr *sa, socklen_t salen, int dir);
+int tcp_drain(int fd);
 int tcp_inspect_request(struct session *s, struct channel *req, int an_bit);
 int tcp_inspect_response(struct session *s, struct channel *rep, int an_bit);
 int tcp_exec_req_rules(struct session *s);
index 0af2ed8a35c14f4386b4132d966a8c5835d29c1d..e03692a3567fb82443469cb04410c0d91ad2c002 100644 (file)
@@ -59,6 +59,7 @@ struct protocol {
        int (*connect)(struct connection *, int data, int delack);  /* connect function if any */
        int (*get_src)(int fd, struct sockaddr *, socklen_t, int dir); /* syscall used to retrieve src addr */
        int (*get_dst)(int fd, struct sockaddr *, socklen_t, int dir); /* syscall used to retrieve dst addr */
+       int (*drain)(int fd);                           /* indicates whether we can safely close the fd */
 
        struct list listeners;                          /* list of listeners using this protocol */
        int nb_listeners;                               /* number of listeners */
index d1b7a367d4fbdfe9b8c08786f968fbece4bfeeaa..315ef7a832692a90b4854651b4051af0d00a679f 100644 (file)
@@ -1210,11 +1210,11 @@ static void event_srv_chk_r(struct connection *conn)
         */
        if (conn->xprt && conn->xprt->shutw)
                conn->xprt->shutw(conn, 0);
-       if (conn->ctrl) {
-               if (!(conn->flags & CO_FL_WAIT_RD))
-                       recv(conn->t.sock.fd, trash.str, trash.size, MSG_NOSIGNAL|MSG_DONTWAIT);
-               setsockopt(conn->t.sock.fd, SOL_SOCKET, SO_LINGER,
-                          (struct linger *) &nolinger, sizeof(struct linger));
+
+       if (conn->ctrl && !(conn->flags & CO_FL_SOCK_RD_SH)) {
+               if (conn->flags & CO_FL_WAIT_RD || !conn->ctrl->drain || !conn->ctrl->drain(conn->t.sock.fd))
+                       setsockopt(conn->t.sock.fd, SOL_SOCKET, SO_LINGER,
+                                  (struct linger *) &nolinger, sizeof(struct linger));
        }
        __conn_data_stop_both(conn);
        task_wakeup(t, TASK_WOKEN_IO);
index e1b5d8b9ee47fb87ea98990b3edb774ef583654f..049988c4e1a2d8386dabcabc1b51cfb16922c837 100644 (file)
@@ -77,6 +77,7 @@ static struct protocol proto_tcpv4 = {
        .enable_all = enable_all_listeners,
        .get_src = tcp_get_src,
        .get_dst = tcp_get_dst,
+       .drain = tcp_drain,
        .listeners = LIST_HEAD_INIT(proto_tcpv4.listeners),
        .nb_listeners = 0,
 };
@@ -98,6 +99,7 @@ static struct protocol proto_tcpv6 = {
        .enable_all = enable_all_listeners,
        .get_src = tcp_get_src,
        .get_dst = tcp_get_dst,
+       .drain = tcp_drain,
        .listeners = LIST_HEAD_INIT(proto_tcpv6.listeners),
        .nb_listeners = 0,
 };
@@ -513,6 +515,41 @@ int tcp_get_dst(int fd, struct sockaddr *sa, socklen_t salen, int dir)
                return getsockname(fd, sa, &salen);
 }
 
+/* Tries to drain any pending incoming data from the socket to reach the
+ * receive shutdown. Returns non-zero if the shutdown was found, otherwise
+ * zero. This is useful to decide whether we can close a connection cleanly
+ * are we must kill it hard.
+ */
+int tcp_drain(int fd)
+{
+       int turns = 2;
+       int len;
+
+       while (turns) {
+#ifdef MSG_TRUNC_CLEARS_INPUT
+               len = recv(fd, NULL, INT_MAX, MSG_DONTWAIT | MSG_NOSIGNAL | MSG_TRUNC);
+               if (len == -1 && errno == EFAULT)
+#endif
+                       len = recv(fd, trash.str, trash.size, MSG_DONTWAIT | MSG_NOSIGNAL);
+
+               if (len == 0)                /* cool, shutdown received */
+                       return 1;
+
+               if (len < 0) {
+                       if (errno == EAGAIN) /* connection not closed yet */
+                               return 0;
+                       if (errno == EINTR)  /* oops, try again */
+                               continue;
+                       /* other errors indicate a dead connection, fine. */
+                       return 1;
+               }
+               /* OK we read some data, let's try again once */
+               turns--;
+       }
+       /* some data are still present, give up */
+       return 0;
+}
+
 /* This is the callback which is set when a connection establishment is pending
  * and we have nothing to send, or if we have an init function we want to call
  * once the connection is established. It updates the FD polling status. It
index adcfd4e927af0faae10ec0699fec2fa16804fad8..7309af00b8b77a3bebcfcba0e67c5cbe434ca44b 100644 (file)
@@ -157,7 +157,8 @@ int session_accept(struct listener *l, int cfd, struct sockaddr_storage *addr)
                 *  - HEALTH mode without HTTP check => just send "OK"
                 *  - TCP mode from monitoring address => just close
                 */
-               recv(cfd, trash.str, trash.size, MSG_DONTWAIT);
+               if (l->proto->drain)
+                       l->proto->drain(cfd);
                if (p->mode == PR_MODE_HTTP ||
                    (p->mode == PR_MODE_HEALTH && (p->options2 & PR_O2_CHK_ANY) == PR_O2_HTTP_CHK))
                        send(cfd, "HTTP/1.0 200 OK\r\n\r\n", 19, MSG_DONTWAIT|MSG_NOSIGNAL|MSG_MORE);
index 8a39dade31d8414bc251e13e63fe4196a3b2e0ec..7523246e0658a3b934708a238c079d7829209163 100644 (file)
@@ -1096,7 +1096,8 @@ int ssl_sock_handshake(struct connection *conn, unsigned int flag)
                                 * TCP sockets. We first try to drain possibly pending
                                 * data to avoid this as much as possible.
                                 */
-                               ret = recv(conn->t.sock.fd, trash.str, trash.size, MSG_NOSIGNAL|MSG_DONTWAIT);
+                               if (conn->ctrl && conn->ctrl->drain)
+                                       conn->ctrl->drain(conn->t.sock.fd);
                                if (!conn->err_code)
                                        conn->err_code = CO_ER_SSL_HANDSHAKE;
                                goto out_error;
@@ -1146,7 +1147,8 @@ int ssl_sock_handshake(struct connection *conn, unsigned int flag)
                         * TCP sockets. We first try to drain possibly pending
                         * data to avoid this as much as possible.
                         */
-                       ret = recv(conn->t.sock.fd, trash.str, trash.size, MSG_NOSIGNAL|MSG_DONTWAIT);
+                       if (conn->ctrl && conn->ctrl->drain)
+                               conn->ctrl->drain(conn->t.sock.fd);
                        if (!conn->err_code)
                                conn->err_code = CO_ER_SSL_HANDSHAKE;
                        goto out_error;