[nginx] QUIC: do not increase underutilized congestion window.

Vladimir Homutov vl at inspert.ru
Fri Apr 25 10:55:11 UTC 2025


missing attachments

-------------- next part --------------
commit 0c7c9d6732a5fe3a3208286c8904db1851ac2cba
Author: Vladimir Homutov <vl at inspert.ru>
Date:   Fri Apr 25 13:35:00 2025 +0300

    QUIC: fixed possible deadlock with ACKs not sent due to congestion.
    
    The commit cd5e4fa1446dff86fafc3b6ffcc11afd527a024f (QUIC: do not increase
    underutilized congestion window) lead to increased possibility of deadlock,
    caused by the fact that quic does not output any packets in congestion.
    
    If both client and server follow the same logic, it is possible that both
    ends are in the same state: waiting for ACKs to increase window, while
    being unable to send even ACK to unblock the other side.
    
    Since packets that contain ACK frames only are not congestion controlled,
    we are definitely allowed to send them, since we also need to respect
    the max_ack_delay.  Currently, the max_ack_delay may be triggered and
    push handler is called, but the output does not send anything, because
    window is closed, thus nothing is sent.
    
    The patch allows to send ACK-only packets in case when the window is closed.
    Probably, this needs to be attached to the actual trigger of max_ack_delay
    timer, but it looks like suggested changes is goed enough for practical reasons.
    
    Also, RFC 9000 13.2.1 says:
        Since packets containing only ACK frames are not congestion controlled,
        an endpoint MUST NOT send more than one such packet in response to
        receiving an ack-eliciting packet.
    
    Probably, this also needs to be accounted.

diff --git a/src/event/quic/ngx_event_quic_output.c b/src/event/quic/ngx_event_quic_output.c
index a92a539f3..4e51518c0 100644
--- a/src/event/quic/ngx_event_quic_output.c
+++ b/src/event/quic/ngx_event_quic_output.c
@@ -55,7 +55,8 @@ static ssize_t ngx_quic_send_segments(ngx_connection_t *c, u_char *buf,
     size_t len, struct sockaddr *sockaddr, socklen_t socklen, size_t segment);
 #endif
 static ssize_t ngx_quic_output_packet(ngx_connection_t *c,
-    ngx_quic_send_ctx_t *ctx, u_char *data, size_t max, size_t min);
+    ngx_quic_send_ctx_t *ctx, u_char *data, size_t max, size_t min,
+    ngx_uint_t ack_only);
 static void ngx_quic_init_packet(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
     ngx_quic_header_t *pkt, ngx_quic_path_t *path);
 static ngx_uint_t ngx_quic_get_padding_level(ngx_connection_t *c);
@@ -116,7 +117,7 @@ ngx_quic_create_datagrams(ngx_connection_t *c)
     ssize_t                 n;
     u_char                 *p;
     uint64_t                preserved_pnum[NGX_QUIC_SEND_CTX_LAST];
-    ngx_uint_t              i, pad;
+    ngx_uint_t              i, pad, ack_only;
     ngx_quic_path_t        *path;
     ngx_quic_send_ctx_t    *ctx;
     ngx_quic_congestion_t  *cg;
@@ -131,7 +132,9 @@ ngx_quic_create_datagrams(ngx_connection_t *c)
     ngx_memzero(preserved_pnum, sizeof(preserved_pnum));
 #endif
 
-    while (cg->in_flight < cg->window) {
+    ack_only = (cg->in_flight >= cg->window);
+
+    while ((cg->in_flight < cg->window) || ack_only) {
 
         p = dst;
 
@@ -158,7 +161,7 @@ ngx_quic_create_datagrams(ngx_connection_t *c)
                 return NGX_OK;
             }
 
-            n = ngx_quic_output_packet(c, ctx, p, len, min);
+            n = ngx_quic_output_packet(c, ctx, p, len, min, ack_only);
             if (n == NGX_ERROR) {
                 return NGX_ERROR;
             }
@@ -186,6 +189,9 @@ ngx_quic_create_datagrams(ngx_connection_t *c)
 
         ngx_quic_commit_send(c);
 
+        /* send pure acks just once */
+        ack_only = 0;
+
         path->sent += len;
     }
 
@@ -331,7 +337,7 @@ ngx_quic_create_segments(ngx_connection_t *c)
     size_t                  len, segsize;
     ssize_t                 n;
     u_char                 *p, *end;
-    ngx_uint_t              nseg, level;
+    ngx_uint_t              nseg, level, ack_only;
     ngx_quic_path_t        *path;
     ngx_quic_send_ctx_t    *ctx;
     ngx_quic_congestion_t  *cg;
@@ -358,13 +364,15 @@ ngx_quic_create_segments(ngx_connection_t *c)
     level = ctx - qc->send_ctx;
     preserved_pnum[level] = ctx->pnum;
 
+    ack_only = (cg->in_flight >= cg->window);
+
     for ( ;; ) {
 
         len = ngx_min(segsize, (size_t) (end - p));
 
-        if (len && cg->in_flight + (p - dst) < cg->window) {
+        if (len && ((cg->in_flight + (p - dst) < cg->window) || ack_only)) {
 
-            n = ngx_quic_output_packet(c, ctx, p, len, len);
+            n = ngx_quic_output_packet(c, ctx, p, len, len, ack_only);
             if (n == NGX_ERROR) {
                 return NGX_ERROR;
             }
@@ -397,6 +405,9 @@ ngx_quic_create_segments(ngx_connection_t *c)
 
             ngx_quic_commit_send(c);
 
+            /* send pure acks just once */
+            ack_only = 0;
+
             path->sent += n;
 
             p = dst;
@@ -521,7 +532,7 @@ ngx_quic_get_padding_level(ngx_connection_t *c)
 
 static ssize_t
 ngx_quic_output_packet(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
-    u_char *data, size_t max, size_t min)
+    u_char *data, size_t max, size_t min, ngx_uint_t ack_only)
 {
     size_t                  len, pad, min_payload, max_payload;
     u_char                 *p;
@@ -589,6 +600,12 @@ ngx_quic_output_packet(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
             break;
         }
 
+        if (ack_only
+            && (f->type != NGX_QUIC_FT_ACK && f->type != NGX_QUIC_FT_ACK_ECN))
+        {
+            continue;
+        }
+
         if (len + f->len > max_payload) {
             rc = ngx_quic_split_frame(c, f, max_payload - len);
 
-------------- next part --------------
A non-text attachment was scrubbed...
Name: logs_info.tar.gz
Type: application/gzip
Size: 24054 bytes
Desc: not available
URL: <http://mailman.nginx.org/pipermail/nginx-devel/attachments/20250425/054fe34b/attachment-0001.bin>


More information about the nginx-devel mailing list