├── .rustfmt.toml ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── test.yml │ ├── benchmarks.yml │ ├── build.yml │ └── release.yml ├── benchmarks ├── results │ └── .gitignore ├── templates │ ├── _vs_table.tpl │ ├── _vs_table_overw.tpl │ ├── pyver.md │ └── main.md ├── pyver.md ├── benchmarks.py ├── server.py ├── README.md └── client.py ├── .cargo └── config.toml ├── .gitignore ├── tests ├── conftest.py ├── tcp │ ├── __init__.py │ ├── test_tcp_conn.py │ └── test_tcp_server.py ├── test_sockets.py ├── test_handles.py └── udp │ └── test_udp.py ├── src ├── utils.rs ├── time.rs ├── lib.rs ├── log.rs ├── sock.rs ├── io.rs ├── py.rs ├── server.rs ├── handles.rs ├── udp.rs └── tcp.rs ├── rloop ├── __init__.py ├── _compat.py ├── futures.py ├── _rloop.pyi ├── server.py ├── transports.py ├── exc.py ├── utils.py ├── subprocess.py └── loop.py ├── Cargo.toml ├── README.md ├── LICENSE ├── Makefile ├── pyproject.toml └── Cargo.lock /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [gi0baro] 2 | -------------------------------------------------------------------------------- /benchmarks/results/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["--cfg", "pyo3_disable_reference_pool"] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | __pycache__ 4 | 5 | .idea/ 6 | *.sublime-* 7 | .venv* 8 | .vscode 9 | 10 | rloop/*.so 11 | target/* 12 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import rloop 4 | 5 | 6 | @pytest.fixture(scope='function') 7 | def loop(): 8 | return rloop.new_event_loop() 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | target-branch: "master" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | - package-ecosystem: "cargo" 9 | target-branch: "master" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | macro_rules! syscall { 2 | ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{ 3 | let res = unsafe { libc::$fn($($arg, )*) }; 4 | if res < 0 { 5 | Err(std::io::Error::last_os_error()) 6 | } else { 7 | Ok(res) 8 | } 9 | }}; 10 | } 11 | 12 | pub(crate) use syscall; 13 | -------------------------------------------------------------------------------- /rloop/__init__.py: -------------------------------------------------------------------------------- 1 | from ._compat import _BaseEventLoopPolicy as __BasePolicy 2 | from ._rloop import __version__ as __version__ 3 | from .loop import RLoop 4 | 5 | 6 | def new_event_loop() -> RLoop: 7 | return RLoop() 8 | 9 | 10 | class EventLoopPolicy(__BasePolicy): 11 | def _loop_factory(self) -> RLoop: 12 | return new_event_loop() 13 | -------------------------------------------------------------------------------- /rloop/_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | _PYV = int(sys.version_info.major * 100 + sys.version_info.minor) 5 | _PY_311 = 311 6 | _PY_314 = 314 7 | 8 | if _PYV < 314: 9 | from asyncio.events import BaseDefaultEventLoopPolicy as _BaseEventLoopPolicy 10 | else: 11 | from asyncio.events import _BaseDefaultEventLoopPolicy as _BaseEventLoopPolicy # noqa 12 | -------------------------------------------------------------------------------- /benchmarks/templates/_vs_table.tpl: -------------------------------------------------------------------------------- 1 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 2 | | --- | --- | --- | --- | --- | --- | 3 | {{ dcmp = _data["rloop"][_ckey][_dkey]["rps"] }} 4 | {{ for lkey, bdata in _data.items(): }} 5 | {{ lbdata = bdata[_ckey][_dkey] }} 6 | | {{ =lkey }} | {{ =lbdata["messages"] }} | {{ =lbdata["rps"] }} ({{ =round(lbdata["rps"] / dcmp * 100, 1) }}%) | {{ =f"{lbdata['latency_mean']}ms" }} | {{ =f"{lbdata['latency_percentiles'][-1][1]}ms" }} | {{ =lbdata["latency_std"] }} | 7 | {{ pass }} 8 | -------------------------------------------------------------------------------- /benchmarks/templates/_vs_table_overw.tpl: -------------------------------------------------------------------------------- 1 | {{ rdata = {} }} 2 | {{ for lkey, bdata in _data.items(): }} 3 | {{ rdata[lkey] = {} }} 4 | {{ for mkey, mdata in bdata[_ckey].items(): }} 5 | {{ rdata[lkey][mkey] = mdata["rps"] }} 6 | {{ pass }} 7 | {{ pass }} 8 | 9 | | Loop | Throughput (1KB) | Throughput (10KB) | Throughput (100KB) | 10 | | --- | --- | --- | --- | 11 | {{ for lkey, mdata in rdata.items(): }} 12 | | {{ =lkey }} | {{ for mkey in mdata.keys(): }}{{ =mdata[mkey] }} ({{ =round(mdata[mkey] / rdata["rloop"][mkey] * 100, 1) }}%) | {{ pass }} 13 | {{ pass }} 14 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | branches: 7 | - master 8 | 9 | env: 10 | UV_PYTHON: 3.13 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: astral-sh/setup-uv@v6 19 | with: 20 | enable-cache: false 21 | - name: Install 22 | run: | 23 | uv python install ${{ env.UV_PYTHON }} 24 | uv venv .venv 25 | uv sync --group lint 26 | - name: Lint 27 | run: | 28 | source .venv/bin/activate 29 | make lint 30 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use crate::handles::BoxedHandle; 4 | 5 | pub(crate) struct Timer { 6 | pub handle: BoxedHandle, 7 | pub when: u128, 8 | } 9 | 10 | impl PartialEq for Timer { 11 | fn eq(&self, _other: &Self) -> bool { 12 | false 13 | } 14 | } 15 | 16 | impl Eq for Timer {} 17 | 18 | impl PartialOrd for Timer { 19 | fn partial_cmp(&self, other: &Self) -> Option { 20 | Some(self.cmp(other)) 21 | } 22 | } 23 | 24 | impl Ord for Timer { 25 | fn cmp(&self, other: &Self) -> Ordering { 26 | if self.when < other.when { 27 | return Ordering::Greater; 28 | } 29 | if self.when > other.when { 30 | return Ordering::Less; 31 | } 32 | Ordering::Equal 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use std::sync::OnceLock; 3 | 4 | pub mod event_loop; 5 | pub mod handles; 6 | mod io; 7 | mod log; 8 | mod py; 9 | mod server; 10 | mod sock; 11 | mod tcp; 12 | mod time; 13 | mod udp; 14 | mod utils; 15 | 16 | pub(crate) fn get_lib_version() -> &'static str { 17 | static LIB_VERSION: OnceLock = OnceLock::new(); 18 | 19 | LIB_VERSION.get_or_init(|| { 20 | let version = env!("CARGO_PKG_VERSION"); 21 | version.replace("-alpha", "a").replace("-beta", "b") 22 | }) 23 | } 24 | 25 | #[pymodule(gil_used = false)] 26 | fn _rloop(_py: Python, module: &Bound) -> PyResult<()> { 27 | module.add("__version__", get_lib_version())?; 28 | 29 | event_loop::init_pymodule(module)?; 30 | handles::init_pymodule(module)?; 31 | server::init_pymodule(module)?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /rloop/futures.py: -------------------------------------------------------------------------------- 1 | from asyncio.futures import Future as _Future 2 | 3 | 4 | class _SyncSockReaderFuture(_Future): 5 | def __init__(self, sock, loop): 6 | super().__init__(loop=loop) 7 | self.__sock = sock 8 | 9 | def cancel(self, msg=None) -> bool: 10 | if self.__sock is not None and self.__sock.fileno() != -1: 11 | self._loop.remove_reader(self.__sock) 12 | self.__sock = None 13 | return super().cancel(msg) 14 | 15 | 16 | class _SyncSockWriterFuture(_Future): 17 | def __init__(self, sock, loop): 18 | super().__init__(loop=loop) 19 | self.__sock = sock 20 | 21 | def cancel(self, msg=None) -> bool: 22 | if self.__sock is not None and self.__sock.fileno() != -1: 23 | self._loop.remove_writer(self.__sock) 24 | self.__sock = None 25 | return super().cancel(msg) 26 | -------------------------------------------------------------------------------- /tests/tcp/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | class BaseProto(asyncio.Protocol): 5 | _done = None 6 | 7 | def __init__(self, create_future=None): 8 | self.state = 'INITIAL' 9 | self.transport = None 10 | self.data = b'' 11 | if create_future: 12 | self._done = create_future() 13 | 14 | def _assert_state(self, *expected): 15 | if self.state not in expected: 16 | raise AssertionError(f'state: {self.state!r}, expected: {expected!r}') 17 | 18 | def connection_made(self, transport): 19 | self.transport = transport 20 | self._assert_state('INITIAL') 21 | self.state = 'CONNECTED' 22 | 23 | def data_received(self, data): 24 | self._assert_state('CONNECTED') 25 | 26 | def eof_received(self): 27 | self._assert_state('CONNECTED') 28 | self.state = 'EOF' 29 | 30 | def connection_lost(self, exc): 31 | self._assert_state('CONNECTED', 'EOF') 32 | self.transport = None 33 | self.state = 'CLOSED' 34 | if self._done: 35 | self._done.set_result(None) 36 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rloop" 3 | version = "0.2.0" 4 | description = "An asyncio event loop implemented in Rust" 5 | authors = ["Giovanni Barillari "] 6 | license = "BSD-3-Clause" 7 | edition = "2024" 8 | 9 | keywords = ["asyncio"] 10 | 11 | readme = "README.md" 12 | homepage = "https://github.com/gi0baro/rloop" 13 | repository = "https://github.com/gi0baro/rloop" 14 | 15 | include = [ 16 | "/Cargo.toml", 17 | "/pyproject.toml", 18 | "/LICENSE", 19 | "/README.md", 20 | "/src", 21 | "/rloop", 22 | "/tests", 23 | "!__pycache__", 24 | "!tests/.pytest_cache", 25 | "!*.so", 26 | ] 27 | 28 | [lib] 29 | name = "_rloop" 30 | crate-type = ["cdylib", "rlib"] 31 | 32 | [dependencies] 33 | anyhow = "=1.0" 34 | mio = { version = "=1.0", features = ["net", "os-ext", "os-poll"] } 35 | papaya = "=0.2" 36 | pyo3 = { version = "=0.26", features = ["anyhow", "extension-module", "generate-import-lib"] } 37 | socket2 = { version = "=0.6", features = ["all"] } 38 | 39 | [target.'cfg(unix)'.dependencies] 40 | libc = "0.2.159" 41 | 42 | [build-dependencies] 43 | pyo3-build-config = "=0.26" 44 | 45 | [profile.release] 46 | codegen-units = 1 47 | debug = false 48 | incremental = false 49 | lto = "fat" 50 | opt-level = 3 51 | panic = "abort" 52 | strip = true 53 | -------------------------------------------------------------------------------- /tests/tcp/test_tcp_conn.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from . import BaseProto 4 | 5 | 6 | class ProtoServer(BaseProto): 7 | def data_received(self, data): 8 | super().data_received(data) 9 | self.data += data 10 | self.transport.write(b'hello back') 11 | self.transport.write_eof() 12 | 13 | 14 | class ProtoClient(BaseProto): 15 | def connection_made(self, transport): 16 | super().connection_made(transport) 17 | transport.write(b'hello') 18 | transport.write_eof() 19 | 20 | def data_received(self, data): 21 | super().data_received(data) 22 | self.data += data 23 | 24 | 25 | def test_tcp_connection_send(loop): 26 | proto_srv = ProtoServer() 27 | proto_cli = ProtoClient(loop.create_future) 28 | 29 | async def main(): 30 | sock = socket.socket() 31 | sock.setblocking(False) 32 | 33 | with sock: 34 | sock.bind(('127.0.0.1', 0)) 35 | addr = sock.getsockname() 36 | srv = await loop.create_server(lambda: proto_srv, sock=sock) 37 | _ = await loop.create_connection(lambda: proto_cli, *addr) 38 | await proto_cli._done 39 | srv.close() 40 | 41 | loop.run_until_complete(main()) 42 | assert proto_cli.state == 'CLOSED' 43 | assert proto_srv.state == 'CLOSED' 44 | assert proto_srv.data == b'hello' 45 | assert proto_cli.data == b'hello back' 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RLoop 2 | 3 | RLoop is an [AsyncIO](https://docs.python.org/3/library/asyncio.html) selector event loop implemented in Rust on top of the [mio crate](https://github.com/tokio-rs/mio). 4 | 5 | > **Warning**: RLoop is currently a work in progress and definitely not suited for *production usage*. 6 | 7 | > **Note:** RLoop is available on Unix systems only. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | pip install rloop 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```python 18 | import asyncio 19 | import rloop 20 | 21 | asyncio.set_event_loop_policy(rloop.EventLoopPolicy()) 22 | loop = asyncio.new_event_loop() 23 | asyncio.set_event_loop(loop) 24 | ``` 25 | 26 | ## Differences from stdlib 27 | 28 | At current time, when compared with the stdlib's event loop, RLoop doesn't support the following features: 29 | 30 | - Unix Domain Sockets 31 | - SSL 32 | - debugging 33 | 34 | RLoop also doesn't implement the following methods: 35 | 36 | - `loop.sendfile` 37 | - `loop.connect_accepted_socket` 38 | - `loop.sock_recvfrom` 39 | - `loop.sock_recvfrom_into` 40 | - `loop.sock_sendto` 41 | - `loop.sock_sendfile` 42 | 43 | ### `call_later` with negative delays 44 | 45 | While the stdlib's event loop will use the actual delay of callbacks when `call_later` is used with negative numbers, RLoop will treat those as `call_soon`, and thus the effective order will follow the invocation order, not the delay. 46 | 47 | ## License 48 | 49 | RLoop is released under the BSD License. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Giovanni Barillari 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := all 2 | pysources = rloop tests 3 | 4 | .PHONY: build-dev 5 | build-dev: 6 | @rm -f rloop/*.so 7 | uv sync --group all 8 | maturin develop --uv 9 | 10 | .PHONY: format 11 | format: 12 | ruff check --fix $(pysources) 13 | ruff format $(pysources) 14 | cargo fmt 15 | 16 | .PHONY: lint-python 17 | lint-python: 18 | ruff check $(pysources) 19 | ruff format --check $(pysources) 20 | 21 | .PHONY: lint-rust 22 | lint-rust: 23 | cargo fmt --version 24 | cargo fmt --all -- --check 25 | cargo clippy --version 26 | cargo clippy --tests -- \ 27 | -D warnings \ 28 | -W clippy::pedantic \ 29 | -W clippy::dbg_macro \ 30 | -A clippy::blocks_in_conditions \ 31 | -A clippy::cast-possible-truncation \ 32 | -A clippy::cast-sign-loss \ 33 | -A clippy::declare-interior-mutable-const \ 34 | -A clippy::inline-always \ 35 | -A clippy::match-bool \ 36 | -A clippy::match-same-arms \ 37 | -A clippy::module-name-repetitions \ 38 | -A clippy::needless-pass-by-value \ 39 | -A clippy::no-effect-underscore-binding \ 40 | -A clippy::similar-names \ 41 | -A clippy::single-match-else \ 42 | -A clippy::too-many-arguments \ 43 | -A clippy::too-many-lines \ 44 | -A clippy::type-complexity \ 45 | -A clippy::unused-self \ 46 | -A clippy::upper-case-acronyms \ 47 | -A clippy::used-underscore-binding \ 48 | -A clippy::used-underscore-items \ 49 | -A clippy::wrong-self-convention 50 | 51 | .PHONY: lint 52 | lint: lint-python lint-rust 53 | 54 | .PHONY: test 55 | test: 56 | pytest -v tests 57 | 58 | .PHONY: all 59 | all: format build-dev lint test 60 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | branches: 7 | - master 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | linux: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: 19 | - '3.9' 20 | - '3.10' 21 | - '3.11' 22 | - '3.12' 23 | - '3.13' 24 | - '3.13t' 25 | - '3.14' 26 | - '3.14t' 27 | 28 | env: 29 | UV_PYTHON: ${{ matrix.python-version }} 30 | steps: 31 | - uses: actions/checkout@v5 32 | - uses: astral-sh/setup-uv@v6 33 | with: 34 | enable-cache: false 35 | - name: Install 36 | run: | 37 | uv python install ${{ env.UV_PYTHON }} 38 | uv venv .venv 39 | uv sync --group build --group test 40 | uv run --no-sync maturin develop --uv 41 | - name: Test 42 | run: | 43 | source .venv/bin/activate 44 | make test 45 | 46 | macos: 47 | runs-on: macos-latest 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | python-version: 52 | - '3.9' 53 | - '3.10' 54 | - '3.11' 55 | - '3.12' 56 | - '3.13' 57 | - '3.13t' 58 | - '3.14' 59 | - '3.14t' 60 | 61 | env: 62 | UV_PYTHON: ${{ matrix.python-version }} 63 | steps: 64 | - uses: actions/checkout@v5 65 | - uses: astral-sh/setup-uv@v6 66 | with: 67 | enable-cache: false 68 | - name: Install 69 | run: | 70 | uv python install ${{ env.UV_PYTHON }} 71 | uv venv .venv 72 | uv sync --group build --group test 73 | uv run --no-sync maturin develop --uv --extras=test 74 | - name: Test 75 | run: | 76 | source .venv/bin/activate 77 | make test 78 | -------------------------------------------------------------------------------- /benchmarks/templates/pyver.md: -------------------------------------------------------------------------------- 1 | # RLoop benchmarks 2 | 3 | ## Python versions 4 | 5 | {{ _common_data = globals().get(f"data{pyvb}") }} 6 | Run at: {{ =datetime.datetime.fromtimestamp(_common_data.run_at).strftime('%a %d %b %Y, %H:%M') }} 7 | Environment: {{ =benv }} (CPUs: {{ =_common_data.cpu }}) 8 | RLoop version: {{ =_common_data.rloop }} 9 | 10 | Comparison between different Python versions. 11 | The only test performed is the raw socket one. 12 | 13 | {{ for mkey, label in [("1024", "1KB"), ("10240", "10KB"), ("102400", "100KB")]: }} 14 | 15 | ### {{ =label }} 16 | 17 | | Python version | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 18 | | --- | --- | --- | --- | --- | --- | 19 | {{ for pykey in ["310", "311", "312", "313"]: }} 20 | {{ _data = globals().get(f"data{pykey}") }} 21 | {{ if not _data: }} 22 | {{ continue }} 23 | {{ bdata = _data.results["raw"]["rloop"]["1"][mkey] }} 24 | | {{ =_data.pyver }} | {{ =bdata["messages"] }} | {{ =bdata["rps"] }} | {{ =f"{bdata['latency_mean']}ms" }} | {{ =f"{bdata['latency_percentiles'][-2][1]}ms" }} | {{ =bdata["latency_std"] }} | 25 | {{ pass }} 26 | 27 | {{ pass }} 28 | 29 | ### 10KB VS other 30 | 31 | | Python version | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 32 | | --- | --- | --- | --- | --- | --- | --- | 33 | {{ for pykey in ["310", "311", "312", "313"]: }} 34 | {{ _data = globals().get(f"data{pykey}") }} 35 | {{ if not _data: }} 36 | {{ continue }} 37 | {{ dcmp = _data.results["raw"]["rloop"]["1"]["10240"]["rps"] }} 38 | {{ for lkey, bdata in _data.results["raw"].items(): }} 39 | {{ lbdata = bdata["1"]["10240"] }} 40 | | {{ =_data.pyver }} | {{ =lkey }} | {{ =lbdata["messages"] }} | {{ =lbdata["rps"] }} ({{ =round(lbdata["rps"] / dcmp * 100, 1) }}%) | {{ =f"{lbdata['latency_mean']}ms" }} | {{ =f"{lbdata['latency_percentiles'][-2][1]}ms" }} | {{ =lbdata["latency_std"] }} | 41 | {{ pass }} 42 | {{ pass }} 43 | -------------------------------------------------------------------------------- /tests/test_sockets.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | 4 | _SIZE = 1024 * 1024 5 | 6 | 7 | async def _recv_all(loop, sock, nbytes): 8 | buf = b'' 9 | while len(buf) < nbytes: 10 | buf += await loop.sock_recv(sock, nbytes - len(buf)) 11 | return buf 12 | 13 | 14 | def test_socket_accept_recv(loop): 15 | async def server(): 16 | sock = socket.socket() 17 | sock.setblocking(False) 18 | 19 | with sock: 20 | sock.bind(('127.0.0.1', 0)) 21 | sock.listen() 22 | 23 | fut = loop.run_in_executor(None, client, sock.getsockname()) 24 | 25 | client_sock, _ = await loop.sock_accept(sock) 26 | with client_sock: 27 | data = await _recv_all(loop, client_sock, _SIZE) 28 | 29 | await fut 30 | 31 | return data 32 | 33 | def client(addr): 34 | sock = socket.socket() 35 | with sock: 36 | sock.connect(addr) 37 | sock.sendall(b'a' * _SIZE) 38 | 39 | data = loop.run_until_complete(server()) 40 | assert data == b'a' * _SIZE 41 | 42 | 43 | def test_socket_accept_send(loop): 44 | state = {'data': b''} 45 | 46 | async def server(): 47 | sock = socket.socket() 48 | sock.setblocking(False) 49 | 50 | with sock: 51 | sock.bind(('127.0.0.1', 0)) 52 | sock.listen() 53 | 54 | fut = loop.run_in_executor(None, client, sock.getsockname()) 55 | 56 | client_sock, _ = await loop.sock_accept(sock) 57 | with client_sock: 58 | await loop.sock_sendall(client_sock, b'a' * _SIZE) 59 | 60 | await fut 61 | 62 | def client(addr): 63 | sock = socket.socket() 64 | with sock: 65 | sock.connect(addr) 66 | while len(state['data']) < _SIZE: 67 | state['data'] += sock.recv(1024 * 16) 68 | 69 | loop.run_until_complete(server()) 70 | assert state['data'] == b'a' * _SIZE 71 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{prelude::*, types::PyDict}; 2 | 3 | pub(crate) enum LogExc { 4 | CBHandle(LogExcCBHandleData), 5 | Transport(LogExcTransportData), 6 | } 7 | 8 | impl LogExc { 9 | pub(crate) fn cb_handle(exc: PyErr, msg: String, handle: Py) -> Self { 10 | Self::CBHandle(LogExcCBHandleData { 11 | base: LogExcBaseData { exc, msg }, 12 | handle, 13 | }) 14 | } 15 | 16 | pub(crate) fn transport(exc: PyErr, msg: String, protocol: Py, transport: Py) -> Self { 17 | Self::Transport(LogExcTransportData { 18 | base: LogExcBaseData { exc, msg }, 19 | protocol, 20 | transport, 21 | }) 22 | } 23 | } 24 | 25 | struct LogExcBaseData { 26 | exc: PyErr, 27 | msg: String, 28 | } 29 | 30 | pub(crate) struct LogExcCBHandleData { 31 | base: LogExcBaseData, 32 | handle: Py, 33 | } 34 | 35 | pub(crate) struct LogExcTransportData { 36 | base: LogExcBaseData, 37 | protocol: Py, 38 | transport: Py, 39 | } 40 | 41 | macro_rules! log_exc_base_data_to_dict { 42 | ($py:expr, $dict:expr, $data:expr) => { 43 | let _ = $dict.set_item(pyo3::intern!($py, "exception"), $data.exc); 44 | let _ = $dict.set_item(pyo3::intern!($py, "message"), $data.msg); 45 | }; 46 | } 47 | 48 | pub(crate) fn log_exc_to_py_ctx(py: Python, exc: LogExc) -> Py { 49 | let dict = PyDict::new(py); 50 | match exc { 51 | LogExc::CBHandle(data) => { 52 | log_exc_base_data_to_dict!(py, dict, data.base); 53 | let _ = dict.set_item(pyo3::intern!(py, "handle"), data.handle); 54 | } 55 | LogExc::Transport(data) => { 56 | log_exc_base_data_to_dict!(py, dict, data.base); 57 | let _ = dict.set_item(pyo3::intern!(py, "protocol"), data.protocol); 58 | let _ = dict.set_item(pyo3::intern!(py, "transport"), data.transport); 59 | } 60 | } 61 | 62 | dict.unbind() 63 | } 64 | -------------------------------------------------------------------------------- /benchmarks/templates/main.md: -------------------------------------------------------------------------------- 1 | # RLoop benchmarks 2 | 3 | Run at: {{ =datetime.datetime.fromtimestamp(data.run_at).strftime('%a %d %b %Y, %H:%M') }} 4 | Environment: {{ =benv }} (CPUs: {{ =data.cpu }}) 5 | Python version: {{ =data.pyver }} 6 | RLoop version: {{ =data.rloop }} 7 | 8 | ### Raw sockets 9 | 10 | TCP echo server with raw sockets comparison using 1KB, 10KB and 100KB messages. 11 | 12 | {{ _data = data.results["raw"] }} 13 | {{ _ckey = "1" }} 14 | {{ include "./_vs_table_overw.tpl" }} 15 | 16 | #### 1KB details 17 | 18 | {{ _dkey, _ckey = "1024", "1" }} 19 | {{ include "./_vs_table.tpl" }} 20 | 21 | #### 10KB details 22 | 23 | {{ _dkey, _ckey = "10240", "1" }} 24 | {{ include "./_vs_table.tpl" }} 25 | 26 | #### 100KB details 27 | 28 | {{ _dkey, _ckey = "102400", "1" }} 29 | {{ include "./_vs_table.tpl" }} 30 | 31 | ### Streams 32 | 33 | TCP echo server with `asyncio` streams comparison using 1KB, 10KB and 100KB messages. 34 | 35 | {{ _data = data.results["stream"] }} 36 | {{ _ckey = "1" }} 37 | {{ include "./_vs_table_overw.tpl" }} 38 | 39 | #### 1KB details 40 | 41 | {{ _dkey, _ckey = "1024", "1" }} 42 | {{ include "./_vs_table.tpl" }} 43 | 44 | #### 10KB details 45 | 46 | {{ _dkey, _ckey = "10240", "1" }} 47 | {{ include "./_vs_table.tpl" }} 48 | 49 | #### 100KB details 50 | 51 | {{ _dkey, _ckey = "102400", "1" }} 52 | {{ include "./_vs_table.tpl" }} 53 | 54 | ### Protocol 55 | 56 | TCP echo server with `asyncio.Protocol` comparison using 1KB, 10KB and 100KB messages. 57 | 58 | {{ _data = data.results["proto"] }} 59 | {{ _ckey = "1" }} 60 | {{ include "./_vs_table_overw.tpl" }} 61 | 62 | #### 1KB details 63 | 64 | {{ _dkey, _ckey = "1024", "1" }} 65 | {{ include "./_vs_table.tpl" }} 66 | 67 | #### 10KB details 68 | 69 | {{ _dkey, _ckey = "10240", "1" }} 70 | {{ include "./_vs_table.tpl" }} 71 | 72 | #### 100KB details 73 | 74 | {{ _dkey, _ckey = "102400", "1" }} 75 | {{ include "./_vs_table.tpl" }} 76 | 77 | ### Other benchmarks 78 | 79 | - [Python versions](./pyver.md) 80 | -------------------------------------------------------------------------------- /rloop/_rloop.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Tuple, TypeVar 2 | from weakref import WeakSet 3 | 4 | __version__: str 5 | 6 | T = TypeVar('T') 7 | 8 | class CBHandle: 9 | def cancel(self): ... 10 | def cancelled(self) -> bool: ... 11 | 12 | class TimerHandle: 13 | when: int 14 | 15 | def cancel(self): ... 16 | def cancelled(self) -> bool: ... 17 | 18 | class EventLoop: 19 | _asyncgens: WeakSet 20 | _asyncgens_shutdown_called: bool 21 | _base_ctx: Any 22 | _clock: int 23 | _closed: bool 24 | _default_executor: Any 25 | _executor_shutdown_called: bool 26 | _exc_handler: Any 27 | _exception_handler: Any 28 | _sig_listening: bool 29 | _sig_loop_handled: bool 30 | _sig_wfd: Any 31 | _signals: set 32 | _ssock_r: Any 33 | _ssock_w: Any 34 | _stopping: bool 35 | _task_factory: Any 36 | _thread_id: int 37 | _watcher_child: Any 38 | 39 | def _run(self): ... 40 | def _call_later(self, delay, callback, args, context) -> TimerHandle: ... 41 | def _sig_add(self, sig, callback, context): ... 42 | def _sig_rem(self, sig) -> bool: ... 43 | def _sig_clear(self): ... 44 | def _ssock_set(self, fd): ... 45 | def _ssock_del(self, fd): ... 46 | def _tcp_conn(self, sock, protocol_factory: Callable[[], T]) -> Tuple[Any, T]: ... 47 | def _tcp_server(self, socks, rsocks, protocol_factory, backlog) -> Server: ... 48 | def _tcp_stream_bound(self, fd) -> bool: ... 49 | def add_reader(self, fd, callback, *args) -> CBHandle: ... 50 | def add_writer(self, fd, callback, *args) -> CBHandle: ... 51 | def call_soon(self, callback, *args, context=None) -> CBHandle: ... 52 | def call_soon_threadsafe(self, callback, *args, context=None) -> CBHandle: ... 53 | def remove_reader(self, fd) -> bool: ... 54 | def remove_writer(self, fd) -> bool: ... 55 | 56 | class Server: 57 | _loop: Any 58 | _sockets: list[int] 59 | 60 | def _close(self): ... 61 | def _is_serving(self) -> bool: ... 62 | def _start_serving(self): ... 63 | def _streams_abort(self): ... 64 | def _streams_close(self): ... 65 | -------------------------------------------------------------------------------- /tests/test_handles.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | 4 | def run_loop(loop): 5 | async def run(): 6 | loop.stop() 7 | 8 | loop.run_until_complete(run()) 9 | 10 | 11 | def test_call_soon(loop): 12 | calls = [] 13 | 14 | def cb(arg): 15 | calls.append(arg) 16 | 17 | loop.call_soon(cb, 1) 18 | loop.call_soon(cb, 2) 19 | run_loop(loop) 20 | assert calls == [1, 2] 21 | 22 | 23 | def test_call_later(loop): 24 | calls = [] 25 | 26 | def cb(arg): 27 | calls.append(arg) 28 | 29 | def stop(): 30 | loop.stop() 31 | 32 | loop.call_later(0.001, cb, 2) 33 | loop.call_later(0.1, stop) 34 | loop.call_soon(cb, 1) 35 | loop.run_forever() 36 | assert calls == [1, 2] 37 | 38 | 39 | def test_call_later_negative(loop): 40 | calls = [] 41 | 42 | def cb(arg): 43 | calls.append(arg) 44 | 45 | loop.call_later(-1.0, cb, 1) 46 | loop.call_later(-2.0, cb, 2) 47 | run_loop(loop) 48 | assert calls == [1, 2] 49 | 50 | 51 | def test_call_at(loop): 52 | def cb(): 53 | loop.stop() 54 | 55 | delay = 0.100 56 | when = loop.time() + delay 57 | loop.call_at(when, cb) 58 | t0 = loop.time() 59 | loop.run_forever() 60 | dt = loop.time() - t0 61 | 62 | assert dt >= delay 63 | 64 | 65 | def test_call_soon_threadsafe(loop): 66 | calls = [] 67 | 68 | def cb(arg): 69 | calls.append(arg) 70 | 71 | def wake(cond): 72 | with cond: 73 | cond.notify_all() 74 | 75 | def stop(): 76 | loop.stop() 77 | 78 | def trun(cond1, cond2, loop, cb): 79 | with cond1: 80 | cond1.wait() 81 | loop.call_soon_threadsafe(cb, 2) 82 | with cond2: 83 | cond2.wait() 84 | loop.call_soon_threadsafe(cb, 4) 85 | 86 | cond1 = threading.Condition() 87 | cond2 = threading.Condition() 88 | t = threading.Thread(target=trun, args=(cond1, cond2, loop, cb)) 89 | t.start() 90 | 91 | loop.call_soon(cb, 1) 92 | loop.call_soon(wake, cond1) 93 | loop.call_later(0.5, cb, 3) 94 | loop.call_later(0.6, wake, cond2) 95 | loop.call_later(1.0, stop) 96 | loop.run_forever() 97 | 98 | assert calls == [1, 2, 3, 4] 99 | -------------------------------------------------------------------------------- /src/sock.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use std::os::raw::c_int; 3 | 4 | use crate::py::sock; 5 | 6 | #[pyclass(frozen, module = "rloop._rloop")] 7 | pub(crate) struct SocketWrapper { 8 | sock: Py, 9 | } 10 | 11 | impl SocketWrapper { 12 | pub fn from_fd(py: Python, fd: usize, family: i32, r#type: socket2::Type, proto: usize) -> Py { 13 | Py::new( 14 | py, 15 | Self { 16 | sock: sock(py) 17 | .unwrap() 18 | .call1::<(i32, c_int, usize, usize)>((family, r#type.into(), proto, fd)) 19 | .unwrap() 20 | .unbind(), 21 | }, 22 | ) 23 | .unwrap() 24 | } 25 | } 26 | 27 | #[pymethods] 28 | impl SocketWrapper { 29 | fn __getattr__(&self, py: Python, name: &str) -> PyResult> { 30 | self.sock.getattr(py, name) 31 | } 32 | } 33 | 34 | // #[pyclass(frozen)] 35 | // pub(crate) struct PseudoSocket { 36 | // fd: usize, 37 | // #[pyo3(get)] 38 | // family: i32, 39 | // stype: socket2::Type, 40 | // #[pyo3(get)] 41 | // proto: usize, 42 | // } 43 | 44 | // impl PseudoSocket { 45 | // pub(crate) fn tcp_stream(fd: usize, family: i32, proto: usize) -> Self { 46 | // Self { 47 | // fd, 48 | // family, 49 | // stype: socket2::Type::STREAM, 50 | // proto, 51 | // } 52 | // } 53 | // } 54 | 55 | // #[pymethods] 56 | // impl PseudoSocket { 57 | // fn r#type(&self) -> c_int { 58 | // self.stype.into() 59 | // } 60 | 61 | // fn getsockname<'p>(&self, py: Python<'p>) -> PyResult> { 62 | // let pysock = sock(py)?.call1((self.family, self.r#type(), self.proto, self.fd))?; 63 | // let ret = pysock.call_method0(pyo3::intern!(py, "getsockname")); 64 | // pysock.call_method0(pyo3::intern!(py, "detach"))?; 65 | // ret 66 | // } 67 | 68 | // fn getpeername<'p>(&self, py: Python<'p>) -> PyResult> { 69 | // let pysock = sock(py)?.call1((self.family, self.r#type(), self.proto, self.fd))?; 70 | // let ret = pysock.call_method0(pyo3::intern!(py, "getpeername")); 71 | // pysock.call_method0(pyo3::intern!(py, "detach"))?; 72 | // ret 73 | // } 74 | // } 75 | -------------------------------------------------------------------------------- /tests/tcp/test_tcp_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import errno 3 | import socket 4 | 5 | import pytest 6 | 7 | from rloop.utils import _HAS_IPv6 8 | 9 | from . import BaseProto 10 | 11 | 12 | _SIZE = 1024 * 1000 13 | 14 | 15 | class EchoProtocol(BaseProto): 16 | def data_received(self, data): 17 | super().data_received(data) 18 | self.transport.write(data) 19 | 20 | 21 | @pytest.mark.skipif(not hasattr(socket, 'SOCK_NONBLOCK'), reason='no socket.SOCK_NONBLOCK') 22 | def test_create_server_stream_bittype(loop): 23 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM | socket.SOCK_NONBLOCK) 24 | with sock: 25 | coro = loop.create_server(lambda: None, sock=sock) 26 | srv = loop.run_until_complete(coro) 27 | srv.close() 28 | loop.run_until_complete(srv.wait_closed()) 29 | 30 | 31 | @pytest.mark.skipif(not _HAS_IPv6, reason='no IPv6') 32 | def test_create_server_ipv6(loop): 33 | async def main(): 34 | srv = await asyncio.start_server(lambda: None, '::1', 0) 35 | try: 36 | assert len(srv.sockets) > 0 37 | finally: 38 | srv.close() 39 | await srv.wait_closed() 40 | 41 | try: 42 | loop.run_until_complete(main()) 43 | except OSError as ex: 44 | if hasattr(errno, 'EADDRNOTAVAIL') and ex.errno == errno.EADDRNOTAVAIL: 45 | pass 46 | else: 47 | raise 48 | 49 | 50 | def test_tcp_server_recv_send(loop): 51 | msg = b'a' * _SIZE 52 | state = {'data': b''} 53 | proto = EchoProtocol() 54 | 55 | async def main(): 56 | sock = socket.socket() 57 | sock.setblocking(False) 58 | 59 | with sock: 60 | sock.bind(('127.0.0.1', 0)) 61 | addr = sock.getsockname() 62 | srv = await loop.create_server(lambda: proto, sock=sock) 63 | fut = loop.run_in_executor(None, client, addr) 64 | await fut 65 | srv.close() 66 | 67 | def client(addr): 68 | sock = socket.socket() 69 | with sock: 70 | sock.connect(addr) 71 | sock.sendall(msg) 72 | while len(state['data']) < _SIZE: 73 | state['data'] += sock.recv(1024 * 16) 74 | 75 | loop.run_until_complete(main()) 76 | assert proto.state == 'CLOSED' 77 | assert state['data'] == msg 78 | 79 | 80 | # TODO: test buffered proto 81 | -------------------------------------------------------------------------------- /rloop/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import asyncio.trsock 3 | 4 | from ._rloop import Server as _Server 5 | 6 | 7 | class Server: 8 | __slots__ = ['_inner', '_sff'] 9 | 10 | def __init__(self, inner: _Server): 11 | self._inner = inner 12 | self._sff = None 13 | 14 | def get_loop(self): 15 | return self._inner._loop 16 | 17 | def is_serving(self): 18 | return self._inner._is_serving() 19 | 20 | async def start_serving(self): 21 | self._inner._start_serving() 22 | 23 | async def serve_forever(self): 24 | if self._sff is not None: 25 | raise RuntimeError(f'server {self!r} is already being awaited on serve_forever()') 26 | # if self._servers is None: 27 | # raise RuntimeError(f'server {self!r} is closed') 28 | 29 | self._inner._start_serving() 30 | self._sff = self._inner._loop.create_future() 31 | 32 | try: 33 | await self._sff 34 | except asyncio.CancelledError: 35 | try: 36 | self.close() 37 | # await self.wait_closed() 38 | finally: 39 | raise 40 | finally: 41 | self._sff = None 42 | 43 | async def wait_closed(self): 44 | return 45 | 46 | # async def wait_closed(self): 47 | # # if self._servers is None or self._waiters is None: 48 | # # return 49 | # waiter = self._loop.create_future() 50 | # self._add_waiter(waiter) 51 | # # self._waiters.append(waiter) 52 | # await waiter 53 | 54 | def close(self): 55 | # sockets = self._sockets 56 | # if sockets is None: 57 | # return 58 | # self._sockets = None 59 | 60 | # for sock in sockets: 61 | # self._loop._stop_serving(sock) 62 | 63 | # self._serving = False 64 | 65 | self._inner._close() 66 | 67 | if self._sff is not None and not self._sff.done(): 68 | self._sff.cancel() 69 | self._sff = None 70 | 71 | def close_clients(self): 72 | self._inner._streams_close() 73 | 74 | def abort_clients(self): 75 | self._inner._streams_abort() 76 | 77 | async def __aenter__(self): 78 | return self 79 | 80 | async def __aexit__(self, *exc): 81 | self.close() 82 | # await self.wait_closed() 83 | 84 | # TODO 85 | @property 86 | def sockets(self): 87 | return tuple(asyncio.trsock.TransportSocket(s) for s in self._inner._sockets) 88 | -------------------------------------------------------------------------------- /benchmarks/pyver.md: -------------------------------------------------------------------------------- 1 | # RLoop benchmarks 2 | 3 | ## Python versions 4 | 5 | Run at: Wed 20 Aug 2025, 17:49 6 | Environment: GHA Linux x86_64 (CPUs: 4) 7 | RLoop version: 0.1.6 8 | 9 | Comparison between different Python versions. 10 | The only test performed is the raw socket one. 11 | 12 | 13 | ### 1KB 14 | 15 | | Python version | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 16 | | --- | --- | --- | --- | --- | --- | 17 | | 3.10 | 166367 | 16636.7 | 0.057ms | 0.078ms | 0.012 | 18 | | 3.11 | 168212 | 16821.2 | 0.057ms | 0.078ms | 0.01 | 19 | | 3.12 | 160149 | 16014.9 | 0.058ms | 0.08ms | 0.011 | 20 | | 3.13 | 164049 | 16404.9 | 0.057ms | 0.079ms | 0.012 | 21 | 22 | 23 | ### 10KB 24 | 25 | | Python version | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 26 | | --- | --- | --- | --- | --- | --- | 27 | | 3.10 | 143889 | 14388.9 | 0.067ms | 0.09ms | 0.013 | 28 | | 3.11 | 145148 | 14514.8 | 0.066ms | 0.089ms | 0.012 | 29 | | 3.12 | 137552 | 13755.2 | 0.07ms | 0.096ms | 0.013 | 30 | | 3.13 | 151036 | 15103.6 | 0.063ms | 0.088ms | 0.012 | 31 | 32 | 33 | ### 100KB 34 | 35 | | Python version | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 36 | | --- | --- | --- | --- | --- | --- | 37 | | 3.10 | 74686 | 7468.6 | 0.131ms | 0.164ms | 0.031 | 38 | | 3.11 | 74573 | 7457.3 | 0.131ms | 0.171ms | 0.033 | 39 | | 3.12 | 72518 | 7251.8 | 0.134ms | 0.171ms | 0.032 | 40 | | 3.13 | 75040 | 7504.0 | 0.13ms | 0.167ms | 0.033 | 41 | 42 | 43 | ### 10KB VS other 44 | 45 | | Python version | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 46 | | --- | --- | --- | --- | --- | --- | --- | 47 | | 3.10 | asyncio | 142436 | 14243.6 (99.0%) | 0.069ms | 0.101ms | 0.015 | 48 | | 3.10 | rloop | 143889 | 14388.9 (100.0%) | 0.067ms | 0.09ms | 0.013 | 49 | | 3.10 | uvloop | 143446 | 14344.6 (99.7%) | 0.067ms | 0.096ms | 0.015 | 50 | | 3.11 | asyncio | 132500 | 13250.0 (91.3%) | 0.073ms | 0.1ms | 0.016 | 51 | | 3.11 | rloop | 145148 | 14514.8 (100.0%) | 0.066ms | 0.089ms | 0.012 | 52 | | 3.11 | uvloop | 139344 | 13934.4 (96.0%) | 0.069ms | 0.097ms | 0.015 | 53 | | 3.12 | asyncio | 137740 | 13774.0 (100.1%) | 0.07ms | 0.105ms | 0.018 | 54 | | 3.12 | rloop | 137552 | 13755.2 (100.0%) | 0.07ms | 0.096ms | 0.013 | 55 | | 3.12 | uvloop | 136740 | 13674.0 (99.4%) | 0.071ms | 0.102ms | 0.014 | 56 | | 3.13 | asyncio | 124749 | 12474.9 (82.6%) | 0.077ms | 0.108ms | 0.016 | 57 | | 3.13 | rloop | 151036 | 15103.6 (100.0%) | 0.063ms | 0.088ms | 0.012 | 58 | | 3.13 | uvloop | 129997 | 12999.7 (86.1%) | 0.074ms | 0.106ms | 0.016 | 59 | -------------------------------------------------------------------------------- /rloop/transports.py: -------------------------------------------------------------------------------- 1 | from asyncio import transports as _transports 2 | 3 | 4 | class _TransportFlowControl(_transports.Transport): 5 | __slots__ = ('_loop', '_protocol_paused', '_high_water', '_low_water') 6 | 7 | def __init__(self, loop=None): 8 | self._loop = loop 9 | self._protocol_paused = False 10 | self._set_write_buffer_limits() 11 | 12 | def _maybe_pause_protocol(self): 13 | size = self.get_write_buffer_size() 14 | if size <= self._high_water: 15 | return 16 | if not self._protocol_paused: 17 | self._protocol_paused = True 18 | try: 19 | self._protocol.pause_writing() 20 | except (SystemExit, KeyboardInterrupt): 21 | raise 22 | except BaseException as exc: 23 | self._loop.call_exception_handler( 24 | { 25 | 'message': 'protocol.pause_writing() failed', 26 | 'exception': exc, 27 | 'transport': self, 28 | 'protocol': self._protocol, 29 | } 30 | ) 31 | 32 | def _maybe_resume_protocol(self): 33 | if self._protocol_paused and self.get_write_buffer_size() <= self._low_water: 34 | self._protocol_paused = False 35 | try: 36 | self._protocol.resume_writing() 37 | except (SystemExit, KeyboardInterrupt): 38 | raise 39 | except BaseException as exc: 40 | self._loop.call_exception_handler( 41 | { 42 | 'message': 'protocol.resume_writing() failed', 43 | 'exception': exc, 44 | 'transport': self, 45 | 'protocol': self._protocol, 46 | } 47 | ) 48 | 49 | def get_write_buffer_limits(self): 50 | return (self._low_water, self._high_water) 51 | 52 | def _set_write_buffer_limits(self, high=None, low=None): 53 | if high is None: 54 | if low is None: 55 | high = 64 * 1024 56 | else: 57 | high = 4 * low 58 | if low is None: 59 | low = high // 4 60 | 61 | if not high >= low >= 0: 62 | raise ValueError(f'high ({high!r}) must be >= low ({low!r}) must be >= 0') 63 | 64 | self._high_water = high 65 | self._low_water = low 66 | 67 | def set_write_buffer_limits(self, high=None, low=None): 68 | self._set_write_buffer_limits(high=high, low=low) 69 | self._maybe_pause_protocol() 70 | 71 | def get_write_buffer_size(self): 72 | raise NotImplementedError 73 | -------------------------------------------------------------------------------- /src/io.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use mio::unix::SourceFd; 3 | #[cfg(unix)] 4 | use std::os::fd::RawFd; 5 | #[cfg(windows)] 6 | use std::os::windows::io::RawSocket; 7 | 8 | use mio::{Interest, Registry, Token, event::Source as MioSource, net::TcpListener}; 9 | 10 | pub(crate) enum Source { 11 | #[cfg(unix)] 12 | FD(RawFd), 13 | #[cfg(windows)] 14 | FD(RawSocket), 15 | TCPListener(TcpListener), 16 | // #[cfg(unix)] 17 | // TCPStream(RawFd), 18 | // #[cfg(windows)] 19 | // TCPStream(RawSocket), 20 | // #[cfg(unix)] 21 | // UDPSocket(RawFd), 22 | // #[cfg(windows)] 23 | // UDPSocket(RawSocket), 24 | } 25 | 26 | #[cfg(windows)] 27 | #[derive(Debug)] 28 | pub struct SourceRawSocket<'a>(pub &'a RawSocket); 29 | 30 | // NOTE: this won't work as `selector()` is not exposed on win 31 | #[cfg(windows)] 32 | impl<'a> MioSource for SourceRawSocket<'a> { 33 | fn register(&mut self, registry: &Registry, token: Token, interests: Interest) -> std::io::Result<()> { 34 | registry.selector().register(*self.0, token, interests) 35 | } 36 | 37 | fn reregister(&mut self, registry: &Registry, token: Token, interests: Interest) -> std::io::Result<()> { 38 | registry.selector().reregister(*self.0, token, interests) 39 | } 40 | 41 | fn deregister(&mut self, registry: &Registry) -> std::io::Result<()> { 42 | registry.selector().deregister(*self.0) 43 | } 44 | } 45 | 46 | impl MioSource for Source { 47 | #[inline] 48 | fn register(&mut self, registry: &Registry, token: Token, interests: Interest) -> std::io::Result<()> { 49 | match self { 50 | #[cfg(unix)] 51 | Self::FD(inner) => SourceFd(inner).register(registry, token, interests), 52 | #[cfg(windows)] 53 | Self::FD(inner) => SourceRawSocket(inner).register(registry, token, interests), 54 | Self::TCPListener(inner) => inner.register(registry, token, interests), 55 | } 56 | } 57 | 58 | #[inline] 59 | fn reregister(&mut self, registry: &Registry, token: Token, interests: Interest) -> std::io::Result<()> { 60 | match self { 61 | #[cfg(unix)] 62 | Self::FD(inner) => SourceFd(inner).reregister(registry, token, interests), 63 | #[cfg(windows)] 64 | Self::FD(inner) => SourceRawSocket(inner).register(registry, token, interests), 65 | Self::TCPListener(inner) => inner.reregister(registry, token, interests), 66 | } 67 | } 68 | 69 | #[inline] 70 | fn deregister(&mut self, registry: &Registry) -> std::io::Result<()> { 71 | match self { 72 | #[cfg(unix)] 73 | Self::FD(inner) => SourceFd(inner).deregister(registry), 74 | #[cfg(windows)] 75 | Self::FD(inner) => SourceRawSocket(inner).register(registry, token, interests), 76 | Self::TCPListener(inner) => inner.deregister(registry), 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rloop/exc.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from asyncio.log import logger as _aio_logger 3 | 4 | 5 | def _default_exception_handler(context): 6 | message = context.get('message') 7 | if not message: 8 | message = 'Unhandled exception in event loop' 9 | 10 | exception = context.get('exception') 11 | if exception is not None: 12 | exc_info = (type(exception), exception, exception.__traceback__) 13 | else: 14 | exc_info = False 15 | 16 | # if ('source_traceback' not in context and 17 | # self._current_handle is not None and 18 | # self._current_handle._source_traceback): 19 | # context['handle_traceback'] = \ 20 | # self._current_handle._source_traceback 21 | 22 | log_lines = [message] 23 | for key in sorted(context): 24 | if key in {'message', 'exception'}: 25 | continue 26 | value = context[key] 27 | if key == 'source_traceback': 28 | tb = ''.join(traceback.format_list(value)) 29 | value = 'Object created at (most recent call last):\n' 30 | value += tb.rstrip() 31 | elif key == 'handle_traceback': 32 | tb = ''.join(traceback.format_list(value)) 33 | value = 'Handle created at (most recent call last):\n' 34 | value += tb.rstrip() 35 | else: 36 | value = repr(value) 37 | log_lines.append(f'{key}: {value}') 38 | 39 | _aio_logger.error('\n'.join(log_lines), exc_info=exc_info) 40 | 41 | 42 | def _exception_handler(context, handler): 43 | if handler is None: 44 | try: 45 | _default_exception_handler(context) 46 | except (KeyboardInterrupt, SystemExit): 47 | raise 48 | except BaseException: 49 | _aio_logger.error('Exception in default exception handler', exc_info=True) 50 | else: 51 | try: 52 | handler(context) 53 | except (KeyboardInterrupt, SystemExit): 54 | raise 55 | except BaseException as exc: 56 | # Exception in the user set custom exception handler. 57 | try: 58 | # Let's try default handler. 59 | _default_exception_handler( 60 | { 61 | 'message': 'Unhandled error in exception handler', 62 | 'exception': exc, 63 | 'context': context, 64 | } 65 | ) 66 | except (KeyboardInterrupt, SystemExit): 67 | raise 68 | except BaseException: 69 | # Guard 'default_exception_handler' in case it is 70 | # overloaded. 71 | _aio_logger.error( 72 | 'Exception in default exception handler ' 73 | 'while handling an unexpected error ' 74 | 'in custom exception handler', 75 | exc_info=True, 76 | ) 77 | -------------------------------------------------------------------------------- /tests/udp/test_udp.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | 4 | import pytest 5 | 6 | 7 | class DatagramProto(asyncio.DatagramProtocol): 8 | done = None 9 | 10 | def __init__(self, create_future=False, loop=None): 11 | self.state = 'INITIAL' 12 | self.nbytes = 0 13 | if create_future: 14 | self.done = loop.create_future() 15 | 16 | def _assert_state(self, expected): 17 | if self.state != expected: 18 | raise AssertionError(f'state: {self.state!r}, expected: {expected!r}') 19 | 20 | def connection_made(self, transport): 21 | self.transport = transport 22 | self._assert_state('INITIAL') 23 | self.state = 'INITIALIZED' 24 | 25 | def datagram_received(self, data, addr): 26 | self._assert_state('INITIALIZED') 27 | self.nbytes += len(data) 28 | 29 | def error_received(self, exc): 30 | self._assert_state('INITIALIZED') 31 | 32 | def connection_lost(self, exc): 33 | self._assert_state('INITIALIZED') 34 | self.state = 'CLOSED' 35 | if self.done: 36 | self.done.set_result(None) 37 | 38 | 39 | def test_create_datagram_endpoint_sock(loop): 40 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 41 | sock.bind(('127.0.0.1', 0)) 42 | sock.setblocking(False) # Make socket non-blocking 43 | fut = loop.create_datagram_endpoint(lambda: DatagramProto(create_future=True, loop=loop), sock=sock) 44 | transport, protocol = loop.run_until_complete(fut) 45 | transport.close() 46 | loop.run_until_complete(protocol.done) 47 | assert protocol.state == 'CLOSED' 48 | 49 | 50 | @pytest.mark.skipif(not hasattr(socket, 'AF_UNIX'), reason='no UDS') 51 | def test_create_datagram_endpoint_sock_unix(loop): 52 | fut = loop.create_datagram_endpoint(lambda: DatagramProto(create_future=True, loop=loop), family=socket.AF_UNIX) 53 | transport, protocol = loop.run_until_complete(fut) 54 | # Check that the socket family is AF_UNIX using get_extra_info 55 | sock_info = transport.get_extra_info('socket') 56 | assert sock_info.family == socket.AF_UNIX 57 | transport.close() 58 | loop.run_until_complete(protocol.done) 59 | assert protocol.state == 'CLOSED' 60 | 61 | 62 | def test_create_datagram_endpoint_local_addr(loop): 63 | fut = loop.create_datagram_endpoint( 64 | lambda: DatagramProto(create_future=True, loop=loop), local_addr=('127.0.0.1', 0) 65 | ) 66 | transport, protocol = loop.run_until_complete(fut) 67 | transport.close() 68 | loop.run_until_complete(protocol.done) 69 | assert protocol.state == 'CLOSED' 70 | 71 | 72 | def test_create_datagram_endpoint_remote_addr(loop): 73 | fut = loop.create_datagram_endpoint( 74 | lambda: DatagramProto(create_future=True, loop=loop), remote_addr=('127.0.0.1', 12345) 75 | ) 76 | transport, protocol = loop.run_until_complete(fut) 77 | transport.close() 78 | loop.run_until_complete(protocol.done) 79 | assert protocol.state == 'CLOSED' 80 | -------------------------------------------------------------------------------- /src/py.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{prelude::*, sync::PyOnceLock}; 2 | use std::convert::Into; 3 | 4 | static ASYNCIO: PyOnceLock> = PyOnceLock::new(); 5 | static ASYNCIO_PROTO_BUF: PyOnceLock> = PyOnceLock::new(); 6 | static SOCKET: PyOnceLock> = PyOnceLock::new(); 7 | static WEAKREF: PyOnceLock> = PyOnceLock::new(); 8 | 9 | fn asyncio(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { 10 | Ok(ASYNCIO 11 | .get_or_try_init(py, || py.import("asyncio").map(Into::into))? 12 | .bind(py)) 13 | } 14 | 15 | fn socket(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { 16 | Ok(SOCKET 17 | .get_or_try_init(py, || py.import("socket").map(Into::into))? 18 | .bind(py)) 19 | } 20 | 21 | pub(crate) fn asyncio_proto_buf(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { 22 | Ok(ASYNCIO_PROTO_BUF 23 | .get_or_try_init(py, || { 24 | let ret = asyncio(py)?.getattr("protocols")?.getattr("BufferedProtocol")?; 25 | Ok::, PyErr>(ret.unbind()) 26 | })? 27 | .bind(py)) 28 | } 29 | 30 | fn weakref(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { 31 | Ok(WEAKREF 32 | .get_or_try_init(py, || py.import("weakref").map(Into::into))? 33 | .bind(py)) 34 | } 35 | 36 | pub(crate) fn copy_context(py: Python) -> Py { 37 | let ctx = unsafe { 38 | let ptr = pyo3::ffi::PyContext_CopyCurrent(); 39 | Bound::from_owned_ptr(py, ptr) 40 | }; 41 | ctx.unbind() 42 | } 43 | 44 | pub(crate) fn sock(py: Python) -> PyResult> { 45 | socket(py)?.getattr(pyo3::intern!(py, "socket")) 46 | } 47 | 48 | pub(crate) fn weakset(py: Python) -> PyResult> { 49 | weakref(py)?.getattr("WeakSet")?.call0() 50 | } 51 | 52 | // pub(crate) fn weakvaldict(py: Python) -> PyResult> { 53 | // weakref(py)?.getattr("WeakValueDictionary")?.call0() 54 | // } 55 | 56 | macro_rules! run_in_ctx0 { 57 | ($py:expr, $ctx:expr, $cb:expr) => { 58 | unsafe { 59 | pyo3::ffi::PyContext_Enter($ctx); 60 | let ptr = pyo3::ffi::compat::PyObject_CallNoArgs($cb); 61 | pyo3::ffi::PyContext_Exit($ctx); 62 | Bound::from_owned_ptr_or_err($py, ptr) 63 | } 64 | }; 65 | } 66 | 67 | #[cfg(not(PyPy))] 68 | macro_rules! run_in_ctx1 { 69 | ($py:expr, $ctx:expr, $cb:expr, $arg:expr) => { 70 | unsafe { 71 | pyo3::ffi::PyContext_Enter($ctx); 72 | let ptr = pyo3::ffi::PyObject_CallOneArg($cb, $arg); 73 | pyo3::ffi::PyContext_Exit($ctx); 74 | Bound::from_owned_ptr_or_err($py, ptr) 75 | } 76 | }; 77 | } 78 | 79 | macro_rules! run_in_ctx { 80 | ($py:expr, $ctx:expr, $cb:expr, $args:expr) => { 81 | unsafe { 82 | pyo3::ffi::PyContext_Enter($ctx); 83 | let ptr = pyo3::ffi::PyObject_CallObject($cb, $args); 84 | pyo3::ffi::PyContext_Exit($ctx); 85 | Bound::from_owned_ptr_or_err($py, ptr) 86 | } 87 | }; 88 | } 89 | 90 | pub(crate) use run_in_ctx; 91 | pub(crate) use run_in_ctx0; 92 | 93 | #[cfg(not(PyPy))] 94 | pub(crate) use run_in_ctx1; 95 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = 'rloop' 3 | authors = [ 4 | { name = 'Giovanni Barillari', email = 'g@baro.dev' } 5 | ] 6 | classifiers = [ 7 | 'Development Status :: 3 - Alpha', 8 | 'Intended Audience :: Developers', 9 | 'License :: OSI Approved :: BSD License', 10 | 'Operating System :: MacOS', 11 | 'Operating System :: POSIX :: Linux', 12 | 'Programming Language :: Python :: Free Threading :: 2 - Beta', 13 | 'Programming Language :: Python :: 3', 14 | 'Programming Language :: Python :: 3.9', 15 | 'Programming Language :: Python :: 3.10', 16 | 'Programming Language :: Python :: 3.11', 17 | 'Programming Language :: Python :: 3.12', 18 | 'Programming Language :: Python :: 3.13', 19 | 'Programming Language :: Python :: Implementation :: CPython', 20 | 'Programming Language :: Python :: Implementation :: PyPy', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Rust', 23 | ] 24 | 25 | dynamic = [ 26 | 'description', 27 | 'keywords', 28 | 'license', 29 | 'readme', 30 | 'version', 31 | ] 32 | 33 | requires-python = '>=3.9' 34 | dependencies = [] 35 | 36 | [dependency-groups] 37 | build = [ 38 | 'maturin~=1.8', 39 | ] 40 | lint = [ 41 | 'ruff~=0.11', 42 | ] 43 | test = [ 44 | 'pytest~=8.3', 45 | 'pytest-asyncio~=0.26', 46 | ] 47 | 48 | all = [ 49 | { include-group = 'build' }, 50 | { include-group = 'lint' }, 51 | { include-group = 'test' }, 52 | ] 53 | 54 | [project.urls] 55 | Homepage = 'https://github.com/gi0baro/rloop' 56 | Funding = 'https://github.com/sponsors/gi0baro' 57 | Source = 'https://github.com/gi0baro/rloop' 58 | 59 | [build-system] 60 | requires = ['maturin>=1.8.0,<2'] 61 | build-backend = 'maturin' 62 | 63 | [tool.maturin] 64 | module-name = 'rloop._rloop' 65 | bindings = 'pyo3' 66 | 67 | [tool.ruff] 68 | line-length = 120 69 | extend-select = [ 70 | # E and F are enabled by default 71 | 'B', # flake8-bugbear 72 | 'C4', # flake8-comprehensions 73 | 'C90', # mccabe 74 | 'I', # isort 75 | 'N', # pep8-naming 76 | 'Q', # flake8-quotes 77 | 'RUF100', # ruff (unused noqa) 78 | 'S', # flake8-bandit 79 | 'W', # pycodestyle 80 | ] 81 | extend-ignore = [ 82 | 'B008', # function calls in args defaults are fine 83 | 'B009', # getattr with constants is fine 84 | 'B034', # re.split won't confuse us 85 | 'B904', # rising without from is fine 86 | 'E501', # leave line length to black 87 | 'N818', # leave to us exceptions naming 88 | 'S101', # assert is fine 89 | ] 90 | flake8-quotes = { inline-quotes = 'single', multiline-quotes = 'double' } 91 | mccabe = { max-complexity = 25 } 92 | 93 | [tool.ruff.format] 94 | quote-style = 'single' 95 | 96 | [tool.ruff.isort] 97 | combine-as-imports = true 98 | lines-after-imports = 2 99 | known-first-party = ['rloop', 'tests'] 100 | 101 | [tool.ruff.per-file-ignores] 102 | 'rloop/_rloop.pyi' = ['I001'] 103 | 'tests/**' = ['B018', 'S110', 'S501'] 104 | 105 | [tool.pytest.ini_options] 106 | asyncio_mode = 'auto' 107 | 108 | [tool.uv] 109 | package = false 110 | -------------------------------------------------------------------------------- /rloop/utils.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import itertools 3 | import os 4 | import socket 5 | 6 | 7 | _HAS_IPv6 = hasattr(socket, 'AF_INET6') 8 | 9 | 10 | def _noop(*args, **kwargs): 11 | return 12 | 13 | 14 | def _can_use_pidfd(): 15 | if not hasattr(os, 'pidfd_open'): 16 | return False 17 | try: 18 | pid = os.getpid() 19 | os.close(os.pidfd_open(pid, 0)) 20 | except OSError: 21 | # blocked by security policy like SECCOMP 22 | return False 23 | return True 24 | 25 | 26 | def _ipaddr_info(host, port, family, type, proto, flowinfo=0, scopeid=0): 27 | # Try to skip getaddrinfo if "host" is already an IP. Users might have 28 | # handled name resolution in their own code and pass in resolved IPs. 29 | if not hasattr(socket, 'inet_pton'): 30 | return 31 | 32 | if proto not in {0, socket.IPPROTO_TCP, socket.IPPROTO_UDP} or host is None: 33 | return None 34 | 35 | if type == socket.SOCK_STREAM: 36 | proto = socket.IPPROTO_TCP 37 | elif type == socket.SOCK_DGRAM: 38 | proto = socket.IPPROTO_UDP 39 | else: 40 | return None 41 | 42 | if port is None: 43 | port = 0 44 | elif isinstance(port, bytes) and port == b'': 45 | port = 0 46 | elif isinstance(port, str) and port == '': 47 | port = 0 48 | else: 49 | # If port's a service name like "http", don't skip getaddrinfo. 50 | try: 51 | port = int(port) 52 | except (TypeError, ValueError): 53 | return None 54 | 55 | if family == socket.AF_UNSPEC: 56 | afs = [socket.AF_INET] 57 | if _HAS_IPv6: 58 | afs.append(socket.AF_INET6) 59 | else: 60 | afs = [family] 61 | 62 | if isinstance(host, bytes): 63 | host = host.decode('idna') 64 | if '%' in host: 65 | # Linux's inet_pton doesn't accept an IPv6 zone index after host, 66 | # like '::1%lo0'. 67 | return None 68 | 69 | for af in afs: 70 | try: 71 | socket.inet_pton(af, host) 72 | # The host has already been resolved. 73 | if _HAS_IPv6 and af == socket.AF_INET6: 74 | return af, type, proto, '', (host, port, flowinfo, scopeid) 75 | else: 76 | return af, type, proto, '', (host, port) 77 | except OSError: 78 | pass 79 | 80 | # "host" is not an IP address. 81 | return None 82 | 83 | 84 | def _interleave_addrinfos(addrinfos, first_address_family_count=1): 85 | addrinfos_by_family = collections.OrderedDict() 86 | for addr in addrinfos: 87 | family = addr[0] 88 | if family not in addrinfos_by_family: 89 | addrinfos_by_family[family] = [] 90 | addrinfos_by_family[family].append(addr) 91 | addrinfos_lists = list(addrinfos_by_family.values()) 92 | 93 | reordered = [] 94 | if first_address_family_count > 1: 95 | reordered.extend(addrinfos_lists[0][: first_address_family_count - 1]) 96 | del addrinfos_lists[0][: first_address_family_count - 1] 97 | reordered.extend(a for a in itertools.chain.from_iterable(itertools.zip_longest(*addrinfos_lists)) if a is not None) 98 | return reordered 99 | 100 | 101 | def _set_reuseport(sock): 102 | if not hasattr(socket, 'SO_REUSEPORT'): 103 | raise ValueError('reuse_port not supported by socket module') 104 | try: 105 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 106 | except OSError: 107 | raise ValueError('reuse_port not supported by socket module, SO_REUSEPORT defined but not implemented.') 108 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use std::sync::atomic; 3 | 4 | use crate::event_loop::EventLoop; 5 | use crate::tcp::TCPServer; 6 | 7 | enum ServerType { 8 | TCP(TCPServer), 9 | // UDP, 10 | // Unix, 11 | } 12 | 13 | #[pyclass(frozen, module = "rloop._rloop")] 14 | pub(crate) struct Server { 15 | #[pyo3(get)] 16 | _loop: Py, 17 | #[pyo3(get)] 18 | _sockets: Py, 19 | closed: atomic::AtomicBool, 20 | serving: atomic::AtomicBool, 21 | servers: Vec, 22 | // serve_forever_fut: RwLock>>, 23 | } 24 | 25 | impl Server { 26 | pub(crate) fn tcp(event_loop: Py, sockets: Py, servers: Vec) -> Self { 27 | let srv: Vec = servers.into_iter().map(ServerType::TCP).collect(); 28 | 29 | Self { 30 | _loop: event_loop, 31 | _sockets: sockets, 32 | closed: false.into(), 33 | serving: false.into(), 34 | servers: srv, 35 | // serve_forever_fut: RwLock::new(None), 36 | // waiters: RwLock::new(Vec::new()), 37 | } 38 | } 39 | } 40 | 41 | #[pymethods] 42 | impl Server { 43 | // needed? 44 | // fn _add_waiter(&self, waiter: Py) { 45 | // let mut guard = self.waiters.write().unwrap(); 46 | // guard.push(waiter); 47 | // } 48 | 49 | // #[getter(_sff)] 50 | // fn _get_sff(&self, py: Python) -> Option> { 51 | // let guard = self.serve_forever_fut.read().unwrap(); 52 | // guard.as_ref().map(|v| v.clone_ref(py)) 53 | // } 54 | 55 | // #[setter(_sff)] 56 | // fn _set_sff(&self, val: Py) { 57 | // let mut guard = self.serve_forever_fut.write().unwrap(); 58 | // *guard = Some(val); 59 | // } 60 | 61 | fn _start_serving(&self, py: Python) -> PyResult<()> { 62 | for server in &self.servers { 63 | match server { 64 | ServerType::TCP(inner) => inner.listen(py, self._loop.clone_ref(py))?, 65 | } 66 | } 67 | self.serving.store(true, atomic::Ordering::Release); 68 | Ok(()) 69 | } 70 | 71 | fn _is_serving(&self) -> bool { 72 | self.serving.load(atomic::Ordering::Relaxed) 73 | } 74 | 75 | fn _close(&self, py: Python) { 76 | if self 77 | .closed 78 | .compare_exchange(false, true, atomic::Ordering::Release, atomic::Ordering::Relaxed) 79 | .is_ok() 80 | { 81 | let event_loop = self._loop.get(); 82 | for server in &self.servers { 83 | match server { 84 | ServerType::TCP(inner) => inner.close(py, event_loop), 85 | // _ => {} 86 | } 87 | } 88 | } 89 | self.serving.store(false, atomic::Ordering::Release); 90 | // Ok(()) 91 | } 92 | 93 | fn _streams_close(&self, py: Python) { 94 | let event_loop = self._loop.get(); 95 | for server in &self.servers { 96 | match server { 97 | ServerType::TCP(inner) => inner.streams_close(py, event_loop), 98 | } 99 | } 100 | } 101 | 102 | fn _streams_abort(&self, py: Python) { 103 | let event_loop = self._loop.get(); 104 | for server in &self.servers { 105 | match server { 106 | ServerType::TCP(inner) => inner.streams_abort(py, event_loop), 107 | } 108 | } 109 | } 110 | } 111 | 112 | pub(crate) fn init_pymodule(module: &Bound) -> PyResult<()> { 113 | module.add_class::()?; 114 | 115 | Ok(()) 116 | } 117 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: benchmarks 2 | 3 | on: workflow_dispatch 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | 9 | jobs: 10 | benchmark-base: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v5 15 | - uses: actions/setup-python@v5 16 | with: 17 | python-version: '3.13' 18 | - uses: pyo3/maturin-action@v1 19 | with: 20 | command: build 21 | args: --release --interpreter python3.13 22 | target: x64 23 | manylinux: auto 24 | container: off 25 | - run: | 26 | export _whl=$(ls target/wheels/rloop-*.whl) 27 | pip install $_whl numpy uvloop 28 | - name: benchmark 29 | working-directory: ./benchmarks 30 | run: | 31 | python benchmarks.py raw stream proto 32 | - name: upload results 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: results-base 36 | path: benchmarks/results/* 37 | 38 | benchmark-pyver: 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | - uses: actions/checkout@v5 43 | - uses: actions/setup-python@v5 44 | with: 45 | python-version: | 46 | 3.10 47 | 3.11 48 | 3.12 49 | 3.13 50 | - uses: pyo3/maturin-action@v1 51 | with: 52 | command: build 53 | args: --release --interpreter python3.10 python3.11 python3.12 python3.13 54 | target: x64 55 | manylinux: auto 56 | container: off 57 | - name: setup venvs 58 | run: | 59 | python3.10 -m venv .venv310 60 | python3.11 -m venv .venv311 61 | python3.12 -m venv .venv312 62 | python3.13 -m venv .venv313 63 | .venv310/bin/pip install $(ls target/wheels/rloop-*-cp310-*.whl) numpy uvloop 64 | .venv311/bin/pip install $(ls target/wheels/rloop-*-cp311-*.whl) numpy uvloop 65 | .venv312/bin/pip install $(ls target/wheels/rloop-*-cp312-*.whl) numpy uvloop 66 | .venv313/bin/pip install $(ls target/wheels/rloop-*-cp313-*.whl) numpy uvloop 67 | - name: benchmark 68 | working-directory: ./benchmarks 69 | run: | 70 | BENCHMARK_EXC_PREFIX=${{ github.workspace }}/.venv310/bin ${{ github.workspace }}/.venv310/bin/python benchmarks.py raw 71 | mv results/data.json results/py310.json 72 | BENCHMARK_EXC_PREFIX=${{ github.workspace }}/.venv311/bin ${{ github.workspace }}/.venv311/bin/python benchmarks.py raw 73 | mv results/data.json results/py311.json 74 | BENCHMARK_EXC_PREFIX=${{ github.workspace }}/.venv312/bin ${{ github.workspace }}/.venv312/bin/python benchmarks.py raw 75 | mv results/data.json results/py312.json 76 | BENCHMARK_EXC_PREFIX=${{ github.workspace }}/.venv313/bin ${{ github.workspace }}/.venv313/bin/python benchmarks.py raw 77 | mv results/data.json results/py313.json 78 | - name: upload results 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: results-pyver 82 | path: benchmarks/results/* 83 | 84 | results: 85 | runs-on: ubuntu-latest 86 | needs: [benchmark-base, benchmark-pyver] 87 | 88 | steps: 89 | - uses: actions/checkout@v5 90 | - uses: gi0baro/setup-noir@v1 91 | - uses: actions/download-artifact@v5 92 | with: 93 | name: results-base 94 | path: benchmarks/results 95 | - run: | 96 | mv benchmarks/results/data.json benchmarks/results/base.json 97 | - uses: actions/download-artifact@v5 98 | with: 99 | name: results-pyver 100 | path: benchmarks/results 101 | - name: render 102 | working-directory: ./benchmarks 103 | run: | 104 | noir -c data:results/base.json -v 'benv=GHA Linux x86_64' templates/main.md > README.md 105 | noir \ 106 | -c data310:results/py310.json \ 107 | -c data311:results/py311.json \ 108 | -c data312:results/py312.json \ 109 | -c data313:results/py313.json \ 110 | -v pyvb=310 -v 'benv=GHA Linux x86_64' \ 111 | templates/pyver.md > pyver.md 112 | - name: open PR 113 | uses: peter-evans/create-pull-request@v7 114 | with: 115 | branch: benchmarks-update 116 | branch-suffix: timestamp 117 | title: Update benchmark results 118 | body: SSIA 119 | commit-message: | 120 | Update benchmark results 121 | add-paths: | 122 | benchmarks/README.md 123 | benchmarks/pyver.md 124 | -------------------------------------------------------------------------------- /benchmarks/benchmarks.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import multiprocessing 4 | import os 5 | import signal 6 | import subprocess 7 | import sys 8 | import time 9 | from contextlib import contextmanager 10 | from pathlib import Path 11 | 12 | 13 | WD = Path(__file__).resolve().parent 14 | CPU = multiprocessing.cpu_count() 15 | LOOPS = ['asyncio', 'rloop', 'uvloop'] 16 | MSGS = [1024, 1024 * 10, 1024 * 100] 17 | CONCURRENCIES = sorted({1, max(CPU / 2, 1), max(CPU - 1, 1)}) 18 | 19 | 20 | @contextmanager 21 | def server(loop, streams=False, proto=False): 22 | exc_prefix = os.environ.get('BENCHMARK_EXC_PREFIX') 23 | py = 'python' 24 | if exc_prefix: 25 | py = f'{exc_prefix}/{py}' 26 | target = WD / 'server.py' 27 | proc_cmd = f'{py} {target} --loop {loop}' 28 | if streams: 29 | proc_cmd += ' --streams' 30 | if proto: 31 | proc_cmd += ' --proto' 32 | 33 | proc = subprocess.Popen(proc_cmd, shell=True, preexec_fn=os.setsid) # noqa: S602 34 | time.sleep(2) 35 | yield proc 36 | os.killpg(os.getpgid(proc.pid), signal.SIGKILL) 37 | 38 | 39 | def client(duration, concurrency, msgsize): 40 | exc_prefix = os.environ.get('BENCHMARK_EXC_PREFIX') 41 | py = 'python' 42 | if exc_prefix: 43 | py = f'{exc_prefix}/{py}' 44 | target = WD / 'client.py' 45 | cmd_parts = [ 46 | py, 47 | str(target), 48 | f'--concurrency {concurrency}', 49 | f'--duration {duration}', 50 | f'--msize {msgsize}', 51 | '--output json', 52 | ] 53 | try: 54 | proc = subprocess.run( # noqa: S602 55 | ' '.join(cmd_parts), 56 | shell=True, 57 | check=True, 58 | capture_output=True, 59 | ) 60 | data = json.loads(proc.stdout.decode('utf8')) 61 | return data 62 | except Exception as e: 63 | print(f'WARN: got exception {e} while loading client data') 64 | return {} 65 | 66 | 67 | def benchmark(msgs=None, concurrencies=None): 68 | concurrencies = concurrencies or CONCURRENCIES 69 | msgs = msgs or MSGS 70 | results = {} 71 | # primer 72 | client(1, 1, 1024) 73 | time.sleep(1) 74 | # warm up 75 | client(1, max(concurrencies), 1024 * 100) 76 | time.sleep(2) 77 | # bench 78 | for concurrency in concurrencies: 79 | cres = results[concurrency] = {} 80 | for msg in msgs: 81 | res = client(10, concurrency, msg) 82 | cres[msg] = res 83 | time.sleep(3) 84 | time.sleep(1) 85 | return results 86 | 87 | 88 | def raw(): 89 | results = {} 90 | for loop in LOOPS: 91 | with server(loop): 92 | results[loop] = benchmark(concurrencies=[CONCURRENCIES[0]]) 93 | return results 94 | 95 | 96 | def stream(): 97 | results = {} 98 | for loop in LOOPS: 99 | with server(loop, streams=True): 100 | results[loop] = benchmark(concurrencies=[CONCURRENCIES[0]]) 101 | return results 102 | 103 | 104 | def proto(): 105 | results = {} 106 | for loop in LOOPS: 107 | with server(loop, proto=True): 108 | results[loop] = benchmark(concurrencies=[CONCURRENCIES[0]]) 109 | return results 110 | 111 | 112 | def concurrency(): 113 | results = {} 114 | for loop in LOOPS: 115 | with server(loop): 116 | results[loop] = benchmark(msgs=[1024], concurrencies=CONCURRENCIES[1:]) 117 | return results 118 | 119 | 120 | def _rloop_version(): 121 | import rloop 122 | 123 | return rloop.__version__ 124 | 125 | 126 | def run(): 127 | all_benchmarks = { 128 | 'raw': raw, 129 | 'stream': stream, 130 | 'proto': proto, 131 | 'concurrency': concurrency, 132 | } 133 | inp_benchmarks = sys.argv[1:] or ['raw'] 134 | run_benchmarks = set(inp_benchmarks) & set(all_benchmarks.keys()) 135 | 136 | now = datetime.datetime.utcnow() 137 | results = {} 138 | for benchmark_key in run_benchmarks: 139 | runner = all_benchmarks[benchmark_key] 140 | results[benchmark_key] = runner() 141 | 142 | with open('results/data.json', 'w') as f: 143 | pyver = sys.version_info 144 | f.write( 145 | json.dumps( 146 | { 147 | 'cpu': CPU, 148 | 'run_at': int(now.timestamp()), 149 | 'pyver': f'{pyver.major}.{pyver.minor}', 150 | 'results': results, 151 | 'rloop': _rloop_version(), 152 | } 153 | ) 154 | ) 155 | 156 | 157 | if __name__ == '__main__': 158 | run() 159 | -------------------------------------------------------------------------------- /benchmarks/server.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import os 4 | import os.path 5 | from socket import AF_INET, AF_UNIX, IPPROTO_TCP, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, TCP_NODELAY, socket 6 | 7 | 8 | PRINT = 0 9 | 10 | 11 | async def echo_server(loop, address, unix): 12 | if unix: 13 | sock = socket(AF_UNIX, SOCK_STREAM) 14 | else: 15 | sock = socket(AF_INET, SOCK_STREAM) 16 | sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 17 | sock.bind(address) 18 | sock.listen(5) 19 | sock.setblocking(False) 20 | if PRINT: 21 | print('Server listening at', address) 22 | with sock: 23 | while True: 24 | client, addr = await loop.sock_accept(sock) 25 | if PRINT: 26 | print('Connection from', addr) 27 | loop.create_task(echo_client(loop, client)) 28 | 29 | 30 | async def echo_client(loop, client): 31 | try: 32 | client.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) 33 | except (OSError, NameError): 34 | pass 35 | 36 | with client: 37 | while True: 38 | data = await loop.sock_recv(client, 102400) 39 | if not data: 40 | break 41 | await loop.sock_sendall(client, data) 42 | if PRINT: 43 | print('Connection closed') 44 | 45 | 46 | async def echo_client_streams(reader, writer): 47 | sock = writer.get_extra_info('socket') 48 | try: 49 | sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) 50 | except (OSError, NameError): 51 | pass 52 | if PRINT: 53 | print('Connection from', sock.getpeername()) 54 | while True: 55 | data = await reader.readline() 56 | if not data: 57 | break 58 | writer.write(data) 59 | if PRINT: 60 | print('Connection closed') 61 | writer.close() 62 | 63 | 64 | class EchoProtocol(asyncio.Protocol): 65 | def connection_made(self, transport): 66 | self.transport = transport 67 | sock = transport.get_extra_info('socket') 68 | try: 69 | sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) 70 | except (OSError, NameError): 71 | pass 72 | 73 | def connection_lost(self, exc): 74 | self.transport = None 75 | 76 | def data_received(self, data): 77 | self.transport.write(data) 78 | 79 | 80 | def run(args): 81 | if args.loop == 'rloop': 82 | import rloop 83 | 84 | loop = rloop.new_event_loop() 85 | print('using RLoop') 86 | elif args.loop == 'uvloop': 87 | import uvloop 88 | 89 | loop = uvloop.new_event_loop() 90 | print('using UVLoop') 91 | else: 92 | loop = asyncio.new_event_loop() 93 | print('using asyncio loop') 94 | 95 | asyncio.set_event_loop(loop) 96 | loop.set_debug(False) 97 | 98 | if args.print: 99 | global PRINT 100 | PRINT = 1 101 | 102 | unix = False 103 | if args.addr.startswith('file:'): 104 | unix = True 105 | addr = args.addr[5:] 106 | if os.path.exists(addr): 107 | os.remove(addr) 108 | else: 109 | addr = args.addr.split(':') 110 | addr[1] = int(addr[1]) 111 | addr = tuple(addr) 112 | 113 | print('serving on: {}'.format(addr)) 114 | 115 | if args.streams: 116 | if args.proto: 117 | print('cannot use --stream and --proto simultaneously') 118 | exit(1) 119 | 120 | print('using asyncio/streams') 121 | if unix: 122 | coro = asyncio.start_unix_server(echo_client_streams, addr, limit=1024 * 1024) 123 | else: 124 | coro = asyncio.start_server(echo_client_streams, *addr, limit=1024 * 1024) 125 | loop.run_until_complete(coro) 126 | elif args.proto: 127 | if args.streams: 128 | print('cannot use --stream and --proto simultaneously') 129 | exit(1) 130 | 131 | print('using simple protocol') 132 | if unix: 133 | coro = loop.create_unix_server(EchoProtocol, addr) 134 | else: 135 | coro = loop.create_server(EchoProtocol, *addr) 136 | loop.run_until_complete(coro) 137 | else: 138 | print('using sock_recv/sock_sendall') 139 | loop.create_task(echo_server(loop, addr, unix)) 140 | try: 141 | print('PID', os.getpid()) 142 | loop.run_forever() 143 | finally: 144 | loop.close() 145 | 146 | 147 | if __name__ == '__main__': 148 | parser = argparse.ArgumentParser() 149 | parser.add_argument('--loop', default='asyncio', type=str) 150 | parser.add_argument('--streams', default=False, action='store_true') 151 | parser.add_argument('--proto', default=False, action='store_true') 152 | parser.add_argument('--addr', default='127.0.0.1:25000', type=str) 153 | parser.add_argument('--print', default=False, action='store_true') 154 | args = parser.parse_args() 155 | run(args) 156 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # RLoop benchmarks 2 | 3 | Run at: Wed 20 Aug 2025, 17:47 4 | Environment: GHA Linux x86_64 (CPUs: 4) 5 | Python version: 3.13 6 | RLoop version: 0.1.6 7 | 8 | ### Raw sockets 9 | 10 | TCP echo server with raw sockets comparison using 1KB, 10KB and 100KB messages. 11 | 12 | 13 | | Loop | Throughput (1KB) | Throughput (10KB) | Throughput (100KB) | 14 | | --- | --- | --- | --- | 15 | | asyncio | 14410.8 (87.5%) | 13549.6 (89.1%) | 9882.8 (116.5%) | 16 | | rloop | 16472.2 (100.0%) | 15214.9 (100.0%) | 8485.9 (100.0%) | 17 | | uvloop | 15889.1 (96.5%) | 14276.3 (93.8%) | 9376.6 (110.5%) | 18 | 19 | 20 | #### 1KB details 21 | 22 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 23 | | --- | --- | --- | --- | --- | --- | 24 | | asyncio | 144108 | 14410.8 (87.5%) | 0.068ms | 0.105ms | 0.012 | 25 | | rloop | 164722 | 16472.2 (100.0%) | 0.058ms | 0.089ms | 0.011 | 26 | | uvloop | 158891 | 15889.1 (96.5%) | 0.06ms | 0.1ms | 0.014 | 27 | 28 | 29 | #### 10KB details 30 | 31 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 32 | | --- | --- | --- | --- | --- | --- | 33 | | asyncio | 135496 | 13549.6 (89.1%) | 0.071ms | 0.113ms | 0.02 | 34 | | rloop | 152149 | 15214.9 (100.0%) | 0.063ms | 0.1ms | 0.014 | 35 | | uvloop | 142763 | 14276.3 (93.8%) | 0.068ms | 0.109ms | 0.014 | 36 | 37 | 38 | #### 100KB details 39 | 40 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 41 | | --- | --- | --- | --- | --- | --- | 42 | | asyncio | 98828 | 9882.8 (116.5%) | 0.1ms | 0.143ms | 0.014 | 43 | | rloop | 84859 | 8485.9 (100.0%) | 0.115ms | 0.24ms | 0.032 | 44 | | uvloop | 93766 | 9376.6 (110.5%) | 0.103ms | 0.166ms | 0.019 | 45 | 46 | 47 | ### Streams 48 | 49 | TCP echo server with `asyncio` streams comparison using 1KB, 10KB and 100KB messages. 50 | 51 | 52 | | Loop | Throughput (1KB) | Throughput (10KB) | Throughput (100KB) | 53 | | --- | --- | --- | --- | 54 | | asyncio | 14196.4 (86.6%) | 12849.9 (87.1%) | 5915.2 (82.6%) | 55 | | rloop | 16401.5 (100.0%) | 14751.1 (100.0%) | 7161.5 (100.0%) | 56 | | uvloop | 14735.5 (89.8%) | 13427.1 (91.0%) | 6186.9 (86.4%) | 57 | 58 | 59 | #### 1KB details 60 | 61 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 62 | | --- | --- | --- | --- | --- | --- | 63 | | asyncio | 141964 | 14196.4 (86.6%) | 0.067ms | 0.101ms | 0.011 | 64 | | rloop | 164015 | 16401.5 (100.0%) | 0.057ms | 0.089ms | 0.01 | 65 | | uvloop | 147355 | 14735.5 (89.8%) | 0.067ms | 0.097ms | 0.012 | 66 | 67 | 68 | #### 10KB details 69 | 70 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 71 | | --- | --- | --- | --- | --- | --- | 72 | | asyncio | 128499 | 12849.9 (87.1%) | 0.075ms | 0.108ms | 0.012 | 73 | | rloop | 147511 | 14751.1 (100.0%) | 0.066ms | 0.096ms | 0.01 | 74 | | uvloop | 134271 | 13427.1 (91.0%) | 0.072ms | 0.105ms | 0.012 | 75 | 76 | 77 | #### 100KB details 78 | 79 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 80 | | --- | --- | --- | --- | --- | --- | 81 | | asyncio | 59152 | 5915.2 (82.6%) | 0.166ms | 0.236ms | 0.035 | 82 | | rloop | 71615 | 7161.5 (100.0%) | 0.136ms | 0.197ms | 0.027 | 83 | | uvloop | 61869 | 6186.9 (86.4%) | 0.159ms | 0.237ms | 0.038 | 84 | 85 | 86 | ### Protocol 87 | 88 | TCP echo server with `asyncio.Protocol` comparison using 1KB, 10KB and 100KB messages. 89 | 90 | 91 | | Loop | Throughput (1KB) | Throughput (10KB) | Throughput (100KB) | 92 | | --- | --- | --- | --- | 93 | | asyncio | 17784.4 (85.3%) | 16494.6 (85.8%) | 11408.8 (96.0%) | 94 | | rloop | 20838.8 (100.0%) | 19225.1 (100.0%) | 11881.8 (100.0%) | 95 | | uvloop | 20296.3 (97.4%) | 18002.2 (93.6%) | 8027.6 (67.6%) | 96 | 97 | 98 | #### 1KB details 99 | 100 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 101 | | --- | --- | --- | --- | --- | --- | 102 | | asyncio | 177844 | 17784.4 (85.3%) | 0.054ms | 0.081ms | 0.007 | 103 | | rloop | 208388 | 20838.8 (100.0%) | 0.045ms | 0.069ms | 0.007 | 104 | | uvloop | 202963 | 20296.3 (97.4%) | 0.046ms | 0.07ms | 0.009 | 105 | 106 | 107 | #### 10KB details 108 | 109 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 110 | | --- | --- | --- | --- | --- | --- | 111 | | asyncio | 164946 | 16494.6 (85.8%) | 0.057ms | 0.088ms | 0.01 | 112 | | rloop | 192251 | 19225.1 (100.0%) | 0.05ms | 0.072ms | 0.008 | 113 | | uvloop | 180022 | 18002.2 (93.6%) | 0.054ms | 0.08ms | 0.007 | 114 | 115 | 116 | #### 100KB details 117 | 118 | | Loop | Total requests | Throughput | Mean latency | 99p latency | Latency stdev | 119 | | --- | --- | --- | --- | --- | --- | 120 | | asyncio | 114088 | 11408.8 (96.0%) | 0.085ms | 0.121ms | 0.01 | 121 | | rloop | 118818 | 11881.8 (100.0%) | 0.081ms | 0.112ms | 0.009 | 122 | | uvloop | 80276 | 8027.6 (67.6%) | 0.122ms | 0.157ms | 0.01 | 123 | 124 | 125 | ### Other benchmarks 126 | 127 | - [Python versions](./pyver.md) 128 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | PY_ALL: 3.9 3.10 3.11 3.12 3.13 3.13t 3.14 3.14t pypy3.9 pypy3.10 pypy3.11 7 | 8 | jobs: 9 | wheels: 10 | name: wheel ${{ matrix.platform || matrix.os }}(${{ matrix.target }}) - ${{ matrix.manylinux || 'auto' }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu, macos] 15 | target: [x86_64, aarch64] 16 | manylinux: [auto] 17 | include: 18 | - os: ubuntu 19 | platform: linux 20 | target: x86_64 21 | manylinux: auto 22 | interpreter: pypy3.9 pypy3.10 pypy3.11 23 | - os: ubuntu 24 | platform: linux 25 | target: i686 26 | interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 27 | - os: ubuntu 28 | platform: linux 29 | target: aarch64 30 | # rust-cross/manylinux2014-cross:aarch64 has issues with `ring` 31 | #container: ghcr.io/rust-cross/manylinux_2_28-cross:aarch64 32 | - os: ubuntu 33 | platform: linux 34 | target: armv7 35 | interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 36 | - os: ubuntu 37 | platform: linux 38 | target: x86_64 39 | manylinux: musllinux_1_1 40 | - os: ubuntu 41 | platform: linux 42 | target: aarch64 43 | manylinux: musllinux_1_1 44 | - os: ubuntu 45 | platform: linux 46 | target: armv7 47 | manylinux: musllinux_1_1 48 | interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 49 | - os: macos 50 | target: x86_64 51 | interpreter: pypy3.9 pypy3.10 pypy3.11 52 | - os: macos 53 | target: aarch64 54 | interpreter: pypy3.9 pypy3.10 pypy3.11 55 | runs-on: ${{ matrix.os }}-latest 56 | steps: 57 | - uses: actions/checkout@v5 58 | - uses: pyo3/maturin-action@v1 59 | with: 60 | rust-toolchain: stable 61 | command: build 62 | args: --release --out dist --interpreter ${{ matrix.interpreter || env.PY_ALL }} 63 | target: ${{ matrix.target }} 64 | manylinux: ${{ matrix.manylinux || 'auto' }} 65 | container: ${{ matrix.container }} 66 | docker-options: -e CI 67 | - name: Upload wheels 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: dist-${{ matrix.platform || matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'auto' }} 71 | path: dist 72 | 73 | wheels-pgo: 74 | name: pgo-wheel ${{ matrix.platform || matrix.os }} (${{ matrix.interpreter }}) 75 | strategy: 76 | fail-fast: false 77 | matrix: 78 | os: [ubuntu-latest, macos-13, macos-14] 79 | manylinux: [auto] 80 | interpreter: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"] 81 | include: 82 | - os: ubuntu-latest 83 | platform: linux 84 | 85 | runs-on: ${{ matrix.os }} 86 | env: 87 | UV_PYTHON: ${{ matrix.interpreter }} 88 | steps: 89 | - uses: actions/checkout@v5 90 | - uses: astral-sh/setup-uv@v6 91 | with: 92 | enable-cache: false 93 | - uses: dtolnay/rust-toolchain@stable 94 | with: 95 | components: llvm-tools 96 | - name: prepare profiling directory 97 | shell: bash 98 | run: mkdir -p ${{ github.workspace }}/profdata 99 | - name: Build initial wheel 100 | uses: PyO3/maturin-action@v1 101 | with: 102 | rust-toolchain: stable 103 | command: build 104 | args: --release --out pgo_wheel --interpreter ${{ matrix.interpreter }} 105 | manylinux: ${{ matrix.manylinux || 'auto' }} 106 | docker-options: -e CI 107 | env: 108 | RUSTFLAGS: "-Cprofile-generate=${{ github.workspace }}/profdata" 109 | - run: | 110 | RUST_HOST=$(rustc -Vv | grep host | cut -d ' ' -f 2) rustup run stable bash -c 'echo LLVM_PROFDATA=$RUSTUP_HOME/toolchains/$RUSTUP_TOOLCHAIN/lib/rustlib/$RUST_HOST/bin/llvm-profdata >> "$GITHUB_ENV"' 111 | shell: bash 112 | - name: Generate PGO data 113 | shell: bash 114 | run: | 115 | uv python install ${{ env.UV_PYTHON }} 116 | uv venv .venv 117 | uv sync --no-install-project --group test 118 | uv pip install rloop --no-index --no-deps --find-links pgo_wheel --force-reinstall 119 | LLVM_PROFILE_FILE=${{ github.workspace }}/profdata/rlp_%m_%p.profraw uv run --no-sync pytest tests 120 | - name: merge PGO data 121 | run: ${{ env.LLVM_PROFDATA }} merge --failure-mode=all -o ${{ github.workspace }}/merged.profdata ${{ github.workspace }}/profdata 122 | - name: Build PGO wheel 123 | uses: PyO3/maturin-action@v1 124 | with: 125 | command: build 126 | args: --release --out dist --interpreter ${{ matrix.interpreter }} 127 | manylinux: ${{ matrix.manylinux || 'auto' }} 128 | rust-toolchain: stable 129 | docker-options: -e CI 130 | env: 131 | RUSTFLAGS: "-Cprofile-use=${{ github.workspace }}/merged.profdata" 132 | - name: Upload wheels 133 | uses: actions/upload-artifact@v4 134 | with: 135 | name: dist-pgo-${{ matrix.platform || matrix.os }}-${{ matrix.interpreter }} 136 | path: dist 137 | -------------------------------------------------------------------------------- /benchmarks/client.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import math 4 | import socket 5 | import time 6 | from concurrent import futures 7 | 8 | import numpy as np 9 | 10 | 11 | _FMT_OUT = """ 12 | {messages} {size}KiB messages in {duration} seconds 13 | Latency: min {latency_min}ms; max {latency_max}ms; mean {latency_mean}ms; 14 | std: {latency_std}ms ({latency_cv}%) 15 | Latency distribution: {latency_percentiles} 16 | Requests/sec: {rps} 17 | Transfer/sec: {transfer}MiB""" 18 | 19 | 20 | def wquant(values, quantiles, weights): 21 | values = np.array(values) 22 | quantiles = np.array(quantiles) 23 | weights = np.array(weights) 24 | assert np.all(quantiles >= 0) and np.all(quantiles <= 1), 'quantiles should be in [0, 1]' 25 | 26 | wqs = np.cumsum(weights) - 0.5 * weights 27 | wqs -= wqs[0] 28 | wqs /= wqs[-1] 29 | 30 | return np.interp(quantiles, wqs, values) 31 | 32 | 33 | def bench(unix, addr, start, duration, timeout, reqsize, msg): 34 | if unix: 35 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 36 | else: 37 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 38 | 39 | sock.settimeout(timeout / 1000) 40 | sock.connect(addr) 41 | 42 | n = 0 43 | latency_stats = np.zeros((timeout * 100,)) 44 | min_latency = float('inf') 45 | max_latency = 0.0 46 | 47 | while time.monotonic() - start < duration: 48 | req_start = time.monotonic() 49 | sock.sendall(msg) 50 | nrecv = 0 51 | while nrecv < reqsize: 52 | resp = sock.recv(reqsize) 53 | if not resp: 54 | raise SystemExit() 55 | nrecv += len(resp) 56 | req_time = round((time.monotonic() - req_start) * 100000) 57 | if req_time > max_latency: 58 | max_latency = req_time 59 | if req_time < min_latency: 60 | min_latency = req_time 61 | latency_stats[req_time] += 1 62 | n += 1 63 | 64 | try: 65 | sock.close() 66 | except OSError: 67 | pass 68 | 69 | return n, latency_stats, min_latency, max_latency 70 | 71 | 72 | def run(args): 73 | unix = False 74 | if args.addr.startswith('file:'): 75 | unix = True 76 | addr = args.addr[5:] 77 | else: 78 | addr = args.addr.split(':') 79 | addr[1] = int(addr[1]) 80 | addr = tuple(addr) 81 | 82 | msg_size = args.msize 83 | msg = (b'x' * (msg_size - 1) + b'\n') * args.mpr 84 | 85 | req_size = msg_size * args.mpr 86 | 87 | timeout = args.timeout * 1000 88 | 89 | wrk = args.concurrency 90 | duration = args.duration 91 | 92 | min_latency = float('inf') 93 | max_latency = 0.0 94 | messages = 0 95 | latency_stats = None 96 | start = time.monotonic() 97 | 98 | with futures.ProcessPoolExecutor(max_workers=wrk) as e: 99 | fs = [] 100 | for _ in range(wrk): 101 | fs.append(e.submit(bench, unix, addr, start, duration, timeout, req_size, msg)) 102 | 103 | res = futures.wait(fs) 104 | for fut in res.done: 105 | t_messages, t_latency_stats, t_min_latency, t_max_latency = fut.result() 106 | messages += t_messages 107 | if latency_stats is None: 108 | latency_stats = t_latency_stats 109 | else: 110 | latency_stats = np.add(latency_stats, t_latency_stats) 111 | if t_max_latency > max_latency: 112 | max_latency = t_max_latency 113 | if t_min_latency < min_latency: 114 | min_latency = t_min_latency 115 | 116 | arange = np.arange(len(latency_stats)) 117 | mean_latency = np.average(arange, weights=latency_stats) 118 | variance = np.average((arange - mean_latency) ** 2, weights=latency_stats) 119 | latency_std = math.sqrt(variance) 120 | latency_cv = latency_std / mean_latency 121 | 122 | percentiles = [50, 75, 90, 95, 99] 123 | percentile_data = [] 124 | 125 | quantiles = wquant(arange, [p / 100 for p in percentiles], weights=latency_stats) 126 | 127 | for i, percentile in enumerate(percentiles): 128 | percentile_data.append((percentile, round(quantiles[i] / 100, 3))) 129 | 130 | data = { 131 | 'messages': messages, 132 | 'transfer': round((messages * msg_size / (1024 * 1024)) / duration, 2), 133 | 'rps': round(messages / duration, 2), 134 | 'latency_min': round(min_latency / 100, 3), 135 | 'latency_mean': round(mean_latency / 100, 3), 136 | 'latency_max': round(max_latency / 100, 3), 137 | 'latency_std': round(latency_std / 100, 3), 138 | 'latency_cv': round(latency_cv * 100, 2), 139 | 'latency_percentiles': percentile_data, 140 | } 141 | 142 | if args.output == 'json': 143 | print(json.dumps(data)) 144 | else: 145 | data['latency_percentiles'] = '; '.join('{}% under {}ms'.format(*v) for v in percentile_data) 146 | output = _FMT_OUT.format(duration=duration, size=round(msg_size / 1024, 2), **data) 147 | print(output) 148 | 149 | 150 | if __name__ == '__main__': 151 | parser = argparse.ArgumentParser() 152 | parser.add_argument('--msize', default=1024, type=int, help='message size in bytes') 153 | parser.add_argument('--mpr', default=1, type=int, help='messages per request') 154 | parser.add_argument('--duration', '-T', default=10, type=int, help='duration of test in seconds') 155 | parser.add_argument('--concurrency', default=1, type=int, help='request concurrency') 156 | parser.add_argument('--timeout', default=2, type=int, help='socket timeout in seconds') 157 | parser.add_argument('--addr', default='127.0.0.1:25000', type=str, help='server address') 158 | parser.add_argument('--output', default='text', type=str, help='output format', choices=['text', 'json']) 159 | run(parser.parse_args()) 160 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | env: 9 | PY_ALL: 3.9 3.10 3.11 3.12 3.13 3.13t 3.14 3.14t pypy3.9 pypy3.10 pypy3.11 10 | 11 | jobs: 12 | sdist: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: pyo3/maturin-action@v1 18 | with: 19 | rust-toolchain: stable 20 | command: sdist 21 | args: --out dist 22 | - name: Upload sdist 23 | uses: actions/upload-artifact@v4 24 | with: 25 | name: dist-sdist 26 | path: dist 27 | 28 | wheels: 29 | name: wheel ${{ matrix.platform || matrix.os }}(${{ matrix.target }}) - ${{ matrix.manylinux || 'auto' }} 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | os: [ubuntu, macos] 34 | target: [x86_64, aarch64] 35 | manylinux: [auto] 36 | include: 37 | - os: ubuntu 38 | platform: linux 39 | target: x86_64 40 | manylinux: auto 41 | interpreter: pypy3.9 pypy3.10 pypy3.11 42 | - os: ubuntu 43 | platform: linux 44 | target: i686 45 | interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 46 | - os: ubuntu 47 | platform: linux 48 | target: aarch64 49 | # rust-cross/manylinux2014-cross:aarch64 has issues with `ring` 50 | #container: ghcr.io/rust-cross/manylinux_2_28-cross:aarch64 51 | - os: ubuntu 52 | platform: linux 53 | target: armv7 54 | interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 55 | - os: ubuntu 56 | platform: linux 57 | target: x86_64 58 | manylinux: musllinux_1_1 59 | - os: ubuntu 60 | platform: linux 61 | target: aarch64 62 | manylinux: musllinux_1_1 63 | - os: ubuntu 64 | platform: linux 65 | target: armv7 66 | manylinux: musllinux_1_1 67 | interpreter: 3.9 3.10 3.11 3.12 3.13 3.14 68 | - os: macos 69 | target: x86_64 70 | interpreter: pypy3.9 pypy3.10 pypy3.11 71 | - os: macos 72 | target: aarch64 73 | interpreter: pypy3.9 pypy3.10 pypy3.11 74 | runs-on: ${{ matrix.os }}-latest 75 | steps: 76 | - uses: actions/checkout@v5 77 | - uses: pyo3/maturin-action@v1 78 | with: 79 | rust-toolchain: stable 80 | command: build 81 | args: --release --out dist --interpreter ${{ matrix.interpreter || env.PY_ALL }} 82 | target: ${{ matrix.target }} 83 | manylinux: ${{ matrix.manylinux || 'auto' }} 84 | container: ${{ matrix.container }} 85 | docker-options: -e CI 86 | - name: Upload wheels 87 | uses: actions/upload-artifact@v4 88 | with: 89 | name: dist-${{ matrix.platform || matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'auto' }} 90 | path: dist 91 | 92 | wheels-pgo: 93 | name: pgo-wheel ${{ matrix.platform || matrix.os }} (${{ matrix.interpreter }}) 94 | strategy: 95 | fail-fast: false 96 | matrix: 97 | os: [ubuntu-latest, macos-13, macos-14] 98 | manylinux: [auto] 99 | interpreter: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"] 100 | include: 101 | - os: ubuntu-latest 102 | platform: linux 103 | 104 | runs-on: ${{ matrix.os }} 105 | env: 106 | UV_PYTHON: ${{ matrix.interpreter }} 107 | steps: 108 | - uses: actions/checkout@v5 109 | - uses: astral-sh/setup-uv@v6 110 | with: 111 | enable-cache: false 112 | - uses: dtolnay/rust-toolchain@stable 113 | with: 114 | components: llvm-tools 115 | - name: prepare profiling directory 116 | shell: bash 117 | run: mkdir -p ${{ github.workspace }}/profdata 118 | - name: Build initial wheel 119 | uses: PyO3/maturin-action@v1 120 | with: 121 | rust-toolchain: stable 122 | command: build 123 | args: --release --out pgo_wheel --interpreter ${{ matrix.interpreter }} 124 | manylinux: ${{ matrix.manylinux || 'auto' }} 125 | docker-options: -e CI 126 | env: 127 | RUSTFLAGS: "-Cprofile-generate=${{ github.workspace }}/profdata" 128 | - run: | 129 | RUST_HOST=$(rustc -Vv | grep host | cut -d ' ' -f 2) rustup run stable bash -c 'echo LLVM_PROFDATA=$RUSTUP_HOME/toolchains/$RUSTUP_TOOLCHAIN/lib/rustlib/$RUST_HOST/bin/llvm-profdata >> "$GITHUB_ENV"' 130 | shell: bash 131 | - name: Generate PGO data 132 | shell: bash 133 | run: | 134 | uv python install ${{ env.UV_PYTHON }} 135 | uv venv .venv 136 | uv sync --no-install-project --group test 137 | uv pip install rloop --no-index --no-deps --find-links pgo_wheel --force-reinstall 138 | LLVM_PROFILE_FILE=${{ github.workspace }}/profdata/rlp_%m_%p.profraw uv run --no-sync pytest tests 139 | - name: merge PGO data 140 | run: ${{ env.LLVM_PROFDATA }} merge --failure-mode=all -o ${{ github.workspace }}/merged.profdata ${{ github.workspace }}/profdata 141 | - name: Build PGO wheel 142 | uses: PyO3/maturin-action@v1 143 | with: 144 | command: build 145 | args: --release --out dist --interpreter ${{ matrix.interpreter }} 146 | manylinux: ${{ matrix.manylinux || 'auto' }} 147 | rust-toolchain: stable 148 | docker-options: -e CI 149 | env: 150 | RUSTFLAGS: "-Cprofile-use=${{ github.workspace }}/merged.profdata" 151 | - name: Upload wheels 152 | uses: actions/upload-artifact@v4 153 | with: 154 | name: dist-pgo-${{ matrix.platform || matrix.os }}-${{ matrix.interpreter }} 155 | path: dist 156 | 157 | release: 158 | runs-on: ubuntu-latest 159 | needs: [ sdist, wheels, wheels-pgo ] 160 | environment: 161 | name: pypi 162 | url: https://pypi.org/p/rloop 163 | permissions: 164 | id-token: write 165 | 166 | steps: 167 | - uses: actions/download-artifact@v5 168 | with: 169 | pattern: dist-* 170 | merge-multiple: true 171 | path: dist 172 | - name: Publish package to pypi 173 | uses: pypa/gh-action-pypi-publish@release/v1 174 | with: 175 | skip-existing: true 176 | -------------------------------------------------------------------------------- /src/handles.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{IntoPyObjectExt, prelude::*}; 2 | use std::sync::atomic; 3 | 4 | use crate::{ 5 | event_loop::{EventLoop, EventLoopRunState}, 6 | log::LogExc, 7 | py::{run_in_ctx, run_in_ctx0}, 8 | }; 9 | 10 | #[cfg(not(PyPy))] 11 | use crate::py::run_in_ctx1; 12 | 13 | pub trait Handle { 14 | fn run(&self, py: Python, event_loop: &EventLoop, state: &mut EventLoopRunState); 15 | fn cancelled(&self) -> bool { 16 | false 17 | } 18 | } 19 | 20 | pub(crate) type BoxedHandle = Box; 21 | 22 | #[pyclass(frozen, module = "rloop._rloop")] 23 | pub(crate) struct CBHandle { 24 | callback: Py, 25 | args: Py, 26 | context: Py, 27 | cancelled: atomic::AtomicBool, 28 | } 29 | 30 | #[pyclass(frozen, module = "rloop._rloop", name = "CBHandle0")] 31 | pub(crate) struct CBHandleNoArgs { 32 | callback: Py, 33 | context: Py, 34 | cancelled: atomic::AtomicBool, 35 | } 36 | 37 | #[pyclass(frozen, module = "rloop._rloop", name = "CBHandle1")] 38 | pub(crate) struct CBHandleOneArg { 39 | callback: Py, 40 | arg: Py, 41 | context: Py, 42 | cancelled: atomic::AtomicBool, 43 | } 44 | 45 | impl CBHandle { 46 | pub(crate) fn new(callback: Py, args: Py, context: Py) -> Self { 47 | Self { 48 | callback, 49 | args, 50 | context, 51 | cancelled: false.into(), 52 | } 53 | } 54 | 55 | #[allow(dead_code)] 56 | pub(crate) fn new0(callback: Py, context: Py) -> CBHandleNoArgs { 57 | CBHandleNoArgs { 58 | callback, 59 | context, 60 | cancelled: false.into(), 61 | } 62 | } 63 | 64 | pub(crate) fn new1(callback: Py, arg: Py, context: Py) -> CBHandleOneArg { 65 | CBHandleOneArg { 66 | callback, 67 | arg, 68 | context, 69 | cancelled: false.into(), 70 | } 71 | } 72 | } 73 | 74 | macro_rules! cbhandle_cancel_impl { 75 | ($handle:ident) => { 76 | #[pymethods] 77 | impl $handle { 78 | fn cancel(&self) { 79 | self.cancelled.store(true, atomic::Ordering::Relaxed); 80 | } 81 | } 82 | }; 83 | } 84 | 85 | macro_rules! cbhandle_cancelled_impl { 86 | () => { 87 | #[inline] 88 | fn cancelled(&self) -> bool { 89 | self.get().cancelled.load(atomic::Ordering::Relaxed) 90 | } 91 | }; 92 | } 93 | 94 | cbhandle_cancel_impl!(CBHandle); 95 | cbhandle_cancel_impl!(CBHandleNoArgs); 96 | cbhandle_cancel_impl!(CBHandleOneArg); 97 | 98 | impl Handle for Py { 99 | cbhandle_cancelled_impl!(); 100 | 101 | fn run(&self, py: Python, event_loop: &EventLoop, _state: &mut EventLoopRunState) { 102 | let rself = self.get(); 103 | let ctx = rself.context.as_ptr(); 104 | let cb = rself.callback.as_ptr(); 105 | let args = rself.args.as_ptr(); 106 | 107 | if let Err(err) = run_in_ctx!(py, ctx, cb, args) { 108 | let err_ctx = LogExc::cb_handle( 109 | err, 110 | format!("Exception in callback {:?}", rself.callback.bind(py)), 111 | self.clone_ref(py).into_py_any(py).unwrap(), 112 | ); 113 | _ = event_loop.log_exception(py, err_ctx); 114 | } 115 | } 116 | } 117 | 118 | impl Handle for Py { 119 | cbhandle_cancelled_impl!(); 120 | 121 | fn run(&self, py: Python, event_loop: &EventLoop, _state: &mut EventLoopRunState) { 122 | let rself = self.get(); 123 | let ctx = rself.context.as_ptr(); 124 | let cb = rself.callback.as_ptr(); 125 | 126 | if let Err(err) = run_in_ctx0!(py, ctx, cb) { 127 | let err_ctx = LogExc::cb_handle( 128 | err, 129 | format!("Exception in callback {:?}", rself.callback.bind(py)), 130 | self.clone_ref(py).into_py_any(py).unwrap(), 131 | ); 132 | _ = event_loop.log_exception(py, err_ctx); 133 | } 134 | } 135 | } 136 | 137 | impl Handle for Py { 138 | #[cfg(not(PyPy))] 139 | fn run(&self, py: Python, event_loop: &EventLoop, _state: &mut EventLoopRunState) { 140 | let rself = self.get(); 141 | let ctx = rself.context.as_ptr(); 142 | let cb = rself.callback.as_ptr(); 143 | let arg = rself.arg.as_ptr(); 144 | 145 | if let Err(err) = run_in_ctx1!(py, ctx, cb, arg) { 146 | let err_ctx = LogExc::cb_handle( 147 | err, 148 | format!("Exception in callback {:?}", rself.callback.bind(py)), 149 | self.clone_ref(py).into_py_any(py).unwrap(), 150 | ); 151 | _ = event_loop.log_exception(py, err_ctx); 152 | } 153 | } 154 | 155 | #[cfg(PyPy)] 156 | fn run(&self, py: Python, event_loop: &EventLoop, _state: &mut EventLoopRunState) { 157 | let rself = self.get(); 158 | let ctx = rself.context.as_ptr(); 159 | let cb = rself.callback.as_ptr(); 160 | let args = (rself.arg.clone_ref(py),).into_py_any(py).unwrap().into_ptr(); 161 | 162 | if let Err(err) = run_in_ctx!(py, ctx, cb, args) { 163 | let err_ctx = LogExc::cb_handle( 164 | err, 165 | format!("Exception in callback {:?}", rself.callback.bind(py)), 166 | self.clone_ref(py).into_py_any(py).unwrap(), 167 | ); 168 | _ = event_loop.log_exception(py, err_ctx); 169 | } 170 | } 171 | } 172 | 173 | #[pyclass(frozen, module = "rloop._rloop")] 174 | pub(crate) struct TimerHandle { 175 | pub handle: Py, 176 | #[pyo3(get)] 177 | when: f64, 178 | } 179 | 180 | impl TimerHandle { 181 | #[allow(clippy::cast_precision_loss)] 182 | pub(crate) fn new(handle: Py, when: u128) -> Self { 183 | Self { 184 | handle, 185 | when: (when as f64) / 1_000_000.0, 186 | } 187 | } 188 | } 189 | 190 | #[pymethods] 191 | impl TimerHandle { 192 | fn cancel(&self) { 193 | self.handle.get().cancel(); 194 | } 195 | 196 | fn cancelled(&self) -> bool { 197 | self.handle.cancelled() 198 | } 199 | } 200 | 201 | pub(crate) fn init_pymodule(module: &Bound) -> PyResult<()> { 202 | module.add_class::()?; 203 | module.add_class::()?; 204 | 205 | Ok(()) 206 | } 207 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.99" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 10 | 11 | [[package]] 12 | name = "autocfg" 13 | version = "1.5.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 16 | 17 | [[package]] 18 | name = "cc" 19 | version = "1.2.37" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" 22 | dependencies = [ 23 | "find-msvc-tools", 24 | "shlex", 25 | ] 26 | 27 | [[package]] 28 | name = "equivalent" 29 | version = "1.0.2" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 32 | 33 | [[package]] 34 | name = "find-msvc-tools" 35 | version = "0.1.1" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" 38 | 39 | [[package]] 40 | name = "heck" 41 | version = "0.5.0" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 44 | 45 | [[package]] 46 | name = "indoc" 47 | version = "2.0.6" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 50 | 51 | [[package]] 52 | name = "libc" 53 | version = "0.2.175" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 56 | 57 | [[package]] 58 | name = "log" 59 | version = "0.4.28" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 62 | 63 | [[package]] 64 | name = "memoffset" 65 | version = "0.9.1" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 68 | dependencies = [ 69 | "autocfg", 70 | ] 71 | 72 | [[package]] 73 | name = "mio" 74 | version = "1.0.4" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 77 | dependencies = [ 78 | "libc", 79 | "log", 80 | "wasi", 81 | "windows-sys 0.59.0", 82 | ] 83 | 84 | [[package]] 85 | name = "once_cell" 86 | version = "1.21.3" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 89 | 90 | [[package]] 91 | name = "papaya" 92 | version = "0.2.3" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" 95 | dependencies = [ 96 | "equivalent", 97 | "seize", 98 | ] 99 | 100 | [[package]] 101 | name = "portable-atomic" 102 | version = "1.11.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 105 | 106 | [[package]] 107 | name = "proc-macro2" 108 | version = "1.0.101" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 111 | dependencies = [ 112 | "unicode-ident", 113 | ] 114 | 115 | [[package]] 116 | name = "pyo3" 117 | version = "0.26.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383" 120 | dependencies = [ 121 | "anyhow", 122 | "indoc", 123 | "libc", 124 | "memoffset", 125 | "once_cell", 126 | "portable-atomic", 127 | "pyo3-build-config", 128 | "pyo3-ffi", 129 | "pyo3-macros", 130 | "unindent", 131 | ] 132 | 133 | [[package]] 134 | name = "pyo3-build-config" 135 | version = "0.26.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" 138 | dependencies = [ 139 | "python3-dll-a", 140 | "target-lexicon", 141 | ] 142 | 143 | [[package]] 144 | name = "pyo3-ffi" 145 | version = "0.26.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105" 148 | dependencies = [ 149 | "libc", 150 | "pyo3-build-config", 151 | ] 152 | 153 | [[package]] 154 | name = "pyo3-macros" 155 | version = "0.26.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded" 158 | dependencies = [ 159 | "proc-macro2", 160 | "pyo3-macros-backend", 161 | "quote", 162 | "syn", 163 | ] 164 | 165 | [[package]] 166 | name = "pyo3-macros-backend" 167 | version = "0.26.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf" 170 | dependencies = [ 171 | "heck", 172 | "proc-macro2", 173 | "pyo3-build-config", 174 | "quote", 175 | "syn", 176 | ] 177 | 178 | [[package]] 179 | name = "python3-dll-a" 180 | version = "0.2.14" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "d381ef313ae70b4da5f95f8a4de773c6aa5cd28f73adec4b4a31df70b66780d8" 183 | dependencies = [ 184 | "cc", 185 | ] 186 | 187 | [[package]] 188 | name = "quote" 189 | version = "1.0.40" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 192 | dependencies = [ 193 | "proc-macro2", 194 | ] 195 | 196 | [[package]] 197 | name = "rloop" 198 | version = "0.2.0" 199 | dependencies = [ 200 | "anyhow", 201 | "libc", 202 | "mio", 203 | "papaya", 204 | "pyo3", 205 | "pyo3-build-config", 206 | "socket2", 207 | ] 208 | 209 | [[package]] 210 | name = "seize" 211 | version = "0.5.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "e4b8d813387d566f627f3ea1b914c068aac94c40ae27ec43f5f33bde65abefe7" 214 | dependencies = [ 215 | "libc", 216 | "windows-sys 0.52.0", 217 | ] 218 | 219 | [[package]] 220 | name = "shlex" 221 | version = "1.3.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 224 | 225 | [[package]] 226 | name = "socket2" 227 | version = "0.6.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 230 | dependencies = [ 231 | "libc", 232 | "windows-sys 0.59.0", 233 | ] 234 | 235 | [[package]] 236 | name = "syn" 237 | version = "2.0.106" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 240 | dependencies = [ 241 | "proc-macro2", 242 | "quote", 243 | "unicode-ident", 244 | ] 245 | 246 | [[package]] 247 | name = "target-lexicon" 248 | version = "0.13.3" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" 251 | 252 | [[package]] 253 | name = "unicode-ident" 254 | version = "1.0.19" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 257 | 258 | [[package]] 259 | name = "unindent" 260 | version = "0.2.4" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" 263 | 264 | [[package]] 265 | name = "wasi" 266 | version = "0.11.1+wasi-snapshot-preview1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 269 | 270 | [[package]] 271 | name = "windows-sys" 272 | version = "0.52.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 275 | dependencies = [ 276 | "windows-targets", 277 | ] 278 | 279 | [[package]] 280 | name = "windows-sys" 281 | version = "0.59.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 284 | dependencies = [ 285 | "windows-targets", 286 | ] 287 | 288 | [[package]] 289 | name = "windows-targets" 290 | version = "0.52.6" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 293 | dependencies = [ 294 | "windows_aarch64_gnullvm", 295 | "windows_aarch64_msvc", 296 | "windows_i686_gnu", 297 | "windows_i686_gnullvm", 298 | "windows_i686_msvc", 299 | "windows_x86_64_gnu", 300 | "windows_x86_64_gnullvm", 301 | "windows_x86_64_msvc", 302 | ] 303 | 304 | [[package]] 305 | name = "windows_aarch64_gnullvm" 306 | version = "0.52.6" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 309 | 310 | [[package]] 311 | name = "windows_aarch64_msvc" 312 | version = "0.52.6" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 315 | 316 | [[package]] 317 | name = "windows_i686_gnu" 318 | version = "0.52.6" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 321 | 322 | [[package]] 323 | name = "windows_i686_gnullvm" 324 | version = "0.52.6" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 327 | 328 | [[package]] 329 | name = "windows_i686_msvc" 330 | version = "0.52.6" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 333 | 334 | [[package]] 335 | name = "windows_x86_64_gnu" 336 | version = "0.52.6" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 339 | 340 | [[package]] 341 | name = "windows_x86_64_gnullvm" 342 | version = "0.52.6" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 345 | 346 | [[package]] 347 | name = "windows_x86_64_msvc" 348 | version = "0.52.6" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 351 | -------------------------------------------------------------------------------- /rloop/subprocess.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | import subprocess 4 | import threading 5 | import warnings 6 | from asyncio import base_subprocess as _base_subp, transports as _transports 7 | 8 | from .exc import _aio_logger 9 | from .transports import _TransportFlowControl 10 | 11 | 12 | class _ChildWatcher: 13 | __slots__ = ['loop'] 14 | 15 | def __init__(self, loop): 16 | self._loop = loop 17 | 18 | @staticmethod 19 | def _waitstatus_to_exitcode(status): 20 | try: 21 | return os.waitstatus_to_exitcode(status) 22 | except ValueError: 23 | return status 24 | 25 | 26 | class _PidfdChildWatcher(_ChildWatcher): 27 | def add_child_handler(self, pid, callback, *args): 28 | pidfd = os.pidfd_open(pid) 29 | self._loop.add_reader(pidfd, self._do_wait, pid, pidfd, callback, args) 30 | 31 | def _do_wait(self, pid, pidfd, callback, args): 32 | self._loop._remove_reader(pidfd) 33 | try: 34 | _, status = os.waitpid(pid, 0) 35 | except ChildProcessError: 36 | # The child process is already reaped 37 | # (may happen if waitpid() is called elsewhere). 38 | returncode = 255 39 | _aio_logger.warning('child process pid %d exit status already read: will report returncode 255', pid) 40 | else: 41 | returncode = self._waitstatus_to_exitcode(status) 42 | 43 | os.close(pidfd) 44 | callback(pid, returncode, *args) 45 | 46 | 47 | class _ThreadedChildWatcher(_ChildWatcher): 48 | def __init__(self, loop): 49 | super().__init__(loop) 50 | self._pid_counter = itertools.count(0) 51 | self._threads = {} 52 | 53 | def __del__(self, _warn=warnings.warn): 54 | threads = [thread for thread in list(self._threads.values()) if thread.is_alive()] 55 | if threads: 56 | _warn(f'{self.__class__} has registered but not finished child processes', ResourceWarning, source=self) 57 | 58 | def add_child_handler(self, pid, callback, *args): 59 | thread = threading.Thread( 60 | target=self._do_waitpid, 61 | name=f'asyncio-waitpid-{next(self._pid_counter)}', 62 | args=(self._loop, pid, callback, args), 63 | daemon=True, 64 | ) 65 | self._threads[pid] = thread 66 | thread.start() 67 | 68 | def _do_waitpid(self, loop, expected_pid, callback, args): 69 | assert expected_pid > 0 70 | 71 | try: 72 | pid, status = os.waitpid(expected_pid, 0) 73 | except ChildProcessError: 74 | # The child process is already reaped 75 | # (may happen if waitpid() is called elsewhere). 76 | pid = expected_pid 77 | returncode = 255 78 | _aio_logger.warning('Unknown child process pid %d, will report returncode 255', pid) 79 | else: 80 | returncode = self._waitstatus_to_exitcode(status) 81 | if loop.get_debug(): 82 | _aio_logger.debug('process %s exited with returncode %s', expected_pid, returncode) 83 | 84 | if loop.is_closed(): 85 | _aio_logger.warning('Loop %r that handles pid %r is closed', loop, pid) 86 | else: 87 | loop.call_soon_threadsafe(callback, pid, returncode, *args) 88 | 89 | self._threads.pop(expected_pid) 90 | 91 | 92 | class _SubProcessTransport(_base_subp.BaseSubprocessTransport): 93 | def _start(self, args, shell, stdin, stdout, stderr, bufsize, **kwargs): 94 | self._proc = subprocess.Popen( # noqa: S603 95 | args, 96 | shell=shell, 97 | stdin=stdin, 98 | stdout=stdout, 99 | stderr=stderr, 100 | universal_newlines=False, 101 | bufsize=bufsize, 102 | **kwargs, 103 | ) 104 | 105 | 106 | class _PipeReadTransport(_transports.ReadTransport): 107 | max_size = 256 * 1024 108 | 109 | def __init__(self, loop, pipe, protocol, waiter, extra=None): 110 | self._loop = loop 111 | self._pipe = pipe 112 | self._fileno = pipe.fileno() 113 | self._protocol = protocol 114 | self._closing = False 115 | self._paused = False 116 | 117 | os.set_blocking(self._fileno, False) 118 | 119 | self._loop.call_soon(self._protocol.connection_made, self) 120 | # only start reading when connection_made() has been called 121 | self._loop.call_soon(self._add_reader, self._fileno, self._read_ready) 122 | # only wake up the waiter when connection_made() has been called 123 | self._loop.call_soon(self._connection_made_waiter_cb, waiter, None) 124 | 125 | @staticmethod 126 | def _connection_made_waiter_cb(fut, result): 127 | if fut.cancelled(): 128 | return 129 | fut.set_result(result) 130 | 131 | def _add_reader(self, fd, callback): 132 | if not self.is_reading(): 133 | return 134 | self._loop.add_reader(fd, callback) 135 | 136 | def is_reading(self): 137 | return not self._paused and not self._closing 138 | 139 | def _read_ready(self): 140 | try: 141 | data = os.read(self._fileno, self.max_size) 142 | except (BlockingIOError, InterruptedError): 143 | pass 144 | except OSError as exc: 145 | self._fatal_error(exc, 'Fatal read error on pipe transport') 146 | else: 147 | if data: 148 | self._protocol.data_received(data) 149 | else: 150 | self._closing = True 151 | self._loop.remove_reader(self._fileno) 152 | self._loop.call_soon(self._protocol.eof_received) 153 | self._loop.call_soon(self._call_connection_lost, None) 154 | 155 | def pause_reading(self): 156 | if not self.is_reading(): 157 | return 158 | self._paused = True 159 | self._loop.remove_reader(self._fileno) 160 | 161 | def resume_reading(self): 162 | if self._closing or not self._paused: 163 | return 164 | self._paused = False 165 | self._loop.add_reader(self._fileno, self._read_ready) 166 | 167 | def set_protocol(self, protocol): 168 | self._protocol = protocol 169 | 170 | def get_protocol(self): 171 | return self._protocol 172 | 173 | def is_closing(self): 174 | return self._closing 175 | 176 | def close(self): 177 | if not self._closing: 178 | self._close(None) 179 | 180 | def __del__(self, _warn=warnings.warn): 181 | if self._pipe is not None: 182 | _warn(f'unclosed transport {self!r}', ResourceWarning, source=self) 183 | self._pipe.close() 184 | 185 | def _fatal_error(self, exc, message='Fatal error on pipe transport'): 186 | self._loop.call_exception_handler( 187 | { 188 | 'message': message, 189 | 'exception': exc, 190 | 'transport': self, 191 | 'protocol': self._protocol, 192 | } 193 | ) 194 | self._close(exc) 195 | 196 | def _close(self, exc): 197 | self._closing = True 198 | self._loop.remove_reader(self._fileno) 199 | self._loop.call_soon(self._call_connection_lost, exc) 200 | 201 | def _call_connection_lost(self, exc): 202 | try: 203 | self._protocol.connection_lost(exc) 204 | finally: 205 | self._pipe.close() 206 | self._pipe = None 207 | self._protocol = None 208 | self._loop = None 209 | 210 | 211 | class _PipeWriteTransport(_TransportFlowControl, _transports.WriteTransport): 212 | def __init__(self, loop, pipe, protocol, waiter, extra=None): 213 | super().__init__(loop) 214 | self._pipe = pipe 215 | self._fileno = pipe.fileno() 216 | self._protocol = protocol 217 | self._buffer = bytearray() 218 | self._conn_lost = 0 219 | self._closing = False # Set when close() or write_eof() called. 220 | 221 | os.set_blocking(self._fileno, False) 222 | 223 | self._loop.call_soon(self._protocol.connection_made, self) 224 | # only start reading when connection_made() has been called 225 | self._loop.call_soon(self._add_reader, self._fileno, self._read_ready) 226 | # only wake up the waiter when connection_made() has been called 227 | self._loop.call_soon(self._connection_made_waiter_cb, waiter, None) 228 | 229 | @staticmethod 230 | def _connection_made_waiter_cb(fut, result): 231 | if fut.cancelled(): 232 | return 233 | fut.set_result(result) 234 | 235 | def _add_reader(self, fd, callback): 236 | self._loop.add_reader(fd, callback) 237 | 238 | def get_write_buffer_size(self): 239 | return len(self._buffer) 240 | 241 | def _read_ready(self): 242 | # Pipe was closed by peer. 243 | if self._buffer: 244 | self._close(BrokenPipeError()) 245 | else: 246 | self._close() 247 | 248 | def write(self, data): 249 | if isinstance(data, bytearray): 250 | data = memoryview(data) 251 | if not data: 252 | return 253 | 254 | if self._conn_lost or self._closing: 255 | self._conn_lost += 1 256 | return 257 | 258 | if not self._buffer: 259 | # Attempt to send it right away first. 260 | try: 261 | n = os.write(self._fileno, data) 262 | except (BlockingIOError, InterruptedError): 263 | n = 0 264 | except (SystemExit, KeyboardInterrupt): 265 | raise 266 | except BaseException as exc: 267 | self._conn_lost += 1 268 | self._fatal_error(exc, 'Fatal write error on pipe transport') 269 | return 270 | if n == len(data): 271 | return 272 | elif n > 0: 273 | data = memoryview(data)[n:] 274 | self._loop.add_writer(self._fileno, self._write_ready) 275 | 276 | self._buffer += data 277 | self._maybe_pause_protocol() 278 | 279 | def _write_ready(self): 280 | assert self._buffer, 'Data should not be empty' 281 | 282 | try: 283 | n = os.write(self._fileno, self._buffer) 284 | except (BlockingIOError, InterruptedError): 285 | pass 286 | except (SystemExit, KeyboardInterrupt): 287 | raise 288 | except BaseException as exc: 289 | self._buffer.clear() 290 | self._conn_lost += 1 291 | # Remove writer here, _fatal_error() doesn't it 292 | # because _buffer is empty. 293 | self._loop.remove_writer(self._fileno) 294 | self._fatal_error(exc, 'Fatal write error on pipe transport') 295 | else: 296 | if n == len(self._buffer): 297 | self._buffer.clear() 298 | self._loop.remove_writer(self._fileno) 299 | self._maybe_resume_protocol() # May append to buffer. 300 | if self._closing: 301 | self._loop.remove_reader(self._fileno) 302 | self._call_connection_lost(None) 303 | return 304 | elif n > 0: 305 | del self._buffer[:n] 306 | 307 | def can_write_eof(self): 308 | return True 309 | 310 | def write_eof(self): 311 | if self._closing: 312 | return 313 | assert self._pipe 314 | self._closing = True 315 | if not self._buffer: 316 | self._loop.remove_reader(self._fileno) 317 | self._loop.call_soon(self._call_connection_lost, None) 318 | 319 | def set_protocol(self, protocol): 320 | self._protocol = protocol 321 | 322 | def get_protocol(self): 323 | return self._protocol 324 | 325 | def is_closing(self): 326 | return self._closing 327 | 328 | def close(self): 329 | if self._pipe is not None and not self._closing: 330 | # write_eof is all what we needed to close the write pipe 331 | self.write_eof() 332 | 333 | def __del__(self, _warn=warnings.warn): 334 | if self._pipe is not None: 335 | _warn(f'unclosed transport {self!r}', ResourceWarning, source=self) 336 | self._pipe.close() 337 | 338 | def abort(self): 339 | self._close(None) 340 | 341 | def _fatal_error(self, exc, message='Fatal error on pipe transport'): 342 | self._loop.call_exception_handler( 343 | { 344 | 'message': message, 345 | 'exception': exc, 346 | 'transport': self, 347 | 'protocol': self._protocol, 348 | } 349 | ) 350 | self._close(exc) 351 | 352 | def _close(self, exc=None): 353 | self._closing = True 354 | if self._buffer: 355 | self._loop.remove_writer(self._fileno) 356 | self._buffer.clear() 357 | self._loop.remove_reader(self._fileno) 358 | self._loop.call_soon(self._call_connection_lost, exc) 359 | 360 | def _call_connection_lost(self, exc): 361 | try: 362 | self._protocol.connection_lost(exc) 363 | finally: 364 | self._pipe.close() 365 | self._pipe = None 366 | self._protocol = None 367 | self._loop = None 368 | -------------------------------------------------------------------------------- /src/udp.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use std::os::fd::{AsRawFd, FromRawFd}; 3 | 4 | use mio::{Interest, net::UdpSocket}; 5 | use pyo3::{IntoPyObject, IntoPyObjectExt, prelude::*, types::PyBytes}; 6 | use std::{ 7 | borrow::Cow, 8 | cell::RefCell, 9 | collections::{HashMap, VecDeque}, 10 | io::ErrorKind, 11 | net::{IpAddr, SocketAddr}, 12 | str::FromStr, 13 | sync::atomic, 14 | }; 15 | 16 | use crate::{ 17 | event_loop::{EventLoop, EventLoopRunState}, 18 | handles::Handle, 19 | log::LogExc, 20 | sock::SocketWrapper, 21 | }; 22 | 23 | struct UDPTransportState { 24 | socket: UdpSocket, 25 | remote_addr: Option, 26 | write_buf: VecDeque<(Box<[u8]>, SocketAddr)>, 27 | write_buf_dsize: usize, 28 | } 29 | 30 | #[pyclass(frozen, unsendable, module = "rloop._rloop")] 31 | pub(crate) struct UDPTransport { 32 | pub fd: usize, 33 | state: RefCell, 34 | pyloop: Py, 35 | // atomics 36 | closing: atomic::AtomicBool, 37 | water_hi: atomic::AtomicUsize, 38 | water_lo: atomic::AtomicUsize, 39 | // py protocol fields 40 | proto: Py, 41 | proto_paused: atomic::AtomicBool, 42 | protom_conn_lost: Py, 43 | protom_datagram_received: Py, 44 | protom_error_received: Py, 45 | // py extras 46 | extra: HashMap>, 47 | sock: Py, 48 | } 49 | 50 | impl UDPTransport { 51 | fn new( 52 | py: Python, 53 | pyloop: Py, 54 | socket: UdpSocket, 55 | pyproto: Bound, 56 | socket_family: i32, 57 | remote_addr: Option, 58 | ) -> Self { 59 | let fd = socket.as_raw_fd() as usize; 60 | let state = UDPTransportState { 61 | socket, 62 | remote_addr, 63 | write_buf: VecDeque::new(), 64 | write_buf_dsize: 0, 65 | }; 66 | 67 | let wh = 1024 * 64; 68 | let wl = wh / 4; 69 | 70 | let protom_conn_lost = pyproto.getattr(pyo3::intern!(py, "connection_lost")).unwrap().unbind(); 71 | let protom_datagram_received = pyproto 72 | .getattr(pyo3::intern!(py, "datagram_received")) 73 | .unwrap() 74 | .unbind(); 75 | let protom_error_received = pyproto.getattr(pyo3::intern!(py, "error_received")).unwrap().unbind(); 76 | let proto = pyproto.unbind(); 77 | 78 | Self { 79 | fd, 80 | state: RefCell::new(state), 81 | pyloop, 82 | closing: false.into(), 83 | water_hi: wh.into(), 84 | water_lo: wl.into(), 85 | proto, 86 | proto_paused: false.into(), 87 | protom_conn_lost, 88 | protom_datagram_received, 89 | protom_error_received, 90 | extra: HashMap::new(), 91 | sock: SocketWrapper::from_fd(py, fd, socket_family, socket2::Type::DGRAM, 0), 92 | } 93 | } 94 | 95 | pub(crate) fn from_py( 96 | py: Python, 97 | pyloop: &Py, 98 | pysock: (i32, i32), 99 | proto_factory: Py, 100 | remote_addr_tup: Option<(String, u16)>, 101 | ) -> Self { 102 | let sock = unsafe { socket2::Socket::from_raw_fd(pysock.0) }; 103 | _ = sock.set_nonblocking(true); 104 | let stds: std::net::UdpSocket = sock.into(); 105 | let socket = UdpSocket::from_std(stds); 106 | 107 | let remote_addr = remote_addr_tup.map(|v| SocketAddr::new(IpAddr::from_str(&v.0).unwrap(), v.1)); 108 | let proto = proto_factory.bind(py).call0().unwrap(); 109 | 110 | Self::new(py, pyloop.clone_ref(py), socket, proto, pysock.1, remote_addr) 111 | } 112 | 113 | pub(crate) fn attach(pyself: &Py, py: Python) -> PyResult> { 114 | let rself = pyself.borrow(py); 115 | rself 116 | .proto 117 | .call_method1(py, pyo3::intern!(py, "connection_made"), (pyself.clone_ref(py),))?; 118 | Ok(rself.proto.clone_ref(py)) 119 | } 120 | 121 | #[inline] 122 | fn write_buf_size_decr(pyself: &Py, py: Python) { 123 | let rself = pyself.borrow(py); 124 | if rself.state.borrow().write_buf_dsize <= rself.water_lo.load(atomic::Ordering::Relaxed) 125 | && rself 126 | .proto_paused 127 | .compare_exchange(true, false, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 128 | .is_ok() 129 | { 130 | Self::proto_resume(pyself, py); 131 | } 132 | } 133 | 134 | #[inline] 135 | fn close_from_write_handle(&self, py: Python, errored: bool) -> bool { 136 | if self.closing.load(atomic::Ordering::Relaxed) { 137 | _ = self.protom_conn_lost.call1( 138 | py, 139 | #[allow(clippy::obfuscated_if_else)] 140 | (errored 141 | .then(|| { 142 | pyo3::exceptions::PyRuntimeError::new_err("socket transport failed") 143 | .into_py_any(py) 144 | .unwrap() 145 | }) 146 | .unwrap_or_else(|| py.None()),), 147 | ); 148 | return true; 149 | } 150 | false 151 | } 152 | 153 | #[inline(always)] 154 | fn call_conn_lost(&self, py: Python, exc: Option) { 155 | _ = self.protom_conn_lost.call1(py, (exc,)); 156 | } 157 | 158 | #[inline] 159 | fn call_datagram_received(&self, py: Python, data: &[u8], addr: SocketAddr) { 160 | let py_data = PyBytes::new(py, data); 161 | let py_addr = (addr.ip().to_string(), addr.port()).into_pyobject(py).unwrap(); 162 | _ = self.protom_datagram_received.call1(py, (py_data, py_addr)); 163 | } 164 | 165 | #[inline] 166 | fn call_error_received(&self, py: Python, exc: PyErr) { 167 | _ = self.protom_error_received.call1(py, (exc,)); 168 | } 169 | 170 | fn write(pyself: &Py, py: Python, data: &[u8], addr: SocketAddr) { 171 | let rself = pyself.borrow(py); 172 | let mut state = rself.state.borrow_mut(); 173 | 174 | let buf_added = match state.write_buf_dsize { 175 | 0 => { 176 | match rself.state.borrow().socket.send_to(data, addr) { 177 | Ok(written) if written == data.len() => 0, 178 | Ok(written) => { 179 | state.write_buf.push_back(((&data[written..]).into(), addr)); 180 | data.len() - written 181 | } 182 | Err(err) 183 | if err.kind() == std::io::ErrorKind::Interrupted 184 | || err.kind() == std::io::ErrorKind::WouldBlock => 185 | { 186 | state.write_buf.push_back((data.into(), addr)); 187 | data.len() 188 | } 189 | Err(err) => { 190 | if state.write_buf_dsize > 0 { 191 | // reset buf_dsize? 192 | rself.pyloop.get().udp_socket_rem(rself.fd, Interest::WRITABLE); 193 | } 194 | if rself 195 | .closing 196 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 197 | .is_ok() 198 | { 199 | rself.pyloop.get().udp_socket_rem(rself.fd, Interest::READABLE); 200 | } 201 | rself.call_conn_lost(py, Some(pyo3::exceptions::PyOSError::new_err(err.to_string()))); 202 | 0 203 | } 204 | } 205 | } 206 | _ => { 207 | state.write_buf.push_back((data.into(), addr)); 208 | data.len() 209 | } 210 | }; 211 | 212 | if buf_added > 0 { 213 | if state.write_buf_dsize == 0 { 214 | rself.pyloop.get().udp_socket_add(rself.fd, Interest::WRITABLE); 215 | } 216 | state.write_buf_dsize += buf_added; 217 | if state.write_buf_dsize > rself.water_hi.load(atomic::Ordering::Relaxed) 218 | && rself 219 | .proto_paused 220 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 221 | .is_ok() 222 | { 223 | Self::proto_pause(pyself, py); 224 | } 225 | } 226 | } 227 | 228 | fn proto_pause(pyself: &Py, py: Python) { 229 | let rself = pyself.borrow(py); 230 | if let Err(err) = rself.proto.call_method0(py, pyo3::intern!(py, "pause_writing")) { 231 | let err_ctx = LogExc::transport( 232 | err, 233 | "protocol.pause_writing() failed".into(), 234 | rself.proto.clone_ref(py), 235 | pyself.clone_ref(py).into_any(), 236 | ); 237 | _ = rself.pyloop.get().log_exception(py, err_ctx); 238 | } 239 | } 240 | 241 | fn proto_resume(pyself: &Py, py: Python) { 242 | let rself = pyself.borrow(py); 243 | if let Err(err) = rself.proto.call_method0(py, pyo3::intern!(py, "resume_writing")) { 244 | let err_ctx = LogExc::transport( 245 | err, 246 | "protocol.resume_writing() failed".into(), 247 | rself.proto.clone_ref(py), 248 | pyself.clone_ref(py).into_any(), 249 | ); 250 | _ = rself.pyloop.get().log_exception(py, err_ctx); 251 | } 252 | } 253 | } 254 | 255 | #[pymethods] 256 | impl UDPTransport { 257 | #[pyo3(signature = (name, default = None))] 258 | fn get_extra_info(&self, py: Python, name: &str, default: Option>) -> Option> { 259 | match name { 260 | "socket" => Some(self.sock.clone_ref(py).into_any()), 261 | "sockname" => self.sock.call_method0(py, pyo3::intern!(py, "getsockname")).ok(), 262 | "peername" => { 263 | if self.state.borrow().remote_addr.is_some() { 264 | self.sock.call_method0(py, pyo3::intern!(py, "getpeername")).ok() 265 | } else { 266 | default 267 | } 268 | } 269 | _ => self.extra.get(name).map(|v| v.clone_ref(py)).or(default), 270 | } 271 | } 272 | 273 | fn is_closing(&self) -> bool { 274 | self.closing.load(atomic::Ordering::Relaxed) 275 | } 276 | 277 | fn close(&self, py: Python) { 278 | if self 279 | .closing 280 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 281 | .is_err() 282 | { 283 | return; 284 | } 285 | 286 | let event_loop = self.pyloop.get(); 287 | event_loop.udp_socket_rem(self.fd, Interest::READABLE); 288 | if self.state.borrow().write_buf_dsize == 0 { 289 | event_loop.udp_socket_rem(self.fd, Interest::WRITABLE); 290 | self.call_conn_lost(py, None); 291 | } 292 | } 293 | 294 | fn abort(&self, py: Python) { 295 | if self.state.borrow().write_buf_dsize > 0 { 296 | self.pyloop.get().udp_socket_rem(self.fd, Interest::WRITABLE); 297 | } 298 | if self 299 | .closing 300 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 301 | .is_ok() 302 | { 303 | self.pyloop.get().udp_socket_rem(self.fd, Interest::READABLE); 304 | } 305 | self.call_conn_lost(py, None); 306 | } 307 | 308 | fn set_protocol(&self, _protocol: Py) -> PyResult<()> { 309 | Err(pyo3::exceptions::PyNotImplementedError::new_err( 310 | "UDPTransport protocol cannot be changed", 311 | )) 312 | } 313 | 314 | fn get_protocol(&self, py: Python) -> Py { 315 | self.proto.clone_ref(py) 316 | } 317 | 318 | #[pyo3(signature = (high = None, low = None))] 319 | fn set_write_buffer_limits(pyself: Py, py: Python, high: Option, low: Option) -> PyResult<()> { 320 | let wh = match high { 321 | None => match low { 322 | None => 1024 * 64, 323 | Some(v) => v * 4, 324 | }, 325 | Some(v) => v, 326 | }; 327 | let wl = match low { 328 | None => wh / 4, 329 | Some(v) => v, 330 | }; 331 | 332 | if wh < wl { 333 | return Err(pyo3::exceptions::PyValueError::new_err( 334 | "high must be >= low must be >= 0", 335 | )); 336 | } 337 | 338 | let rself = pyself.borrow(py); 339 | rself.water_hi.store(wh, atomic::Ordering::Relaxed); 340 | rself.water_lo.store(wl, atomic::Ordering::Relaxed); 341 | 342 | if rself.state.borrow().write_buf_dsize > wh 343 | && rself 344 | .proto_paused 345 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 346 | .is_ok() 347 | { 348 | Self::proto_pause(&pyself, py); 349 | } 350 | 351 | Ok(()) 352 | } 353 | 354 | fn get_write_buffer_size(&self) -> usize { 355 | self.state.borrow().write_buf_dsize 356 | } 357 | 358 | fn get_write_buffer_limits(&self) -> (usize, usize) { 359 | ( 360 | self.water_lo.load(atomic::Ordering::Relaxed), 361 | self.water_hi.load(atomic::Ordering::Relaxed), 362 | ) 363 | } 364 | 365 | fn sendto(pyself: Py, py: Python, data: Cow<[u8]>, addr: Option<(String, u16)>) -> PyResult<()> { 366 | let rself = pyself.borrow(py); 367 | 368 | if rself.closing.load(atomic::Ordering::Relaxed) { 369 | return Err(pyo3::exceptions::PyRuntimeError::new_err( 370 | "Cannot send on closing transport", 371 | )); 372 | } 373 | if data.is_empty() { 374 | return Ok(()); 375 | } 376 | 377 | match addr 378 | .map(|v| SocketAddr::new(IpAddr::from_str(&v.0).unwrap(), v.1)) 379 | .or_else(|| rself.state.borrow().remote_addr) 380 | { 381 | Some(addr) => { 382 | Self::write(&pyself, py, &data, addr); 383 | Ok(()) 384 | } 385 | None => Err(pyo3::exceptions::PyValueError::new_err("No remote address specified")), 386 | } 387 | } 388 | } 389 | 390 | pub(crate) struct UDPReadHandle { 391 | pub fd: usize, 392 | } 393 | 394 | impl Handle for UDPReadHandle { 395 | fn run(&self, py: Python, event_loop: &EventLoop, state: &mut EventLoopRunState) { 396 | let pytransport = event_loop.get_udp_transport(self.fd, py); 397 | let transport = pytransport.borrow(py); 398 | 399 | loop { 400 | match { 401 | let trxstate = transport.state.borrow(); 402 | trxstate.socket.recv_from(&mut state.read_buf) 403 | } { 404 | Ok((size, addr)) => { 405 | // Call the protocol's datagram_received method 406 | // Now state is not borrowed, so sendto can work 407 | transport.call_datagram_received(py, &state.read_buf[..size], addr); 408 | } 409 | Err(err) if err.kind() == ErrorKind::WouldBlock => { 410 | // No more data available 411 | break; 412 | } 413 | Err(err) if err.kind() == ErrorKind::Interrupted => { 414 | // Interrupted by signal, continue 415 | } 416 | Err(err) => { 417 | // Other error - call error_received and close 418 | let py_err = pyo3::exceptions::PyOSError::new_err(err.to_string()); 419 | transport.call_error_received(py, py_err); 420 | event_loop.udp_socket_close(self.fd); 421 | break; 422 | } 423 | } 424 | } 425 | } 426 | } 427 | 428 | pub(crate) struct UDPWriteHandle { 429 | pub fd: usize, 430 | } 431 | 432 | impl UDPWriteHandle { 433 | #[inline] 434 | fn write(&self, transport: &UDPTransport) -> Option { 435 | let mut ret = 0; 436 | let mut state = transport.state.borrow_mut(); 437 | 438 | while let Some((data, addr)) = state.write_buf.pop_front() { 439 | match state.socket.send_to(&data, addr) { 440 | Ok(written) if written < data.len() => { 441 | state.write_buf.push_front(((&data[written..]).into(), addr)); 442 | ret += written; 443 | break; 444 | } 445 | Ok(written) => ret += written, 446 | Err(err) if err.kind() == std::io::ErrorKind::Interrupted => { 447 | state.write_buf.push_front((data, addr)); 448 | } 449 | Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { 450 | state.write_buf.push_front((data, addr)); 451 | break; 452 | } 453 | _ => { 454 | state.write_buf.clear(); 455 | state.write_buf_dsize = 0; 456 | return None; 457 | } 458 | } 459 | } 460 | state.write_buf_dsize -= ret; 461 | Some(ret) 462 | } 463 | } 464 | 465 | impl Handle for UDPWriteHandle { 466 | fn run(&self, py: Python, event_loop: &EventLoop, _state: &mut EventLoopRunState) { 467 | let pytransport = event_loop.get_udp_transport(self.fd, py); 468 | let transport = pytransport.borrow(py); 469 | let stream_close; 470 | 471 | if let Some(written) = self.write(&transport) { 472 | if written > 0 { 473 | UDPTransport::write_buf_size_decr(&pytransport, py); 474 | } 475 | stream_close = match transport.state.borrow().write_buf.is_empty() { 476 | true => transport.close_from_write_handle(py, false), 477 | false => false, 478 | }; 479 | } else { 480 | stream_close = transport.close_from_write_handle(py, true); 481 | } 482 | 483 | if transport.state.borrow().write_buf.is_empty() { 484 | event_loop.udp_socket_rem(self.fd, Interest::WRITABLE); 485 | } 486 | if stream_close { 487 | event_loop.udp_socket_close(self.fd); 488 | } 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /src/tcp.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use std::os::fd::{AsRawFd, FromRawFd}; 3 | 4 | use anyhow::Result; 5 | use mio::{ 6 | Interest, 7 | net::{TcpListener, TcpStream}, 8 | }; 9 | use pyo3::{IntoPyObjectExt, buffer::PyBuffer, prelude::*, types::PyBytes}; 10 | use std::{ 11 | borrow::Cow, 12 | cell::RefCell, 13 | collections::{HashMap, VecDeque}, 14 | io::Read, 15 | sync::atomic, 16 | }; 17 | 18 | use crate::{ 19 | event_loop::{EventLoop, EventLoopRunState}, 20 | handles::{BoxedHandle, CBHandle, Handle}, 21 | log::LogExc, 22 | py::{asyncio_proto_buf, copy_context}, 23 | sock::SocketWrapper, 24 | utils::syscall, 25 | }; 26 | 27 | pub(crate) struct TCPServer { 28 | pub fd: i32, 29 | sfamily: i32, 30 | backlog: i32, 31 | protocol_factory: Py, 32 | } 33 | 34 | impl TCPServer { 35 | pub(crate) fn from_fd(fd: i32, sfamily: i32, backlog: i32, protocol_factory: Py) -> Self { 36 | Self { 37 | fd, 38 | sfamily, 39 | backlog, 40 | protocol_factory, 41 | } 42 | } 43 | 44 | pub(crate) fn listen(&self, py: Python, pyloop: Py) -> Result<()> { 45 | let sock = unsafe { socket2::Socket::from_raw_fd(self.fd) }; 46 | sock.listen(self.backlog)?; 47 | 48 | let stdl: std::net::TcpListener = sock.into(); 49 | let listener = TcpListener::from_std(stdl); 50 | let sref = TCPServerRef { 51 | fd: self.fd as usize, 52 | pyloop: pyloop.clone_ref(py), 53 | sfamily: self.sfamily, 54 | proto_factory: self.protocol_factory.clone_ref(py), 55 | }; 56 | pyloop.get().tcp_listener_add(listener, sref); 57 | 58 | Ok(()) 59 | } 60 | 61 | pub(crate) fn close(&self, py: Python, event_loop: &EventLoop) { 62 | self.streams_abort(py, event_loop); 63 | _ = event_loop.tcp_listener_rem(self.fd as usize); 64 | // if closed {} 65 | // Ok(()) 66 | } 67 | 68 | pub(crate) fn streams_close(&self, py: Python, event_loop: &EventLoop) { 69 | let mut transports = Vec::new(); 70 | event_loop.with_tcp_listener_streams(self.fd as usize, |streams| { 71 | for stream_fd in &streams.pin() { 72 | transports.push(event_loop.get_tcp_transport(*stream_fd, py)); 73 | } 74 | }); 75 | for transport in transports { 76 | transport.borrow(py).close(py); 77 | } 78 | } 79 | 80 | pub(crate) fn streams_abort(&self, py: Python, event_loop: &EventLoop) { 81 | let mut transports = Vec::new(); 82 | event_loop.with_tcp_listener_streams(self.fd as usize, |streams| { 83 | for stream_fd in &streams.pin() { 84 | transports.push(event_loop.get_tcp_transport(*stream_fd, py)); 85 | } 86 | }); 87 | for transport in transports { 88 | transport.borrow(py).abort(py); 89 | } 90 | } 91 | } 92 | 93 | pub(crate) struct TCPServerRef { 94 | pub fd: usize, 95 | pyloop: Py, 96 | sfamily: i32, 97 | proto_factory: Py, 98 | } 99 | 100 | impl TCPServerRef { 101 | #[inline] 102 | pub(crate) fn new_stream(&self, py: Python, stream: TcpStream) -> (Py, BoxedHandle) { 103 | let proto = self.proto_factory.bind(py).call0().unwrap(); 104 | 105 | let transport = TCPTransport::new( 106 | py, 107 | self.pyloop.clone_ref(py), 108 | stream, 109 | proto, 110 | self.sfamily, 111 | Some(self.fd), 112 | ); 113 | let conn_made = transport 114 | .proto 115 | .getattr(py, pyo3::intern!(py, "connection_made")) 116 | .unwrap(); 117 | let pytransport = Py::new(py, transport).unwrap(); 118 | let conn_handle = Py::new( 119 | py, 120 | CBHandle::new1(conn_made, pytransport.clone_ref(py).into_any(), copy_context(py)), 121 | ) 122 | .unwrap(); 123 | 124 | (pytransport, Box::new(conn_handle)) 125 | } 126 | } 127 | struct TCPTransportState { 128 | stream: TcpStream, 129 | write_buf: VecDeque>, 130 | write_buf_dsize: usize, 131 | } 132 | 133 | #[pyclass(frozen, unsendable, module = "rloop._rloop")] 134 | pub(crate) struct TCPTransport { 135 | pub fd: usize, 136 | pub lfd: Option, 137 | state: RefCell, 138 | pyloop: Py, 139 | // atomics 140 | closing: atomic::AtomicBool, 141 | paused: atomic::AtomicBool, 142 | water_hi: atomic::AtomicUsize, 143 | water_lo: atomic::AtomicUsize, 144 | weof: atomic::AtomicBool, 145 | // py protocol fields 146 | proto: Py, 147 | proto_buffered: bool, 148 | proto_paused: atomic::AtomicBool, 149 | protom_buf_get: Py, 150 | protom_conn_lost: Py, 151 | protom_recv_data: Py, 152 | // py extras 153 | extra: HashMap>, 154 | sock: Py, 155 | } 156 | 157 | impl TCPTransport { 158 | fn new( 159 | py: Python, 160 | pyloop: Py, 161 | stream: TcpStream, 162 | pyproto: Bound, 163 | socket_family: i32, 164 | lfd: Option, 165 | ) -> Self { 166 | let fd = stream.as_raw_fd() as usize; 167 | let state = TCPTransportState { 168 | stream, 169 | write_buf: VecDeque::new(), 170 | write_buf_dsize: 0, 171 | }; 172 | 173 | let wh = 1024 * 64; 174 | let wl = wh / 4; 175 | 176 | let mut proto_buffered = false; 177 | let protom_buf_get: Py; 178 | let protom_recv_data: Py; 179 | if pyproto.is_instance(asyncio_proto_buf(py).unwrap()).unwrap() { 180 | proto_buffered = true; 181 | protom_buf_get = pyproto.getattr(pyo3::intern!(py, "get_buffer")).unwrap().unbind(); 182 | protom_recv_data = pyproto.getattr(pyo3::intern!(py, "buffer_updated")).unwrap().unbind(); 183 | } else { 184 | protom_buf_get = py.None(); 185 | protom_recv_data = pyproto.getattr(pyo3::intern!(py, "data_received")).unwrap().unbind(); 186 | } 187 | let protom_conn_lost = pyproto.getattr(pyo3::intern!(py, "connection_lost")).unwrap().unbind(); 188 | let proto = pyproto.unbind(); 189 | 190 | Self { 191 | fd, 192 | lfd, 193 | state: RefCell::new(state), 194 | pyloop, 195 | closing: false.into(), 196 | paused: false.into(), 197 | water_hi: wh.into(), 198 | water_lo: wl.into(), 199 | weof: false.into(), 200 | proto, 201 | proto_buffered, 202 | proto_paused: false.into(), 203 | protom_buf_get, 204 | protom_conn_lost, 205 | protom_recv_data, 206 | extra: HashMap::new(), 207 | sock: SocketWrapper::from_fd(py, fd, socket_family, socket2::Type::STREAM, 0), 208 | } 209 | } 210 | 211 | pub(crate) fn from_py(py: Python, pyloop: &Py, pysock: (i32, i32), proto_factory: Py) -> Self { 212 | let sock = unsafe { socket2::Socket::from_raw_fd(pysock.0) }; 213 | _ = sock.set_nonblocking(true); 214 | let stdl: std::net::TcpStream = sock.into(); 215 | let stream = TcpStream::from_std(stdl); 216 | 217 | let proto = proto_factory.bind(py).call0().unwrap(); 218 | 219 | Self::new(py, pyloop.clone_ref(py), stream, proto, pysock.1, None) 220 | } 221 | 222 | pub(crate) fn attach(pyself: &Py, py: Python) -> PyResult> { 223 | let rself = pyself.borrow(py); 224 | rself 225 | .proto 226 | .call_method1(py, pyo3::intern!(py, "connection_made"), (pyself.clone_ref(py),))?; 227 | Ok(rself.proto.clone_ref(py)) 228 | } 229 | 230 | #[inline] 231 | fn write_buf_size_decr(pyself: &Py, py: Python) { 232 | let rself = pyself.borrow(py); 233 | if rself.state.borrow().write_buf_dsize <= rself.water_lo.load(atomic::Ordering::Relaxed) 234 | && rself 235 | .proto_paused 236 | .compare_exchange(true, false, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 237 | .is_ok() 238 | { 239 | Self::proto_resume(pyself, py); 240 | } 241 | } 242 | 243 | #[inline] 244 | fn close_from_read_handle(&self, py: Python, event_loop: &EventLoop) -> bool { 245 | if self 246 | .closing 247 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 248 | .is_err() 249 | { 250 | return false; 251 | } 252 | 253 | if !self.state.borrow_mut().write_buf.is_empty() { 254 | return false; 255 | } 256 | 257 | event_loop.tcp_stream_rem(self.fd, Interest::WRITABLE); 258 | _ = self.protom_conn_lost.call1(py, (py.None(),)); 259 | true 260 | } 261 | 262 | #[inline] 263 | fn close_from_write_handle(&self, py: Python, errored: bool) -> Option { 264 | if self.closing.load(atomic::Ordering::Relaxed) { 265 | _ = self.protom_conn_lost.call1( 266 | py, 267 | #[allow(clippy::obfuscated_if_else)] 268 | (errored 269 | .then(|| { 270 | pyo3::exceptions::PyRuntimeError::new_err("socket transport failed") 271 | .into_py_any(py) 272 | .unwrap() 273 | }) 274 | .unwrap_or_else(|| py.None()),), 275 | ); 276 | return Some(true); 277 | } 278 | self.weof.load(atomic::Ordering::Relaxed).then_some(false) 279 | } 280 | 281 | #[inline(always)] 282 | fn call_conn_lost(&self, py: Python, err: Option) { 283 | _ = self.protom_conn_lost.call1(py, (err,)); 284 | self.pyloop.get().tcp_stream_close(py, self.fd); 285 | } 286 | 287 | fn try_write(pyself: &Py, py: Python, data: &[u8]) -> PyResult<()> { 288 | let rself = pyself.borrow(py); 289 | 290 | if rself.weof.load(atomic::Ordering::Relaxed) { 291 | return Err(pyo3::exceptions::PyRuntimeError::new_err("Cannot write after EOF")); 292 | } 293 | if data.is_empty() { 294 | return Ok(()); 295 | } 296 | 297 | let mut state = rself.state.borrow_mut(); 298 | let buf_added = match state.write_buf_dsize { 299 | #[allow(clippy::cast_possible_wrap)] 300 | 0 => match syscall!(write(rself.fd as i32, data.as_ptr().cast(), data.len())) { 301 | Ok(written) if written as usize == data.len() => 0, 302 | Ok(written) => { 303 | let written = written as usize; 304 | state.write_buf.push_back((&data[written..]).into()); 305 | data.len() - written 306 | } 307 | Err(err) 308 | if err.kind() == std::io::ErrorKind::Interrupted 309 | || err.kind() == std::io::ErrorKind::WouldBlock => 310 | { 311 | state.write_buf.push_back(data.into()); 312 | data.len() 313 | } 314 | Err(err) => { 315 | if state.write_buf_dsize > 0 { 316 | // reset buf_dsize? 317 | rself.pyloop.get().tcp_stream_rem(rself.fd, Interest::WRITABLE); 318 | } 319 | if rself 320 | .closing 321 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 322 | .is_ok() 323 | { 324 | rself.pyloop.get().tcp_stream_rem(rself.fd, Interest::READABLE); 325 | } 326 | rself.call_conn_lost(py, Some(pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))); 327 | 0 328 | } 329 | }, 330 | _ => { 331 | state.write_buf.push_back(data.into()); 332 | data.len() 333 | } 334 | }; 335 | if buf_added > 0 { 336 | if state.write_buf_dsize == 0 { 337 | rself.pyloop.get().tcp_stream_add(rself.fd, Interest::WRITABLE); 338 | } 339 | state.write_buf_dsize += buf_added; 340 | if state.write_buf_dsize > rself.water_hi.load(atomic::Ordering::Relaxed) 341 | && rself 342 | .proto_paused 343 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 344 | .is_ok() 345 | { 346 | Self::proto_pause(pyself, py); 347 | } 348 | } 349 | 350 | Ok(()) 351 | } 352 | 353 | fn proto_pause(pyself: &Py, py: Python) { 354 | let rself = pyself.borrow(py); 355 | if let Err(err) = rself.proto.call_method0(py, pyo3::intern!(py, "pause_writing")) { 356 | let err_ctx = LogExc::transport( 357 | err, 358 | "protocol.pause_writing() failed".into(), 359 | rself.proto.clone_ref(py), 360 | pyself.clone_ref(py).into_any(), 361 | ); 362 | _ = rself.pyloop.get().log_exception(py, err_ctx); 363 | } 364 | } 365 | 366 | fn proto_resume(pyself: &Py, py: Python) { 367 | let rself = pyself.borrow(py); 368 | if let Err(err) = rself.proto.call_method0(py, pyo3::intern!(py, "resume_writing")) { 369 | let err_ctx = LogExc::transport( 370 | err, 371 | "protocol.resume_writing() failed".into(), 372 | rself.proto.clone_ref(py), 373 | pyself.clone_ref(py).into_any(), 374 | ); 375 | _ = rself.pyloop.get().log_exception(py, err_ctx); 376 | } 377 | } 378 | } 379 | 380 | #[pymethods] 381 | impl TCPTransport { 382 | #[pyo3(signature = (name, default = None))] 383 | fn get_extra_info(&self, py: Python, name: &str, default: Option>) -> Option> { 384 | match name { 385 | "socket" => Some(self.sock.clone_ref(py).into_any()), 386 | "sockname" => self.sock.call_method0(py, pyo3::intern!(py, "getsockname")).ok(), 387 | "peername" => self.sock.call_method0(py, pyo3::intern!(py, "getpeername")).ok(), 388 | _ => self.extra.get(name).map(|v| v.clone_ref(py)).or(default), 389 | } 390 | } 391 | 392 | fn is_closing(&self) -> bool { 393 | self.closing.load(atomic::Ordering::Relaxed) 394 | } 395 | 396 | fn close(&self, py: Python) { 397 | if self 398 | .closing 399 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 400 | .is_err() 401 | { 402 | return; 403 | } 404 | 405 | let event_loop = self.pyloop.get(); 406 | event_loop.tcp_stream_rem(self.fd, Interest::READABLE); 407 | if self.state.borrow().write_buf_dsize == 0 { 408 | // set conn lost? 409 | event_loop.tcp_stream_rem(self.fd, Interest::WRITABLE); 410 | self.call_conn_lost(py, None); 411 | } 412 | } 413 | 414 | fn set_protocol(&self, _protocol: Py) -> PyResult<()> { 415 | Err(pyo3::exceptions::PyNotImplementedError::new_err( 416 | "TCPTransport protocol cannot be changed", 417 | )) 418 | } 419 | 420 | fn get_protocol(&self, py: Python) -> Py { 421 | self.proto.clone_ref(py) 422 | } 423 | 424 | fn is_reading(&self) -> bool { 425 | !self.closing.load(atomic::Ordering::Relaxed) && !self.paused.load(atomic::Ordering::Relaxed) 426 | } 427 | 428 | fn pause_reading(&self) { 429 | if self.closing.load(atomic::Ordering::Relaxed) { 430 | return; 431 | } 432 | if self 433 | .paused 434 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 435 | .is_err() 436 | { 437 | return; 438 | } 439 | self.pyloop.get().tcp_stream_rem(self.fd, Interest::READABLE); 440 | } 441 | 442 | fn resume_reading(&self) { 443 | if self.closing.load(atomic::Ordering::Relaxed) { 444 | return; 445 | } 446 | if self 447 | .paused 448 | .compare_exchange(true, false, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 449 | .is_err() 450 | { 451 | return; 452 | } 453 | self.pyloop.get().tcp_stream_add(self.fd, Interest::READABLE); 454 | } 455 | 456 | #[pyo3(signature = (high = None, low = None))] 457 | fn set_write_buffer_limits(pyself: Py, py: Python, high: Option, low: Option) -> PyResult<()> { 458 | let wh = match high { 459 | None => match low { 460 | None => 1024 * 64, 461 | Some(v) => v * 4, 462 | }, 463 | Some(v) => v, 464 | }; 465 | let wl = match low { 466 | None => wh / 4, 467 | Some(v) => v, 468 | }; 469 | 470 | if wh < wl { 471 | return Err(pyo3::exceptions::PyValueError::new_err( 472 | "high must be >= low must be >= 0", 473 | )); 474 | } 475 | 476 | let rself = pyself.borrow(py); 477 | rself.water_hi.store(wh, atomic::Ordering::Relaxed); 478 | rself.water_lo.store(wl, atomic::Ordering::Relaxed); 479 | 480 | if rself.state.borrow().write_buf_dsize > wh 481 | && rself 482 | .proto_paused 483 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 484 | .is_ok() 485 | { 486 | Self::proto_pause(&pyself, py); 487 | } 488 | 489 | Ok(()) 490 | } 491 | 492 | fn get_write_buffer_size(&self) -> usize { 493 | self.state.borrow().write_buf_dsize 494 | } 495 | 496 | fn get_write_buffer_limits(&self) -> (usize, usize) { 497 | ( 498 | self.water_lo.load(atomic::Ordering::Relaxed), 499 | self.water_hi.load(atomic::Ordering::Relaxed), 500 | ) 501 | } 502 | 503 | fn write(pyself: Py, py: Python, data: Cow<[u8]>) -> PyResult<()> { 504 | Self::try_write(&pyself, py, &data) 505 | } 506 | 507 | fn writelines(pyself: Py, py: Python, data: &Bound) -> PyResult<()> { 508 | let pybytes = PyBytes::new(py, &[0; 0]); 509 | let pybytesj = pybytes.call_method1(pyo3::intern!(py, "join"), (data,))?; 510 | let bytes = pybytesj.extract::>()?; 511 | Self::try_write(&pyself, py, &bytes) 512 | } 513 | 514 | fn write_eof(&self) { 515 | if self.closing.load(atomic::Ordering::Relaxed) { 516 | return; 517 | } 518 | if self 519 | .weof 520 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 521 | .is_err() 522 | { 523 | return; 524 | } 525 | 526 | let state = self.state.borrow(); 527 | if state.write_buf_dsize == 0 { 528 | _ = state.stream.shutdown(std::net::Shutdown::Write); 529 | } 530 | } 531 | 532 | fn can_write_eof(&self) -> bool { 533 | true 534 | } 535 | 536 | fn abort(&self, py: Python) { 537 | if self.state.borrow().write_buf_dsize > 0 { 538 | self.pyloop.get().tcp_stream_rem(self.fd, Interest::WRITABLE); 539 | } 540 | if self 541 | .closing 542 | .compare_exchange(false, true, atomic::Ordering::Relaxed, atomic::Ordering::Relaxed) 543 | .is_ok() 544 | { 545 | self.pyloop.get().tcp_stream_rem(self.fd, Interest::READABLE); 546 | } 547 | self.call_conn_lost(py, None); 548 | } 549 | } 550 | 551 | pub(crate) struct TCPReadHandle { 552 | pub fd: usize, 553 | } 554 | 555 | impl TCPReadHandle { 556 | #[inline] 557 | fn recv_direct(&self, py: Python, transport: &TCPTransport, buf: &mut [u8]) -> (Option>, bool) { 558 | let (read, closed) = self.read_into(&mut transport.state.borrow_mut().stream, buf); 559 | if read > 0 { 560 | let rbuf = &buf[..read]; 561 | let pydata = unsafe { PyBytes::from_ptr(py, rbuf.as_ptr(), read) }; 562 | return (Some(pydata.into_any().unbind()), closed); 563 | } 564 | (None, closed) 565 | } 566 | 567 | #[inline] 568 | fn recv_buffered(&self, py: Python, transport: &TCPTransport) -> (Option>, bool) { 569 | // NOTE: `PuBuffer.as_mut_slice` exists, but it returns a slice of `Cell`, 570 | // which is smth we can't really use to read from `TcpStream`. 571 | // So even if this sucks, we copy data back and forth, at least until 572 | // we figure out a way to actually use `PyBuffer` directly. 573 | let pybuf: PyBuffer = PyBuffer::get(&transport.protom_buf_get.bind(py).call1((-1,)).unwrap()).unwrap(); 574 | let mut vbuf = pybuf.to_vec(py).unwrap(); 575 | let (read, closed) = self.read_into(&mut transport.state.borrow_mut().stream, vbuf.as_mut_slice()); 576 | if read > 0 { 577 | _ = pybuf.copy_from_slice(py, &vbuf[..]); 578 | return (Some(read.into_py_any(py).unwrap()), closed); 579 | } 580 | (None, closed) 581 | } 582 | 583 | #[inline(always)] 584 | fn read_into(&self, stream: &mut TcpStream, buf: &mut [u8]) -> (usize, bool) { 585 | let mut len = 0; 586 | let mut closed = false; 587 | 588 | loop { 589 | match stream.read(&mut buf[len..]) { 590 | Ok(0) => { 591 | if len < buf.len() { 592 | closed = true; 593 | } 594 | break; 595 | } 596 | Ok(readn) => len += readn, 597 | Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {} 598 | _ => break, 599 | } 600 | } 601 | 602 | (len, closed) 603 | } 604 | 605 | #[inline] 606 | fn recv_eof(&self, py: Python, event_loop: &EventLoop, transport: &TCPTransport) -> bool { 607 | event_loop.tcp_stream_rem(self.fd, Interest::READABLE); 608 | if let Ok(pyr) = transport.proto.call_method0(py, pyo3::intern!(py, "eof_received")) 609 | && let Ok(true) = pyr.is_truthy(py) 610 | { 611 | return false; 612 | } 613 | transport.close_from_read_handle(py, event_loop) 614 | } 615 | } 616 | 617 | impl Handle for TCPReadHandle { 618 | fn run(&self, py: Python, event_loop: &EventLoop, state: &mut EventLoopRunState) { 619 | let pytransport = event_loop.get_tcp_transport(self.fd, py); 620 | let transport = pytransport.borrow(py); 621 | 622 | // NOTE: we need to consume all the data coming from the socket even when it exceeds the buffer, 623 | // otherwise we won't get another readable event from the poller 624 | let mut close = false; 625 | loop { 626 | let (data, eof) = match transport.proto_buffered { 627 | true => self.recv_buffered(py, &transport), 628 | false => self.recv_direct(py, &transport, &mut state.read_buf), 629 | }; 630 | 631 | if let Some(data) = data { 632 | _ = transport.protom_recv_data.call1(py, (data,)); 633 | if !eof { 634 | continue; 635 | } 636 | } 637 | 638 | if eof { 639 | close = self.recv_eof(py, event_loop, &transport); 640 | } 641 | 642 | break; 643 | } 644 | 645 | if close { 646 | event_loop.tcp_stream_close(py, self.fd); 647 | } 648 | } 649 | } 650 | 651 | pub(crate) struct TCPWriteHandle { 652 | pub fd: usize, 653 | } 654 | 655 | impl TCPWriteHandle { 656 | #[inline] 657 | fn write(&self, transport: &TCPTransport) -> Option { 658 | #[allow(clippy::cast_possible_wrap)] 659 | let fd = self.fd as i32; 660 | let mut ret = 0; 661 | let mut state = transport.state.borrow_mut(); 662 | while let Some(data) = state.write_buf.pop_front() { 663 | match syscall!(write(fd, data.as_ptr().cast(), data.len())) { 664 | Ok(written) if (written as usize) < data.len() => { 665 | let written = written as usize; 666 | state.write_buf.push_front((&data[written..]).into()); 667 | ret += written; 668 | break; 669 | } 670 | Ok(written) => ret += written as usize, 671 | Err(err) if err.kind() == std::io::ErrorKind::Interrupted => { 672 | state.write_buf.push_front(data); 673 | } 674 | Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { 675 | state.write_buf.push_front(data); 676 | break; 677 | } 678 | _ => { 679 | state.write_buf.clear(); 680 | state.write_buf_dsize = 0; 681 | return None; 682 | } 683 | } 684 | } 685 | state.write_buf_dsize -= ret; 686 | Some(ret) 687 | } 688 | } 689 | 690 | impl Handle for TCPWriteHandle { 691 | fn run(&self, py: Python, event_loop: &EventLoop, _state: &mut EventLoopRunState) { 692 | let pytransport = event_loop.get_tcp_transport(self.fd, py); 693 | let transport = pytransport.borrow(py); 694 | let stream_close; 695 | 696 | if let Some(written) = self.write(&transport) { 697 | if written > 0 { 698 | TCPTransport::write_buf_size_decr(&pytransport, py); 699 | } 700 | stream_close = match transport.state.borrow().write_buf.is_empty() { 701 | true => transport.close_from_write_handle(py, false), 702 | false => None, 703 | }; 704 | } else { 705 | stream_close = transport.close_from_write_handle(py, true); 706 | } 707 | 708 | if transport.state.borrow().write_buf.is_empty() { 709 | event_loop.tcp_stream_rem(self.fd, Interest::WRITABLE); 710 | } 711 | 712 | match stream_close { 713 | Some(true) => event_loop.tcp_stream_close(py, self.fd), 714 | Some(false) => { 715 | _ = transport.state.borrow().stream.shutdown(std::net::Shutdown::Write); 716 | } 717 | _ => {} 718 | } 719 | } 720 | } 721 | -------------------------------------------------------------------------------- /rloop/loop.py: -------------------------------------------------------------------------------- 1 | import asyncio as __asyncio 2 | import errno 3 | import os 4 | import signal 5 | import socket 6 | import subprocess 7 | import sys 8 | import threading 9 | import warnings 10 | from asyncio.coroutines import iscoroutine as _iscoroutine, iscoroutinefunction as _iscoroutinefunction 11 | from asyncio.events import _get_running_loop, _set_running_loop 12 | from asyncio.futures import Future as _Future, isfuture as _isfuture, wrap_future as _wrap_future 13 | from asyncio.staggered import staggered_race as _staggered_race 14 | from asyncio.tasks import Task as _Task, ensure_future as _ensure_future, gather as _gather 15 | from concurrent.futures import ThreadPoolExecutor 16 | from contextvars import copy_context as _copy_context 17 | from itertools import chain as _iterchain 18 | from typing import Union 19 | 20 | from ._compat import _PY_311, _PYV 21 | from ._rloop import CBHandle, EventLoop as __BaseLoop, TimerHandle 22 | from .exc import _exception_handler 23 | from .futures import _SyncSockReaderFuture, _SyncSockWriterFuture 24 | from .server import Server 25 | from .subprocess import ( 26 | _PidfdChildWatcher, 27 | _PipeReadTransport, 28 | _PipeWriteTransport, 29 | _SubProcessTransport, 30 | _ThreadedChildWatcher, 31 | ) 32 | from .utils import _can_use_pidfd, _HAS_IPv6, _interleave_addrinfos, _ipaddr_info, _noop, _set_reuseport 33 | 34 | 35 | class RLoop(__BaseLoop, __asyncio.AbstractEventLoop): 36 | def __init__(self): 37 | super().__init__() 38 | self._exc_handler = _exception_handler 39 | self._watcher_child = _PidfdChildWatcher(self) if _can_use_pidfd() else _ThreadedChildWatcher(self) 40 | 41 | #: running methods 42 | def run_forever(self): 43 | try: 44 | _old_agen_hooks = self._run_forever_pre() 45 | self._run() 46 | finally: 47 | self._run_forever_post(_old_agen_hooks) 48 | 49 | def _run_forever_pre(self): 50 | self._check_closed() 51 | self._check_running() 52 | # self._set_coroutine_origin_tracking(self._debug) 53 | 54 | _old_agen_hooks = sys.get_asyncgen_hooks() 55 | sys.set_asyncgen_hooks(firstiter=self._asyncgen_firstiter_hook, finalizer=self._asyncgen_finalizer_hook) 56 | 57 | self._thread_id = threading.get_ident() 58 | self._ssock_start() 59 | self._signals_resume() 60 | _set_running_loop(self) 61 | 62 | return _old_agen_hooks 63 | 64 | def _run_forever_post(self, _old_agen_hooks): 65 | _set_running_loop(None) 66 | self._signals_pause() 67 | self._ssock_stop() 68 | self._thread_id = 0 69 | self._stopping = False 70 | # self._set_coroutine_origin_tracking(False) 71 | # Restore any pre-existing async generator hooks. 72 | if _old_agen_hooks is not None: 73 | sys.set_asyncgen_hooks(*_old_agen_hooks) 74 | self._old_agen_hooks = None 75 | 76 | def run_until_complete(self, future): 77 | self._check_closed() 78 | self._check_running() 79 | 80 | new_task = not _isfuture(future) 81 | future = _ensure_future(future, loop=self) 82 | if new_task: 83 | # An exception is raised if the future didn't complete, so there 84 | # is no need to log the "destroy pending task" message 85 | future._log_destroy_pending = False 86 | 87 | future.add_done_callback(self._run_until_complete_cb) 88 | try: 89 | self.run_forever() 90 | except: 91 | if new_task and future.done() and not future.cancelled(): 92 | # The coroutine raised a BaseException. Consume the exception 93 | # to not log a warning, the caller doesn't have access to the 94 | # local task. 95 | future.exception() 96 | raise 97 | finally: 98 | future.remove_done_callback(self._run_until_complete_cb) 99 | if not future.done(): 100 | raise RuntimeError('Event loop stopped before Future completed.') 101 | 102 | return future.result() 103 | 104 | def _run_until_complete_cb(self, fut): 105 | if not fut.cancelled(): 106 | exc = fut.exception() 107 | if isinstance(exc, (SystemExit, KeyboardInterrupt)): 108 | # Issue #336: run_forever() already finished, 109 | # no need to stop it. 110 | return 111 | self.stop() 112 | 113 | def stop(self): 114 | self._stopping = True 115 | 116 | def _check_running(self): 117 | if self.is_running(): 118 | raise RuntimeError('This event loop is already running') 119 | if _get_running_loop() is not None: 120 | raise RuntimeError('Cannot run the event loop while another loop is running') 121 | 122 | def is_running(self) -> bool: 123 | return bool(self._thread_id) 124 | 125 | def _check_closed(self): 126 | if self._closed: 127 | raise RuntimeError('Event loop is closed') 128 | 129 | def is_closed(self) -> bool: 130 | return self._closed 131 | 132 | def close(self): 133 | if self.is_running(): 134 | raise RuntimeError('Cannot close a running event loop') 135 | if self._closed: 136 | return 137 | # if self._debug: 138 | # logger.debug("Close %r", self) 139 | self._closed = True 140 | 141 | self._signals_clear() 142 | 143 | self._executor_shutdown_called = True 144 | executor = self._default_executor 145 | if executor is not None: 146 | self._default_executor = None 147 | executor.shutdown(wait=False) 148 | 149 | async def shutdown_asyncgens(self): 150 | self._asyncgens_shutdown_called = True 151 | 152 | if not len(self._asyncgens): 153 | return 154 | 155 | closing_agens = list(self._asyncgens) 156 | self._asyncgens.clear() 157 | 158 | results = await _gather(*[ag.aclose() for ag in closing_agens], return_exceptions=True) 159 | 160 | for result, agen in zip(results, closing_agens): 161 | if isinstance(result, Exception): 162 | self.call_exception_handler( 163 | { 164 | 'message': f'an error occurred during closing of asynchronous generator {agen!r}', 165 | 'exception': result, 166 | 'asyncgen': agen, 167 | } 168 | ) 169 | 170 | def _asyncgen_finalizer_hook(self, agen): 171 | self._asyncgens.discard(agen) 172 | if not self.is_closed(): 173 | self.call_soon_threadsafe(self.create_task, agen.aclose()) 174 | 175 | def _asyncgen_firstiter_hook(self, agen): 176 | if self._asyncgens_shutdown_called: 177 | warnings.warn( # noqa: B028 178 | f'asynchronous generator {agen!r} was scheduled after loop.shutdown_asyncgens() call', 179 | ResourceWarning, 180 | source=self, 181 | ) 182 | 183 | self._asyncgens.add(agen) 184 | 185 | async def shutdown_default_executor(self, timeout=None): 186 | self._executor_shutdown_called = True 187 | if self._default_executor is None: 188 | return 189 | 190 | future = self.create_future() 191 | thread = threading.Thread(target=self._executor_shutdown, args=(future,)) 192 | thread.start() 193 | try: 194 | await future 195 | finally: 196 | thread.join(timeout) 197 | 198 | if thread.is_alive(): 199 | warnings.warn( 200 | f'The executor did not finish joining its threads within {timeout} seconds.', 201 | RuntimeWarning, 202 | stacklevel=2, 203 | ) 204 | self._default_executor.shutdown(wait=False) 205 | 206 | def _executor_shutdown(self, future): 207 | try: 208 | self._default_executor.shutdown(wait=True) 209 | self.call_soon_threadsafe(future.set_result, None) 210 | except Exception as ex: 211 | self.call_soon_threadsafe(future.set_exception, ex) 212 | 213 | #: callback scheduling methods 214 | # def _timer_handle_cancelled(self, handle): 215 | # raise NotImplementedError 216 | 217 | def call_later(self, delay, callback, *args, context=None) -> Union[CBHandle, TimerHandle]: 218 | if delay <= 0: 219 | return self.call_soon(callback, *args, context=context or _copy_context()) 220 | delay = round(delay * 1_000_000) 221 | return self._call_later(delay, callback, args, context or _copy_context()) 222 | 223 | def call_at(self, when, callback, *args, context=None) -> Union[CBHandle, TimerHandle]: 224 | delay = round((when - self.time()) * 1_000_000) 225 | if delay <= 0: 226 | return self.call_soon(callback, *args, context=context or _copy_context()) 227 | return self._call_later(delay, callback, args, context or _copy_context()) 228 | 229 | def time(self) -> float: 230 | return self._clock / 1_000_000 231 | 232 | def create_future(self) -> _Future: 233 | return _Future(loop=self) 234 | 235 | if _PYV >= _PY_311: 236 | 237 | def create_task(self, coro, *, name=None, context=None) -> _Task: 238 | self._check_closed() 239 | if self._task_factory is None: 240 | task = _Task(coro, loop=self, name=name, context=context) 241 | if task._source_traceback: 242 | del task._source_traceback[-1] 243 | else: 244 | if context is None: 245 | # Use legacy API if context is not needed 246 | task = self._task_factory(self, coro) 247 | else: 248 | task = self._task_factory(self, coro, context=context) 249 | 250 | task.set_name(name) 251 | 252 | return task 253 | else: 254 | 255 | def create_task(self, coro, *, name=None, context=None) -> _Task: 256 | self._check_closed() 257 | if self._task_factory is None: 258 | task = _Task(coro, loop=self, name=name) 259 | if task._source_traceback: 260 | del task._source_traceback[-1] 261 | else: 262 | if context is None: 263 | # Use legacy API if context is not needed 264 | task = self._task_factory(self, coro) 265 | else: 266 | task = self._task_factory(self, coro, context=context) 267 | 268 | task.set_name(name) 269 | 270 | return task 271 | 272 | #: threads methods 273 | def run_in_executor(self, executor, fn, *args): 274 | if _iscoroutine(fn) or _iscoroutinefunction(fn): 275 | raise TypeError('Coroutines cannot be used with executors') 276 | 277 | self._check_closed() 278 | 279 | if executor is None: 280 | executor = self._default_executor 281 | if self._executor_shutdown_called: 282 | raise RuntimeError('Executor shutdown has been called') 283 | 284 | if executor is None: 285 | executor = ThreadPoolExecutor() 286 | self._default_executor = executor 287 | 288 | return _wrap_future(executor.submit(fn, *args), loop=self) 289 | 290 | def set_default_executor(self, executor): 291 | self._default_executor = executor 292 | 293 | #: network I/O methods 294 | async def getaddrinfo(self, host, port, *, family=0, type=0, proto=0, flags=0): 295 | return await self.run_in_executor(None, socket.getaddrinfo, host, port, family, type, proto, flags) 296 | 297 | async def getnameinfo(self, sockaddr, flags=0): 298 | return await self.run_in_executor(None, socket.getnameinfo, sockaddr, flags) 299 | 300 | async def create_connection( 301 | self, 302 | protocol_factory, 303 | host=None, 304 | port=None, 305 | *, 306 | ssl=None, 307 | family=0, 308 | proto=0, 309 | flags=0, 310 | sock=None, 311 | local_addr=None, 312 | server_hostname=None, 313 | ssl_handshake_timeout=None, 314 | ssl_shutdown_timeout=None, 315 | happy_eyeballs_delay=None, 316 | interleave=None, 317 | all_errors=False, 318 | ): 319 | # TODO 320 | if ssl: 321 | raise NotImplementedError 322 | 323 | if server_hostname is not None and not ssl: 324 | raise ValueError('server_hostname is only meaningful with ssl') 325 | 326 | if server_hostname is None and ssl: 327 | if not host: 328 | raise ValueError('You must set server_hostname when using ssl without a host') 329 | server_hostname = host 330 | 331 | if ssl_handshake_timeout is not None and not ssl: 332 | raise ValueError('ssl_handshake_timeout is only meaningful with ssl') 333 | 334 | if ssl_shutdown_timeout is not None and not ssl: 335 | raise ValueError('ssl_shutdown_timeout is only meaningful with ssl') 336 | 337 | # TODO 338 | # if sock is not None: 339 | # _check_ssl_socket(sock) 340 | 341 | if happy_eyeballs_delay is not None and interleave is None: 342 | # If using happy eyeballs, default to interleave addresses by family 343 | interleave = 1 344 | 345 | if host is not None or port is not None: 346 | if sock is not None: 347 | raise ValueError('host/port and sock can not be specified at the same time') 348 | 349 | infos = await self._ensure_resolved( 350 | (host, port), family=family, type=socket.SOCK_STREAM, proto=proto, flags=flags 351 | ) 352 | if not infos: 353 | raise OSError('getaddrinfo() returned empty list') 354 | 355 | if local_addr is not None: 356 | laddr_infos = await self._ensure_resolved( 357 | local_addr, family=family, type=socket.SOCK_STREAM, proto=proto, flags=flags 358 | ) 359 | if not laddr_infos: 360 | raise OSError('getaddrinfo() returned empty list') 361 | else: 362 | laddr_infos = None 363 | 364 | if interleave: 365 | infos = _interleave_addrinfos(infos, interleave) 366 | 367 | exceptions = [] 368 | if happy_eyeballs_delay is None: 369 | # not using happy eyeballs 370 | for addrinfo in infos: 371 | try: 372 | sock = await self._connect_sock(exceptions, addrinfo, laddr_infos) 373 | break 374 | except OSError: 375 | continue 376 | else: # using happy eyeballs 377 | sock = ( 378 | await _staggered_race( 379 | ( 380 | # can't use functools.partial as it keeps a reference 381 | # to exceptions 382 | lambda addrinfo=addrinfo: self._connect_sock(exceptions, addrinfo, laddr_infos) 383 | for addrinfo in infos 384 | ), 385 | happy_eyeballs_delay, 386 | loop=self, 387 | ) 388 | )[0] # can't use sock, _, _ as it keeks a reference to exceptions 389 | 390 | if sock is None: 391 | exceptions = [exc for sub in exceptions for exc in sub] 392 | try: 393 | if all_errors: 394 | raise ExceptionGroup('create_connection failed', exceptions) # noqa: F821 395 | if len(exceptions) == 1: 396 | raise exceptions[0] 397 | else: 398 | # If they all have the same str(), raise one. 399 | model = str(exceptions[0]) 400 | if all(str(exc) == model for exc in exceptions): 401 | raise exceptions[0] 402 | # Raise a combined exception so the user can see all 403 | # the various error messages. 404 | raise OSError('Multiple exceptions: {}'.format(', '.join(str(exc) for exc in exceptions))) 405 | finally: 406 | exceptions = None 407 | 408 | else: 409 | if sock is None: 410 | raise ValueError('host and port was not specified and no sock specified') 411 | if sock.type != socket.SOCK_STREAM: 412 | # We allow AF_INET, AF_INET6, AF_UNIX as long as they 413 | # are SOCK_STREAM. 414 | # We support passing AF_UNIX sockets even though we have 415 | # a dedicated API for that: create_unix_connection. 416 | # Disallowing AF_UNIX in this method, breaks backwards 417 | # compatibility. 418 | raise ValueError(f'A Stream Socket was expected, got {sock!r}') 419 | 420 | sock.setblocking(False) 421 | rsock = (sock.fileno(), sock.family) 422 | sock.detach() 423 | 424 | # TODO: ssl 425 | transport, protocol = self._tcp_conn(rsock, protocol_factory) 426 | # transport, protocol = await self._create_connection_transport( 427 | # sock, 428 | # protocol_factory, 429 | # ssl, 430 | # server_hostname, 431 | # ssl_handshake_timeout=ssl_handshake_timeout, 432 | # ssl_shutdown_timeout=ssl_shutdown_timeout, 433 | # ) 434 | 435 | return transport, protocol 436 | 437 | async def _connect_sock(self, exceptions, addr_info, local_addr_infos=None): 438 | my_exceptions = [] 439 | exceptions.append(my_exceptions) 440 | family, type_, proto, _, address = addr_info 441 | sock = None 442 | try: 443 | sock = socket.socket(family=family, type=type_, proto=proto) 444 | sock.setblocking(False) 445 | if local_addr_infos is not None: 446 | for lfamily, _, _, _, laddr in local_addr_infos: 447 | # skip local addresses of different family 448 | if lfamily != family: 449 | continue 450 | try: 451 | sock.bind(laddr) 452 | break 453 | except OSError as exc: 454 | msg = f'error while attempting to bind on address {laddr!r}: {str(exc).lower()}' 455 | exc = OSError(exc.errno, msg) 456 | my_exceptions.append(exc) 457 | else: # all bind attempts failed 458 | if my_exceptions: 459 | raise my_exceptions.pop() 460 | else: 461 | raise OSError(f'no matching local address with {family=} found') 462 | await self.sock_connect(sock, address) 463 | return sock 464 | except OSError as exc: 465 | my_exceptions.append(exc) 466 | if sock is not None: 467 | sock.close() 468 | raise 469 | except: 470 | if sock is not None: 471 | sock.close() 472 | raise 473 | finally: 474 | exceptions = my_exceptions = None 475 | 476 | async def create_server( 477 | self, 478 | protocol_factory, 479 | host=None, 480 | port=None, 481 | *, 482 | family=socket.AF_UNSPEC, 483 | flags=socket.AI_PASSIVE, 484 | sock=None, 485 | backlog=100, 486 | ssl=None, 487 | reuse_address=None, 488 | reuse_port=None, 489 | keep_alive=None, 490 | ssl_handshake_timeout=None, 491 | ssl_shutdown_timeout=None, 492 | start_serving=True, 493 | ): 494 | # TODO 495 | if ssl: 496 | raise NotImplementedError 497 | 498 | if isinstance(ssl, bool): 499 | raise TypeError('ssl argument must be an SSLContext or None') 500 | 501 | if ssl_handshake_timeout is not None and ssl is None: 502 | raise ValueError('ssl_handshake_timeout is only meaningful with ssl') 503 | 504 | if ssl_shutdown_timeout is not None and ssl is None: 505 | raise ValueError('ssl_shutdown_timeout is only meaningful with ssl') 506 | 507 | # TODO 508 | # if sock is not None: 509 | # _check_ssl_socket(sock) 510 | 511 | if host is not None or port is not None: 512 | if sock is not None: 513 | raise ValueError('host/port and sock can not be specified at the same time') 514 | 515 | if reuse_address is None: 516 | reuse_address = os.name == 'posix' and sys.platform != 'cygwin' 517 | 518 | sockets = [] 519 | if host == '': 520 | hosts = [None] 521 | elif isinstance(host, str) or not isinstance(host, (tuple, list)): 522 | hosts = [host] 523 | else: 524 | hosts = host 525 | 526 | fs = [self._create_server_getaddrinfo(host, port, family=family, flags=flags) for host in hosts] 527 | infos = await _gather(*fs) 528 | infos = set(_iterchain.from_iterable(infos)) 529 | 530 | completed = False 531 | try: 532 | for res in infos: 533 | af, socktype, proto, canonname, sa = res 534 | try: 535 | sock = socket.socket(af, socktype, proto) 536 | except socket.error: 537 | # Assume it's a bad family/type/protocol combination. 538 | continue 539 | sockets.append(sock) 540 | if reuse_address: 541 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) 542 | if reuse_port: 543 | _set_reuseport(sock) 544 | if keep_alive: 545 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) 546 | # Disable IPv4/IPv6 dual stack support (enabled by 547 | # default on Linux) which makes a single socket 548 | # listen on both address families. 549 | if _HAS_IPv6 and af == socket.AF_INET6 and hasattr(socket, 'IPPROTO_IPV6'): 550 | sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, True) 551 | try: 552 | sock.bind(sa) 553 | except OSError as err: 554 | msg = 'error while attempting to bind on address %r: %s' % (sa, str(err).lower()) 555 | if err.errno == errno.EADDRNOTAVAIL: 556 | # Assume the family is not enabled (bpo-30945) 557 | sockets.pop() 558 | sock.close() 559 | continue 560 | raise OSError(err.errno, msg) from None 561 | 562 | if not sockets: 563 | raise OSError('could not bind on any address out of %r' % ([info[4] for info in infos],)) 564 | 565 | completed = True 566 | finally: 567 | if not completed: 568 | for sock in sockets: 569 | sock.close() 570 | else: 571 | if sock is None: 572 | raise ValueError('Neither host/port nor sock were specified') 573 | if sock.type != socket.SOCK_STREAM: 574 | raise ValueError(f'A Stream Socket was expected, got {sock!r}') 575 | sockets = [sock] 576 | 577 | rsocks = [] 578 | for sock in sockets: 579 | sock.setblocking(False) 580 | rsocks.append((sock.fileno(), sock.family)) 581 | sock.detach() 582 | 583 | # TODO: ssl 584 | # server = self._tcp_server(sockets, rsocks, protocol_factory, backlog, 585 | # ssl, ssl_handshake_timeout, 586 | # ssl_shutdown_timeout) 587 | server = Server(self._tcp_server(sockets, rsocks, protocol_factory, backlog)) 588 | 589 | if start_serving: 590 | await server.start_serving() 591 | 592 | return server 593 | 594 | async def _create_server_getaddrinfo(self, host, port, family, flags): 595 | infos = await self._ensure_resolved((host, port), family=family, type=socket.SOCK_STREAM, flags=flags) 596 | if not infos: 597 | raise OSError(f'getaddrinfo({host!r}) returned empty list') 598 | 599 | return infos 600 | 601 | async def sendfile(self, transport, file, offset=0, count=None, *, fallback=True): 602 | raise NotImplementedError 603 | 604 | async def start_tls( 605 | self, 606 | transport, 607 | protocol, 608 | sslcontext, 609 | *, 610 | server_side=False, 611 | server_hostname=None, 612 | ssl_handshake_timeout=None, 613 | ssl_shutdown_timeout=None, 614 | ): 615 | raise NotImplementedError 616 | 617 | async def create_unix_connection( 618 | self, 619 | protocol_factory, 620 | path=None, 621 | *, 622 | ssl=None, 623 | sock=None, 624 | server_hostname=None, 625 | ssl_handshake_timeout=None, 626 | ssl_shutdown_timeout=None, 627 | ): 628 | raise NotImplementedError 629 | 630 | async def create_unix_server( 631 | self, 632 | protocol_factory, 633 | path=None, 634 | *, 635 | sock=None, 636 | backlog=100, 637 | ssl=None, 638 | ssl_handshake_timeout=None, 639 | ssl_shutdown_timeout=None, 640 | start_serving=True, 641 | ): 642 | raise NotImplementedError 643 | 644 | async def connect_accepted_socket( 645 | self, protocol_factory, sock, *, ssl=None, ssl_handshake_timeout=None, ssl_shutdown_timeout=None 646 | ): 647 | raise NotImplementedError 648 | 649 | async def create_datagram_endpoint( 650 | self, 651 | protocol_factory, 652 | local_addr=None, 653 | remote_addr=None, 654 | *, 655 | family=0, 656 | proto=0, 657 | flags=0, 658 | #: not in stdlib 659 | # reuse_address=None, 660 | reuse_port=None, 661 | allow_broadcast=None, 662 | sock=None, 663 | ): 664 | if sock is not None: 665 | if getattr(sock, 'type', None) != socket.SOCK_DGRAM: 666 | raise ValueError(f'A datagram socket was expected, got {sock!r}') 667 | if any((local_addr, remote_addr, family, proto, flags, reuse_port, allow_broadcast)): 668 | raise ValueError('socket modifier keyword arguments can not be used when sock is specified.') 669 | sock.setblocking(False) 670 | r_addr = None 671 | else: 672 | if not (local_addr or remote_addr): 673 | if family == 0: 674 | raise ValueError('unexpected address family') 675 | addr_info = (family, proto, None, None) 676 | elif hasattr(socket, 'AF_UNIX') and family == socket.AF_UNIX: 677 | for addr in (local_addr, remote_addr): 678 | if addr is not None and not isinstance(addr, str): 679 | raise TypeError('string is expected') 680 | addr_info = (family, proto, local_addr, remote_addr) 681 | else: 682 | addr_info, infos = None, None 683 | for addr in (local_addr, remote_addr): 684 | if addr is None: 685 | continue 686 | if not (isinstance(addr, tuple) and len(addr) == 2): 687 | raise TypeError('2-tuple is expected') 688 | infos = await self._ensure_resolved( 689 | addr, family=family, type=socket.SOCK_DGRAM, proto=proto, flags=flags 690 | ) 691 | break 692 | 693 | if not infos: 694 | raise OSError('getaddrinfo() returned empty list') 695 | if local_addr is not None: 696 | addr_info = (infos[0][0], infos[0][2], infos[0][4], None) 697 | if remote_addr is not None: 698 | addr_info = (infos[0][0], infos[0][2], None, infos[0][4]) 699 | if not addr_info: 700 | raise ValueError('can not get address information') 701 | 702 | sock = None 703 | r_addr = None 704 | sfam, spro, sladdr, sraddr = addr_info 705 | try: 706 | sock = socket.socket(family=sfam, type=socket.SOCK_DGRAM, proto=spro) 707 | #: not in stdlib 708 | # if reuse_address: 709 | # sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 710 | if reuse_port: 711 | _set_reuseport(sock) 712 | if allow_broadcast: 713 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 714 | sock.setblocking(False) 715 | if sladdr: 716 | sock.bind(sladdr) 717 | if sraddr: 718 | if not allow_broadcast: 719 | await self.sock_connect(sock, sraddr) 720 | r_addr = sraddr 721 | except OSError: 722 | if sock is not None: 723 | sock.close() 724 | raise 725 | 726 | # Create the transport 727 | transport, protocol = self._udp_conn((sock.fileno(), sock.family), protocol_factory, r_addr) 728 | # sock is now owned by the transport, prevent close 729 | sock.detach() 730 | return transport, protocol 731 | 732 | #: pipes and subprocesses methods 733 | async def connect_read_pipe(self, protocol_factory, pipe): 734 | protocol = protocol_factory() 735 | waiter = self.create_future() 736 | transport = _PipeReadTransport(self, pipe, protocol, waiter) 737 | try: 738 | await waiter 739 | except BaseException: 740 | transport.close() 741 | raise 742 | return transport, protocol 743 | 744 | async def connect_write_pipe(self, protocol_factory, pipe): 745 | protocol = protocol_factory() 746 | waiter = self.create_future() 747 | transport = _PipeWriteTransport(self, pipe, protocol, waiter) 748 | try: 749 | await waiter 750 | except BaseException: 751 | transport.close() 752 | raise 753 | return transport, protocol 754 | 755 | def subprocess_shell( 756 | self, protocol_factory, cmd, *, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs 757 | ): 758 | return self.__subprocess_run( # noqa: S604 759 | protocol_factory, (cmd,), stdin=stdin, stdout=stdout, stderr=stderr, shell=True, **kwargs 760 | ) 761 | 762 | def subprocess_exec( 763 | self, protocol_factory, *args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs 764 | ): 765 | return self.__subprocess_run( 766 | protocol_factory, args, stdin=stdin, stdout=stdout, stderr=stderr, shell=False, **kwargs 767 | ) 768 | 769 | async def __subprocess_run( 770 | self, 771 | protocol_factory, 772 | args, 773 | stdin=subprocess.PIPE, 774 | stdout=subprocess.PIPE, 775 | stderr=subprocess.PIPE, 776 | shell=False, 777 | bufsize=0, 778 | universal_newlines=False, 779 | executable=None, 780 | **kwargs, 781 | ): 782 | if universal_newlines: 783 | raise ValueError('universal_newlines not supported') 784 | if bufsize != 0: 785 | raise ValueError('bufsize must be 0') 786 | 787 | if executable is not None: 788 | args[0] = executable 789 | 790 | waiter = self.create_future() 791 | proto = protocol_factory() 792 | transp = _SubProcessTransport( 793 | self, 794 | proto, 795 | args, 796 | shell=shell, 797 | stdin=stdin, 798 | stdout=stdout, 799 | stderr=stderr, 800 | bufsize=bufsize, 801 | waiter=waiter, 802 | **kwargs, 803 | ) 804 | self._watcher_child.add_child_handler(transp.get_pid(), self._child_watcher_callback, transp) 805 | 806 | try: 807 | await waiter 808 | except (KeyboardInterrupt, SystemExit): 809 | raise 810 | except BaseException: 811 | transp.close() 812 | await transp._wait() 813 | raise 814 | 815 | return transp, proto 816 | 817 | def _child_watcher_callback(self, pid, returncode, transp): 818 | self.call_soon_threadsafe(transp._process_exited, returncode) 819 | 820 | #: completion based I/O methods 821 | def _ensure_fd_no_transport(self, fd): 822 | fileno = fd 823 | if not isinstance(fileno, int): 824 | try: 825 | fileno = int(fileno.fileno()) 826 | except (AttributeError, TypeError, ValueError): 827 | raise ValueError(f'Invalid file object: {fd!r}') from None 828 | if self._tcp_stream_bound(fileno): 829 | raise RuntimeError(f'File descriptor {fd!r} is used by transport') 830 | 831 | def sock_recv(self, sock, nbytes) -> _Future: 832 | future = _SyncSockReaderFuture(sock, self) 833 | fd = sock.fileno() 834 | self._ensure_fd_no_transport(fd) 835 | self.add_reader(fd, self._sock_recv, future, sock, nbytes) 836 | return future 837 | 838 | def _sock_recv(self, fut, sock, n): 839 | try: 840 | data = sock.recv(n) 841 | except (BlockingIOError, InterruptedError): 842 | return 843 | except (SystemExit, KeyboardInterrupt): 844 | raise 845 | except BaseException as exc: 846 | fut.set_exception(exc) 847 | self.remove_reader(sock.fileno()) 848 | else: 849 | fut.set_result(data) 850 | self.remove_reader(sock.fileno()) 851 | 852 | def sock_recv_into(self, sock, buf) -> _Future: 853 | future = _SyncSockReaderFuture(sock, self) 854 | fd = sock.fileno() 855 | self._ensure_fd_no_transport(fd) 856 | self.add_reader(fd, self._sock_recv_into, future, sock, buf) 857 | return future 858 | 859 | def _sock_recv_into(self, fut, sock, buf): 860 | try: 861 | data = sock.recv_into(buf) 862 | except (BlockingIOError, InterruptedError): 863 | return 864 | except (KeyboardInterrupt, SystemExit): 865 | raise 866 | except BaseException as exc: 867 | fut.set_exception(exc) 868 | self.remove_reader(sock.fileno()) 869 | else: 870 | fut.set_result(data) 871 | self.remove_reader(sock.fileno()) 872 | 873 | # async def sock_recvfrom(self, sock, bufsize): 874 | # raise NotImplementedError 875 | 876 | # async def sock_recvfrom_into(self, sock, buf, nbytes=0): 877 | # raise NotImplementedError 878 | 879 | async def sock_sendall(self, sock, data): 880 | if not data: 881 | return 882 | 883 | try: 884 | n = sock.send(data) 885 | except (BlockingIOError, InterruptedError): 886 | n = 0 887 | 888 | if n == len(data): 889 | return 890 | 891 | fd = sock.fileno() 892 | self._ensure_fd_no_transport(fd) 893 | future = _SyncSockWriterFuture(sock, self) 894 | self.add_writer(fd, self._sock_sendall, future, sock, memoryview(data), [n]) 895 | return await future 896 | 897 | def _sock_sendall(self, fut, sock, data, pos): 898 | start = pos[0] 899 | 900 | try: 901 | n = sock.send(data[start:]) 902 | except (BlockingIOError, InterruptedError): 903 | return 904 | except (SystemExit, KeyboardInterrupt): 905 | raise 906 | except BaseException as exc: 907 | fut.set_exception(exc) 908 | self.remove_writer(sock.fileno()) 909 | return 910 | 911 | start += n 912 | 913 | if start == len(data): 914 | fut.set_result(None) 915 | self.remove_writer(sock.fileno()) 916 | else: 917 | pos[0] = start 918 | 919 | # async def sock_sendto(self, sock, data, address): 920 | # raise NotImplementedError 921 | 922 | async def sock_connect(self, sock, address): 923 | if sock.family == socket.AF_INET or (_HAS_IPv6 and sock.family == socket.AF_INET6): 924 | resolved = await self._ensure_resolved( 925 | address, 926 | family=sock.family, 927 | type=sock.type, 928 | proto=sock.proto, 929 | ) 930 | _, _, _, _, address = resolved[0] 931 | 932 | fut = self._sock_connect(sock, address) 933 | if fut is not None: 934 | await fut 935 | 936 | async def _ensure_resolved(self, address, *, family=0, type=socket.SOCK_STREAM, proto=0, flags=0): 937 | host, port = address[:2] 938 | info = _ipaddr_info(host, port, family, type, proto, *address[2:]) 939 | if info is not None: 940 | # "host" is already a resolved IP. 941 | return [info] 942 | else: 943 | return await self.getaddrinfo(host, port, family=family, type=type, proto=proto, flags=flags) 944 | 945 | def _sock_connect(self, sock, address) -> _Future: 946 | try: 947 | sock.connect(address) 948 | except (BlockingIOError, InterruptedError): 949 | pass 950 | else: 951 | return 952 | 953 | fd = sock.fileno() 954 | self._ensure_fd_no_transport(fd) 955 | future = _SyncSockWriterFuture(sock, self) 956 | self.add_writer(fd, self._sock_connect_cb, future, sock, address) 957 | return future 958 | 959 | def _sock_connect_cb(self, fut, sock, address): 960 | try: 961 | err = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) 962 | if err != 0: 963 | # Jump to any except clause below. 964 | raise OSError(err, 'Connect call failed %s' % (address,)) 965 | except (BlockingIOError, InterruptedError): 966 | return 967 | except (KeyboardInterrupt, SystemExit): 968 | raise 969 | except BaseException as exc: 970 | fut.set_exception(exc) 971 | self.remove_writer(sock.fileno()) 972 | else: 973 | fut.set_result(None) 974 | self.remove_writer(sock.fileno()) 975 | 976 | def sock_accept(self, sock) -> _Future: 977 | fd = sock.fileno() 978 | self._ensure_fd_no_transport(fd) 979 | future = _SyncSockReaderFuture(sock, self) 980 | self.add_reader(fd, self._sock_accept, future, sock) 981 | return future 982 | 983 | def _sock_accept(self, fut, sock): 984 | try: 985 | conn, address = sock.accept() 986 | conn.setblocking(False) 987 | except (BlockingIOError, InterruptedError): 988 | return 989 | except (SystemExit, KeyboardInterrupt): 990 | raise 991 | except BaseException as exc: 992 | fut.set_exception(exc) 993 | self.remove_reader(sock.fileno()) 994 | else: 995 | fut.set_result((conn, address)) 996 | self.remove_reader(sock.fileno()) 997 | 998 | # async def sock_sendfile(self, sock, file, offset=0, count=None, *, fallback=None): 999 | # raise NotImplementedError 1000 | 1001 | #: signals 1002 | def _ssock_start(self): 1003 | if self._ssock_w is not None: 1004 | raise RuntimeError('self-socket has been already setup') 1005 | 1006 | self._ssock_r, self._ssock_w = socket.socketpair() 1007 | try: 1008 | self._ssock_r.setblocking(False) 1009 | self._ssock_w.setblocking(False) 1010 | self._ssock_set(self._ssock_r.fileno(), self._ssock_w.fileno()) 1011 | except Exception: 1012 | self._ssock_del(self._ssock_r.fileno()) 1013 | self._ssock_w = None 1014 | self._ssock_r = None 1015 | raise 1016 | 1017 | def _ssock_stop(self): 1018 | if not self._ssock_w: 1019 | raise RuntimeError('self-socket has not been setup') 1020 | 1021 | self._ssock_del(self._ssock_r.fileno()) 1022 | self._ssock_w = None 1023 | self._ssock_r = None 1024 | 1025 | def add_signal_handler(self, sig, callback, *args): 1026 | if not self.__is_main_thread(): 1027 | raise ValueError('Signals can only be handled from the main thread') 1028 | 1029 | if _iscoroutine(callback) or _iscoroutinefunction(callback): 1030 | raise TypeError('Coroutines cannot be used as signals handlers') 1031 | 1032 | self._check_closed() 1033 | self._check_signal(sig) 1034 | self._sig_add(sig, callback, args, _copy_context()) 1035 | try: 1036 | # register a dummy signal handler so Python will write the signal no in the wakeup fd 1037 | signal.signal(sig, _noop) 1038 | # set SA_RESTART to limit EINTR occurrences 1039 | signal.siginterrupt(sig, False) 1040 | except OSError as exc: 1041 | self._sig_rem(sig) 1042 | if exc.errno == errno.EINVAL: 1043 | raise RuntimeError(f'signum {sig} cannot be caught') 1044 | raise 1045 | 1046 | def remove_signal_handler(self, sig): 1047 | if not self._is_main_thread(): 1048 | raise ValueError('Signals can only be handled from the main thread') 1049 | 1050 | if not self._sig_rem(sig): 1051 | return False 1052 | 1053 | if sig == signal.SIGINT: 1054 | handler = signal.default_int_handler 1055 | else: 1056 | handler = signal.SIG_DFL 1057 | 1058 | try: 1059 | signal.signal(sig, handler) 1060 | except OSError as exc: 1061 | if exc.errno == errno.EINVAL: 1062 | raise RuntimeError(f'signum {sig} cannot be caught') 1063 | raise 1064 | 1065 | return True 1066 | 1067 | def __is_main_thread(self): 1068 | return threading.main_thread().ident == threading.current_thread().ident 1069 | 1070 | def _check_signal(self, sig): 1071 | if not isinstance(sig, int): 1072 | raise TypeError(f'sig must be an int, not {sig!r}') 1073 | 1074 | if sig not in signal.valid_signals(): 1075 | raise ValueError(f'invalid signal number {sig}') 1076 | 1077 | def __set_sig_wfd(self, fd): 1078 | if fd >= 0: 1079 | return signal.set_wakeup_fd(fd, warn_on_full_buffer=False) 1080 | return signal.set_wakeup_fd(fd) 1081 | 1082 | def _signals_resume(self): 1083 | if not self.__is_main_thread(): 1084 | return 1085 | 1086 | if self._sig_listening: 1087 | raise RuntimeError('Signals handling has been already setup') 1088 | 1089 | try: 1090 | fd = self._ssock_w.fileno() 1091 | self._sig_wfd = self.__set_sig_wfd(fd) 1092 | except Exception: 1093 | raise 1094 | 1095 | self._sig_listening = True 1096 | 1097 | def _signals_pause(self): 1098 | if not self.__is_main_thread(): 1099 | if self._sig_listening: 1100 | raise RuntimeError('Cannot pause signals handling outside of the main thread') 1101 | return 1102 | 1103 | if not self._sig_listening: 1104 | raise RuntimeError('Signals handling has not been setup') 1105 | 1106 | self._sig_listening = False 1107 | self.__set_sig_wfd(self._sig_wfd) 1108 | 1109 | def _signals_clear(self): 1110 | if not self.__is_main_thread(): 1111 | return 1112 | 1113 | if self._sig_listening: 1114 | raise RuntimeError('Cannot clear signals handling while listening') 1115 | 1116 | if self._ssock_r: 1117 | raise RuntimeError('Signals handling was not cleaned up') 1118 | 1119 | self._sig_clear() 1120 | 1121 | #: task factory 1122 | def set_task_factory(self, factory): 1123 | self._task_factory = factory 1124 | 1125 | def get_task_factory(self): 1126 | return self._task_factory 1127 | 1128 | #: error handlers 1129 | def get_exception_handler(self): 1130 | return self._exception_handler 1131 | 1132 | def set_exception_handler(self, handler): 1133 | self._exception_handler = handler 1134 | 1135 | def call_exception_handler(self, context): 1136 | return self._exc_handler(context, self._exception_handler) 1137 | 1138 | #: debug management 1139 | def get_debug(self) -> bool: 1140 | return False 1141 | 1142 | # TODO 1143 | def set_debug(self, enabled: bool): 1144 | return 1145 | --------------------------------------------------------------------------------