Merge branch 'master' into hfujita/docker-noble

This commit is contained in:
Kazuho Oku 2025-04-29 11:58:31 +09:00
commit b438f61c37
46 changed files with 1364 additions and 532 deletions

View File

@ -5,7 +5,7 @@ on: [push, pull_request]
jobs:
build:
name: "${{ matrix.name }}"
runs-on: [ubuntu-20.04]
runs-on: [ubuntu-24.04]
# We want to run on external PRs, but not on our own internal PRs as they'll be run
# by the push to the branch.
@ -25,7 +25,7 @@ jobs:
command: make -f misc/docker-ci/check.mk ossl3.0 BUILD_ARGS=-j6 TEST_ENV='TEST_JOBS=4 TEST_PLATFORM=github-actions'
- name: boringssl
command: make -f misc/docker-ci/check.mk boringssl BUILD_ARGS=-j6 TEST_ENV='TEST_JOBS=4 TEST_PLATFORM=github-actions'
- name: ASan (Ubuntu 20.04)
- name: ASan (Ubuntu 24.04)
command: make -f misc/docker-ci/check.mk asan BUILD_ARGS=-j6 TEST_ENV='TEST_JOBS=4 TEST_PLATFORM=github-actions'
- name: Coverage
command: make -f misc/docker-ci/check.mk coverage BUILD_ARGS=-j6 TEST_ENV='TEST_JOBS=4 TEST_PLATFORM=github-actions'
@ -35,6 +35,15 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: setup-docker
uses: docker/setup-docker-action@v4
with:
daemon-config: |
{
"ipv6": true,
"fixed-cidr-v6": "2001:db8:1::/64"
}
channel: stable
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
@ -46,8 +55,6 @@ jobs:
shell: 'script -q -e -c "bash -xe {0}"'
run: |
chmod -R ugo+w .
# restart docker with ipv6 support
cat /etc/docker/daemon.json | perl -pe 's| }|, "ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64" }|' | sudo tee /etc/docker/daemon.json > /dev/null; sudo systemctl restart docker; sleep 10
${{ matrix.command }}
# if there's a summary markdown in the container, show it.
container_id=$(docker container ls -al --format '{{ .ID }}')

View File

@ -7,7 +7,7 @@ on:
jobs:
submit:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3

View File

@ -165,6 +165,16 @@ ELSE ()
ENDIF ()
OPTION(WITH_AEGIS "enable AEGIS (requires libaegis)" ${WITH_AEGIS_DEFAULT})
IF (PKG_CONFIG_FOUND)
PKG_CHECK_MODULES(LIBURING liburing)
ENDIF ()
IF (LIBURING_FOUND)
SET(WITH_IO_URING_DEFAULT "ON")
ELSE ()
SET(WITH_IO_URING_DEFAULT "OFF")
ENDIF ()
OPTION(WITH_IO_URING "whether or not to use io_uring" ${WITH_IO_URING_DEFAULT})
INCLUDE_DIRECTORIES(
include
deps/cloexec
@ -781,6 +791,11 @@ ENDIF ()
IF (WITH_AEGIS)
SET(STANDALONE_COMPILE_FLAGS "${STANDALONE_COMPILE_FLAGS} -DPTLS_HAVE_AEGIS=1")
ENDIF ()
IF (WITH_IO_URING)
# io_uring is enabled only for standalone server, as it is impossible to cleanly shutdown synchronously
SET(STANDALONE_COMPILE_FLAGS "${STANDALONE_COMPILE_FLAGS} -DH2O_USE_IO_URING=1")
LIST(APPEND STANDALONE_SOURCE_FILES lib/common/io_uring.c)
ENDIF ()
ADD_EXECUTABLE(h2o ${STANDALONE_SOURCE_FILES})
SET_TARGET_PROPERTIES(h2o PROPERTIES COMPILE_FLAGS "${STANDALONE_COMPILE_FLAGS}")
TARGET_INCLUDE_DIRECTORIES(h2o PUBLIC ${OPENSSL_INCLUDE_DIR})
@ -798,6 +813,11 @@ IF (WITH_AEGIS)
TARGET_INCLUDE_DIRECTORIES(h2o PUBLIC ${aegis_INCLUDE_DIR})
TARGET_LINK_LIBRARIES(h2o ${aegis_LIBRARIES})
ENDIF ()
IF (WITH_IO_URING)
TARGET_INCLUDE_DIRECTORIES(h2o PUBLIC ${LIBURING_INCLUDE_DIRS})
TARGET_LINK_DIRECTORIES(h2o PUBLIC ${LIBURING_LIBRARY_DIRS})
TARGET_LINK_LIBRARIES(h2o ${LIBURING_LIBRARIES})
ENDIF ()
TARGET_LINK_LIBRARIES(h2o ${EXTRA_LIBS})
INSTALL(TARGETS h2o

View File

@ -88,6 +88,9 @@ This document describes the configuration directives common to all the protocols
<li><a href="configure/base_directives.html#handshake-timeout">
<code>handshake-timeout</code>
</a></li>
<li><a href="configure/base_directives.html#io_uring-batch-size">
<code>io_uring-batch-size</code>
</a></li>
<li><a href="configure/base_directives.html#limit-request-body">
<code>limit-request-body</code>
</a></li>
@ -103,6 +106,9 @@ This document describes the configuration directives common to all the protocols
<li><a href="configure/base_directives.html#max-reprocesses">
<code>max-reprocesses</code>
</a></li>
<li><a href="configure/base_directives.html#max-spare-pipes">
<code>max-spare-pipes</code>
</a></li>
<li><a href="configure/base_directives.html#neverbleed-offload">
<code>neverbleed-offload</code>
</a></li>
@ -711,6 +717,29 @@ Times spent for receiving <a href="configure/base_directives.html#listen-proxy-p
<dd><code><pre>handshake-timeout: 10</pre></code>
</dl>
<div id="io_uring-batch-size" class="directive-head">
<div class="directive-since">experimental</div>
<h3><a href="configure/base_directives.html#io_uring-batch-size"><code>"io_uring-batch-size"</code></a></h3>
</div>
<dl class="directive-desc">
<dt>Description:</dt>
<dd>
<p>
Number of io_uring calls to issue at once. Increasing this number might reduce overhead.
</p>
</dd>
<dt><a href="configure/syntax_and_structure.html#config_levels">Level</a>:</dt>
<dd>global</dd>
<dt>Default:</dt>
<dd><code><pre>io_uring-batch-size: 1</pre></code>
<dt>See also:</dt>
<dd><a href="configure/file_directives.html#file.io_uring"><code>file.io_uring</code></a>
</dd>
</dl>
<div id="limit-request-body" class="directive-head">
<h3><a href="configure/base_directives.html#limit-request-body"><code>"limit-request-body"</code></a></h3>
</div>
@ -816,6 +845,41 @@ Limits the number of internal redirects.
<dt>Default:</dt>
<dd><code><pre>max-reprocesses: 5</pre></code>
</dl>
<div id="max-spare-pipes" class="directive-head">
<h3><a href="configure/base_directives.html#max-spare-pipes"><code>"max-spare-pipes"</code></a></h3>
</div>
<dl class="directive-desc">
<dt>Description:</dt>
<dd>
<p>
This setting specifies the maximum number of pipes retained for reuse, when <code>file.io_uring</code> or <code>proxy.zerocopy</code> is used.
</p>
<p>
The setting can be used to reduce lock contention in the kernel under high load.
</p>
<p>
This maximum is applied per each worker thread.
</p>
<p>
Setting this value to 0 will cause no pipes to be retained by h2o; the pipes will be closed after they are used.
In this case, h2o will create new pipes each time they are needed.
</p>
<p>
In previous versions, this configuration directive was called <code>proxy.max-spare-pipes</code>.
</p>
</dd>
<dt><a href="configure/syntax_and_structure.html#config_levels">Level</a>:</dt>
<dd>global</dd>
<dt>Default:</dt>
<dd><code><pre>0</pre></code>
<dt>See also:</dt>
<dd><a href="configure/file_directives.html#file.io_uring"><code>file.io_uring</code></a>, <a href="configure/proxy_directives.html#proxy.zerocopy"><code>proxy.zerocopy</code></a>
</dd>
</dl>
<div id="neverbleed-offload" class="directive-head">
<div class="directive-since">experimental</div>
<h3><a href="configure/base_directives.html#neverbleed-offload"><code>"neverbleed-offload"</code></a></h3>

View File

@ -86,6 +86,9 @@ Other directives modify the behavior of the mappings defined by the two.
<li><a href="configure/file_directives.html#file.index">
<code>file.index</code>
</a></li>
<li><a href="configure/file_directives.html#file.io_uring">
<code>file.io_uring</code>
</a></li>
<li><a href="configure/file_directives.html#file.mime.addtypes">
<code>file.mime.addtypes</code>
</a></li>
@ -267,6 +270,35 @@ The sequence of filenames are searched from left to right, and the first file th
</dd>
</dl>
<div id="file.io_uring" class="directive-head">
<div class="directive-since">experimental</div>
<h3><a href="configure/file_directives.html#file.io_uring"><code>"file.io_uring"</code></a></h3>
</div>
<dl class="directive-desc">
<dt>Description:</dt>
<dd>
<p>
If io_uring should be used for serving files.
</p>
<p>
By default, H2O uses system calls such as pread (2) or sendfile (2), which block if the file being read is not in the page cache.
This can prevent H2O's worker threads from making progress on any connection handled by the affected thread.
When this flag is enabled, H2O is no longer blocked by the I/O calls, as the data is asynchronously copied from disk to the page cache before h2o attempts to access it.
</p>
</dd>
<dt><a href="configure/syntax_and_structure.html#config_levels">Level</a>:</dt>
<dd>global, host, path</dd>
<dt>Default:</dt>
<dd><code><pre>file.io_uring: OFF</pre></code>
<dt>See also:</dt>
<dd><a href="configure/base_directives.html#io_uring-batch-size"><code>io_uring-batch-size</code></a>,
<a href="configure/base_directives.html#max-spare-pipes"><code>max-spare-pipes</code></a>
</dd>
</dl>
<div id="file.mime.addtypes" class="directive-head">
<h3><a href="configure/file_directives.html#file.mime.addtypes"><code>"file.mime.addtypes"</code></a></h3>
</div>

View File

@ -161,9 +161,6 @@ Following sections describe the configuration directives defined for the module.
<li><a href="configure/proxy_directives.html#proxy.max-buffer-size">
<code>proxy.max-buffer-size</code>
</a></li>
<li><a href="configure/proxy_directives.html#proxy.max-spare-pipes">
<code>proxy.max-spare-pipes</code>
</a></li>
<li><a href="configure/proxy_directives.html#proxy.preserve-host">
<code>proxy.preserve-host</code>
</a></li>
@ -852,37 +849,6 @@ But if the backend server has enough concurrency, <code>proxy.max-buffer-size</c
</dd>
</dl>
<div id="proxy.max-spare-pipes" class="directive-head">
<h3><a href="configure/proxy_directives.html#proxy.max-spare-pipes"><code>"proxy.max-spare-pipes"</code></a></h3>
</div>
<dl class="directive-desc">
<dt>Description:</dt>
<dd>
<p>
This setting specifies the maximum number of pipes retained for reuse, when <code>proxy.zerocopy</code> is used.
</p>
<p>
This maximum is applied per each worker thread.
The intention of this setting is to reduce lock contention in the kernel under high load when zerocopy is used.
When this setting is set to a non-zero value, specified number of pipes will be allocated upon startup for each worker thread.
</p>
<p>
Setting this value to 0 will cause no pipes to be retained by h2o; the pipes will be closed after they are used.
In this case, h2o will create new pipes each time they are needed.
</p>
</dd>
<dt><a href="configure/syntax_and_structure.html#config_levels">Level</a>:</dt>
<dd>global</dd>
<dt>Default:</dt>
<dd><code><pre>0</pre></code>
<dt>See also:</dt>
<dd><a href="configure/proxy_directives.html#proxy.zerocopy"><code>proxy.zerocopy</code></a>
</dd>
</dl>
<div id="proxy.preserve-host" class="directive-head">
<h3><a href="configure/proxy_directives.html#proxy.preserve-host"><code>"proxy.preserve-host"</code></a></h3>
</div>
@ -1203,7 +1169,7 @@ This approach does not reduce the total amount of bytes flowing through the CPU,
<dt>Default:</dt>
<dd><code><pre>proxy.zerocopy: OFF</pre></code>
<dt>See also:</dt>
<dd><a href="configure/base_directives.html#ssl-offload"><code>ssl-offload</code></a>, <a href="configure/proxy_directives.html#proxy.max-spare-pipes"><code>proxy.max-spare-pipes</code></a>
<dd><a href="configure/base_directives.html#max-spare-pipes"><code>proxy.max-spare-pipes</code></a>, <a href="configure/base_directives.html#ssl-offload"><code>ssl-offload</code></a>
</dd>
</dl>

View File

@ -896,6 +896,19 @@ Times spent for receiving the PROXY protocol and TLS handshake are counted.
.RE
.RE
.SS io_uring-batch-size
Number of io_uring calls to issue at once. Increasing this number might reduce overhead.
.PP
.PP
.BR See\ also:
file.io_uring
.RE
.RE
.SS limit-request-body
Maximum size of request body in bytes (e.g. content of POST).
@ -956,6 +969,32 @@ Limits the number of internal redirects.
.RE
.RE
.SS max-spare-pipes
This setting specifies the maximum number of pipes retained for reuse, when file.io_uring or proxy.zerocopy is used.
.PP
The setting can be used to reduce lock contention in the kernel under high load.
This maximum is applied per each worker thread.
Setting this value to 0 will cause no pipes to be retained by h2o; the pipes will be closed after they are used.
In this case, h2o will create new pipes each time they are needed.
In previous versions, this configuration directive was called proxy.max-spare-pipes.
.PP
.BR See\ also:
file.io_uring, proxy.zerocopy
.RE
.RE
.SS neverbleed-offload
Sets an offload engine to be used with neverbleed.
@ -2923,6 +2962,25 @@ file.dir
.RE
.RE
.SS file.io_uring
If io_uring should be used for serving files.
.PP
By default, H2O uses system calls such as pread (2) or sendfile (2), which block if the file being read is not in the page cache.
This can prevent H2O's worker threads from making progress on any connection handled by the affected thread.
When this flag is enabled, H2O is no longer blocked by the I/O calls, as the data is asynchronously copied from disk to the page cache before h2o attempts to access it.
.PP
.BR See\ also:
io_uring-batch-size,
max-spare-pipes
.RE
.RE
.SS file.mime.addtypes
The directive modifies the MIME mappings by adding the specified MIME type mappings.
@ -3821,28 +3879,6 @@ temp-buffer-threshold
.RE
.RE
.SS proxy.max-spare-pipes
This setting specifies the maximum number of pipes retained for reuse, when proxy.zerocopy is used.
.PP
This maximum is applied per each worker thread.
The intention of this setting is to reduce lock contention in the kernel under high load when zerocopy is used.
When this setting is set to a non-zero value, specified number of pipes will be allocated upon startup for each worker thread.
Setting this value to 0 will cause no pipes to be retained by h2o; the pipes will be closed after they are used.
In this case, h2o will create new pipes each time they are needed.
.PP
.BR See\ also:
proxy.zerocopy
.RE
.RE
.SS proxy.preserve-host
A boolean flag (ON or OFF) designating whether or not to pass Host header from incoming request to upstream.
@ -4039,7 +4075,7 @@ This approach does not reduce the total amount of bytes flowing through the CPU,
.PP
.BR See\ also:
ssl-offload, proxy.max-spare-pipes
proxy.max-spare-pipes, ssl-offload
.RE
.RE

File diff suppressed because one or more lines are too long

View File

@ -46,6 +46,19 @@ provider h2o {
*/
probe socket_write_tls_record(struct st_h2o_socket_t *sock, size_t write_size, size_t bytes_buffered);
/**
*
*/
probe io_uring_splice(struct st_h2o_io_uring_cmd_t *cmd);
/**
*
*/
probe io_uring_submit(struct st_h2o_io_uring_cmd_t *cmd);
/**
*
*/
probe io_uring_end(struct st_h2o_io_uring_cmd_t *cmd);
/**
* HTTP-level event, indicating that a request has been received.
*/

View File

@ -64,6 +64,7 @@
08F320E01E7A9CBD0038FA5A /* net.c in Sources */ = {isa = PBXBuildFile; fileRef = 08790DD91D80153600A04BC1 /* net.c */; settings = {COMPILER_FLAGS = "-Wno-conversion"; }; };
08F320E11E7A9CBF0038FA5A /* read.c in Sources */ = {isa = PBXBuildFile; fileRef = 0812AB291D7FD54700004F23 /* read.c */; settings = {COMPILER_FLAGS = "-Wno-conversion"; }; };
08F320E21E7A9CC20038FA5A /* sds.c in Sources */ = {isa = PBXBuildFile; fileRef = 08790DDB1D80154C00A04BC1 /* sds.c */; settings = {COMPILER_FLAGS = "-Wno-conversion"; }; };
08FE0365299F9F8B00141FA2 /* io_uring.h in Headers */ = {isa = PBXBuildFile; fileRef = 08FE0364299F9F8B00141FA2 /* io_uring.h */; };
100A55101C2BB15600C4E3E0 /* http_request.c in Sources */ = {isa = PBXBuildFile; fileRef = 100A550E1C2BB15100C4E3E0 /* http_request.c */; };
101788B219B561AA0084C6D8 /* socket.c in Sources */ = {isa = PBXBuildFile; fileRef = 101788B119B561AA0084C6D8 /* socket.c */; };
101B670C19ADD3380084A351 /* access_log.c in Sources */ = {isa = PBXBuildFile; fileRef = 101B670B19ADD3380084A351 /* access_log.c */; };
@ -714,6 +715,7 @@
082E1CCD2692F52200603AED /* driver_common.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = driver_common.cc; sourceTree = "<group>"; };
082E1CCE2692F52200603AED /* h3_header_generator.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = h3_header_generator.c; sourceTree = "<group>"; };
082E224A2692F52E00603AED /* quicly_mock.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = quicly_mock.c; sourceTree = "<group>"; };
083453D62DBF48CE00CAB789 /* 80issues3479.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = 80issues3479.t; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
084FC7C01D54B90D00E89F66 /* http2_debug_state.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = http2_debug_state.c; sourceTree = "<group>"; };
084FC7C31D54BB9200E89F66 /* http2_debug_state.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = http2_debug_state.c; sourceTree = "<group>"; };
08529A932AD4F6FF00A2C76B /* 80http2-reset-flood.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "80http2-reset-flood.t"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
@ -761,6 +763,8 @@
08AD2A3E2CCB840800301CD6 /* 90h2olog-qlog.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "90h2olog-qlog.t"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
08AD2A3F2CCB840800301CD6 /* 90h2olog-socket.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "90h2olog-socket.t"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
08AD2A402CCF219600301CD6 /* h2olog */ = {isa = PBXFileReference; lastKnownFileType = text; path = h2olog; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
08B2EBF42D9F5E4000715398 /* test.pl */ = {isa = PBXFileReference; lastKnownFileType = text.script.perl; path = test.pl; sourceTree = "<group>"; };
08B2EBF62DA09FD000715398 /* 40http3-io-batch10.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "40http3-io-batch10.t"; sourceTree = "<group>"; };
08B3297E29407664009D6766 /* hpke.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = hpke.c; sourceTree = "<group>"; };
08B329832941B407009D6766 /* 40tls-ech.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "40tls-ech.t"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
08B3D43A26042CAF002F195C /* 50connect-deadlock.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "50connect-deadlock.t"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
@ -780,6 +784,8 @@
08E9CC4D1E41F6660049DD26 /* embedded.c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = embedded.c.h; sourceTree = "<group>"; };
08EFC4CF27FD8FAA004C532C /* 50file-shrink.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "50file-shrink.t"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
08EFC4DF27FE9F96004C532C /* throttle_resp.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = throttle_resp.c; sourceTree = "<group>"; };
08FE0364299F9F8B00141FA2 /* io_uring.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = io_uring.h; sourceTree = "<group>"; };
08FE0366299FA1FE00141FA2 /* io_uring.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = io_uring.c; sourceTree = "<group>"; };
100A550C1C22857B00C4E3E0 /* 50mruby-htpasswd.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "50mruby-htpasswd.t"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
100A550E1C2BB15100C4E3E0 /* http_request.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = http_request.c; sourceTree = "<group>"; };
100A55131C2E5FAC00C4E3E0 /* 50mruby-http-request.t */ = {isa = PBXFileReference; lastKnownFileType = text; path = "50mruby-http-request.t"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.perl; };
@ -1508,6 +1514,14 @@
path = 50mruby;
sourceTree = "<group>";
};
08B2EBF52D9F5E4000715398 /* 40http3 */ = {
isa = PBXGroup;
children = (
08B2EBF42D9F5E4000715398 /* test.pl */,
);
path = 40http3;
sourceTree = "<group>";
};
08B4D4391D75A2950079DFB5 /* hiredis */ = {
isa = PBXGroup;
children = (
@ -1574,6 +1588,7 @@
10D0905419F102C70043D458 /* http1client.c */,
7D0341FA1FE4D5B60052E0A1 /* http2client.c */,
E9E50472214A5B8A004DC170 /* http3client.c */,
08FE0366299FA1FE00141FA2 /* io_uring.c */,
10A3D3D11B4CDBDC00327CF9 /* memcached.c */,
08CEA9D126701D7600B4BB6B /* rand.c */,
08790DE31D8276EA00A04BC1 /* redis.c */,
@ -2007,6 +2022,7 @@
E9E50475214B3D28004DC170 /* http3_common.h */,
E9757708221BB78C00D1EF74 /* http3_internal.h */,
E9E5049321501CA5004DC170 /* http3_server.h */,
08FE0364299F9F8B00141FA2 /* io_uring.h */,
10A3D3D01B4CDBC700327CF9 /* memcached.h */,
10D0904219F0BA780043D458 /* linklist.h */,
10D0903919F0A51C0043D458 /* memory.h */,
@ -2248,12 +2264,14 @@
E9F677CA1FF47BD0006476D3 /* 40http2-h2spec.t */,
080590262A5E463A00B2B10E /* 40http2-header-softerror.t */,
E9BCE6921FF344D4003CEA11 /* 40http2-request-window-size.t */,
08B2EBF52D9F5E4000715398 /* 40http3 */,
E98041C22230C45E008B9745 /* 40http3.t */,
086001BE2730B6E30043886F /* 40http3-corrupted-scid-initial.t */,
E90BB43B24F38937006507EA /* 40http3-concurrency.t */,
E90BB43C24F4834D006507EA /* 40http3-forward.t */,
E9D49A5025FB561A00F4A80D /* 40http3-forward-initial.t */,
0810E6AA286DACB600333FA4 /* 40http3-invalid-token.t */,
08B2EBF62DA09FD000715398 /* 40http3-io-batch10.t */,
085BDAE72B1DB4C2002851EA /* 40http3-priority.t */,
E9414F8D24EBFBDA00273C59 /* 40http3-reconnect.t */,
E9FC525C234B0A630076F35D /* 40http3-retry.t */,
@ -2398,6 +2416,7 @@
105534FE1A46134A00627ECB /* 80issues61.t */,
1092E0011BEB1DDC001074BF /* 80issues579.t */,
104481271BFC05FC0007863F /* 80issues595.t */,
083453D62DBF48CE00CAB789 /* 80issues3479.t */,
10952D591C5082F7000D664C /* 80issues-from-proxy-reproxy-to-different-host.t */,
E9BCE6911FF326AC003CEA11 /* 80no-handler-vs-h2-post.t */,
109EEFD81D77B336001F11D1 /* 80one-byte-window.t */,
@ -2966,6 +2985,7 @@
0812AB211D7FCFEB00004F23 /* async.h in Headers */,
10D0903A19F0A51C0043D458 /* memory.h in Headers */,
7DA3F5AF20E0B0400000222F /* token_table.h in Headers */,
08FE0365299F9F8B00141FA2 /* io_uring.h in Headers */,
104B9A2D1A4BE029009EEE64 /* version.h in Headers */,
E980423B22463044008B9745 /* cc.h in Headers */,
E9E5049421501CA5004DC170 /* http3_server.h in Headers */,

View File

@ -400,6 +400,10 @@ struct st_h2o_globalconf_t {
* SSL handshake timeout
*/
uint64_t handshake_timeout;
/**
* maximum number of pipes to retain for reuse
*/
size_t max_spare_pipes;
struct {
/**
@ -534,10 +538,6 @@ struct st_h2o_globalconf_t {
* maximum size to buffer for the response
*/
size_t max_buffer_size;
/**
* maximum number of pipes to retain for reuse
*/
size_t max_spare_pipes;
/**
* a boolean flag if set to true, instructs to use zero copy (i.e., splice to pipe then splice to socket) if possible
*/
@ -685,6 +685,13 @@ struct st_h2o_context_t {
* open file cache
*/
h2o_filecache_t *filecache;
/**
* the list of spare pipes currently retained for reuse
*/
struct {
int (*pipes)[2];
size_t count;
} spare_pipes;
/**
* context scope storage for general use
*/
@ -787,13 +794,6 @@ struct st_h2o_context_t {
* the default connection pool for proxy
*/
h2o_httpclient_connection_pool_t connpool;
/**
* the list of spare pipes currently retained for reuse
*/
struct {
int (*pipes)[2];
size_t count;
} spare_pipes;
} proxy;
struct {
@ -1653,7 +1653,15 @@ static int h2o_send_state_is_in_progress(h2o_send_state_t s);
* @param state describes if the output is final, has an error, or is in progress
*/
void h2o_send(h2o_req_t *req, h2o_iovec_t *bufs, size_t bufcnt, h2o_send_state_t state);
/**
* Same as `h2o_send` but sends `h2o_sendvec_t`s
*/
void h2o_sendvec(h2o_req_t *req, h2o_sendvec_t *vecs, size_t veccnt, h2o_send_state_t state);
/**
* Wrapper around `h2o_sendvec` that sends the contents of pipe
*/
void h2o_send_from_pipe(h2o_req_t *req, int pipefd, size_t len, h2o_send_state_t send_state);
/**
* creates an uninitialized prefilter and returns pointer to it
*/
@ -1823,6 +1831,14 @@ static void *h2o_context_get_logger_context(h2o_context_t *ctx, h2o_logger_t *lo
* return the address associated with the key in the context storage
*/
static void **h2o_context_get_storage(h2o_context_t *ctx, size_t *key, void (*dispose_cb)(void *));
/**
* provides a new pipe that has O_NONBLOCK set, possibly reusing a cached one that is empty; returns a boolean indicating success
*/
int h2o_context_new_pipe(h2o_context_t *ctx, int fds[2]);
/**
* returns a pipe to the spare pipe cache
*/
void h2o_context_return_spare_pipe(h2o_context_t *ctx, int fds[2]);
/* built-in generators */
@ -2169,7 +2185,8 @@ enum {
H2O_FILE_FLAG_NO_ETAG = 0x1,
H2O_FILE_FLAG_DIR_LISTING = 0x2,
H2O_FILE_FLAG_SEND_COMPRESSED = 0x4,
H2O_FILE_FLAG_GUNZIP = 0x8
H2O_FILE_FLAG_GUNZIP = 0x8,
H2O_FILE_FLAG_IO_URING = 0x16,
};
typedef struct st_h2o_file_handler_t h2o_file_handler_t;

81
include/h2o/io_uring.h Normal file
View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2022,2023 Kazuho Oku, Fastly
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#ifndef h2o__io_uring_h
#define h2o__io_uring_h
#if !H2O_USE_IO_URING
#error "this file may be included only when io_uring support is available"
#endif
#include <liburing.h>
typedef struct st_h2o_io_uring_cmd_t h2o_io_uring_cmd_t;
typedef void (*h2o_io_uring_cb)(h2o_io_uring_cmd_t *);
struct st_h2o_io_uring_queue_t {
struct st_h2o_io_uring_cmd_t *head;
struct st_h2o_io_uring_cmd_t **tail;
size_t size;
};
typedef struct st_h2o_io_uring_t {
struct io_uring uring;
h2o_socket_t *sock_notify;
struct st_h2o_io_uring_queue_t submission, completion;
h2o_timer_t delayed;
} h2o_io_uring_t;
/**
* Object used for buffering and tracking requests to io_uring. It is defined here as a public type, because h2o-probes.d referes to
* the type, in addition to io_uring.c.
*/
struct st_h2o_io_uring_cmd_t {
struct {
h2o_io_uring_cb func;
void *data;
} cb;
int result;
struct st_h2o_io_uring_cmd_t *next;
struct {
int fd_in;
int64_t off_in;
int fd_out;
int64_t off_out;
unsigned nbytes;
unsigned splice_flags;
} splice_;
};
extern size_t h2o_io_uring_batch_size;
/**
* initializes structure related to async I/O of `loop`
*/
void h2o_io_uring_init(h2o_loop_t *loop);
/**
* Calls splice using io_uring. The callback might get called synchronously, depending on the condition (e.g., if data being read is
* in page cache and h2o_io_uring_batch_size == 1).
*/
void h2o_io_uring_splice(h2o_loop_t *loop, int fd_in, int64_t off_in, int fd_out, int64_t off_out, unsigned nbytes,
unsigned splice_flags, h2o_io_uring_cb cb, void *data);
#endif

View File

@ -111,6 +111,9 @@ typedef struct st_h2o_socket_t h2o_socket_t;
typedef void (*h2o_socket_cb)(h2o_socket_t *sock, const char *err);
/* used by probes */
struct st_h2o_io_uring_cmd_t;
#if H2O_USE_LIBUV
#include "socket/uv-binding.h"
#else

View File

@ -84,6 +84,11 @@ int h2o_evloop_run(h2o_evloop_t *loop, int32_t max_wait);
static void h2o_timer_link(h2o_evloop_t *loop, uint64_t delay_ticks, h2o_timer_t *timer);
#define h2o_timer_unlink h2o_timerwheel_unlink
#if H2O_USE_IO_URING
struct st_h2o_io_uring_t;
struct st_h2o_io_uring_t *h2o_evloop__io_uring(h2o_evloop_t *loop);
#endif
/* inline definitions */
static inline struct timeval h2o_gettimeofday(h2o_evloop_t *loop)

222
lib/common/io_uring.c Normal file
View File

@ -0,0 +1,222 @@
/*
* Copyright (c) 2022,2023 Kazuho Oku, Fastly
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include "h2o/socket.h"
#include "h2o/io_uring.h"
#include "../probes_.h"
size_t h2o_io_uring_batch_size = 1;
static void init_queue(struct st_h2o_io_uring_queue_t *queue)
{
queue->head = NULL;
queue->tail = &queue->head;
queue->size = 0;
}
static void insert_queue(struct st_h2o_io_uring_queue_t *queue, struct st_h2o_io_uring_cmd_t *cmd)
{
assert(cmd->next == NULL);
*queue->tail = cmd;
queue->tail = &cmd->next;
++queue->size;
}
static struct st_h2o_io_uring_cmd_t *pop_queue(struct st_h2o_io_uring_queue_t *queue)
{
struct st_h2o_io_uring_cmd_t *popped;
if ((popped = queue->head) != NULL) {
if ((queue->head = popped->next) == NULL)
queue->tail = &queue->head;
--queue->size;
popped->next = NULL;
}
return popped;
}
static int submit_commands(struct st_h2o_io_uring_t *io_uring, int can_delay)
{
if (can_delay && io_uring->submission.size < h2o_io_uring_batch_size)
return 0;
int made_progress = 0;
while (io_uring->submission.head != NULL) {
struct io_uring_sqe *sqe;
if ((sqe = io_uring_get_sqe(&io_uring->uring)) == NULL)
break;
struct st_h2o_io_uring_cmd_t *cmd = pop_queue(&io_uring->submission);
assert(cmd != NULL);
H2O_PROBE(IO_URING_SUBMIT, cmd);
io_uring_prep_splice(sqe, cmd->splice_.fd_in, cmd->splice_.off_in, cmd->splice_.fd_out, cmd->splice_.off_out,
cmd->splice_.nbytes, cmd->splice_.splice_flags);
sqe->user_data = (uint64_t)cmd;
made_progress = 1;
}
if (made_progress) {
int ret;
while ((ret = io_uring_submit(&io_uring->uring)) == -EINTR)
;
if (ret < 0)
h2o_fatal("io_uring_submit:%s", strerror(-ret));
}
return made_progress;
}
static int check_completion(struct st_h2o_io_uring_t *io_uring, struct st_h2o_io_uring_cmd_t *cmd_sync)
{
int cmd_sync_done = 0, ret;
while (1) {
struct st_h2o_io_uring_cmd_t *cmd;
{ /* obtain completed command and its result */
struct io_uring_cqe *cqe;
while ((ret = io_uring_peek_cqe(&io_uring->uring, &cqe)) == -EINTR)
;
if (ret != 0)
break;
cmd = (struct st_h2o_io_uring_cmd_t *)cqe->user_data;
cmd->result = cqe->res;
io_uring_cqe_seen(&io_uring->uring, cqe);
}
/* link to completion list or indicate to the caller that `cmd_sync` has completed */
if (cmd == cmd_sync) {
cmd_sync_done = 1;
} else {
insert_queue(&io_uring->completion, cmd);
}
}
return cmd_sync_done;
}
static int dispatch_completed(struct st_h2o_io_uring_t *io_uring)
{
if (io_uring->completion.head == NULL)
return 0;
do {
struct st_h2o_io_uring_cmd_t *cmd = pop_queue(&io_uring->completion);
H2O_PROBE(IO_URING_END, cmd);
cmd->cb.func(cmd);
free(cmd);
} while (io_uring->completion.head != NULL);
return 1;
}
static void start_command(h2o_loop_t *loop, struct st_h2o_io_uring_t *io_uring, struct st_h2o_io_uring_cmd_t *cmd)
{
int needs_timer = 0;
insert_queue(&io_uring->submission, cmd);
submit_commands(io_uring, 1);
/* if we have submitted all commands up to the current one, fetch completion events in hope that we might be able to complete
* the current one synchronously (as doing so improves locality) */
if (io_uring->submission.head == NULL) {
if (check_completion(io_uring, cmd)) {
cmd->cb.func(cmd);
free(cmd);
cmd = NULL;
}
if (io_uring->completion.head != NULL)
needs_timer = 1;
} else {
needs_timer = 1;
}
if (needs_timer && !h2o_timer_is_linked(&io_uring->delayed))
h2o_timer_link(loop, 0, &io_uring->delayed);
}
void h2o_io_uring_splice(h2o_loop_t *loop, int fd_in, int64_t off_in, int fd_out, int64_t off_out, unsigned nbytes,
unsigned splice_flags, h2o_io_uring_cb cb, void *data)
{
struct st_h2o_io_uring_t *io_uring = h2o_evloop__io_uring(loop);
/* build command */
struct st_h2o_io_uring_cmd_t *cmd = h2o_mem_alloc(sizeof(*cmd));
*cmd = (struct st_h2o_io_uring_cmd_t){
.cb.func = cb,
.cb.data = data,
.splice_.fd_in = fd_in,
.splice_.off_in = off_in,
.splice_.fd_out = fd_out,
.splice_.off_out = off_out,
.splice_.nbytes = nbytes,
.splice_.splice_flags = splice_flags,
};
H2O_PROBE(IO_URING_SPLICE, cmd);
start_command(loop, io_uring, cmd);
}
static void run_uring(struct st_h2o_io_uring_t *io_uring)
{
/* Repeatedly read cqe, until we bocome certain we haven't issued more read commands. */
do {
check_completion(io_uring, NULL);
} while (dispatch_completed(io_uring) || submit_commands(io_uring, 0));
assert(io_uring->completion.head == NULL);
}
static void on_notify(h2o_socket_t *sock, const char *err)
{
assert(err == NULL);
h2o_loop_t *loop = h2o_socket_get_loop(sock);
struct st_h2o_io_uring_t *io_uring = h2o_evloop__io_uring(loop);
run_uring(io_uring);
}
static void on_delayed(h2o_timer_t *_timer)
{
struct st_h2o_io_uring_t *io_uring = H2O_STRUCT_FROM_MEMBER(struct st_h2o_io_uring_t, delayed, _timer);
run_uring(io_uring);
}
void h2o_io_uring_init(h2o_loop_t *loop)
{
struct st_h2o_io_uring_t *io_uring = h2o_evloop__io_uring(loop);
int ret;
if ((ret = io_uring_queue_init(16, &io_uring->uring, 0)) != 0)
h2o_fatal("io_uring_queue_init:%s", strerror(-ret));
io_uring->sock_notify = h2o_evloop_socket_create(loop, io_uring->uring.ring_fd, H2O_SOCKET_FLAG_DONT_READ);
h2o_socket_read_start(io_uring->sock_notify, on_notify);
init_queue(&io_uring->submission);
init_queue(&io_uring->completion);
h2o_timer_init(&io_uring->delayed, on_delayed);
}

View File

@ -23,6 +23,9 @@
#include <limits.h>
#include <stdio.h>
#include <sys/epoll.h>
#if H2O_USE_IO_URING
#include "h2o/io_uring.h"
#endif
#if 0
#define DEBUG_LOG(...) h2o_error_printf(__VA_ARGS__)
@ -33,6 +36,9 @@
struct st_h2o_evloop_epoll_t {
h2o_evloop_t super;
int ep;
#if H2O_USE_IO_URING
h2o_io_uring_t io_uring;
#endif
};
static int change_epoll_mode(struct st_h2o_evloop_socket_t *sock, uint32_t events)
@ -312,6 +318,7 @@ static void evloop_do_dispose(h2o_evloop_t *_loop)
struct st_h2o_evloop_epoll_t *loop = (struct st_h2o_evloop_epoll_t *)_loop;
close(loop->ep);
}
h2o_evloop_t *h2o_evloop_create(void)
{
struct st_h2o_evloop_epoll_t *loop = (struct st_h2o_evloop_epoll_t *)create_evloop(sizeof(*loop));
@ -321,5 +328,17 @@ h2o_evloop_t *h2o_evloop_create(void)
h2o_fatal("h2o_evloop_create: epoll_create1 failed:%d:%s\n", errno, h2o_strerror_r(errno, buf, sizeof(buf)));
}
#if H2O_USE_IO_URING
h2o_io_uring_init(&loop->super);
#endif
return &loop->super;
}
#if H2O_USE_IO_URING
struct st_h2o_io_uring_t *h2o_evloop__io_uring(h2o_evloop_t *_loop)
{
struct st_h2o_evloop_epoll_t *loop = (struct st_h2o_evloop_epoll_t *)_loop;
return &loop->io_uring;
}
#endif

View File

@ -413,6 +413,14 @@ static int on_config_handshake_timeout(h2o_configurator_command_t *cmd, h2o_conf
return config_timeout(cmd, node, &ctx->globalconf->handshake_timeout);
}
static int on_config_max_spare_pipes(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node)
{
if (h2o_configurator_scanf(cmd, node, "%zu", &ctx->globalconf->max_spare_pipes) != 0)
return -1;
return 0;
}
static int on_config_http1_request_timeout(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node)
{
return config_timeout(cmd, node, &ctx->globalconf->http1.req_timeout);
@ -1042,6 +1050,12 @@ void h2o_configurator__init_core(h2o_globalconf_t *conf)
h2o_configurator_define_command(&c->super, "handshake-timeout",
H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_handshake_timeout);
h2o_configurator_define_command(&c->super, "max-spare-pipes",
H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_max_spare_pipes);
h2o_configurator_define_command(&c->super, "proxy.max-spare-pipes" /* for compatibility, retain old name as an alias */,
H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_max_spare_pipes);
h2o_configurator_define_command(&c->super, "http1-request-timeout",
H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_http1_request_timeout);

View File

@ -23,6 +23,7 @@
#include <stddef.h>
#include <stdlib.h>
#include <sys/time.h>
#include "cloexec.h"
#include "h2o.h"
#include "h2o/memcached.h"
@ -93,6 +94,7 @@ void h2o_context_init(h2o_context_t *ctx, h2o_loop_t *loop, h2o_globalconf_t *co
ctx->queue = h2o_multithread_create_queue(loop);
h2o_multithread_register_receiver(ctx->queue, &ctx->receivers.hostinfo_getaddr, h2o_hostinfo_getaddr_receiver);
ctx->filecache = h2o_filecache_create(config->filecache.capacity);
ctx->spare_pipes.pipes = h2o_mem_alloc(sizeof(ctx->spare_pipes.pipes[0]) * config->max_spare_pipes);
h2o_linklist_init_anchor(&ctx->_conns.active);
h2o_linklist_init_anchor(&ctx->_conns.idle);
@ -109,20 +111,8 @@ void h2o_context_init(h2o_context_t *ctx, h2o_loop_t *loop, h2o_globalconf_t *co
ctx->proxy.client_ctx.protocol_selector.ratio.http2 = ctx->globalconf->proxy.protocol_ratio.http2;
ctx->proxy.client_ctx.protocol_selector.ratio.http3 = ctx->globalconf->proxy.protocol_ratio.http3;
ctx->proxy.connpool.socketpool = &ctx->globalconf->proxy.global_socketpool;
ctx->proxy.spare_pipes.pipes = h2o_mem_alloc(sizeof(ctx->proxy.spare_pipes.pipes[0]) * config->proxy.max_spare_pipes);
h2o_linklist_init_anchor(&ctx->proxy.connpool.http2.conns);
#ifdef __linux__
/* pre-fill the pipe cache at context init */
for (i = 0; i < config->proxy.max_spare_pipes; ++i) {
if (pipe2(ctx->proxy.spare_pipes.pipes[i], O_NONBLOCK | O_CLOEXEC) != 0) {
char errbuf[256];
h2o_fatal("pipe2(2) failed:%s", h2o_strerror_r(errno, errbuf, sizeof(errbuf)));
}
ctx->proxy.spare_pipes.count++;
}
#endif
ctx->_module_configs = h2o_mem_alloc(sizeof(*ctx->_module_configs) * config->_num_config_slots);
memset(ctx->_module_configs, 0, sizeof(*ctx->_module_configs) * config->_num_config_slots);
@ -146,19 +136,12 @@ void h2o_context_init(h2o_context_t *ctx, h2o_loop_t *loop, h2o_globalconf_t *co
void h2o_context_dispose(h2o_context_t *ctx)
{
h2o_globalconf_t *config = ctx->globalconf;
size_t i, j;
for (size_t i = 0; i < ctx->proxy.spare_pipes.count; ++i) {
close(ctx->proxy.spare_pipes.pipes[i][0]);
close(ctx->proxy.spare_pipes.pipes[i][1]);
}
free(ctx->proxy.spare_pipes.pipes);
h2o_socketpool_unregister_loop(&ctx->globalconf->proxy.global_socketpool, ctx->loop);
for (i = 0; config->hosts[i] != NULL; ++i) {
for (size_t i = 0; config->hosts[i] != NULL; ++i) {
h2o_hostconf_t *hostconf = config->hosts[i];
for (j = 0; j != hostconf->paths.size; ++j) {
for (size_t j = 0; j != hostconf->paths.size; ++j) {
h2o_pathconf_t *pathconf = hostconf->paths.entries[j];
h2o_context_dispose_pathconf_context(ctx, pathconf);
}
@ -168,11 +151,17 @@ void h2o_context_dispose(h2o_context_t *ctx)
free(ctx->_module_configs);
/* what should we do here? assert(!h2o_linklist_is_empty(&ctx->http2._conns); */
for (size_t i = 0; i < ctx->spare_pipes.count; ++i) {
close(ctx->spare_pipes.pipes[i][0]);
close(ctx->spare_pipes.pipes[i][1]);
}
free(ctx->spare_pipes.pipes);
h2o_filecache_destroy(ctx->filecache);
ctx->filecache = NULL;
/* clear storage */
for (i = 0; i != ctx->storage.size; ++i) {
for (size_t i = 0; i != ctx->storage.size; ++i) {
h2o_context_storage_item_t *item = ctx->storage.entries + i;
if (item->dispose != NULL) {
item->dispose(item->data);
@ -298,3 +287,59 @@ void h2o_conn_set_state(h2o_conn_t *conn, h2o_conn_state_t state)
link_conn(conn);
}
}
int h2o_context_new_pipe(h2o_context_t *ctx, int fds[2])
{
if (ctx->spare_pipes.count > 0) {
int *src = ctx->spare_pipes.pipes[--ctx->spare_pipes.count];
fds[0] = src[0];
fds[1] = src[1];
return 1;
}
#ifdef __linux__
return pipe2(fds, O_NONBLOCK | O_CLOEXEC) == 0;
#else
if (cloexec_pipe(fds) != 0)
return 0;
fcntl(fds[0], F_SETFL, O_NONBLOCK);
fcntl(fds[1], F_SETFL, O_NONBLOCK);
return 1;
#endif
}
static int empty_pipe(int fd)
{
ssize_t ret;
char buf[1024];
drain_more:
while ((ret = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR)
;
if (ret == 0) {
return 0;
} else if (ret == -1) {
if (errno == EAGAIN)
return 1;
return 0;
} else if (ret == sizeof(buf)) {
goto drain_more;
}
return 1;
}
void h2o_context_return_spare_pipe(h2o_context_t *ctx, int fds[2])
{
assert(fds[0] != -1);
assert(fds[1] != -1);
if (ctx->spare_pipes.count < ctx->globalconf->max_spare_pipes && empty_pipe(fds[0])) {
int *dst = ctx->spare_pipes.pipes[ctx->spare_pipes.count++];
dst[0] = fds[0];
dst[1] = fds[1];
} else {
close(fds[0]);
close(fds[1]);
}
}

View File

@ -306,27 +306,6 @@ static h2o_httpclient_t *detach_client(struct rp_generator_t *self)
return client;
}
static int empty_pipe(int fd)
{
ssize_t ret;
char buf[1024];
drain_more:
while ((ret = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR)
;
if (ret == 0) {
return 0;
} else if (ret == -1) {
if (errno == EAGAIN)
return 1;
return 0;
} else if (ret == sizeof(buf)) {
goto drain_more;
}
return 1;
}
static void do_close(struct rp_generator_t *self)
{
/**
@ -346,16 +325,7 @@ static void do_close(struct rp_generator_t *self)
}
h2o_timer_unlink(&self->send_headers_timeout);
if (self->pipe_reader.fds[0] != -1) {
h2o_context_t *ctx = self->src_req->conn->ctx;
if (ctx->proxy.spare_pipes.count < ctx->globalconf->proxy.max_spare_pipes && empty_pipe(self->pipe_reader.fds[0])) {
int *dst = ctx->proxy.spare_pipes.pipes[ctx->proxy.spare_pipes.count++];
dst[0] = self->pipe_reader.fds[0];
dst[1] = self->pipe_reader.fds[1];
} else {
close(self->pipe_reader.fds[0]);
close(self->pipe_reader.fds[1]);
}
h2o_context_return_spare_pipe(self->src_req->conn->ctx, self->pipe_reader.fds);
self->pipe_reader.fds[0] = -1;
}
}
@ -395,47 +365,6 @@ static void do_send(struct rp_generator_t *self)
h2o_send(self->src_req, vecs, veccnt, ststate);
}
static int from_pipe_read(h2o_sendvec_t *vec, void *dst, size_t len)
{
struct rp_generator_t *self = (void *)vec->cb_arg[0];
while (len != 0) {
ssize_t ret;
while ((ret = read(self->pipe_reader.fds[0], dst, len)) == -1 && errno == EINTR)
;
if (ret <= 0) {
assert(errno != EAGAIN);
return 0;
}
dst += ret;
len -= ret;
vec->len -= ret;
}
return 1;
}
static size_t from_pipe_send(h2o_sendvec_t *vec, int sockfd, size_t len)
{
#ifdef __linux__
struct rp_generator_t *self = (void *)vec->cb_arg[0];
ssize_t bytes_sent;
while ((bytes_sent = splice(self->pipe_reader.fds[0], NULL, sockfd, NULL, len, SPLICE_F_NONBLOCK)) == -1 && errno == EINTR)
;
if (bytes_sent == -1 && errno == EAGAIN)
return 0;
if (bytes_sent <= 0)
return SIZE_MAX;
vec->len -= bytes_sent;
return bytes_sent;
#else
h2o_fatal("%s:not implemented", __FUNCTION__);
#endif
}
static void do_send_from_pipe(struct rp_generator_t *self)
{
h2o_send_state_t send_state = self->had_body_error ? H2O_SEND_STATE_ERROR
@ -452,16 +381,13 @@ static void do_send_from_pipe(struct rp_generator_t *self)
return;
}
static const h2o_sendvec_callbacks_t callbacks = {.read_ = from_pipe_read, .send_ = from_pipe_send};
h2o_sendvec_t vec = {.callbacks = &callbacks};
if ((vec.len = self->body_bytes_read - self->body_bytes_sent) > H2O_PULL_SENDVEC_MAX_SIZE)
vec.len = H2O_PULL_SENDVEC_MAX_SIZE;
vec.cb_arg[0] = (uint64_t)self;
vec.cb_arg[1] = 0; /* unused */
size_t len;
if ((len = self->body_bytes_read - self->body_bytes_sent) > H2O_PULL_SENDVEC_MAX_SIZE)
len = H2O_PULL_SENDVEC_MAX_SIZE;
self->body_bytes_sent += vec.len;
self->body_bytes_sent += len;
self->pipe_inflight = 1;
h2o_sendvec(self->src_req, &vec, 1, send_state);
h2o_send_from_pipe(self->src_req, self->pipe_reader.fds[0], len, send_state);
}
static void do_proceed(h2o_generator_t *generator, h2o_req_t *req)
@ -717,20 +643,13 @@ static h2o_httpclient_body_cb on_head(h2o_httpclient_t *client, const char *errs
/* switch to using pipe reader, if the opportunity is provided */
if (args->pipe_reader != NULL) {
#ifdef __linux__
if (req->conn->ctx->proxy.spare_pipes.count > 0) {
int *src = req->conn->ctx->proxy.spare_pipes.pipes[--req->conn->ctx->proxy.spare_pipes.count];
self->pipe_reader.fds[0] = src[0];
self->pipe_reader.fds[1] = src[1];
if (h2o_context_new_pipe(req->conn->ctx, self->pipe_reader.fds)) {
args->pipe_reader->fd = self->pipe_reader.fds[1];
args->pipe_reader->on_body_piped = on_body_piped;
} else {
if (pipe2(self->pipe_reader.fds, O_NONBLOCK | O_CLOEXEC) != 0) {
char errbuf[256];
h2o_fatal("pipe2(2) failed:%s", h2o_strerror_r(errno, errbuf, sizeof(errbuf)));
}
assert(self->pipe_reader.fds[0] == -1); /* check the field remains marked as unused */
h2o_req_log_error(req, "lib/core/proxy.c", "failed to allocate zero-copy pipe; falling back to read/write");
}
args->pipe_reader->fd = self->pipe_reader.fds[1];
args->pipe_reader->on_body_piped = on_body_piped;
#endif
}
/* if httpclient has no received body at this time, immediately send only headers using zero timeout */

View File

@ -19,6 +19,7 @@
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
@ -551,6 +552,55 @@ void h2o_sendvec(h2o_req_t *req, h2o_sendvec_t *bufs, size_t bufcnt, h2o_send_st
do_sendvec(req, bufs, bufcnt, state);
}
static int from_pipe_read(h2o_sendvec_t *vec, void *dst, size_t len)
{
int fd = vec->cb_arg[0];
while (len != 0) {
ssize_t ret;
while ((ret = read(fd, dst, len)) == -1 && errno == EINTR)
;
if (ret <= 0) {
assert(errno != EAGAIN);
return 0;
}
dst += ret;
len -= ret;
vec->len -= ret;
}
return 1;
}
#ifdef __linux__
static size_t from_pipe_send(h2o_sendvec_t *vec, int sockfd, size_t len)
{
int fd = vec->cb_arg[0];
ssize_t bytes_sent;
while ((bytes_sent = splice(fd, NULL, sockfd, NULL, len, SPLICE_F_NONBLOCK)) == -1 && errno == EINTR)
;
if (bytes_sent == -1 && errno == EAGAIN)
return 0;
if (bytes_sent <= 0)
return SIZE_MAX;
vec->len -= bytes_sent;
return bytes_sent;
}
#else
#define from_pipe_send NULL
#endif
void h2o_send_from_pipe(h2o_req_t *req, int pipefd, size_t len, h2o_send_state_t send_state)
{
static const h2o_sendvec_callbacks_t callbacks = {.read_ = from_pipe_read, .send_ = from_pipe_send};
h2o_sendvec_t vec = {.callbacks = &callbacks, .len = len, .cb_arg[0] = pipefd};
h2o_sendvec(req, &vec, 1, send_state);
}
h2o_req_prefilter_t *h2o_add_prefilter(h2o_req_t *req, size_t alignment, size_t sz)
{
h2o_req_prefilter_t *prefilter = h2o_mem_alloc_pool_aligned(&req->pool, alignment, sz);

View File

@ -127,6 +127,24 @@ static int on_config_dir_listing(h2o_configurator_command_t *cmd, h2o_configurat
return 0;
}
static int on_config_io_uring(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node)
{
struct st_h2o_file_configurator_t *self = (void *)cmd->configurator;
switch (h2o_configurator_get_one_of(cmd, node, "OFF,ON")) {
case 0: /* off */
self->vars->flags &= ~H2O_FILE_FLAG_IO_URING;
break;
case 1: /* on */
self->vars->flags |= H2O_FILE_FLAG_IO_URING;
break;
default: /* error */
return -1;
}
return 0;
}
static const char **dup_strlist(const char **s)
{
size_t i;
@ -194,4 +212,8 @@ void h2o_file_register_configurator(h2o_globalconf_t *globalconf)
(H2O_CONFIGURATOR_FLAG_ALL_LEVELS & ~H2O_CONFIGURATOR_FLAG_EXTENSION) |
H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_dir_listing);
h2o_configurator_define_command(&self->super, "file.io_uring",
(H2O_CONFIGURATOR_FLAG_ALL_LEVELS & ~H2O_CONFIGURATOR_FLAG_EXTENSION) |
H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_io_uring);
}

View File

@ -523,14 +523,6 @@ static int on_config_emit_missing_date_header(h2o_configurator_command_t *cmd, h
return 0;
}
static int on_config_max_spare_pipes(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node)
{
if (h2o_configurator_scanf(cmd, node, "%zu", &ctx->globalconf->proxy.max_spare_pipes) != 0)
return -1;
return 0;
}
static int on_config_zerocopy(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node)
{
ssize_t ret = h2o_configurator_get_one_of(cmd, node, "OFF,ON,ALWAYS");
@ -805,8 +797,6 @@ void h2o_proxy_register_configurator(h2o_globalconf_t *conf)
on_config_emit_missing_date_header);
h2o_configurator_define_command(&c->super, "proxy.zerocopy", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_zerocopy);
h2o_configurator_define_command(&c->super, "proxy.max-spare-pipes",
H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR, on_config_max_spare_pipes);
h2o_configurator_define_headers_commands(conf, &c->super, "proxy.header", get_headers_commands);
h2o_configurator_define_command(&c->super, "proxy.max-buffer-size",
H2O_CONFIGURATOR_FLAG_ALL_LEVELS | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,

View File

@ -34,8 +34,10 @@
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include "h2o.h"
#if H2O_USE_IO_URING
#include "h2o/io_uring.h"
#endif
#define MAX_BUF_SIZE 65000
#define BOUNDARY_SIZE 20
@ -47,7 +49,6 @@ struct st_h2o_sendfile_generator_t {
h2o_filecache_ref_t *ref;
off_t off;
} file;
h2o_req_t *req;
size_t bytesleft;
h2o_iovec_t content_encoding;
unsigned send_vary : 1;
@ -66,6 +67,13 @@ struct st_h2o_sendfile_generator_t {
char last_modified[H2O_TIMESTR_RFC1123_LEN + 1];
char etag[H2O_FILECACHE_ETAG_MAXLEN + 1];
} header_bufs;
#if H2O_USE_IO_URING
/**
* back pointer to the request which is necessary for splicing async; becomes NULL when the generator is stopped
*/
h2o_req_t *src_req;
int splice_fds[2];
#endif
};
struct st_h2o_file_handler_t {
@ -119,6 +127,19 @@ static void close_file(struct st_h2o_sendfile_generator_t *self)
h2o_filecache_close_file(self->file.ref);
self->file.ref = NULL;
}
#if H2O_USE_IO_URING
if (self->splice_fds[0] != -1) {
if (self->src_req != NULL) {
h2o_context_return_spare_pipe(self->src_req->conn->ctx, self->splice_fds);
} else {
/* TODO return pipe upon abrupt close too? maybe that's not need */
close(self->splice_fds[0]);
close(self->splice_fds[1]);
}
self->splice_fds[0] = -1;
self->splice_fds[1] = -1;
}
#endif
}
static void on_generator_dispose(void *_self)
@ -127,6 +148,40 @@ static void on_generator_dispose(void *_self)
close_file(self);
}
#if H2O_USE_IO_URING
static void do_stop_async_splice(h2o_generator_t *_self, h2o_req_t *req)
{
struct st_h2o_sendfile_generator_t *self = (void *)_self;
self->src_req = NULL;
}
static void do_proceed_on_splice_complete(h2o_io_uring_cmd_t *cmd)
{
struct st_h2o_sendfile_generator_t *self = cmd->cb.data;
if (self->src_req == NULL) {
h2o_mem_release_shared(self);
return;
}
h2o_mem_release_shared(self);
if (cmd->result <= 0) {
assert(cmd->result != -EINTR); /* could this ever happen? */
h2o_send(self->src_req, NULL, 0, H2O_SEND_STATE_ERROR);
return;
}
self->file.off += cmd->result;
self->bytesleft -= cmd->result;
h2o_send_from_pipe(self->src_req, self->splice_fds[0], cmd->result,
self->bytesleft != 0 ? H2O_SEND_STATE_IN_PROGRESS : H2O_SEND_STATE_FINAL);
}
#endif
static int do_pread(h2o_sendvec_t *src, void *dst, size_t len)
{
struct st_h2o_sendfile_generator_t *self = (void *)src->cb_arg[0];
@ -149,7 +204,7 @@ static int do_pread(h2o_sendvec_t *src, void *dst, size_t len)
}
#if defined(__linux__)
size_t do_sendfile(int sockfd, int filefd, off_t off, size_t len)
static size_t do_sendfile(int sockfd, int filefd, off_t off, size_t len)
{
off_t iooff = off;
ssize_t ret;
@ -160,7 +215,7 @@ size_t do_sendfile(int sockfd, int filefd, off_t off, size_t len)
return ret;
}
#elif defined(__APPLE__)
size_t do_sendfile(int sockfd, int filefd, off_t off, size_t len)
static size_t do_sendfile(int sockfd, int filefd, off_t off, size_t len)
{
off_t iolen = len;
int ret;
@ -171,7 +226,7 @@ size_t do_sendfile(int sockfd, int filefd, off_t off, size_t len)
return iolen;
}
#elif defined(__FreeBSD__)
size_t do_sendfile(int sockfd, int filefd, off_t off, size_t len)
static size_t do_sendfile(int sockfd, int filefd, off_t off, size_t len)
{
off_t outlen;
int ret;
@ -182,10 +237,10 @@ size_t do_sendfile(int sockfd, int filefd, off_t off, size_t len)
return outlen;
}
#else
#define NO_SENDFILE 1
#define sendvec_sendfile NULL
#endif
#if !NO_SENDFILE
static size_t sendvec_send(h2o_sendvec_t *src, int sockfd, size_t len)
#if !defined(sendvec_sendfile)
static size_t sendvec_sendfile(h2o_sendvec_t *src, int sockfd, size_t len)
{
struct st_h2o_sendfile_generator_t *self = (void *)src->cb_arg[0];
ssize_t bytes_sent = do_sendfile(sockfd, self->file.ref->fd, (off_t)src->cb_arg[1], len);
@ -199,32 +254,27 @@ static size_t sendvec_send(h2o_sendvec_t *src, int sockfd, size_t len)
static void do_proceed(h2o_generator_t *_self, h2o_req_t *req)
{
static const h2o_sendvec_callbacks_t sendvec_callbacks = {
do_pread,
#if !NO_SENDFILE
sendvec_send,
#endif
};
struct st_h2o_sendfile_generator_t *self = (void *)_self;
h2o_sendvec_t vec;
h2o_send_state_t send_state;
size_t bytes_to_send = self->bytesleft < H2O_PULL_SENDVEC_MAX_SIZE ? self->bytesleft : H2O_PULL_SENDVEC_MAX_SIZE;
vec.len = self->bytesleft < H2O_PULL_SENDVEC_MAX_SIZE ? self->bytesleft : H2O_PULL_SENDVEC_MAX_SIZE;
vec.callbacks = &sendvec_callbacks;
vec.cb_arg[0] = (uint64_t)self;
vec.cb_arg[1] = self->file.off;
/* if io_uring is to be used, addref so that the self would not be released, then call `h2o_io_uring_splice_file` */
#if H2O_USE_IO_URING
if (self->splice_fds[0] != -1) {
h2o_mem_addref_shared(self);
h2o_io_uring_splice(self->src_req->conn->ctx->loop, self->file.ref->fd, self->file.off, self->splice_fds[1], -1,
bytes_to_send, 0, do_proceed_on_splice_complete, self);
return;
}
#endif
static const h2o_sendvec_callbacks_t sendvec_callbacks = {.read_ = do_pread, .send_ = sendvec_sendfile};
h2o_sendvec_t vec = {
.callbacks = &sendvec_callbacks, .len = bytes_to_send, .cb_arg[0] = (uint64_t)self, .cb_arg[1] = self->file.off};
self->file.off += vec.len;
self->bytesleft -= vec.len;
if (self->bytesleft == 0) {
send_state = H2O_SEND_STATE_FINAL;
} else {
send_state = H2O_SEND_STATE_IN_PROGRESS;
}
/* send (closed in do_pread) */
h2o_sendvec(req, &vec, 1, send_state);
h2o_sendvec(req, &vec, 1, self->bytesleft != 0 ? H2O_SEND_STATE_IN_PROGRESS : H2O_SEND_STATE_FINAL);
}
static void do_multirange_proceed(h2o_generator_t *_self, h2o_req_t *req)
@ -334,7 +384,6 @@ Opened:
self->super.stop = NULL;
self->file.ref = fileref;
self->file.off = 0;
self->req = NULL;
self->bytesleft = self->file.ref->st.st_size;
self->ranged.range_count = 0;
self->ranged.range_infos = NULL;
@ -342,6 +391,18 @@ Opened:
self->send_vary = (flags & H2O_FILE_FLAG_SEND_COMPRESSED) != 0;
self->send_etag = (flags & H2O_FILE_FLAG_NO_ETAG) == 0;
self->gunzip = gunzip;
#if H2O_USE_IO_URING
int try_async_splice = (flags & H2O_FILE_FLAG_IO_URING) != 0 && self->bytesleft != 0;
if (try_async_splice && h2o_context_new_pipe(req->conn->ctx, self->splice_fds)) {
self->super.stop = do_stop_async_splice;
self->src_req = req;
} else {
if (try_async_splice)
h2o_req_log_error(req, "lib/handler/file.c", "failed to allocate a pipe for async I/O; falling back to blocking I/O");
self->splice_fds[0] = -1;
self->splice_fds[1] = -1;
}
#endif
return self;
}
@ -378,9 +439,6 @@ static void send_decompressed(h2o_ostream_t *_self, h2o_req_t *req, h2o_sendvec_
static void do_send_file(struct st_h2o_sendfile_generator_t *self, h2o_req_t *req, int status, const char *reason,
h2o_iovec_t mime_type, h2o_mime_attributes_t *mime_attr, int is_get)
{
/* link the request */
self->req = req;
/* setup response */
req->res.status = status;
req->res.reason = reason;

View File

@ -106,6 +106,13 @@ const char h2o_hpack_err_invalid_content_length_header[] = "invalid content-leng
const char h2o_hpack_soft_err_found_invalid_char_in_header_name[] = "found an invalid character in header name";
const char h2o_hpack_soft_err_found_invalid_char_in_header_value[] = "found an invalid character in header value";
static int header_value_valid_as_whole(const char *s, size_t len)
{
if (len != 0 && (s[0] == 0x20 || s[0] == 0x09 || s[len - 1] == 0x20 || s[len - 1] == 0x09))
return 0;
return 1;
}
size_t h2o_hpack_decode_huffman(char *_dst, unsigned *soft_errors, const uint8_t *src, size_t len, int is_name,
const char **err_desc)
{
@ -140,7 +147,7 @@ size_t h2o_hpack_decode_huffman(char *_dst, unsigned *soft_errors, const uint8_t
}
}
} else {
if ((seen_char_types & NGHTTP2_HUFF_INVALID_FOR_HEADER_VALUE) != 0)
if ((seen_char_types & NGHTTP2_HUFF_INVALID_FOR_HEADER_VALUE) != 0 || !header_value_valid_as_whole(_dst, dst - _dst))
*soft_errors |= H2O_HPACK_SOFT_ERROR_BIT_INVALID_VALUE;
}
@ -183,13 +190,6 @@ int h2o_hpack_validate_header_name(unsigned *soft_errors, const char *s, size_t
return 1;
}
static int header_value_valid_as_whole(const char *s, size_t len)
{
if (len != 0 && (s[0] == 0x20 || s[0] == 0x09 || s[len - 1] == 0x20 || s[len - 1] == 0x09))
return 0;
return 1;
}
void h2o_hpack_validate_header_value(unsigned *soft_errors, const char *s, size_t len)
{
/* surrounding whitespace RFC 9113 8.2.1 */
@ -240,8 +240,6 @@ static h2o_iovec_t *decode_string(h2o_mem_pool_t *pool, unsigned *soft_errors, c
if ((ret->len = h2o_hpack_decode_huffman(ret->base, soft_errors, *src, len, is_header_name, err_desc)) == SIZE_MAX)
return NULL;
ret->base[ret->len] = '\0';
if (!is_header_name && !header_value_valid_as_whole(ret->base, ret->len))
*soft_errors |= H2O_HPACK_SOFT_ERROR_BIT_INVALID_VALUE;
} else {
if (len > src_end - *src)
return NULL;

View File

@ -50,9 +50,10 @@ RUN (cd curl-7.81.0 && ./configure --prefix=/usr/local --with-openssl --without-
# cpan modules
RUN apt-get install --yes cpanminus
RUN apt-get install --yes libfcgi-perl libfcgi-procmanager-perl libipc-signal-perl libjson-perl liblist-moreutils-perl libplack-perl libscope-guard-perl libtest-exception-perl libwww-perl libio-socket-ssl-perl
RUN apt-get install --yes libbsd-resource-perl libfcgi-perl libfcgi-procmanager-perl libipc-signal-perl libjson-perl liblist-moreutils-perl libplack-perl libscope-guard-perl libtest-exception-perl libwww-perl libio-socket-ssl-perl
ENV PERL_CPANM_OPT="--mirror https://cpan.metacpan.org/"
RUN cpanm -n Test::More Starlet Protocol::HTTP2 Net::DNS::Nameserver Test::TCP
RUN cpanm -n Test::More Starlet Protocol::HTTP2 Test::TCP
RUN cpanm -n NLNETLABS/Net-DNS-1.36.tar.gz # Net-DNS 1.39 has issues around use of alarm, fork, etc.
# h2spec
RUN curl -Ls https://github.com/summerwind/h2spec/releases/download/v2.6.0/h2spec_linux_amd64.tar.gz | tar zx -C /usr/local/bin

View File

@ -46,6 +46,7 @@ RUN apt-get install --yes libnghttp2-dev \
# perl
RUN apt-get install --yes \
cpanminus \
libbsd-resource-perl \
libfcgi-perl \
libfcgi-procmanager-perl \
libipc-signal-perl \

View File

@ -54,6 +54,7 @@ RUN apt-get install --yes \
# perl
RUN apt-get install --yes \
cpanminus \
libbsd-resource-perl \
libfcgi-perl \
libfcgi-procmanager-perl \
libipc-signal-perl \
@ -97,7 +98,7 @@ RUN mkdir -p /opt/src \
RUN apt-get install --yes golang-go
RUN mkdir -p /opt/src && cd /opt/src && git clone --depth=1 https://github.com/google/boringssl.git \
&& (cd boringssl && mkdir build && cd build \
&& cmake .. -DCMAKE_INSTALL_PREFIX=/opt/boringssl && make install && cp decrepit/libdecrepit.a /opt/boringssl/lib/ \
&& cmake .. -DCMAKE_INSTALL_PREFIX=/opt/boringssl && make install && cp libdecrepit.a /opt/boringssl/lib/ \
&& make clean \
)

View File

@ -1,5 +1,6 @@
IMAGE_NAME=h2oserver/h2o-ci
VARIANT=unknown
DOCKER_OPTS=
PROJECT_DIR=$(shell pwd)
ALL:
@ -8,10 +9,12 @@ ALL:
@echo 'Command is either `build` or `push`.'
@echo 'Variant might be ubuntu1604, ubuntu2004 (corresponds to misc/docker-ci/Dockerfile.$$variant).'
@echo ''
@echo 'DOCKER_OPTS can also be set; e.g., DOCKER_OPTS=--network=host'
@echo ''
build:
cd misc/docker-ci/docker-root && docker build --tag "$(IMAGE_NAME):$(VARIANT)" -f "../Dockerfile.$(VARIANT)" .
cd misc/docker-ci/docker-root && docker build $(DOCKER_OPTS) --tag "$(IMAGE_NAME):$(VARIANT)" -f "../Dockerfile.$(VARIANT)" .
push: build
docker push "$(IMAGE_NAME):$(VARIANT)"

View File

@ -95,6 +95,9 @@
#if H2O_USE_MRUBY
#include "h2o/mruby_.h"
#endif
#if H2O_USE_IO_URING
#include "h2o/io_uring.h"
#endif
#include "standalone.h"
#include "../lib/probes_.h"
@ -3466,6 +3469,22 @@ static int on_config_neverbleed_offload(h2o_configurator_command_t *cmd, h2o_con
return 0;
}
static int on_config_io_uring_batch_size(h2o_configurator_command_t *cmd, h2o_configurator_context_t *ctx, yoml_t *node)
{
#if H2O_USE_IO_URING
if (h2o_configurator_scanf(cmd, node, "%zu", &h2o_io_uring_batch_size) != 0)
return -1;
if (h2o_io_uring_batch_size == 0) {
h2o_configurator_errprintf(cmd, node, "value must be above zero");
return -1;
}
return 0;
#else
h2o_configurator_errprintf(cmd, node, "support for io_uring is not available");
return -1;
#endif
}
static yoml_t *load_config(yoml_parse_args_t *parse_args, yoml_t *source)
{
FILE *fp;
@ -4600,6 +4619,9 @@ static void setup_configurators(void)
on_config_ssl_offload);
h2o_configurator_define_command(c, "neverbleed-offload", H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_neverbleed_offload);
h2o_configurator_define_command(c, "io_uring-batch-size",
H2O_CONFIGURATOR_FLAG_GLOBAL | H2O_CONFIGURATOR_FLAG_EXPECT_SCALAR,
on_config_io_uring_batch_size);
}
h2o_access_log_register_configurator(&conf.globalconf);
@ -4776,6 +4798,9 @@ int main(int argc, char **argv)
#endif
#if PTLS_HAVE_AEGIS
printf("libaegis: YES\n");
#endif
#if H2O_USE_IO_URING
printf("io_uring: YES\n");
#endif
exit(0);
case 'h':

View File

@ -455,6 +455,19 @@ $ctx->{directive}->(
Times spent for receiving <a href="configure/base_directives.html#listen-proxy-protocol">the PROXY protocol</a> and TLS handshake are counted.
? })
<?
$ctx->{directive}->(
name => "io_uring-batch-size",
levels => [ qw(global) ],
default => "io_uring-batch-size: 1",
desc => q{Number of io_uring calls to issue at once. Increasing this number might reduce overhead.},
experimental => 1,
see_also => render_mt(<<'EOT'),
<a href="configure/file_directives.html#file.io_uring"><code>file.io_uring</code></a>
EOT
)->(sub {})
?>
<?
$ctx->{directive}->(
name => "limit-request-body",
@ -516,6 +529,33 @@ $ctx->{directive}->(
desc => q{Limits the number of internal redirects.},
)->(sub {});
<?
$ctx->{directive}->(
name => "max-spare-pipes",
levels => [ qw(global) ],
desc => q{This setting specifies the maximum number of pipes retained for reuse, when <code>file.io_uring</code> or <code>proxy.zerocopy</code> is used.},
default => 0,
see_also => render_mt(<<'EOT'),
<a href="configure/file_directives.html#file.io_uring"><code>file.io_uring</code></a>, <a href="configure/proxy_directives.html#proxy.zerocopy"><code>proxy.zerocopy</code></a>
EOT
)->(sub {
?>
<p>
The setting can be used to reduce lock contention in the kernel under high load.
</p>
<p>
This maximum is applied per each worker thread.
</p>
<p>
Setting this value to 0 will cause no pipes to be retained by h2o; the pipes will be closed after they are used.
In this case, h2o will create new pipes each time they are needed.
</p>
<p>
In previous versions, this configuration directive was called <code>proxy.max-spare-pipes</code>.
</p>
? })
<?
$ctx->{directive}->(
name => "neverbleed-offload",
levels => [ qw(global) ],

View File

@ -114,6 +114,26 @@ The sequence of filenames are searched from left to right, and the first file th
</p>
? })
<?
$ctx->{directive}->(
name => "file.io_uring",
levels => [ qw(global host path) ],
default => "file.io_uring: OFF",
desc => q{If io_uring should be used for serving files.},
experimental => 1,
see_also => render_mt(<<'EOT'),
<a href="configure/base_directives.html#io_uring-batch-size"><code>io_uring-batch-size</code></a>,
<a href="configure/base_directives.html#max-spare-pipes"><code>max-spare-pipes</code></a>
EOT
)->(sub {
?>
<p>
By default, H2O uses system calls such as pread (2) or sendfile (2), which block if the file being read is not in the page cache.
This can prevent H2O's worker threads from making progress on any connection handled by the affected thread.
When this flag is enabled, H2O is no longer blocked by the I/O calls, as the data is asynchronously copied from disk to the page cache before h2o attempts to access it.
</p>
? })
<?
$ctx->{directive}->(
name => "file.mime.addtypes",

View File

@ -361,28 +361,6 @@ But if the backend server has enough concurrency, <code>proxy.max-buffer-size</c
</p>
? })
<?
$ctx->{directive}->(
name => "proxy.max-spare-pipes",
levels => [ qw(global) ],
desc => q{This setting specifies the maximum number of pipes retained for reuse, when <code>proxy.zerocopy</code> is used.},
default => 0,
see_also => render_mt(<<'EOT'),
<a href="configure/proxy_directives.html#proxy.zerocopy"><code>proxy.zerocopy</code></a>
EOT
)->(sub {
?>
<p>
This maximum is applied per each worker thread.
The intention of this setting is to reduce lock contention in the kernel under high load when zerocopy is used.
When this setting is set to a non-zero value, specified number of pipes will be allocated upon startup for each worker thread.
</p>
<p>
Setting this value to 0 will cause no pipes to be retained by h2o; the pipes will be closed after they are used.
In this case, h2o will create new pipes each time they are needed.
</p>
? })
<?
$ctx->{directive}->(
name => "proxy.preserve-host",
@ -567,7 +545,7 @@ $ctx->{directive}->(
desc => q{Sets the use of zerocopy operations for forwarding the response body.},
experimental => 1,
see_also => render_mt(<<'EOT'),
<a href="configure/base_directives.html#ssl-offload"><code>ssl-offload</code></a>, <a href="configure/proxy_directives.html#proxy.max-spare-pipes"><code>proxy.max-spare-pipes</code></a>
<a href="configure/base_directives.html#max-spare-pipes"><code>proxy.max-spare-pipes</code></a>, <a href="configure/base_directives.html#ssl-offload"><code>ssl-offload</code></a>
EOT
)->(sub {
?>

View File

@ -194,17 +194,45 @@ static void test_decode_literal_invalid_value(void)
{
h2o_qpack_decoder_t *dec = h2o_qpack_create_decoder(4096, 10);
static const uint8_t input[] = {
0, 0, /* required_inesrt_count=0, base=0 */
0xd1, /* :method: GET */
0xd7, /* :scheme: https */
0x50, 3, 'a', '\n', 'b', /* :authority: a\nb */
0xc1, /* :path: / */
};
do_test_decode_request(dec, 0, h2o_iovec_init(input, sizeof(input)), H2O_HTTP2_ERROR_INVALID_HEADER_CHAR,
h2o_hpack_soft_err_found_invalid_char_in_header_value, h2o_iovec_init(H2O_STRLIT("GET")),
&H2O_URL_SCHEME_HTTPS, h2o_iovec_init(H2O_STRLIT("a\nb")), h2o_iovec_init(H2O_STRLIT("/")), SIZE_MAX,
NULL, 0, h2o_iovec_init(NULL, 0));
{
static const uint8_t input[] = {
0, 0, /* required_inesrt_count=0, base=0 */
0xd1, /* :method: GET */
0xd7, /* :scheme: https */
0x50, 3, 'a', '\n', 'b', /* :authority: a\nb */
0xc1, /* :path: / */
};
do_test_decode_request(dec, 0, h2o_iovec_init(input, sizeof(input)), H2O_HTTP2_ERROR_INVALID_HEADER_CHAR,
h2o_hpack_soft_err_found_invalid_char_in_header_value, h2o_iovec_init(H2O_STRLIT("GET")),
&H2O_URL_SCHEME_HTTPS, h2o_iovec_init(H2O_STRLIT("a\nb")), h2o_iovec_init(H2O_STRLIT("/")), SIZE_MAX,
NULL, 0, h2o_iovec_init(NULL, 0));
}
{
static const uint8_t input[] = {
0, 0, /* required_inesrt_count=0, base=0 */
0xd1, /* :method: GET */
0xd7, /* :scheme: https */
0x50, 3, ' ', 'a', 'b', /* :authority: SP ab */
0xc1, /* :path: / */
};
do_test_decode_request(dec, 0, h2o_iovec_init(input, sizeof(input)), H2O_HTTP2_ERROR_INVALID_HEADER_CHAR,
h2o_hpack_soft_err_found_invalid_char_in_header_value, h2o_iovec_init(H2O_STRLIT("GET")),
&H2O_URL_SCHEME_HTTPS, h2o_iovec_init(H2O_STRLIT(" ab")), h2o_iovec_init(H2O_STRLIT("/")), SIZE_MAX,
NULL, 0, h2o_iovec_init(NULL, 0));
}
{
static const uint8_t input[] = {
0, 0, /* required_inesrt_count=0, base=0 */
0xd1, /* :method: GET */
0xd7, /* :scheme: https */
0x50, 0x83, 0x50, 0x71, 0xff, /* :authority: SP ab (in huffman)*/
0xc1, /* :path: / */
};
do_test_decode_request(dec, 0, h2o_iovec_init(input, sizeof(input)), H2O_HTTP2_ERROR_INVALID_HEADER_CHAR,
h2o_hpack_soft_err_found_invalid_char_in_header_value, h2o_iovec_init(H2O_STRLIT("GET")),
&H2O_URL_SCHEME_HTTPS, h2o_iovec_init(H2O_STRLIT(" ab")), h2o_iovec_init(H2O_STRLIT("/")), SIZE_MAX,
NULL, 0, h2o_iovec_init(NULL, 0));
}
h2o_qpack_destroy_decoder(dec);
}

View File

@ -35,7 +35,7 @@ EOT
# Start 1k concurrent requests by sending 1k POST headers followed by 1k DATA END_STREAMs.
my $output = run_with_h2get_simple($server, <<"EOR");
N = 1000
N = 900
puts "Send HEADERS"
(0..(N-1)).each { |it|
@ -94,6 +94,6 @@ my @logs = do {
};
debug(join "\n", map { join " ", @$_ } @logs);
is scalar(grep { $_->[0] == 200 } @logs), 1000, "accepted all requests";
is scalar(grep { $_->[0] == 200 } @logs), 900, "accepted all requests";
done_testing();

View File

@ -0,0 +1,2 @@
exec $^X, "t/40http3/test.pl", "--batch-size=1";
die "failed to invoke $^X t/40http3/test.pl:$!";

View File

@ -0,0 +1,2 @@
exec $^X, "t/40http3/test.pl", "--batch-size=10";
die "failed to invoke $^X t/40http3/test.pl:$!";

View File

@ -1,202 +1,2 @@
use strict;
use warnings;
use Digest::MD5 qw(md5_hex);
use Net::EmptyPort qw(wait_port);
use File::Temp qw(tempdir);
use Test::More;
use t::Util;
my $tempdir = tempdir(CLEANUP => 1);
my $client_prog = bindir() . "/h2o-httpclient";
plan skip_all => "$client_prog not found"
unless -e $client_prog;
my $quic_port = empty_port({
host => "127.0.0.1",
proto => "udp",
});
sub doit {
my $num_threads = shift;
my $conf = << "EOT";
listen:
type: quic
port: $quic_port
ssl:
key-file: examples/h2o/server.key
certificate-file: examples/h2o/server.crt
num-threads: $num_threads
hosts:
default:
paths:
/:
file.dir: t/assets/doc_root
EOT
if (server_features()->{mruby}) {
$conf .= << 'EOT';
/echo:
mruby.handler: |
Proc.new do |env|
[200, {}, [env["rack.input"].read]]
end
EOT
}
my $guard = spawn_h2o($conf);
wait_port({port => $quic_port, proto => 'udp'});
for (1..100) {
subtest "hello world" => sub {
my $resp = `$client_prog -3 100 https://127.0.0.1:$quic_port 2>&1`;
like $resp, qr{^HTTP/.*\n\nhello\n$}s;
};
subtest "large file" => sub {
my $resp = `$client_prog -3 100 https://127.0.0.1:$quic_port/halfdome.jpg 2> $tempdir/log`;
is $?, 0;
diag do {
open my $fh, "-|", "share/h2o/annotate-backtrace-symbols < $tempdir/log"
or die "failed to open $tempdir/log through annotated-backtrace-symbols:$?";
local $/;
<$fh>;
} if $? != 0;
is length($resp), (stat "t/assets/doc_root/halfdome.jpg")[7];
is md5_hex($resp), md5_file("t/assets/doc_root/halfdome.jpg");
};
subtest "more than stream-concurrency" => sub {
my $resp = `$client_prog -3 100 -t 1000 https://127.0.0.1:$quic_port 2> /dev/null`;
is $resp, "hello\n" x 1000;
};
subtest "post" => sub {
plan skip_all => 'mruby support is off'
unless server_features()->{mruby};
foreach my $cl (1, 100, 10000, 1000000) {
my $resp = `$client_prog -3 100 -b $cl -c 100000 https://127.0.0.1:$quic_port/echo 2> /dev/null`;
is length($resp), $cl;
ok +($resp =~ /^a+$/s); # don't use of `like` to avoid excess amount of log lines on mismatch
}
};
}
};
subtest "single-thread" => sub {
doit(1);
};
subtest "multi-thread" => sub {
doit(16);
};
subtest "slow-echo-chunked" => sub {
plan skip_all => 'mruby support is off'
unless server_features()->{mruby};
my $guard = spawn_h2o(<< "EOT");
listen:
type: quic
port: $quic_port
ssl:
key-file: examples/h2o/server.key
certificate-file: examples/h2o/server.crt
hosts:
default:
paths:
/echo:
mruby.handler: |
Proc.new do |env|
[200, {}, env["rack.input"]]
end
EOT
wait_port({port => $quic_port, proto => 'udp'});
my $resp = `$client_prog -3 100 -t 5 -d 1000 -b 10 -c 2 -i 1000 https://127.0.0.1:$quic_port/echo 2> /dev/null`;
is length($resp), 50;
is $resp, 'a' x 50;
};
subtest "body-then-close" => sub {
my $upstream = spawn_server(
argv => [
qw(plackup -s Starlet --max-workers 10 --access-log /dev/null --listen), "$tempdir/upstream.sock",
ASSETS_DIR . "/upstream.psgi",
],
is_ready => sub { !! -e "$tempdir/upstream.sock" },
);
my $server = spawn_h2o(<< "EOT");
http3-idle-timeout: 5
listen:
type: quic
port: $quic_port
ssl:
key-file: examples/h2o/server.key
certificate-file: examples/h2o/server.crt
hosts:
default:
paths:
/:
proxy.reverse.url: http://[unix:$tempdir/upstream.sock]/
EOT
my $fetch = sub {
my $qp = shift;
open my $fh, "-|", "$client_prog -3 100 https://127.0.0.1:$quic_port/suspend-body$qp 2>&1"
or die "failed to spawn $client_prog:$!";
local $/;
join "", <$fh>;
};
like $fetch->(""), qr{^HTTP/3 200\n.*\n\nx$}s;
like $fetch->("?delay-fin"), qr{^HTTP/3 200\n.*\n\nx$}s;
};
subtest "large-headers" => sub {
my $server = spawn_h2o(<< "EOT");
listen:
type: quic
port: $quic_port
ssl:
key-file: examples/h2o/server.key
certificate-file: examples/h2o/server.crt
hosts:
default:
paths:
/:
file.dir: t/assets/doc_root
EOT
my $fetch = sub {
my ($query, $opts) = @_;
open my $fh, "-|", "$client_prog -3 100 $opts https://127.0.0.1:$quic_port/$query 2>&1"
or die "failed to spawn $client_prog:$!";
local $/;
join "", <$fh>;
};
like $fetch->("", ""), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "no headers";
# When generating headers, 'X' is used, as it is 8 bits in cleartext and also in static huffman.
# TODO: can we check that the error is stream-level?
subtest "single header" => sub {
plan skip_all => "linux cannot handle args longer than 128KB"
if $^O eq 'linux';
like $fetch->("", "-H a:" . "X" x 409600), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
unlike $fetch->("", "-H a:" . "X" x 512000), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly above limit";
};
subtest "some large headers" => sub {
like $fetch->("", join " ", map { "-H a:" . "X" x 65536 } (0..5)), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
unlike $fetch->("", join " ", map { "-H a:" . "X" x 65536 } (0..7)), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly above limit";
};
subtest "many headers" => sub {
like $fetch->("", join " ", map { "-H a:" . "X" x 4096 } (0..90)), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
unlike $fetch->("", join " ", map { "-H a:" . "X" x 4096 } (0..110)), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly above limit";
};
subtest "URI" => sub {
like $fetch->("?q=" . "X" x 120000, ""), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
subtest "above linux limit" => sub {
plan skip_all => "linux cannot handle args longer than 128KB"
if $^O eq 'linux';
like $fetch->("?q=" . "X" x 409600, ""), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
unlike $fetch->("?q=" . "X" x 512000, ""), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
};
};
};
done_testing;
exec $^X, "t/40http3/test.pl";
die "failed to invoke $^X t/40http3/test.pl:$!";

221
t/40http3/test.pl Normal file
View File

@ -0,0 +1,221 @@
use strict;
use warnings;
use Digest::MD5 qw(md5_hex);
use Getopt::Long;
use Net::EmptyPort qw(wait_port);
use File::Temp qw(tempdir);
use Test::More;
use t::Util;
my $io_uring_batch_size;
GetOptions(
'batch-size=i' => \$io_uring_batch_size
) or exit(1);
my $tempdir = tempdir(CLEANUP => 1);
my $client_prog = bindir() . "/h2o-httpclient";
plan skip_all => "$client_prog not found"
unless -e $client_prog;
plan skip_all => "io_uring is not available"
if $io_uring_batch_size && !server_features()->{io_uring};
my $quic_port = empty_port({
host => "127.0.0.1",
proto => "udp",
});
sub doit {
my $num_threads = shift;
my $conf = << "EOT";
listen:
type: quic
port: $quic_port
ssl:
key-file: examples/h2o/server.key
certificate-file: examples/h2o/server.crt
num-threads: $num_threads
hosts:
default:
paths:
/:
file.dir: t/assets/doc_root
EOT
if (server_features()->{mruby}) {
$conf .= << 'EOT';
/echo:
mruby.handler: |
Proc.new do |env|
[200, {}, [env["rack.input"].read]]
end
EOT
}
if ($io_uring_batch_size) {
$conf .= << "EOT";
file.io_uring: ON
io_uring-batch-size: $io_uring_batch_size
EOT
} else {
$conf .= << "EOT";
file.io_uring: OFF
EOT
}
my $guard = spawn_h2o($conf);
wait_port({port => $quic_port, proto => 'udp'});
for (1..100) {
subtest "hello world" => sub {
my $resp = `$client_prog -3 100 https://127.0.0.1:$quic_port 2>&1`;
like $resp, qr{^HTTP/.*\n\nhello\n$}s;
};
subtest "large file" => sub {
my $resp = `$client_prog -3 100 https://127.0.0.1:$quic_port/halfdome.jpg 2> $tempdir/log`;
is $?, 0;
diag do {
open my $fh, "-|", "share/h2o/annotate-backtrace-symbols < $tempdir/log"
or die "failed to open $tempdir/log through annotated-backtrace-symbols:$?";
local $/;
<$fh>;
} if $? != 0;
is length($resp), (stat "t/assets/doc_root/halfdome.jpg")[7];
is md5_hex($resp), md5_file("t/assets/doc_root/halfdome.jpg");
};
subtest "more than stream-concurrency" => sub {
my $resp = `$client_prog -3 100 -t 1000 https://127.0.0.1:$quic_port 2> /dev/null`;
is $resp, "hello\n" x 1000;
};
subtest "post" => sub {
plan skip_all => 'mruby support is off'
unless server_features()->{mruby};
foreach my $cl (1, 100, 10000, 1000000) {
my $resp = `$client_prog -3 100 -b $cl -c 100000 https://127.0.0.1:$quic_port/echo 2> /dev/null`;
is length($resp), $cl;
ok +($resp =~ /^a+$/s); # don't use of `like` to avoid excess amount of log lines on mismatch
}
};
}
};
subtest "single-thread" => sub {
doit(1);
};
subtest "multi-thread" => sub {
doit(16);
};
subtest "slow-echo-chunked" => sub {
plan skip_all => 'mruby support is off'
unless server_features()->{mruby};
my $guard = spawn_h2o(<< "EOT");
listen:
type: quic
port: $quic_port
ssl:
key-file: examples/h2o/server.key
certificate-file: examples/h2o/server.crt
hosts:
default:
paths:
/echo:
mruby.handler: |
Proc.new do |env|
[200, {}, env["rack.input"]]
end
EOT
wait_port({port => $quic_port, proto => 'udp'});
my $resp = `$client_prog -3 100 -t 5 -d 1000 -b 10 -c 2 -i 1000 https://127.0.0.1:$quic_port/echo 2> /dev/null`;
is length($resp), 50;
is $resp, 'a' x 50;
};
subtest "body-then-close" => sub {
my $upstream = spawn_server(
argv => [
qw(plackup -s Starlet --max-workers 10 --access-log /dev/null --listen), "$tempdir/upstream.sock",
ASSETS_DIR . "/upstream.psgi",
],
is_ready => sub { !! -e "$tempdir/upstream.sock" },
);
my $server = spawn_h2o(<< "EOT");
http3-idle-timeout: 5
listen:
type: quic
port: $quic_port
ssl:
key-file: examples/h2o/server.key
certificate-file: examples/h2o/server.crt
hosts:
default:
paths:
/:
proxy.reverse.url: http://[unix:$tempdir/upstream.sock]/
EOT
my $fetch = sub {
my $qp = shift;
open my $fh, "-|", "$client_prog -3 100 https://127.0.0.1:$quic_port/suspend-body$qp 2>&1"
or die "failed to spawn $client_prog:$!";
local $/;
join "", <$fh>;
};
like $fetch->(""), qr{^HTTP/3 200\n.*\n\nx$}s;
like $fetch->("?delay-fin"), qr{^HTTP/3 200\n.*\n\nx$}s;
};
subtest "large-headers" => sub {
my $server = spawn_h2o(<< "EOT");
listen:
type: quic
port: $quic_port
ssl:
key-file: examples/h2o/server.key
certificate-file: examples/h2o/server.crt
hosts:
default:
paths:
/:
file.dir: t/assets/doc_root
EOT
my $fetch = sub {
my ($query, $opts) = @_;
open my $fh, "-|", "$client_prog -3 100 $opts https://127.0.0.1:$quic_port/$query 2>&1"
or die "failed to spawn $client_prog:$!";
local $/;
join "", <$fh>;
};
like $fetch->("", ""), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "no headers";
# When generating headers, 'X' is used, as it is 8 bits in cleartext and also in static huffman.
# TODO: can we check that the error is stream-level?
subtest "single header" => sub {
plan skip_all => "linux cannot handle args longer than 128KB"
if $^O eq 'linux';
like $fetch->("", "-H a:" . "X" x 409600), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
unlike $fetch->("", "-H a:" . "X" x 512000), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly above limit";
};
subtest "some large headers" => sub {
like $fetch->("", join " ", map { "-H a:" . "X" x 65536 } (0..5)), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
unlike $fetch->("", join " ", map { "-H a:" . "X" x 65536 } (0..7)), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly above limit";
};
subtest "many headers" => sub {
like $fetch->("", join " ", map { "-H a:" . "X" x 4096 } (0..90)), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
unlike $fetch->("", join " ", map { "-H a:" . "X" x 4096 } (0..110)), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly above limit";
};
subtest "URI" => sub {
like $fetch->("?q=" . "X" x 120000, ""), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
subtest "above linux limit" => sub {
plan skip_all => "linux cannot handle args longer than 128KB"
if $^O eq 'linux';
like $fetch->("?q=" . "X" x 409600, ""), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
unlike $fetch->("?q=" . "X" x 512000, ""), qr{^HTTP/3 200\n.*\n\nhello\n$}s, "slightly below limit";
};
};
};
done_testing;

View File

@ -9,63 +9,93 @@ my %files = map { do {
+($_ => { size => +(stat $fn)[7], md5 => md5_file($fn) });
} } qw(index.txt halfdome.jpg);
my $server = spawn_h2o(<< "EOT");
sub run_tests {
my $extra_conf = shift;
my $server = spawn_h2o(<< "EOT");
hosts:
default:
paths:
/:
file.dir: @{[ DOC_ROOT ]}
$extra_conf
EOT
my $port = $server->{port};
my $tls_port = $server->{tls_port};
my $port = $server->{port};
my $tls_port = $server->{tls_port};
subtest 'curl' => sub {
run_with_curl($server, sub {
my ($proto, $port, $curl) = @_;
for my $file (sort keys %files) {
my $content = `$curl --silent --show-error $proto://127.0.0.1:$port/$file`;
is length($content), $files{$file}->{size}, "$file (size)";
is md5_hex($content), $files{$file}->{md5}, "$file (md5)";
}
});
subtest 'curl' => sub {
run_with_curl($server, sub {
my ($proto, $port, $curl) = @_;
for my $file (sort keys %files) {
my $content = `$curl --silent --show-error $proto://127.0.0.1:$port/$file`;
is length($content), $files{$file}->{size}, "$file (size)";
is md5_hex($content), $files{$file}->{md5}, "$file (md5)";
}
});
};
subtest 'nghttp' => sub {
plan skip_all => 'nghttp not found'
unless prog_exists('nghttp');
my $doit = sub {
my ($proto, $port) = @_;
my $opt = $proto eq 'http' ? '-u' : '';
for my $file (sort keys %files) {
my $content = `nghttp $opt $proto://127.0.0.1:$port/$file`;
is length($content), $files{$file}->{size}, "$proto://127.0.0.1/$file (size)";
is md5_hex($content), $files{$file}->{md5}, "$proto://127.0.0.1/$file (md5)";
}
my $out = `nghttp -u -m 100 $proto://127.0.0.1:$port/index.txt`;
is $out, "hello\n" x 100, "$proto://127.0.0.1/index.txt x 100 times";
};
$doit->('http', $port);
subtest 'https' => sub {
plan skip_all => 'OpenSSL does not support protocol negotiation; it is too old'
unless openssl_can_negotiate();
$doit->('https', $tls_port);
};
subtest 'dtsu' => sub {
plan skip_all => 'OpenSSL does not support protocol negotiation; it is too old'
unless openssl_can_negotiate();
for my $table_size (0, 1024) {
my $content = `nghttp --header-table-size=$table_size https://127.0.0.1:$tls_port/index.txt`;
is $content, "hello\n";
}
};
};
subtest 'ab' => sub {
plan skip_all => 'ab not found'
unless prog_exists('ab');
ok(system("ab -c 10 -n 10000 -k http://127.0.0.1:$port/index.txt") == 0);
ok(system("ab -f tls1 -c 10 -n 10000 -k https://127.0.0.1:$tls_port/index.txt") == 0);
};
}
subtest 'default' => sub {
run_tests(<< "EOT");
file.io_uring: OFF
EOT
};
subtest 'nghttp' => sub {
plan skip_all => 'nghttp not found'
unless prog_exists('nghttp');
my $doit = sub {
my ($proto, $port) = @_;
my $opt = $proto eq 'http' ? '-u' : '';
for my $file (sort keys %files) {
my $content = `nghttp $opt $proto://127.0.0.1:$port/$file`;
is length($content), $files{$file}->{size}, "$proto://127.0.0.1/$file (size)";
is md5_hex($content), $files{$file}->{md5}, "$proto://127.0.0.1/$file (md5)";
subtest 'io_uring' => sub {
plan skip_all => 'io_uring is not availble'
unless server_features()->{io_uring};
subtest 'file.io_uring=off' => sub {
run_tests('file.io_uring: OFF');
};
for my $batch_size (qw(1 10)) {
for my $spare_pipes (qw(0 10)) {
subtest "(batch_size,spare_pipes)=($batch_size,$spare_pipes)" => sub {
run_tests(join "\n", <<"EOT");
file.io_uring: ON
io_uring-batch-size: $batch_size
max-spare-pipes: $spare_pipes
EOT
};
}
my $out = `nghttp -u -m 100 $proto://127.0.0.1:$port/index.txt`;
is $out, "hello\n" x 100, "$proto://127.0.0.1/index.txt x 100 times";
};
$doit->('http', $port);
subtest 'https' => sub {
plan skip_all => 'OpenSSL does not support protocol negotiation; it is too old'
unless openssl_can_negotiate();
$doit->('https', $tls_port);
};
subtest 'dtsu' => sub {
plan skip_all => 'OpenSSL does not support protocol negotiation; it is too old'
unless openssl_can_negotiate();
for my $table_size (0, 1024) {
my $content = `nghttp --header-table-size=$table_size https://127.0.0.1:$tls_port/index.txt`;
is $content, "hello\n";
}
};
};
subtest 'ab' => sub {
plan skip_all => 'ab not found'
unless prog_exists('ab');
ok(system("ab -c 10 -n 10000 -k http://127.0.0.1:$port/index.txt") == 0);
ok(system("ab -f tls1 -c 10 -n 10000 -k https://127.0.0.1:$tls_port/index.txt") == 0);
}
};
done_testing;

View File

@ -26,6 +26,7 @@ subtest "basic" => sub {
);
# spawn server
my $server = spawn_h2o(<< "EOT");
file.io_uring: OFF
hosts:
default:
paths:
@ -228,6 +229,7 @@ subtest "cache-digest" => sub {
my $server = spawn_h2o(sub {
my ($port, $tls_port) = @_;
return << "EOT";
file.io_uring: OFF
hosts:
"127.0.0.1:$tls_port":
paths:

View File

@ -13,6 +13,7 @@ plan skip_all => 'php-cgi not found'
# spawn h2o
my $server = spawn_h2o(<< "EOT");
file.io_uring: OFF
file.custom-handler:
extension: .php
fastcgi.spawn: "exec php-cgi"

View File

@ -30,6 +30,7 @@ hosts:
file.dir: $tempdir
header.add: "X-Traffic: 200000" # takes about 5 seconds
throttle-response: ON
file.io_uring: OFF
EOT
wait_port({port => $quic_port, proto => "udp"});

View File

@ -6,7 +6,7 @@ use t::Util;
# commands that are meant to exist without any documentation are listed here
my @private_commands = (
qw(mruby.handler_path proxy.connect.proxy-status proxy.http2.max-concurrent_streams proxy-status.identity), # deprecated
qw(mruby.handler_path proxy.connect.proxy-status proxy.http2.max-concurrent_streams proxy.max-spare-pipes proxy-status.identity), # deprecated
qw(header.cookie.unset header.cookie.unsetunless), # removing "cookie" headers in response?
qw(http3-ack-frequency http3-allow-delayed-ack quic-nodes self-trace), # highly experimental and therefore undocumented
);

74
t/80issues3479.t Normal file
View File

@ -0,0 +1,74 @@
use strict;
use warnings;
use BSD::Resource;
use File::Temp qw(tempdir);
use Path::Tiny;
use Test::More;
use t::Util;
plan skip_all => 'io_uring support is off'
unless server_features()->{io_uring};
plan skip_all => 'h2load not found'
unless prog_exists('h2load');
my $tempdir = tempdir(CLEANUP => 1);
# upstream is spawned before setrlimit(2)
my $upstream = spawn_h2o(<< "EOT");
num-threads: 1
hosts:
default:
paths:
/:
file.dir: t/assets/doc_root
EOT
# allow each process open only as many as 30 files
setrlimit(RLIMIT_NOFILE, 30, 30);
subtest "file" => sub {
doit(<< "EOT");
file.io_uring: ON
hosts:
default:
paths:
/:
file.dir: t/assets/doc_root
EOT
};
subtest "proxy" => sub {
doit(<< "EOT");
hosts:
default:
paths:
"/":
proxy.reverse.url: http://127.0.0.1:@{[$upstream->{port}]}/
EOT
};
undef $upstream;
done_testing();
sub doit {
my $conf = shift;
my $server = spawn_h2o(<< "EOT");
num-threads: 1
max-connections: 20
error-log: $tempdir/error.log
$conf
EOT
# Check we get some 200s (we might get 503s due to not being able to open the file rather than pipe), and that we see some
# pipe errors. The test is run twice to make sure that the server did not crash in the first iteration.
for (1..2) {
subtest "run $_" => sub {
my $bench_out = `h2load -n 400 -c 20 -t 1 http://127.0.0.1:@{[$server->{port}]}/halfdome.jpg`;
diag $bench_out;
like $bench_out, qr/status codes: [1-9][0-9]* 2xx,/, "some 200s";
like path("$tempdir/error.log")->slurp,
qr{failed to allocate a pipe for async I/O; falling back to blocking I/O}, "some pipe errors";
};
}
}

View File

@ -26,6 +26,7 @@ my $upstream = spawn_server(
# spawn server
my $server = spawn_h2o(<< "EOT");
file.io_uring: OFF
hosts:
default:
paths:

View File

@ -84,7 +84,7 @@ subtest "h2olog -S=0.00", sub {
diag "h2olog output:\n", $trace;
}
is $trace, "", "nothing has been logged";
unlike $trace, qr{^(?!.+"type":"async-io-).+}m, "nothing logged other than async-io-";
};
subtest "QUIC", sub {