Skip to content

Commit 51f6579

Browse files
Stop daemon response flushing from defeating mod_ratelimit.
When proxying a response body back from a daemon process, mod_wsgi forced a flush both whenever the daemon socket momentarily ran dry and after each response-buffer-size worth of data. Both forced the mod_ratelimit RATE_LIMIT filter to emit short writes, so depending on the Apache version a configured rate limit was either largely ineffective or applied far too aggressively, never honoured accurately for a daemon mode response. mod_wsgi requires Apache 2.4, whose core output filter already bounds how much response data is buffered in the child processes, so these forced flushes are not needed to limit memory use and have been removed. Response data is now passed straight on, so a downstream pacing or batching filter receives full-size writes and works correctly. A new response-flush-delay option (default 5 milliseconds) controls how long mod_wsgi waits for more data before flushing when the daemon socket runs dry, so a transient stall during an active transfer no longer triggers a flush while a genuinely idle application still has its partial output flushed within the delay. Setting it to 0 flushes on any stall and is not recommended when a pacing filter is in use. The response-buffer-size option is repurposed as a coarse runaway guard that forces a flush only once that many bytes have been passed downstream without one, bounding a downstream filter that buffers without draining. Its default is raised from 65536 bytes to 8388608 bytes accordingly.
1 parent 1f05ce1 commit 51f6579

7 files changed

Lines changed: 250 additions & 62 deletions

File tree

docs/configuration-directives/WSGIDaemonProcess.rst

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -769,20 +769,69 @@ Options which can be supplied to the ``WSGIDaemonProcess`` directive are:
769769
.. _response-buffer-size:
770770

771771
**response-buffer-size=nnn**
772-
Defines the maximum number of bytes that will be buffered for a
773-
response in the Apache child processes when proxying the response body
774-
from the WSGI application. The default size is 65536 bytes. Be careful
775-
increasing this to provide extra buffering of responses as it
776-
contributes to the runtime memory size of the Apache child processes.
772+
Acts as a coarse upper bound, in bytes, on how much response content is
773+
passed down the output filter chain without a flush when proxying a
774+
response body from the WSGI application. If this many bytes are passed
775+
without a flush occurring for another reason, a flush is forced. The
776+
default is 8388608 bytes (8 MB), and if specified the value must be at
777+
least 65536 bytes.
778+
779+
Its only purpose is to bound a downstream output filter that buffers
780+
response data without draining it, capping such buffering to roughly
781+
this many bytes per request. The Apache core output filter already
782+
bounds normal in-memory buffering of the response in the Apache child
783+
processes, so the default is set high and the guard does not affect
784+
normal operation. Setting it low forces frequent flushes that can
785+
interfere with a downstream pacing or batching output filter such as
786+
``mod_ratelimit`` (see ``response-flush-delay``), so it should only be
787+
reduced if you specifically need to cap such a filter.
777788

778789
.. _response-socket-timeout:
779790

780791
**response-socket-timeout=nnn**
781792
Defines the maximum number of seconds allowed to pass before timing out
782-
on a write operation back to the HTTP client when the response buffer
783-
has filled and data is being forcibly flushed. Defaults to 0 seconds
784-
indicating that it will default to the value of the ``socket-timeout``
785-
option.
793+
on a write operation back to the HTTP client when transferring the
794+
response body. Defaults to 0 seconds indicating that it will default to
795+
the value of the ``socket-timeout`` option.
796+
797+
.. _response-flush-delay:
798+
799+
**response-flush-delay=nnn**
800+
Defines, in **milliseconds**, how long mod_wsgi will wait for further
801+
response data to arrive from the daemon process before flushing the
802+
data already read out to the HTTP client. Defaults to 5 milliseconds.
803+
804+
When proxying a response back from a daemon process, mod_wsgi reads the
805+
data as it becomes available and would otherwise flush to the client
806+
every time the daemon socket momentarily ran dry. During a bulk
807+
transfer that emptying is usually just an artefact of timing, and
808+
flushing on it defeats a downstream output filter that paces or batches
809+
data, most notably ``mod_ratelimit`` (whose ``RATE_LIMIT`` filter is
810+
forced to emit a short write on every flush and so throttles the
811+
response far below the configured rate). Waiting a brief period for
812+
more data lets mod_wsgi coalesce it into larger writes so such a filter
813+
can do its job, while a genuinely idle application still has its partial
814+
output flushed within the delay so streaming responses remain
815+
responsive.
816+
817+
The default of 5 milliseconds is comfortably longer than the time it
818+
takes the local daemon process to produce more data during an active
819+
transfer, so it does not affect throughput, while bounding any extra
820+
latency added to a paused streaming response to that same small value.
821+
822+
Setting the value to 0 disables the wait, so data is flushed on any
823+
momentary stall. This is generally not recommended. In particular, do
824+
not set it to 0 when a downstream pacing or batching output filter such
825+
as ``mod_ratelimit`` is in use: doing so reintroduces a flush on every
826+
momentary stall, which prevents such a filter from accumulating
827+
full-size writes and can throttle responses far below the configured
828+
rate. A value of 0 is only appropriate for latency-sensitive streaming
829+
where no such filter is present and even a few milliseconds of flush
830+
delay must be avoided.
831+
832+
Note that, unlike the various ``*-timeout`` options which are expressed
833+
in seconds, this option is expressed in milliseconds because the useful
834+
values are far smaller than one second.
786835

787836
.. _server-metrics:
788837

docs/release-notes/version-6.0.2.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,52 @@
22
Version 6.0.2
33
=============
44

5+
Features Changed
6+
----------------
7+
8+
* The way mod_wsgi proxies a response body back from a daemon process to
9+
the HTTP client has been reworked so that it no longer interferes with
10+
downstream output filters that pace or batch data, most notably
11+
``mod_ratelimit``. Previously mod_wsgi forced a flush both every time the
12+
daemon socket momentarily ran dry and after each ``response-buffer-size``
13+
worth of data had been passed on. Both kinds of flush forced the
14+
``mod_ratelimit`` ``RATE_LIMIT`` filter to emit a short write and so
15+
throttled daemon mode responses far below the configured rate. As
16+
mod_wsgi requires Apache 2.4, whose core output filter already bounds how
17+
much response data is buffered in the Apache child processes, these
18+
forced flushes are no longer needed to limit memory use and have been
19+
removed. mod_wsgi now passes response data straight on, so a pacing
20+
filter receives full-size writes and works correctly, while a genuinely
21+
idle application still has its partial output flushed promptly so
22+
streaming responses remain responsive.
23+
24+
The visible effect of the previous behaviour varied with the version of
25+
Apache in use. With some versions of mod_ratelimit the forced flushes
26+
effectively let the response through at close to full speed, so a
27+
configured rate limit had little effect; with others they caused the
28+
rate limit to be applied far too aggressively, throttling the response
29+
well below the configured rate. In neither case could the rate limit be
30+
honoured accurately for a daemon mode response, which it now is.
31+
32+
Two options on the :doc:`../configuration-directives/WSGIDaemonProcess`
33+
directive control this behaviour:
34+
35+
- The new ``response-flush-delay`` option (default 5 milliseconds) sets
36+
how long mod_wsgi waits for more response data before flushing when the
37+
daemon socket runs dry, so a transient stall during an active transfer
38+
does not trigger a flush while a genuinely paused application still has
39+
its output flushed within that delay. Setting it to 0 flushes on any
40+
stall and is not recommended when a pacing filter such as
41+
``mod_ratelimit`` is in use, as it can throttle responses far below the
42+
configured rate.
43+
44+
- The ``response-buffer-size`` option has changed meaning. It is now a
45+
coarse runaway guard that forces a flush only once this many bytes have
46+
been passed downstream without one, in order to bound a downstream
47+
filter that buffers without draining. Its default has been raised from
48+
65536 bytes to 8388608 bytes (8 MB) accordingly; normal bounding of
49+
response memory use is handled by the Apache core output filter.
50+
551
Bugs Fixed
652
----------
753

src/express/apache.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
header-buffer-size=%(header_buffer_size)s \\
244244
response-buffer-size=%(response_buffer_size)s \\
245245
response-socket-timeout=%(response_socket_timeout)s \\
246+
response-flush-delay=%(response_flush_delay)s \\
246247
server-metrics=%(server_metrics_flag)s%(daemon_switch_interval_option)s
247248
</IfDefine>
248249
<IfDefine !MOD_WSGI_MULTIPROCESS>
@@ -273,6 +274,7 @@
273274
receive-buffer-size=%(receive_buffer_size)s \\
274275
response-buffer-size=%(response_buffer_size)s \\
275276
response-socket-timeout=%(response_socket_timeout)s \\
277+
response-flush-delay=%(response_flush_delay)s \\
276278
server-metrics=%(server_metrics_flag)s%(daemon_switch_interval_option)s
277279
</IfDefine>
278280
</IfDefine>

src/express/options.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -395,18 +395,32 @@ def add_option(platforms, *args, **kwargs):
395395
'indicating internal default of 32768 bytes is used.')
396396

397397
add_option('unix', '--response-buffer-size', type='int', default=0,
398-
metavar='NUMBER', help='Maximum amount of response content '
399-
'that will be allowed to be buffered in the Apache child '
400-
'worker process when proxying the response from a daemon '
401-
'process. Defaults to 0, indicating internal default of '
402-
'65536 bytes is used.')
398+
metavar='NUMBER', help='Coarse upper bound, in bytes, on how '
399+
'much response content is passed down the output filter chain '
400+
'without a flush when proxying the response from a daemon '
401+
'process. Only bounds a downstream filter that buffers without '
402+
'draining; the Apache core output filter handles normal memory '
403+
'bounding. Defaults to 0, indicating the internal default of '
404+
'8388608 bytes (8 MB) is used. Setting it low can interfere with '
405+
'a downstream pacing filter such as mod_ratelimit.')
403406

404407
add_option('unix', '--response-socket-timeout', type='int', default=0,
405408
metavar='SECONDS', help='Maximum number of seconds allowed '
406409
'to pass before timing out on a write operation back to the '
407-
'HTTP client when the response buffer has filled and data is '
408-
'being forcibly flushed. Defaults to 0 seconds indicating that '
409-
'it will default to the value of the \'socket-timeout\' option.')
410+
'HTTP client when transferring the response body. Defaults to 0 '
411+
'seconds indicating that it will default to the value of the '
412+
'\'socket-timeout\' option.')
413+
414+
add_option('unix', '--response-flush-delay', type='int', default=5,
415+
metavar='MILLISECONDS', help='Number of milliseconds mod_wsgi '
416+
'will wait for further response data from a daemon process before '
417+
'flushing what it has to the HTTP client. Waiting briefly lets a '
418+
'downstream output filter such as mod_ratelimit pace or batch the '
419+
'response correctly instead of being forced to emit a short write '
420+
'on every momentary stall. Defaults to 5 milliseconds. Setting it '
421+
'to 0 flushes on any stall and is not recommended when a pacing '
422+
'filter such as mod_ratelimit is in use, as it can throttle '
423+
'responses far below the configured rate.')
410424

411425
add_option('all', '--enable-sendfile', action='store_true',
412426
default=False, help='Flag indicating whether sendfile() support '

src/server/wsgi_daemon.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ const char *wsgi_add_daemon_process(cmd_parms *cmd, void *mconfig,
149149

150150
int response_socket_timeout = 0;
151151

152+
int response_flush_delay = -1;
153+
152154
const char *script_user = NULL;
153155
const char *script_group = NULL;
154156

@@ -510,6 +512,17 @@ const char *wsgi_add_daemon_process(cmd_parms *cmd, void *mconfig,
510512
if (response_socket_timeout < 0)
511513
return "Invalid response socket timeout for WSGI daemon process.";
512514
}
515+
else if (!strcmp(option, "response-flush-delay"))
516+
{
517+
if (!*value)
518+
return "Invalid response flush delay for WSGI daemon process.";
519+
520+
/* Value is in milliseconds; 0 disables the delay. */
521+
522+
response_flush_delay = atoi(value);
523+
if (response_flush_delay < 0)
524+
return "Invalid response flush delay for WSGI daemon process.";
525+
}
513526
else if (!strcmp(option, "socket-user"))
514527
{
515528
uid_t socket_uid;
@@ -741,6 +754,13 @@ const char *wsgi_add_daemon_process(cmd_parms *cmd, void *mconfig,
741754

742755
entry->response_socket_timeout = apr_time_from_sec(response_socket_timeout);
743756

757+
/* Default the response flush delay to 5 milliseconds. */
758+
759+
if (response_flush_delay < 0)
760+
response_flush_delay = 5;
761+
762+
entry->response_flush_delay = apr_time_from_msec(response_flush_delay);
763+
744764
entry->script_user = script_user;
745765
entry->script_group = script_group;
746766

src/server/wsgi_daemon.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ typedef struct
127127
int header_buffer_size;
128128
int response_buffer_size;
129129
apr_time_t response_socket_timeout;
130+
apr_time_t response_flush_delay;
130131
const char *script_user;
131132
const char *script_group;
132133
int cpu_time_limit;

0 commit comments

Comments
 (0)