Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions .github/workflows/build-rsloop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# 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'
- '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 }}
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: 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:
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
5 changes: 5 additions & 0 deletions docs/packages/rsloop.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: rsloop
source-code: https://github.com/RustedBytes/rsloop
license: Apache-2.0
versions:
- version: 0.1.64
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
From fecb91d56058e1882a5944d178f253d0b12a3eb2 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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<StreamTransportCore>, err: String) {
+ let err = if core.is_closing() { None } else { Some(err) };
+ core.enqueue_pending_read_event(PendingReadEvent::ConnectionLost(err));
+}
Loading