From bc9d0da9a2418251a94fb41b4606c7deddf8f515 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Tue, 13 Oct 2026 22:03:02 +0000 Subject: [PATCH 1/2] Port rsloop 0.1.64 to riscv64 --- .github/workflows/build-rsloop.yml | 114 +++++++++++++++++++++++++++++ docs/packages/rsloop.yaml | 5 ++ 2 files changed, 119 insertions(+) create mode 100644 .github/workflows/build-rsloop.yml create mode 100644 docs/packages/rsloop.yaml diff --git a/.github/workflows/build-rsloop.yml b/.github/workflows/build-rsloop.yml new file mode 100644 index 000000000000..223b0b46119c --- /dev/null +++ b/.github/workflows/build-rsloop.yml @@ -0,0 +1,114 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `linux` job of +# https://github.com/RustedBytes/rsloop/blob/v0.1.64/.github/workflows/wheels.yml +# and the `test-supported-pythons` job of +# https://github.com/RustedBytes/rsloop/blob/v0.1.64/.github/workflows/tests.yml +name: Build rsloop wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/rsloop.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-rsloop.yml' + - 'docs/packages/rsloop.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-rsloop.yml' + - 'docs/packages/rsloop.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: rsloop + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build rsloop ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 240 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + python: ["cp312", "cp313", "cp314", "cp314t"] + + env: + RSLOOP_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout rsloop v${{ env.RSLOOP_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: RustedBytes/rsloop + ref: v${{ env.RSLOOP_VERSION }} + persist-credentials: false + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # Upstream builds with the nightly its rust-toolchain.toml pins; the + # openssl CLI generates the TLS fixtures tests/test_tls.py needs. + CIBW_BEFORE_ALL_LINUX: >- + dnf install -y openssl && + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | + sh -s -- -y --profile minimal + --default-toolchain "$(sed -n 's/^channel = "\(.*\)"$/\1/p' rust-toolchain.toml)" + CIBW_ENVIRONMENT_LINUX: >- + PATH="$PATH:$HOME/.cargo/bin" + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + CIBW_TEST_REQUIRES: >- + pytest>=8.4 pytest-mock>=3.14 typing-extensions>=4.0 websockets>=13.0 + CIBW_TEST_SOURCES: tests scripts benches examples pyproject.toml + CIBW_TEST_COMMAND: >- + python -c "import rsloop._loop as m; assert m.__file__.endswith('.so'), m.__file__" && + python scripts/generate_test_tls_certs.py && + python -u scripts/run_python_tests.py + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: rsloop-${{ env.RSLOOP_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish rsloop ${{ matrix.version }} + needs: [setup, build_wheels] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: rsloop-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/rsloop.yaml b/docs/packages/rsloop.yaml new file mode 100644 index 000000000000..4772dccd5080 --- /dev/null +++ b/docs/packages/rsloop.yaml @@ -0,0 +1,5 @@ +package-name: rsloop +source-code: https://github.com/RustedBytes/rsloop +license: Apache-2.0 +versions: +- version: 0.1.64 From 2635a7e727c336e1226e46ca278a08980dd8879c Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 24 Oct 2026 00:20:42 +0000 Subject: [PATCH 2/2] rsloop: patch TLS close race that fails test_tls.py on riscv64 The cp312 job failed test_client_config_cache_reuses_and_invalidates with "peer closed connection without sending TLS close_notify" from wait_closed(). close() shuts the socket down in both directions after sending close_notify, which wakes the TLS reader worker with EOF before the peer's close_notify may arrive; the reader then reports an error for a transport the caller closed itself. Report connection_lost(None) for read failures once the transport is closing, as asyncio does. --- .github/workflows/build-rsloop.yml | 13 +++ ...ally-closed-transport-s-read-side-lo.patch | 82 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 patches/rsloop/0.1.64/0001-tls-report-a-locally-closed-transport-s-read-side-lo.patch diff --git a/.github/workflows/build-rsloop.yml b/.github/workflows/build-rsloop.yml index 223b0b46119c..c24c92643a19 100644 --- a/.github/workflows/build-rsloop.yml +++ b/.github/workflows/build-rsloop.yml @@ -19,11 +19,13 @@ on: paths: - '.github/workflows/build-rsloop.yml' - 'docs/packages/rsloop.yaml' + - 'patches/rsloop/**' push: branches: [main] paths: - '.github/workflows/build-rsloop.yml' - 'docs/packages/rsloop.yaml' + - 'patches/rsloop/**' concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -65,6 +67,17 @@ jobs: ref: v${{ env.RSLOOP_VERSION }} persist-credentials: false + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + # The TLS reader reported its own close()'s socket shutdown as a missing + # peer close_notify, failing test_tls.py at random on the slow runner. + - name: Patch rsloop source + run: git apply -v python-wheels/patches/rsloop/${{ env.RSLOOP_VERSION }}/*.patch + - name: Build wheels uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 with: diff --git a/patches/rsloop/0.1.64/0001-tls-report-a-locally-closed-transport-s-read-side-lo.patch b/patches/rsloop/0.1.64/0001-tls-report-a-locally-closed-transport-s-read-side-lo.patch new file mode 100644 index 000000000000..478ceccf9cc0 --- /dev/null +++ b/patches/rsloop/0.1.64/0001-tls-report-a-locally-closed-transport-s-read-side-lo.patch @@ -0,0 +1,82 @@ +From fecb91d56058e1882a5944d178f253d0b12a3eb2 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat, 24 Oct 2026 00:19:55 +0000 +Subject: [PATCH] tls: report a locally closed transport's read-side loss as + clean + +Upstream-Status: To upstream [not yet submitted] + +close() marks the transport closing, then the TLS writer worker sends +close_notify and shuts the socket down in both directions. That +shutdown(SHUT_RDWR) wakes the TLS reader worker with EOF (or ECONNRESET) +before the peer's own close_notify may have arrived, so rustls reports +"peer closed connection without sending TLS close_notify". The reader +enqueues that as ConnectionLost(Some(err)), and when it beats the +writer's ConnectionLost(None) the error surfaces from wait_closed() of a +stream the caller closed itself. + +The reader already reports ConnectionLost(None) when it notices the +transport is closing before a read; do the same when a read fails after +close() started. asyncio does not report read errors on a transport the +user closed either. + +The window is wide on slow hardware: on the riscv64 runner it failed +tests/test_tls.py::TestTls::test_client_config_cache_reuses_and_invalidates. +On x86_64, delaying the server's close by 20ms in that test's pattern and +pinning to one CPU turns 35/400 connections into this error (8 more into +ECONNRESET); with this patch 0/3200 fail, and the TLS test modules go +from 4/40 failing runs to 0/120. +--- + src/transport/stream/reader.rs | 24 +++++++++++++++++++----- + 1 file changed, 19 insertions(+), 5 deletions(-) + +diff --git a/src/transport/stream/reader.rs b/src/transport/stream/reader.rs +index 6a11af0..e1c937e 100644 +--- a/src/transport/stream/reader.rs ++++ b/src/transport/stream/reader.rs +@@ -303,16 +303,14 @@ pub(super) fn run_tls_reader( + TlsReadOutcome::Continue => continue, + TlsReadOutcome::Eof => {} + TlsReadOutcome::ConnectionLost(err) => { +- core.enqueue_pending_read_event(PendingReadEvent::ConnectionLost(Some(err))); ++ enqueue_tls_read_failure(&core, err); + return; + } + } + + let (fd, pollable) = tls_socket_wait_target(&tls_state); + if let Err(err) = wait_socket_ready(fd, pollable, true, false) { +- core.enqueue_pending_read_event(PendingReadEvent::ConnectionLost(Some( +- err.to_string(), +- ))); ++ enqueue_tls_read_failure(&core, err.to_string()); + return; + } + +@@ -323,9 +321,25 @@ pub(super) fn run_tls_reader( + return; + } + TlsReadOutcome::ConnectionLost(err) => { +- core.enqueue_pending_read_event(PendingReadEvent::ConnectionLost(Some(err))); ++ enqueue_tls_read_failure(&core, err); + return; + } + } + } + } ++ ++/// Report a TLS read failure as connection loss, unless the transport is ++/// already closing. ++/// ++/// `close()` marks the transport closing, then the writer sends close_notify ++/// and shuts the socket down in both directions. That shutdown wakes this ++/// reader with EOF (or ECONNRESET) before the peer's own close_notify may ++/// have arrived, so rustls reports "peer closed connection without sending ++/// TLS close_notify". The loss is self-inflicted: like asyncio, report a ++/// locally closed transport's `connection_lost()` with `None` instead of ++/// letting the race surface as an error from `wait_closed()`. ++#[cfg_attr(feature = "profile", hotpath::measure)] ++fn enqueue_tls_read_failure(core: &Arc, err: String) { ++ let err = if core.is_closing() { None } else { Some(err) }; ++ core.enqueue_pending_read_event(PendingReadEvent::ConnectionLost(err)); ++}