From ea1ba92f05077c85f264a8f9e3772b5708258cf7 Mon Sep 17 00:00:00 2001 From: bneradt Date: Fri, 21 Aug 2026 18:16:29 -0500 Subject: [PATCH] Use io_uring for socket readiness Use a process-wide io_uring to multiplex socket readiness waits on supported Linux systems while preserving poll as the automatic fallback. Add auto, off, and required modes for same-binary production A/B tests, including fail-fast behavior when a canary cannot keep using io_uring. Production validation: compare --io-uring required with --io-uring off before merging. --- .github/workflows/ci.yml | 83 +- CMakeLists.txt | 5 + README.md | 40 +- cmake/IoUring.cmake | 48 ++ docker/alpine_3.24/Dockerfile | 1 + docker/fedora_44/Dockerfile | 1 + docker/ubuntu_26.04/Dockerfile | 1 + src/CMakeLists.txt | 7 +- src/client/verifier-client.cc | 19 + src/core/http.cc | 5 +- src/core/http.h | 5 +- src/core/socket_io.cc | 752 ++++++++++++++++++ src/core/socket_io.h | 71 ++ src/server/verifier-server.cc | 22 +- tests/tools/uranium/plugin.py | 5 +- tests/tools/uranium/runtime.py | 14 +- tests/unit_tests/CMakeLists.txt | 1 + tests/unit_tests/test_socket_io.cc | 95 +++ .../sigint_shutdown/test_sigint_shutdown.py | 10 +- .../sigint_shutdown/verify_sigint_shutdown.py | 13 +- tools/CI-IMAGES.md | 10 +- 21 files changed, 1178 insertions(+), 30 deletions(-) create mode 100644 cmake/IoUring.cmake create mode 100644 src/core/socket_io.cc create mode 100644 src/core/socket_io.h create mode 100644 tests/unit_tests/test_socket_io.cc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f83888ff..51047df4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,7 +97,7 @@ jobs: uses: actions/checkout@v6 - name: Configure - run: cmake --preset dev + run: cmake --preset dev -DPV_ENABLE_IO_URING=OFF - name: Build run: cmake --build --preset dev --parallel @@ -113,6 +113,38 @@ jobs: path: build/dev/Testing if-no-files-found: ignore + unit-tests-io-uring: + name: Unit tests (io_uring, Ubuntu 26.04, Clang) + runs-on: ubuntu-24.04 + timeout-minutes: 60 + container: + image: ci.trafficserver.apache.org/proxy-verifier/ubuntu:26.04 + options: --security-opt seccomp=unconfined + env: + CC: clang + CXX: clang++ + PV_TEST_REQUIRE_IO_URING: "1" + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Configure + run: cmake --preset dev -DPV_ENABLE_IO_URING=ON + + - name: Build + run: cmake --build --preset dev --parallel + + - name: Run unit tests + run: ctest --preset dev + + - name: Upload unit-test logs + if: failure() + uses: actions/upload-artifact@v6 + with: + name: unit-test-io-uring-logs + path: build/dev/Testing + if-no-files-found: ignore + urtests: name: Uranium tests (Ubuntu 26.04, GCC) runs-on: ubuntu-24.04 @@ -127,7 +159,10 @@ jobs: uses: actions/checkout@v6 - name: Configure - run: cmake --preset dev -DURTEST_SANDBOX=/tmp/proxy-verifier-urtest + run: >- + cmake --preset dev + -DPV_ENABLE_IO_URING=OFF + -DURTEST_SANDBOX=/tmp/proxy-verifier-urtest - name: Build run: cmake --build --preset dev --parallel @@ -152,3 +187,47 @@ jobs: name: urtest-sandbox path: artifacts/urtest-sandbox.tar.gz if-no-files-found: ignore + + urtests-io-uring: + name: Uranium tests (io_uring, Ubuntu 26.04, GCC) + runs-on: ubuntu-24.04 + timeout-minutes: 60 + container: + image: ci.trafficserver.apache.org/proxy-verifier/ubuntu:26.04 + options: --security-opt seccomp=unconfined + env: + CC: gcc + CXX: g++ + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Configure + run: >- + cmake --preset dev + -DPV_ENABLE_IO_URING=ON + -DURTEST_SANDBOX=/tmp/proxy-verifier-urtest-io-uring + + - name: Build + run: cmake --build --preset dev --parallel + + - name: Run Uranium tests + run: ./build/dev/urtest.sh -v -n "$(nproc)" --verifier-io-uring required + + - name: Collect Uranium sandbox + if: failure() + run: | + mkdir -p artifacts + if [[ -d /tmp/proxy-verifier-urtest-io-uring ]]; then + tar --create --gzip \ + --file="${GITHUB_WORKSPACE}/artifacts/urtest-io-uring-sandbox.tar.gz" \ + --directory=/tmp proxy-verifier-urtest-io-uring + fi + + - name: Upload Uranium sandbox + if: failure() + uses: actions/upload-artifact@v6 + with: + name: urtest-io-uring-sandbox + path: artifacts/urtest-io-uring-sandbox.tar.gz + if-no-files-found: ignore diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c6d08f4..13d09feb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,8 @@ option(PV_ENABLE_ASAN "Build the verifier binaries with AddressSanitizer instrumentation." OFF) option(PV_RELEASE_LAYOUT "Install release artifacts into a platform-specific bindir." OFF) +option(PV_ENABLE_IO_URING + "Use io_uring for socket readiness when liburing is available." ON) option( BUILD_PORTABLE "Enable portable build settings such as conservative Linux amd64 ISA flags." @@ -153,6 +155,9 @@ pv_fetch_dependencies() include(HttpDependencies) pv_find_http_dependencies(PV_HTTP_DEPENDENCY_TARGETS) +include(IoUring) +pv_find_io_uring(PV_IO_URING_TARGET) + add_subdirectory(src) string(SHA256 _pv_source_hash "${CMAKE_SOURCE_DIR}") string(SUBSTRING "${_pv_source_hash}" 0 8 _pv_source_hash) diff --git a/README.md b/README.md index 40eaa53d..ab774cb9 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Table of Contents * [--tls-secrets-log-file <secrets_log_file_name>](#--tls-secrets-log-file-secrets_log_file_name) * [--send-buffer-size <size>](#--send-buffer-size-size) * [--poll-timeout <timeout_ms>](#--poll-timeout-timeout_ms) + * [--io-uring <mode>](#--io-uring-mode) * [Tools](#tools) * [Replay Gen replay-gen.py](tools/replay-gen.py) * [-n,--number <NUMBER>](#-n--number-number) @@ -1832,12 +1833,18 @@ presets. The build requires: * OpenSSL 3.5 or newer development files * nghttp2 1.60 or newer development files * nghttp3 0.8 or newer development files +* liburing 2.0 or newer development files on Linux (optional) CMake fetches pinned `libswoc` and `yaml-cpp` sources during configure. HTTP/3 uses OpenSSL 3.5 or newer for the native QUIC transport and nghttp3 for HTTP/3 framing and QPACK. HTTP/2 uses nghttp2. Install these libraries through the system package manager before configuring Proxy Verifier. ngtcp2 is not -required because OpenSSL provides the QUIC transport. +required because OpenSSL provides the QUIC transport. On Linux, CMake enables +io_uring socket readiness support when liburing is installed. Builds without +liburing, non-Linux builds, and kernels that cannot initialize io_uring retain +the existing `poll(2)` behavior. Dynamic Linux builds that enable io_uring +support require the liburing shared library at runtime; portable Linux release +binaries link it statically. For Mac builds, the following brew command can be helpful: @@ -1868,9 +1875,9 @@ This places the build-tree binaries under `build/dev/bin`. The Dockerfiles under `docker/alpine_3.24`, `docker/fedora_44`, and `docker/ubuntu_26.04` are build and development environments, not deployment -images. Each installs OpenSSL, nghttp2, and nghttp3 from its distribution along -with the Proxy Verifier build, formatting, license-audit, and Uranium/pytest -toolchains. +images. Each installs OpenSSL, nghttp2, nghttp3, and liburing from its +distribution along with the Proxy Verifier build, formatting, license-audit, +and Uranium/pytest toolchains. GitHub Actions uses only the Ubuntu 26.04 image. Alpine 3.24 is used to build the portable Linux release binaries, while Fedora 44 provides an additional current-distribution development environment. Users who prefer Alpine or @@ -2355,6 +2362,31 @@ non-blocking sockets with a timeout. By default, this timeout is set to 5 seconds (5,000 milliseconds). This optional argument provides a way to specify a different timeout in milliseconds for these operations. +#### --io-uring \ + +On Linux builds with liburing, Proxy Verifier uses one process-wide io_uring +to multiplex socket readiness waits. This avoids creating an io_uring for each +connection thread. The HTTP, TLS, and replay state machines remain unchanged, +and Proxy Verifier transparently uses `poll(2)` when io_uring is unavailable. + +The mode can be one of: + +* `auto` uses io_uring when both the build and running kernel support it, then + falls back to `poll(2)` if initialization is unavailable. This is the + default. +* `off` always uses `poll(2)`. This is useful as the control side of a + production performance comparison using the same binary. +* `required` exits with an error unless io_uring initializes successfully and + does not fall back if the backend later fails. Use this for an io_uring + production canary so kernel security policy or a container seccomp profile + cannot silently turn the test into a `poll(2)` run. + +Regardless of the selected mode, zero or negative timeouts use a direct +`poll(2)` call because they do not benefit from asynchronous dispatch. + +Set the CMake option `PV_ENABLE_IO_URING=OFF` to omit io_uring support even if +liburing is installed. + ## Tools This section describes how to use some of the scripts under the [tools](tools) directory. diff --git a/cmake/IoUring.cmake b/cmake/IoUring.cmake new file mode 100644 index 00000000..ec2f3a14 --- /dev/null +++ b/cmake/IoUring.cmake @@ -0,0 +1,48 @@ +# @file +# +# Copyright 2026, Verizon Media SPDX-License-Identifier: Apache-2.0 +# + +include_guard(GLOBAL) + +function(pv_find_io_uring out_var) + set(_pv_io_uring_target "") + if(PV_ENABLE_IO_URING AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(PV_STATIC_BUILD) + set(CMAKE_FIND_LIBRARY_SUFFIXES "${CMAKE_STATIC_LIBRARY_SUFFIX}") + endif() + find_package(PkgConfig REQUIRED) + pkg_check_modules(PV_LIBURING QUIET IMPORTED_TARGET GLOBAL "liburing>=2.0") + if(PV_LIBURING_FOUND) + if(PV_STATIC_BUILD) + if(NOT PV_LIBURING_LINK_LIBRARIES) + message(FATAL_ERROR "Static builds require a static liburing archive") + endif() + foreach(_pv_io_uring_library IN LISTS PV_LIBURING_LINK_LIBRARIES) + if(NOT _pv_io_uring_library MATCHES + "\\${CMAKE_STATIC_LIBRARY_SUFFIX}$") + message( + FATAL_ERROR + "Static builds require a static liburing archive, but pkg-config resolved " + "${_pv_io_uring_library}") + endif() + endforeach() + endif() + set(_pv_io_uring_target PkgConfig::PV_LIBURING) + message(STATUS "Building with io_uring socket readiness support") + else() + message( + STATUS "liburing was not found; using the poll socket readiness backend" + ) + endif() + elseif(PV_ENABLE_IO_URING) + message( + STATUS "io_uring is not available on ${CMAKE_SYSTEM_NAME}; using poll") + else() + message(STATUS "io_uring support is disabled; using poll") + endif() + + set(${out_var} + "${_pv_io_uring_target}" + PARENT_SCOPE) +endfunction() diff --git a/docker/alpine_3.24/Dockerfile b/docker/alpine_3.24/Dockerfile index 411e220e..8edf25ac 100644 --- a/docker/alpine_3.24/Dockerfile +++ b/docker/alpine_3.24/Dockerfile @@ -24,6 +24,7 @@ RUN apk add --no-cache \ gcompat \ git \ libffi-dev \ + liburing-dev \ linux-headers \ nghttp2-dev \ nghttp2-static \ diff --git a/docker/fedora_44/Dockerfile b/docker/fedora_44/Dockerfile index 313705ba..5e491659 100644 --- a/docker/fedora_44/Dockerfile +++ b/docker/fedora_44/Dockerfile @@ -30,6 +30,7 @@ dnf install -y \ libffi-devel \ libnghttp2-devel \ libnghttp3-devel \ + liburing-devel \ openssl-devel \ pkgconf-pkg-config \ procps-ng \ diff --git a/docker/ubuntu_26.04/Dockerfile b/docker/ubuntu_26.04/Dockerfile index 4736e0e2..6873bd31 100644 --- a/docker/ubuntu_26.04/Dockerfile +++ b/docker/ubuntu_26.04/Dockerfile @@ -38,6 +38,7 @@ apt-get install -y --no-install-recommends \ libnghttp2-dev \ libnghttp3-dev \ libssl-dev \ + liburing-dev \ libzstd-dev \ lbzip2 \ pkg-config \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 624fcc36..851a4a70 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,7 @@ set(PV_CORE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/core/Localizer.cc" "${CMAKE_CURRENT_SOURCE_DIR}/core/ProxyVerifier.cc" "${CMAKE_CURRENT_SOURCE_DIR}/core/proxy_protocol_util.cc" + "${CMAKE_CURRENT_SOURCE_DIR}/core/socket_io.cc" "${CMAKE_CURRENT_SOURCE_DIR}/core/verification.cc" "${CMAKE_CURRENT_SOURCE_DIR}/core/YamlParser.cc") @@ -29,7 +30,11 @@ target_compile_definitions(verifier-core target_include_directories(verifier-core PUBLIC "${PROJECT_SOURCE_DIR}/src") target_link_libraries( verifier-core PUBLIC Threads::Threads libswoc-static yaml-cpp::yaml-cpp - ${PV_HTTP_DEPENDENCY_TARGETS}) + ${PV_HTTP_DEPENDENCY_TARGETS} ${PV_IO_URING_TARGET}) + +if(PV_IO_URING_TARGET) + target_compile_definitions(verifier-core PRIVATE PV_HAVE_LIBURING=1) +endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_link_libraries(verifier-core PUBLIC stdc++fs) diff --git a/src/client/verifier-client.cc b/src/client/verifier-client.cc index 51ec6aaa..c7f9537a 100644 --- a/src/client/verifier-client.cc +++ b/src/client/verifier-client.cc @@ -12,6 +12,7 @@ #include "core/https.h" #include "core/ProxyVerifier.h" #include "core/YamlParser.h" +#include "core/socket_io.h" #include #include @@ -747,6 +748,16 @@ Engine::parse_args() Errata errata; auto args{arguments.get("run")}; + auto const io_uring_arg = arguments.get("io-uring"); + auto &&[socket_io_backend, socket_io_errata] = + configure_socket_io(io_uring_arg ? io_uring_arg[0] : "auto"); + static_cast(socket_io_backend); + errata.note(std::move(socket_io_errata)); + if (!errata.is_ok()) { + process_exit_code = 1; + return false; + } + if (args.size() < 1) { errata.note(S_ERROR, R"("run" command requires a directory path as an argument.)"); process_exit_code = 1; @@ -1396,6 +1407,14 @@ main(int /* argc */, char const *argv[]) "", 1, "") + .add_option( + "--io-uring", + "", + "Select the socket readiness backend: auto, off, or required. " + "Default is auto.", + "", + 1, + "auto") .add_option( "--keys", "-k", diff --git a/src/core/http.cc b/src/core/http.cc index 906c66a1..7d261ad6 100644 --- a/src/core/http.cc +++ b/src/core/http.cc @@ -6,6 +6,7 @@ */ #include "core/http.h" +#include "core/socket_io.h" #include "core/verification.h" #include "core/ProxyVerifier.h" @@ -1394,9 +1395,7 @@ Session::poll_for_data_on_socket(chrono::milliseconds timeout, short events) if (is_closed()) { return {-1, Errata(S_DIAG, "Poll called on a closed connection.")}; } - auto const timeout_ms = std::max(timeout.count(), 0); - struct pollfd pfd = {.fd = _fd, .events = events, .revents = 0}; - return ::poll(&pfd, 1, timeout_ms); + return poll_for_socket_io(_fd, timeout, events); } swoc::Rv diff --git a/src/core/http.h b/src/core/http.h index f3c66679..b7cd8e99 100644 --- a/src/core/http.h +++ b/src/core/http.h @@ -883,8 +883,9 @@ struct Ssn }; /** A session reader. - * This is essentially a wrapper around a socket to support use of @c poll on - * the socket. The goal is to enable a read operation that waits for data but + * This is essentially a wrapper around a socket with timed readiness waits. + * Linux builds use io_uring when it is available and otherwise fall back to + * @c poll. The goal is to enable a read operation that waits for data but * returns as soon as any data is available. */ class Session diff --git a/src/core/socket_io.cc b/src/core/socket_io.cc new file mode 100644 index 00000000..960487fe --- /dev/null +++ b/src/core/socket_io.cc @@ -0,0 +1,752 @@ +/** @file + * Socket readiness backend selection and polling. + * + * Copyright 2026, Verizon Media + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "core/socket_io.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/ProxyVerifier.h" + +#if defined(PV_HAVE_LIBURING) +#include +#include +#include +#endif + +using swoc::TextView; +using namespace swoc::literals; +namespace chrono = std::chrono; + +namespace +{ +class SocketIoService +{ +public: + explicit SocketIoService(SocketIoMode mode); + ~SocketIoService(); + + SocketIoService(SocketIoService const &) = delete; + SocketIoService &operator=(SocketIoService const &) = delete; + + SocketIoBackend backend() const; + bool fallback_allowed() const; + std::string const &failure_reason() const; + int poll(int fd, chrono::milliseconds timeout, short events); + +private: +#if defined(PV_HAVE_LIBURING) + using Clock = chrono::steady_clock; + using TimePoint = Clock::time_point; + + struct PollOperation; + using DeadlineMap = std::multimap; + + struct PollOperation + { + public: + PollOperation(int socket_fd, short poll_events, TimePoint operation_deadline); + + int wait(); + void complete(int operation_result, int operation_error); + + public: + int const fd; + short const events; + TimePoint const deadline; + bool timeout_requested = false; + bool shutdown_requested = false; + DeadlineMap::iterator deadline_entry; + bool deadline_registered = false; + + private: + std::mutex m_mutex; + std::condition_variable m_cv; + int m_result = -1; + int m_error = 0; + bool m_complete = false; + }; + + static constexpr unsigned SUBMISSION_QUEUE_SIZE = 256; + static constexpr unsigned COMPLETION_QUEUE_SIZE = 8'192; + static constexpr std::uint64_t WAKE_TOKEN = 1; + static constexpr std::uint64_t IGNORE_TOKEN = 2; + + bool initialize(); + void run(); + void notify(); + void drain_notification(); + void process_commands(); + void process_completion(io_uring_cqe const &completion); + unsigned reap_completions(); + void drain_completions(); + void expire_deadlines(TimePoint now); + bool request_cancellation(std::shared_ptr const &operation); + void request_shutdown(); + void fail_service(int error); + bool submit_pending(); + io_uring_sqe *get_submission(); + chrono::nanoseconds time_until_next_deadline(TimePoint now); + + io_uring m_ring{}; + int m_event_fd = -1; + std::thread m_completion_thread; + std::mutex m_command_mutex; + std::deque> m_commands; + std::deque m_deferred_completions; + std::unordered_map> m_pending; + DeadlineMap m_deadlines; + bool m_accepting_requests = false; + bool m_stop_requested = false; + bool m_stop_after_pending = false; + bool m_ring_initialized = false; +#endif + + std::atomic m_available = false; + std::string m_failure_reason; + bool const m_fallback_allowed; +}; + +SocketIoService & +socket_io_service(SocketIoMode mode = SocketIoMode::AUTO) +{ + static SocketIoService service{mode}; + return service; +} + +int +poll_with_system_call(int fd, chrono::milliseconds timeout, short events) +{ + auto const timeout_ms = std::max(timeout.count(), 0); + pollfd descriptor = {.fd = fd, .events = events, .revents = 0}; + return ::poll(&descriptor, 1, timeout_ms); +} +} // namespace + +#if defined(PV_HAVE_LIBURING) +SocketIoService::PollOperation::PollOperation( + int socket_fd, + short poll_events, + TimePoint operation_deadline) + : fd(socket_fd) + , events(poll_events) + , deadline(operation_deadline) +{ +} + +int +SocketIoService::PollOperation::wait() +{ + std::unique_lock lock{m_mutex}; + m_cv.wait(lock, [this] { return m_complete; }); + if (m_result < 0) { + errno = m_error; + } + return m_result; +} + +void +SocketIoService::PollOperation::complete(int operation_result, int operation_error) +{ + { + std::lock_guard lock{m_mutex}; + if (m_complete) { + return; + } + m_result = operation_result; + m_error = operation_error; + m_complete = true; + } + m_cv.notify_one(); +} + +#endif + +SocketIoService::SocketIoService(SocketIoMode mode) + : m_fallback_allowed(mode != SocketIoMode::REQUIRED) +{ +#if defined(PV_HAVE_LIBURING) + if (mode != SocketIoMode::OFF) { + initialize(); + } else { + m_failure_reason = "io_uring was disabled"; + } +#else + static_cast(mode); + m_failure_reason = "this build does not include liburing support"; +#endif +} + +SocketIoService::~SocketIoService() +{ +#if defined(PV_HAVE_LIBURING) + if (m_completion_thread.joinable()) { + if (m_available) { + { + std::lock_guard lock{m_command_mutex}; + m_accepting_requests = false; + m_stop_requested = true; + } + notify(); + } + m_completion_thread.join(); + } + if (m_ring_initialized) { + if (m_event_fd >= 0) { + ::close(m_event_fd); + } + io_uring_queue_exit(&m_ring); + } +#endif +} + +SocketIoBackend +SocketIoService::backend() const +{ + return m_available ? SocketIoBackend::IO_URING : SocketIoBackend::POLL; +} + +bool +SocketIoService::fallback_allowed() const +{ + return m_fallback_allowed; +} + +std::string const & +SocketIoService::failure_reason() const +{ + return m_failure_reason; +} + +int +SocketIoService::poll(int fd, chrono::milliseconds timeout, short events) +{ +#if defined(PV_HAVE_LIBURING) + auto operation = std::make_shared(fd, events, Clock::now() + timeout); + { + std::lock_guard lock{m_command_mutex}; + if (!m_accepting_requests) { + errno = ECANCELED; + return -1; + } + m_commands.push_back(operation); + } + notify(); + return operation->wait(); +#else + static_cast(fd); + static_cast(timeout); + static_cast(events); + errno = ENOSYS; + return -1; +#endif +} + +#if defined(PV_HAVE_LIBURING) +bool +SocketIoService::initialize() +{ + io_uring_params parameters{}; + parameters.flags = IORING_SETUP_CQSIZE; + parameters.cq_entries = COMPLETION_QUEUE_SIZE; + auto const setup_result = io_uring_queue_init_params(SUBMISSION_QUEUE_SIZE, &m_ring, ¶meters); + if (setup_result < 0) { + m_failure_reason = std::string{"io_uring_queue_init failed: "} + strerror(-setup_result); + return false; + } + m_ring_initialized = true; + + if ((parameters.features & IORING_FEAT_NODROP) == 0) { + m_failure_reason = "the kernel does not support lossless io_uring completions"; + io_uring_queue_exit(&m_ring); + m_ring_initialized = false; + return false; + } + + auto *probe = io_uring_get_probe_ring(&m_ring); + if (probe == nullptr || !io_uring_opcode_supported(probe, IORING_OP_POLL_ADD) || + !io_uring_opcode_supported(probe, IORING_OP_ASYNC_CANCEL)) + { + m_failure_reason = "the kernel does not support required io_uring operations"; + if (probe != nullptr) { + io_uring_free_probe(probe); + } + io_uring_queue_exit(&m_ring); + m_ring_initialized = false; + return false; + } + io_uring_free_probe(probe); + + m_event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (m_event_fd < 0) { + m_failure_reason = std::string{"eventfd initialization failed: "} + strerror(errno); + io_uring_queue_exit(&m_ring); + m_ring_initialized = false; + return false; + } + + auto *submission = get_submission(); + if (submission == nullptr) { + m_failure_reason = "could not allocate the initial io_uring submission"; + ::close(m_event_fd); + m_event_fd = -1; + io_uring_queue_exit(&m_ring); + m_ring_initialized = false; + return false; + } + io_uring_prep_poll_add(submission, m_event_fd, POLLIN); + submission->user_data = WAKE_TOKEN; + if (!submit_pending()) { + m_failure_reason = std::string{"initial io_uring submission failed: "} + strerror(errno); + ::close(m_event_fd); + m_event_fd = -1; + io_uring_queue_exit(&m_ring); + m_ring_initialized = false; + return false; + } + + m_accepting_requests = true; + m_available = true; + try { + m_completion_thread = std::thread{&SocketIoService::run, this}; + } catch (std::system_error const &error) { + m_accepting_requests = false; + m_available = false; + m_failure_reason = + std::string{"io_uring completion thread initialization failed: "} + error.what(); + ::close(m_event_fd); + m_event_fd = -1; + io_uring_queue_exit(&m_ring); + m_ring_initialized = false; + return false; + } + return true; +} + +void +SocketIoService::notify() +{ + std::uint64_t increment = 1; + while (::write(m_event_fd, &increment, sizeof(increment)) < 0) { + if (errno == EINTR) { + continue; + } + // EAGAIN means a previous notification is still pending. + break; + } +} + +void +SocketIoService::drain_notification() +{ + std::uint64_t value = 0; + while (::read(m_event_fd, &value, sizeof(value)) < 0 && errno == EINTR) { + } +} + +io_uring_sqe * +SocketIoService::get_submission() +{ + auto *submission = io_uring_get_sqe(&m_ring); + if (submission == nullptr && submit_pending()) { + submission = io_uring_get_sqe(&m_ring); + if (submission == nullptr) { + errno = EAGAIN; + } + } + return submission; +} + +bool +SocketIoService::submit_pending() +{ + while (io_uring_sq_ready(&m_ring) > 0) { + auto const result = io_uring_submit(&m_ring); + if (result > 0) { + continue; + } + if (result == -EINTR) { + continue; + } + if (result == -EBUSY) { + // IORING_FEAT_NODROP reports CQ overflow through EBUSY. Reaping the + // available completions makes room for the kernel to flush overflowed + // entries on the next submission attempt. Defer processing so callers + // cannot be re-entered while they hold references into service state. + if (reap_completions() > 0) { + continue; + } + } + errno = result < 0 ? -result : EIO; + return false; + } + return true; +} + +void +SocketIoService::process_commands() +{ + drain_notification(); + + std::deque> commands; + bool stop_requested = false; + { + std::lock_guard lock{m_command_mutex}; + commands.swap(m_commands); + stop_requested = m_stop_requested; + } + + if (stop_requested) { + for (auto const &operation : commands) { + operation->complete(-1, ECANCELED); + } + request_shutdown(); + return; + } + + for (auto const &operation : commands) { + auto *submission = get_submission(); + if (submission == nullptr) { + operation->complete(-1, errno); + continue; + } + m_pending.emplace(operation.get(), operation); + operation->deadline_entry = m_deadlines.emplace(operation->deadline, operation.get()); + operation->deadline_registered = true; + io_uring_prep_poll_add(submission, operation->fd, operation->events); + submission->user_data = reinterpret_cast(operation.get()); + } + + auto *submission = get_submission(); + if (submission == nullptr) { + fail_service(errno); + return; + } + io_uring_prep_poll_add(submission, m_event_fd, POLLIN); + submission->user_data = WAKE_TOKEN; + if (!submit_pending()) { + fail_service(errno); + } +} + +bool +SocketIoService::request_cancellation(std::shared_ptr const &operation) +{ + auto *submission = get_submission(); + if (submission == nullptr) { + return false; + } + io_uring_prep_cancel(submission, operation.get(), 0); + submission->user_data = IGNORE_TOKEN; + return true; +} + +void +SocketIoService::request_shutdown() +{ + m_stop_after_pending = true; + std::vector> pending_operations; + pending_operations.reserve(m_pending.size()); + for (auto const &[operation_pointer, operation] : m_pending) { + static_cast(operation_pointer); + pending_operations.push_back(operation); + } + for (auto const &operation : pending_operations) { + operation->shutdown_requested = true; + if (!request_cancellation(operation)) { + fail_service(errno); + return; + } + } + if (m_pending.empty()) { + return; + } + if (!submit_pending()) { + fail_service(errno); + } +} + +void +SocketIoService::process_completion(io_uring_cqe const &completion) +{ + if (completion.user_data == WAKE_TOKEN) { + process_commands(); + return; + } + if (completion.user_data == IGNORE_TOKEN) { + return; + } + + auto *operation_pointer = reinterpret_cast(completion.user_data); + auto spot = m_pending.find(operation_pointer); + if (spot == m_pending.end()) { + return; + } + + auto operation = std::move(spot->second); + m_pending.erase(spot); + if (operation->deadline_registered) { + m_deadlines.erase(operation->deadline_entry); + operation->deadline_registered = false; + } + if (operation->shutdown_requested) { + operation->complete(-1, ECANCELED); + } else if (completion.res > 0) { + operation->complete(1, 0); + } else if (operation->timeout_requested) { + operation->complete(0, 0); + } else if (completion.res >= 0) { + operation->complete(completion.res == 0 ? 0 : 1, 0); + } else { + operation->complete(-1, -completion.res); + } +} + +unsigned +SocketIoService::reap_completions() +{ + unsigned completion_count = 0; + io_uring_cqe *completion = nullptr; + while (io_uring_peek_cqe(&m_ring, &completion) == 0) { + m_deferred_completions.push_back(*completion); + io_uring_cqe_seen(&m_ring, completion); + ++completion_count; + } + return completion_count; +} + +void +SocketIoService::drain_completions() +{ + reap_completions(); + // Completion processing can reap more CQEs into the back of this deque; + // copying and popping the front before dispatch makes that safe. + while (!m_deferred_completions.empty()) { + auto const completed_entry = m_deferred_completions.front(); + m_deferred_completions.pop_front(); + process_completion(completed_entry); + } +} + +void +SocketIoService::expire_deadlines(TimePoint now) +{ + while (!m_deadlines.empty()) { + auto const entry = m_deadlines.begin(); + if (entry->first > now) { + break; + } + auto const operation_pointer = entry->second; + auto const spot = m_pending.find(operation_pointer); + if (spot == m_pending.end()) { + m_deadlines.erase(entry); + continue; + } + auto const operation = spot->second; + m_deadlines.erase(entry); + operation->deadline_registered = false; + operation->timeout_requested = true; + if (!request_cancellation(operation)) { + fail_service(errno); + return; + } + } +} + +chrono::nanoseconds +SocketIoService::time_until_next_deadline(TimePoint now) +{ + if (!m_deadlines.empty()) { + return std::max( + chrono::duration_cast(m_deadlines.begin()->first - now), + chrono::nanoseconds{1}); + } + return chrono::nanoseconds::max(); +} + +void +SocketIoService::fail_service(int error) +{ + if (!m_available.load()) { + return; + } + m_failure_reason = std::string{"io_uring runtime failure: "} + strerror(error); + if (!m_available.exchange(false)) { + return; + } + swoc::Errata errata; + errata.note( + S_ERROR, + "The io_uring socket I/O backend was disabled after a runtime failure: {}. {}", + strerror(error), + m_fallback_allowed ? "Future waits will use poll(2)." : "poll(2) fallback is disabled."); + std::deque> commands; + { + std::lock_guard lock{m_command_mutex}; + m_accepting_requests = false; + commands.swap(m_commands); + } + for (auto const &operation : commands) { + operation->complete(-1, error); + } + for (auto const &[operation_pointer, operation] : m_pending) { + static_cast(operation_pointer); + operation->deadline_registered = false; + operation->complete(-1, error); + } + m_deferred_completions.clear(); + m_deadlines.clear(); + m_pending.clear(); + m_stop_after_pending = true; +} + +void +SocketIoService::run() +{ + while (!m_stop_after_pending || !m_pending.empty()) { + drain_completions(); + auto const now = Clock::now(); + expire_deadlines(now); + if (!submit_pending()) { + fail_service(errno); + break; + } + if (m_stop_after_pending && m_pending.empty()) { + break; + } + if (!m_deferred_completions.empty()) { + continue; + } + + io_uring_cqe *completion = nullptr; + auto const wait_duration = time_until_next_deadline(now); + int result = 0; + if (wait_duration == chrono::nanoseconds::max()) { + result = io_uring_wait_cqe(&m_ring, &completion); + } else { + __kernel_timespec timeout = { + .tv_sec = chrono::duration_cast(wait_duration).count(), + .tv_nsec = (wait_duration % chrono::seconds{1}).count(), + }; + result = io_uring_wait_cqe_timeout(&m_ring, &completion, &timeout); + } + + if (result == 0) { + auto const completed_entry = *completion; + io_uring_cqe_seen(&m_ring, completion); + process_completion(completed_entry); + } else if (result != -ETIME && result != -EINTR) { + fail_service(-result); + break; + } + } +} +#endif + +swoc::Rv +parse_socket_io_mode(TextView value) +{ + swoc::Rv result{SocketIoMode::AUTO}; + if (value == "auto"_tv) { + result = SocketIoMode::AUTO; + } else if (value == "off"_tv) { + result = SocketIoMode::OFF; + } else if (value == "required"_tv) { + result = SocketIoMode::REQUIRED; + } else { + result.note( + S_ERROR, + R"(Invalid --io-uring value "{}". Expected auto, off, or required.)", + value); + } + return result; +} + +swoc::Rv +configure_socket_io(TextView value) +{ + swoc::Rv result{SocketIoBackend::POLL}; + auto &&[mode, mode_errata] = parse_socket_io_mode(value); + result.note(std::move(mode_errata)); + if (!result.is_ok()) { + return result; + } + + auto &&[backend, backend_errata] = configure_socket_io(mode); + result.result() = backend; + result.note(std::move(backend_errata)); + if (!result.is_ok()) { + return result; + } + + auto const &service = socket_io_service(); + if (backend == SocketIoBackend::POLL && mode == SocketIoMode::AUTO) { + result.note( + S_INFO, + "Using the poll(2) socket I/O backend because io_uring is unavailable: {}.", + service.failure_reason()); + } else { + result.note(S_INFO, "Using the {} socket I/O backend.", socket_io_backend_name(backend)); + } + return result; +} + +swoc::Rv +configure_socket_io(SocketIoMode mode) +{ + auto &service = socket_io_service(mode); + swoc::Rv result{service.backend()}; + if (mode == SocketIoMode::REQUIRED && service.backend() != SocketIoBackend::IO_URING) { + result.note(S_ERROR, "io_uring is required but unavailable: {}.", service.failure_reason()); + } + return result; +} + +TextView +socket_io_backend_name(SocketIoBackend backend) +{ + return backend == SocketIoBackend::IO_URING ? "io_uring"_tv : "poll"_tv; +} + +int +poll_for_socket_io(int fd, chrono::milliseconds timeout, short events) +{ + auto &service = socket_io_service(); + if (timeout.count() <= 0) { + return poll_with_system_call(fd, timeout, events); + } + if (service.backend() == SocketIoBackend::IO_URING) { + auto const start_time = chrono::steady_clock::now(); + auto const result = service.poll(fd, timeout, events); + if (result >= 0) { + return result; + } + if (!service.fallback_allowed()) { + return result; + } + timeout -= + chrono::duration_cast(chrono::steady_clock::now() - start_time); + } else if (!service.fallback_allowed()) { + errno = EIO; + return -1; + } + return poll_with_system_call(fd, timeout, events); +} diff --git a/src/core/socket_io.h b/src/core/socket_io.h new file mode 100644 index 00000000..7699eb91 --- /dev/null +++ b/src/core/socket_io.h @@ -0,0 +1,71 @@ +/** @file + * Socket readiness backend selection and polling. + * + * Copyright 2026, Verizon Media + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "swoc/Errata.h" +#include "swoc/TextView.h" + +/** The requested socket I/O backend policy. */ +enum class SocketIoMode { + AUTO, ///< Use io_uring when it is available, otherwise use poll. + OFF, ///< Always use poll. + REQUIRED, ///< Require io_uring initialization to succeed. +}; + +/** The socket I/O backend selected for this process. */ +enum class SocketIoBackend { + POLL, ///< The portable poll(2) backend. + IO_URING ///< The Linux io_uring backend. +}; + +/** Parse a socket I/O backend policy. + * + * @param[in] value The policy name: auto, off, or required. + * @return The parsed policy, or an error for an unsupported value. + */ +swoc::Rv parse_socket_io_mode(swoc::TextView value); + +/** Parse and configure the process-wide socket I/O backend. + * + * @param[in] value The requested backend policy name. + * @return The selected backend, or an error for an invalid or unavailable + * configuration. + */ +swoc::Rv configure_socket_io(swoc::TextView value); + +/** Configure the process-wide socket I/O backend. + * + * This must be called before the first call to poll_for_socket_io(). Repeated + * calls return the backend selected by the first call. + * + * @param[in] mode The requested backend policy. + * @return The selected backend, or an error if io_uring was required but was + * unavailable. + */ +swoc::Rv configure_socket_io(SocketIoMode mode); + +/** Get a printable name for a socket I/O backend. + * + * @param[in] backend The backend to describe. + * @return The backend name. + */ +swoc::TextView socket_io_backend_name(SocketIoBackend backend); + +/** Wait for events on a socket using the configured backend. + * + * io_uring is used when it was compiled in and can be initialized by the + * running kernel. Otherwise this transparently calls poll(2). + * + * @param[in] fd The socket file descriptor. + * @param[in] timeout The maximum time to wait. + * @param[in] events The poll event mask to wait for. + * @return 0 on timeout, -1 on failure, or a positive value when ready. + */ +int poll_for_socket_io(int fd, std::chrono::milliseconds timeout, short events); diff --git a/src/server/verifier-server.cc b/src/server/verifier-server.cc index a297d777..dab6d6a8 100644 --- a/src/server/verifier-server.cc +++ b/src/server/verifier-server.cc @@ -12,6 +12,7 @@ #include "core/ProxyVerifier.h" #include "core/YamlParser.h" #include "core/proxy_protocol_util.h" +#include "core/socket_io.h" #include #include @@ -814,12 +815,11 @@ void TF_Accept(int socket_fd, bool do_https, bool do_http3) { std::unique_ptr session; - struct pollfd pfd = {.fd = socket_fd, .events = POLLIN, .revents = 0}; while (!Shutdown_Flag) { swoc::Errata errata; // Poll so that we can set a timeout and check whether the user requested a shutdown. - auto const poll_return = ::poll(&pfd, 1, Thread_Sleep_Interval.count()); + auto const poll_return = poll_for_socket_io(socket_fd, Thread_Sleep_Interval, POLLIN); if (poll_return == 0) { // poll timed out. continue; @@ -964,6 +964,16 @@ Engine::command_run() Poll_Timeout = milliseconds(poll_timeout_arg_int); errata.note(S_DIAG, "Poll timeout set to {}ms.", poll_timeout_arg_int); + auto const io_uring_arg = arguments.get("io-uring"); + auto &&[socket_io_backend, socket_io_errata] = + configure_socket_io(io_uring_arg ? io_uring_arg[0] : "auto"); + static_cast(socket_io_backend); + errata.note(std::move(socket_io_errata)); + if (!errata.is_ok()) { + process_exit_code = 1; + return; + } + auto server_addr_http_arg{arguments.get("listen-http")}; auto server_addr_https_arg{arguments.get("listen-https")}; auto server_addr_http3_arg{arguments.get("listen-http3")}; @@ -1324,6 +1334,14 @@ main(int /* argc */, char const *argv[]) "", 1, "") + .add_option( + "--io-uring", + "", + "Select the socket readiness backend: auto, off, or required. " + "Default is auto.", + "", + 1, + "auto") .add_option( "--strict", "-s", diff --git a/tests/tools/uranium/plugin.py b/tests/tools/uranium/plugin.py index 5ba8cd37..ab871c62 100644 --- a/tests/tools/uranium/plugin.py +++ b/tests/tools/uranium/plugin.py @@ -23,6 +23,8 @@ def pytest_addoption(parser: pytest.Parser) -> None: group.addoption("--verifier-bin", help="Directory containing verifier-client and verifier-server") group.addoption("--sandbox", help="Directory for isolated Uranium test process trees") + group.addoption("--verifier-io-uring", choices=("auto", "off", "required"), default="auto", + help="Socket I/O mode passed to verifier processes") group.addoption("--urtest-shard-index", type=int, help="Zero-based CI shard to collect") group.addoption("--urtest-shard-count", type=int, help="Total number of CI shards") @@ -100,8 +102,9 @@ def uranium(pytestconfig: pytest.Config, request: pytest.FixtureRequest) -> Iter if missing: raise pytest.UsageError("Uranium tests require " + " and ".join(missing)) worker = os.environ.get("PYTEST_XDIST_WORKER", "main") + io_uring_mode = pytestconfig.getoption("verifier_io_uring") runtime = Uranium(repository_root, Path(verifier_bin), - Path(sandbox) / worker, request.node.nodeid) + Path(sandbox) / worker, request.node.nodeid, io_uring_mode) setattr(request.node, "_uranium_runtime", runtime) with runtime.execution_lock(): yield runtime diff --git a/tests/tools/uranium/runtime.py b/tests/tools/uranium/runtime.py index aeae73b4..327a5034 100644 --- a/tests/tools/uranium/runtime.py +++ b/tests/tools/uranium/runtime.py @@ -40,12 +40,13 @@ def _tcp_open(port: int, address: str = "127.0.0.1") -> bool: class Uranium: """Own paths, ports, processes, and sandboxes for one pytest item.""" - def __init__(self, repository_root: Path, verifier_bin: Path, sandbox_root: Path, - nodeid: str) -> None: + def __init__(self, repository_root: Path, verifier_bin: Path, sandbox_root: Path, nodeid: str, + io_uring_mode: str) -> None: self.repository_root = repository_root.resolve() self.verifier_bin = verifier_bin.resolve() self.sandbox_root = sandbox_root.resolve() self.nodeid = nodeid + self.io_uring_mode = io_uring_mode required = [self.verifier_bin / "verifier-client", self.verifier_bin / "verifier-server"] if missing := [str(path) for path in required if not path.is_file()]: raise RuntimeConfigError("Missing required test programs: " + ", ".join(missing)) @@ -179,7 +180,9 @@ def _make_process(self, spec: ProcessSpec, case_directory: Path) -> ManagedProce def _client_command(self, spec: ProcessSpec, directory: Path) -> list[str | Path]: options = spec.options - command: list[str | Path] = [self.verifier_bin / "verifier-client", "run"] + command: list[str | Path] = [ + self.verifier_bin / "verifier-client", "run", "--io-uring", self.io_uring_mode + ] if replay_path := options.get("replay_path"): command.append(self._resolve_path(spec.case.suite, replay_path)) use_ipv6 = bool(options.get("use_ipv6", False)) @@ -206,7 +209,9 @@ def _client_command(self, spec: ProcessSpec, directory: Path) -> list[str | Path def _server_command(self, spec: ProcessSpec, directory: Path) -> list[str | Path]: options = spec.options - command: list[str | Path] = [self.verifier_bin / "verifier-server", "run"] + command: list[str | Path] = [ + self.verifier_bin / "verifier-server", "run", "--io-uring", self.io_uring_mode + ] use_ipv6 = bool(options.get("use_ipv6", False)) for protocol, argument in (("http", "--listen-http"), ("https", "--listen-https")): ports = self._configured_ports(spec, protocol) @@ -357,6 +362,7 @@ def _resolve_text(self, value: Any) -> str: text = str(value) text = text.replace("{verifier-client}", str(self.verifier_bin / "verifier-client")) text = text.replace("{verifier-server}", str(self.verifier_bin / "verifier-server")) + text = text.replace("{verifier-io-uring}", self.io_uring_mode) patterns = [ (r"\{uranium-port:(\d+):(http|https|http3):(\d+)\}", self._replace_port), (r"\{uranium-output:(\d+):(stdout|stderr)\}", self._replace_output), diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index febea584..d4cb97f4 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -9,6 +9,7 @@ set(PV_UNIT_TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/test_chunk_parsing.cc" "${CMAKE_CURRENT_SOURCE_DIR}/test_http.cc" "${CMAKE_CURRENT_SOURCE_DIR}/test_https.cc" + "${CMAKE_CURRENT_SOURCE_DIR}/test_socket_io.cc" "${CMAKE_CURRENT_SOURCE_DIR}/test_verification.cc" "${CMAKE_CURRENT_SOURCE_DIR}/unit_test_main.cc") diff --git a/tests/unit_tests/test_socket_io.cc b/tests/unit_tests/test_socket_io.cc new file mode 100644 index 00000000..4828a8cd --- /dev/null +++ b/tests/unit_tests/test_socket_io.cc @@ -0,0 +1,95 @@ +/** @file + * Unit tests for the socket I/O backend. + * + * Copyright 2026, Verizon Media + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "catch.hpp" + +#include "core/socket_io.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals; + +TEST_CASE("Socket I/O modes are parsed", "[socket_io]") +{ + CHECK(parse_socket_io_mode("auto").result() == SocketIoMode::AUTO); + CHECK(parse_socket_io_mode("off").result() == SocketIoMode::OFF); + CHECK(parse_socket_io_mode("required").result() == SocketIoMode::REQUIRED); + CHECK_FALSE(parse_socket_io_mode("invalid").is_ok()); +} + +TEST_CASE("Socket I/O reports the selected backend", "[socket_io]") +{ + auto const require_io_uring = std::getenv("PV_TEST_REQUIRE_IO_URING") != nullptr; + auto const mode = require_io_uring ? SocketIoMode::REQUIRED : SocketIoMode::AUTO; + auto &&[backend, errata] = configure_socket_io(mode); + REQUIRE(errata.is_ok()); + auto const name = socket_io_backend_name(backend); + if (require_io_uring) { + REQUIRE(backend == SocketIoBackend::IO_URING); + CHECK(name == "io_uring"); + } else { + CHECK((name == "poll" || name == "io_uring")); + } +} + +TEST_CASE("Socket I/O waits for readiness and timeout", "[socket_io]") +{ + int sockets[2] = {-1, -1}; + REQUIRE(::socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0); + + CHECK(poll_for_socket_io(sockets[0], 0ms, POLLIN) == 0); + for (int attempt = 0; attempt < 8; ++attempt) { + CHECK(poll_for_socket_io(sockets[0], 1ms, POLLIN) == 0); + } + + char const content = 'x'; + REQUIRE(::write(sockets[1], &content, sizeof(content)) == sizeof(content)); + CHECK(poll_for_socket_io(sockets[0], 0ms, POLLIN) > 0); + CHECK(poll_for_socket_io(sockets[0], 100ms, POLLIN) > 0); + CHECK(poll_for_socket_io(sockets[0], 100ms, POLLOUT) > 0); + + ::close(sockets[0]); + ::close(sockets[1]); +} + +TEST_CASE("Socket I/O handles concurrent readiness waits", "[socket_io]") +{ + static constexpr size_t SOCKET_COUNT = 32; + std::array, SOCKET_COUNT> sockets; + for (auto &pair : sockets) { + REQUIRE(::socketpair(AF_UNIX, SOCK_STREAM, 0, pair.data()) == 0); + } + + std::array results{}; + std::vector waiters; + waiters.reserve(SOCKET_COUNT); + for (size_t index = 0; index < SOCKET_COUNT; ++index) { + waiters.emplace_back( + [&, index] { results[index] = poll_for_socket_io(sockets[index][0], 500ms, POLLIN); }); + } + + char const content = 'x'; + for (auto const &pair : sockets) { + CHECK(::write(pair[1], &content, sizeof(content)) == sizeof(content)); + } + for (auto &waiter : waiters) { + waiter.join(); + } + + for (size_t index = 0; index < SOCKET_COUNT; ++index) { + CHECK(results[index] > 0); + ::close(sockets[index][0]); + ::close(sockets[index][1]); + } +} diff --git a/tests/uranium_tests/sigint_shutdown/test_sigint_shutdown.py b/tests/uranium_tests/sigint_shutdown/test_sigint_shutdown.py index 850f254d..75f36313 100644 --- a/tests/uranium_tests/sigint_shutdown/test_sigint_shutdown.py +++ b/tests/uranium_tests/sigint_shutdown/test_sigint_shutdown.py @@ -16,7 +16,10 @@ case = suite.case("Verify verifier-server exits promptly on SIGINT during replay.") process = case.add_process( "server-sigint", - ["python3", verifier_script, "--mode", "server", "--verifier-server", "{verifier-server}"], + [ + "python3", verifier_script, "--mode", "server", "--verifier-server", "{verifier-server}", + "--io-uring", "{verifier-io-uring}" + ], copies=[verifier_script], ) process.stdout.contains( @@ -26,7 +29,10 @@ case = suite.case("Verify verifier-client exits promptly on SIGINT during replay.") process = case.add_process( "client-sigint", - ["python3", verifier_script, "--mode", "client", "--verifier-client", "{verifier-client}"], + [ + "python3", verifier_script, "--mode", "client", "--verifier-client", "{verifier-client}", + "--io-uring", "{verifier-io-uring}" + ], copies=[verifier_script], ) process.stdout.contains( diff --git a/tests/uranium_tests/sigint_shutdown/verify_sigint_shutdown.py b/tests/uranium_tests/sigint_shutdown/verify_sigint_shutdown.py index 6ee4fc22..9c66ad15 100644 --- a/tests/uranium_tests/sigint_shutdown/verify_sigint_shutdown.py +++ b/tests/uranium_tests/sigint_shutdown/verify_sigint_shutdown.py @@ -67,7 +67,7 @@ def wait_for_exit(process: subprocess.Popen[str], log_path: Path) -> float: return time.monotonic() - start -def run_server_check(verifier_server: Path) -> None: +def run_server_check(verifier_server: Path, io_uring_mode: str) -> None: replay = textwrap.dedent("""\ meta: version: '1.0' @@ -103,6 +103,8 @@ def run_server_check(verifier_server: Path) -> None: [ str(verifier_server), "run", + "--io-uring", + io_uring_mode, "--listen-http", f"127.0.0.1:{port}", str(replay_path), @@ -149,7 +151,7 @@ def run_server_check(verifier_server: Path) -> None: print(f"OK: server exited promptly in {exit_duration:.3f}s") -def run_client_check(verifier_client: Path) -> None: +def run_client_check(verifier_client: Path, io_uring_mode: str) -> None: replay = textwrap.dedent("""\ meta: version: '1.0' @@ -180,6 +182,8 @@ def run_client_check(verifier_client: Path) -> None: [ str(verifier_client), "run", + "--io-uring", + io_uring_mode, "--connect-http", f"127.0.0.1:{unused_port}", str(replay_path), @@ -211,18 +215,19 @@ def main() -> int: parser.add_argument("--mode", choices=("server", "client"), required=True) parser.add_argument("--verifier-server") parser.add_argument("--verifier-client") + parser.add_argument("--io-uring", choices=("auto", "off", "required"), default="auto") args = parser.parse_args() if args.mode == "server": if args.verifier_server is None: fail("--verifier-server is required in server mode") verifier_server = Path(args.verifier_server) - run_server_check(verifier_server) + run_server_check(verifier_server, args.io_uring) else: if args.verifier_client is None: fail("--verifier-client is required in client mode") verifier_client = Path(args.verifier_client) - run_client_check(verifier_client) + run_client_check(verifier_client, args.io_uring) return 0 diff --git a/tools/CI-IMAGES.md b/tools/CI-IMAGES.md index 73c46c0e..f08f2210 100644 --- a/tools/CI-IMAGES.md +++ b/tools/CI-IMAGES.md @@ -11,10 +11,10 @@ Proxy Verifier provides Dockerfiles for Alpine 3.24, Fedora 44, and Ubuntu 26.04 build environments. GitHub Actions uses only Ubuntu 26.04. The Alpine and Fedora Dockerfiles are available for users who prefer those distributions for their local build environment. Alpine is also the portable Linux release -environment. Each Dockerfile installs its distribution's OpenSSL, nghttp2, and -nghttp3 development packages, along with compilers, CMake, Ninja, `uv`, -formatting tools, and a checksum-verified Apache RAT JAR. The Ubuntu image -includes both GCC and Clang for the CI test jobs. +environment. Each Dockerfile installs its distribution's OpenSSL, nghttp2, +nghttp3, and liburing development packages, along with compilers, CMake, Ninja, +`uv`, formatting tools, and a checksum-verified Apache RAT JAR. The Ubuntu +image includes both GCC and Clang for the CI test jobs. These images are for development, release builds, and CI workflows, not deployment. Use the statically linked binaries attached to Proxy Verifier @@ -81,7 +81,7 @@ docker run --rm --platform "${platform}" \ java -version test -r "${RAT_JAR}" java -jar "${RAT_JAR}" --help >/dev/null - pkg-config --modversion openssl libnghttp2 libnghttp3 + pkg-config --modversion openssl libnghttp2 libnghttp3 liburing ' ```