rustwasgi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rustwasgi-0.1.0/.github/workflows/CI.yml +186 -0
- rustwasgi-0.1.0/.gitignore +72 -0
- rustwasgi-0.1.0/AGENTS.md +79 -0
- rustwasgi-0.1.0/Cargo.lock +919 -0
- rustwasgi-0.1.0/Cargo.toml +23 -0
- rustwasgi-0.1.0/PKG-INFO +13 -0
- rustwasgi-0.1.0/README.md +360 -0
- rustwasgi-0.1.0/bench/bench.py +436 -0
- rustwasgi-0.1.0/deploy/rustwasgi.service +23 -0
- rustwasgi-0.1.0/docs/report.md +146 -0
- rustwasgi-0.1.0/examples/asgi2_app.py +27 -0
- rustwasgi-0.1.0/examples/asgi_app.py +41 -0
- rustwasgi-0.1.0/examples/bench_app.py +17 -0
- rustwasgi-0.1.0/examples/factory_app.py +13 -0
- rustwasgi-0.1.0/examples/fastapi_app.py +49 -0
- rustwasgi-0.1.0/examples/fastapi_lifespan.py +37 -0
- rustwasgi-0.1.0/examples/lifespan_state_app.py +31 -0
- rustwasgi-0.1.0/examples/websocket_app.py +30 -0
- rustwasgi-0.1.0/pyproject.toml +28 -0
- rustwasgi-0.1.0/python/rustwasgi/__init__.py +15 -0
- rustwasgi-0.1.0/python/rustwasgi/__main__.py +63 -0
- rustwasgi-0.1.0/python/rustwasgi/gunicorn.py +167 -0
- rustwasgi-0.1.0/src/asgi.rs +726 -0
- rustwasgi-0.1.0/src/cli.rs +67 -0
- rustwasgi-0.1.0/src/lib.rs +495 -0
- rustwasgi-0.1.0/src/lifespan.rs +306 -0
- rustwasgi-0.1.0/src/metrics.rs +191 -0
- rustwasgi-0.1.0/src/runtime.rs +182 -0
- rustwasgi-0.1.0/src/server.rs +1164 -0
- rustwasgi-0.1.0/src/socket.rs +158 -0
- rustwasgi-0.1.0/src/ws.rs +663 -0
- rustwasgi-0.1.0/tests/_util.py +173 -0
- rustwasgi-0.1.0/tests/test_asgi.py +173 -0
- rustwasgi-0.1.0/tests/test_bridge.py +73 -0
- rustwasgi-0.1.0/tests/test_fastapi.py +97 -0
- rustwasgi-0.1.0/tests/test_gunicorn.py +215 -0
- rustwasgi-0.1.0/tests/test_lifespan.py +52 -0
- rustwasgi-0.1.0/tests/test_websocket.py +65 -0
- rustwasgi-0.1.0/uv.lock +1392 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# This file is autogenerated by maturin v1.15.0
|
|
2
|
+
# To update, run
|
|
3
|
+
#
|
|
4
|
+
# maturin generate-ci github
|
|
5
|
+
#
|
|
6
|
+
name: CI
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches:
|
|
11
|
+
- main
|
|
12
|
+
- master
|
|
13
|
+
tags:
|
|
14
|
+
- '*'
|
|
15
|
+
pull_request:
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
linux:
|
|
23
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
24
|
+
strategy:
|
|
25
|
+
matrix:
|
|
26
|
+
platform:
|
|
27
|
+
- runner: ubuntu-22.04
|
|
28
|
+
target: x86_64
|
|
29
|
+
- runner: ubuntu-22.04
|
|
30
|
+
target: x86
|
|
31
|
+
- runner: ubuntu-22.04
|
|
32
|
+
target: aarch64
|
|
33
|
+
- runner: ubuntu-22.04
|
|
34
|
+
target: armv7
|
|
35
|
+
- runner: ubuntu-22.04
|
|
36
|
+
target: s390x
|
|
37
|
+
- runner: ubuntu-22.04
|
|
38
|
+
target: ppc64le
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v6
|
|
41
|
+
- uses: actions/setup-python@v6
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.x"
|
|
44
|
+
- name: Build wheels
|
|
45
|
+
uses: PyO3/maturin-action@v1
|
|
46
|
+
with:
|
|
47
|
+
target: ${{ matrix.platform.target }}
|
|
48
|
+
args: --release --out dist --find-interpreter
|
|
49
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
50
|
+
manylinux: auto
|
|
51
|
+
- name: Upload wheels
|
|
52
|
+
uses: actions/upload-artifact@v6
|
|
53
|
+
with:
|
|
54
|
+
name: wheels-linux-${{ matrix.platform.target }}
|
|
55
|
+
path: dist
|
|
56
|
+
|
|
57
|
+
musllinux:
|
|
58
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
59
|
+
strategy:
|
|
60
|
+
matrix:
|
|
61
|
+
platform:
|
|
62
|
+
- runner: ubuntu-22.04
|
|
63
|
+
target: x86_64
|
|
64
|
+
- runner: ubuntu-22.04
|
|
65
|
+
target: x86
|
|
66
|
+
- runner: ubuntu-22.04
|
|
67
|
+
target: aarch64
|
|
68
|
+
- runner: ubuntu-22.04
|
|
69
|
+
target: armv7
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v6
|
|
72
|
+
- uses: actions/setup-python@v6
|
|
73
|
+
with:
|
|
74
|
+
python-version: "3.x"
|
|
75
|
+
- name: Build wheels
|
|
76
|
+
uses: PyO3/maturin-action@v1
|
|
77
|
+
with:
|
|
78
|
+
target: ${{ matrix.platform.target }}
|
|
79
|
+
args: --release --out dist --find-interpreter
|
|
80
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
81
|
+
manylinux: musllinux_1_2
|
|
82
|
+
- name: Upload wheels
|
|
83
|
+
uses: actions/upload-artifact@v6
|
|
84
|
+
with:
|
|
85
|
+
name: wheels-musllinux-${{ matrix.platform.target }}
|
|
86
|
+
path: dist
|
|
87
|
+
|
|
88
|
+
windows:
|
|
89
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
90
|
+
strategy:
|
|
91
|
+
matrix:
|
|
92
|
+
platform:
|
|
93
|
+
- runner: windows-latest
|
|
94
|
+
target: x64
|
|
95
|
+
python_arch: x64
|
|
96
|
+
- runner: windows-latest
|
|
97
|
+
target: x86
|
|
98
|
+
python_arch: x86
|
|
99
|
+
- runner: windows-11-arm
|
|
100
|
+
target: aarch64
|
|
101
|
+
python_arch: arm64
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v6
|
|
104
|
+
- uses: actions/setup-python@v6
|
|
105
|
+
with:
|
|
106
|
+
python-version: "3.13"
|
|
107
|
+
architecture: ${{ matrix.platform.python_arch }}
|
|
108
|
+
- name: Build wheels
|
|
109
|
+
uses: PyO3/maturin-action@v1
|
|
110
|
+
with:
|
|
111
|
+
target: ${{ matrix.platform.target }}
|
|
112
|
+
args: --release --out dist --find-interpreter
|
|
113
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
114
|
+
- name: Upload wheels
|
|
115
|
+
uses: actions/upload-artifact@v6
|
|
116
|
+
with:
|
|
117
|
+
name: wheels-windows-${{ matrix.platform.target }}
|
|
118
|
+
path: dist
|
|
119
|
+
|
|
120
|
+
macos:
|
|
121
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
122
|
+
strategy:
|
|
123
|
+
matrix:
|
|
124
|
+
platform:
|
|
125
|
+
- runner: macos-15-intel
|
|
126
|
+
target: x86_64
|
|
127
|
+
- runner: macos-latest
|
|
128
|
+
target: aarch64
|
|
129
|
+
steps:
|
|
130
|
+
- uses: actions/checkout@v6
|
|
131
|
+
- uses: actions/setup-python@v6
|
|
132
|
+
with:
|
|
133
|
+
python-version: "3.x"
|
|
134
|
+
- name: Build wheels
|
|
135
|
+
uses: PyO3/maturin-action@v1
|
|
136
|
+
with:
|
|
137
|
+
target: ${{ matrix.platform.target }}
|
|
138
|
+
args: --release --out dist --find-interpreter
|
|
139
|
+
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
140
|
+
- name: Upload wheels
|
|
141
|
+
uses: actions/upload-artifact@v6
|
|
142
|
+
with:
|
|
143
|
+
name: wheels-macos-${{ matrix.platform.target }}
|
|
144
|
+
path: dist
|
|
145
|
+
|
|
146
|
+
sdist:
|
|
147
|
+
runs-on: ubuntu-latest
|
|
148
|
+
steps:
|
|
149
|
+
- uses: actions/checkout@v6
|
|
150
|
+
- name: Build sdist
|
|
151
|
+
uses: PyO3/maturin-action@v1
|
|
152
|
+
with:
|
|
153
|
+
command: sdist
|
|
154
|
+
args: --out dist
|
|
155
|
+
- name: Upload sdist
|
|
156
|
+
uses: actions/upload-artifact@v6
|
|
157
|
+
with:
|
|
158
|
+
name: wheels-sdist
|
|
159
|
+
path: dist
|
|
160
|
+
|
|
161
|
+
release:
|
|
162
|
+
name: Release
|
|
163
|
+
runs-on: ubuntu-latest
|
|
164
|
+
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
|
|
165
|
+
needs: [linux, musllinux, windows, macos, sdist]
|
|
166
|
+
permissions:
|
|
167
|
+
# Use to sign the release artifacts
|
|
168
|
+
id-token: write
|
|
169
|
+
# Used to upload release artifacts
|
|
170
|
+
contents: write
|
|
171
|
+
# Used to generate artifact attestation
|
|
172
|
+
attestations: write
|
|
173
|
+
steps:
|
|
174
|
+
- uses: actions/download-artifact@v7
|
|
175
|
+
- name: Generate artifact attestation
|
|
176
|
+
uses: actions/attest@v4
|
|
177
|
+
with:
|
|
178
|
+
subject-path: 'wheels-*/*'
|
|
179
|
+
- name: Install uv
|
|
180
|
+
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
181
|
+
uses: astral-sh/setup-uv@v7
|
|
182
|
+
- name: Publish to PyPI
|
|
183
|
+
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
184
|
+
run: uv publish 'wheels-*/*'
|
|
185
|
+
env:
|
|
186
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/target
|
|
2
|
+
|
|
3
|
+
# Byte-compiled / optimized / DLL files
|
|
4
|
+
__pycache__/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
|
|
8
|
+
# C extensions
|
|
9
|
+
*.so
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
.Python
|
|
13
|
+
.venv/
|
|
14
|
+
env/
|
|
15
|
+
bin/
|
|
16
|
+
build/
|
|
17
|
+
develop-eggs/
|
|
18
|
+
dist/
|
|
19
|
+
eggs/
|
|
20
|
+
lib/
|
|
21
|
+
lib64/
|
|
22
|
+
parts/
|
|
23
|
+
sdist/
|
|
24
|
+
var/
|
|
25
|
+
include/
|
|
26
|
+
man/
|
|
27
|
+
venv/
|
|
28
|
+
*.egg-info/
|
|
29
|
+
.installed.cfg
|
|
30
|
+
*.egg
|
|
31
|
+
|
|
32
|
+
# Installer logs
|
|
33
|
+
pip-log.txt
|
|
34
|
+
pip-delete-this-directory.txt
|
|
35
|
+
pip-selfcheck.json
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.coverage
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
|
|
45
|
+
# Translations
|
|
46
|
+
*.mo
|
|
47
|
+
|
|
48
|
+
# Mr Developer
|
|
49
|
+
.mr.developer.cfg
|
|
50
|
+
.project
|
|
51
|
+
.pydevproject
|
|
52
|
+
|
|
53
|
+
# Rope
|
|
54
|
+
.ropeproject
|
|
55
|
+
|
|
56
|
+
# Django stuff:
|
|
57
|
+
*.log
|
|
58
|
+
*.pot
|
|
59
|
+
|
|
60
|
+
.DS_Store
|
|
61
|
+
|
|
62
|
+
# Sphinx documentation
|
|
63
|
+
docs/_build/
|
|
64
|
+
|
|
65
|
+
# PyCharm
|
|
66
|
+
.idea/
|
|
67
|
+
|
|
68
|
+
# VSCode
|
|
69
|
+
.vscode/
|
|
70
|
+
|
|
71
|
+
# Pyenv
|
|
72
|
+
.python-version
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# AGENTS.md — rustwasgi
|
|
2
|
+
|
|
3
|
+
Rust + PyO3 ASGI server (hyper/Tokio) deployed as a **Gunicorn custom worker**
|
|
4
|
+
(`python/rustwasgi/gunicorn.py`). 26 pytest tests cover the production path.
|
|
5
|
+
|
|
6
|
+
## Environment
|
|
7
|
+
|
|
8
|
+
- `.venv` is uv-managed and has **no `pip` binary**. Never call `pip`/`python -m pip`.
|
|
9
|
+
- Install: `uv pip install <pkg>` (with venv active or `VIRTUAL_ENV=.venv`)
|
|
10
|
+
- Always use `.venv/bin/python`; system `python3` is a different interpreter.
|
|
11
|
+
- Console scripts used by tests live in `.venv/bin/` (`gunicorn`, `pytest`).
|
|
12
|
+
- Toolchain (verify with `rustc --version`): rustc ~1.97, `maturin 1.15.0` at
|
|
13
|
+
`.venv/bin/maturin`, `pyo3 0.29.0`, `edition = "2024"`, hyper 1.x,
|
|
14
|
+
tokio-tungstenite 0.26. No `wrk`/`hey`/`ab`/`perf`/`pidstat`/`strace`;
|
|
15
|
+
benchmarks use `bench/bench.py`, profiling uses `/proc` task stats +
|
|
16
|
+
`RUSTWASGI_PROFILE=1` (see `src/metrics.rs`).
|
|
17
|
+
|
|
18
|
+
## Build / verify (order matters)
|
|
19
|
+
|
|
20
|
+
1. `cargo check` → `cargo clippy --all-targets -- -D warnings` → `cargo fmt --check`
|
|
21
|
+
2. `maturin develop` (**must** run with venv active; needs `VIRTUAL_ENV` set)
|
|
22
|
+
3. `.venv/bin/python -m pytest tests/ -v` (spawns real servers on free ports)
|
|
23
|
+
- No git repo here: `cargo fix` needs `--allow-no-vcs`; don't use bare `pkill -f`
|
|
24
|
+
with a pattern that also matches your own shell command line.
|
|
25
|
+
- CI (`.github/workflows/CI.yml`, autogenerated `maturin generate-ci`) only
|
|
26
|
+
builds wheels + sdist, runs **no tests**. Verify locally.
|
|
27
|
+
|
|
28
|
+
## Layout / config
|
|
29
|
+
|
|
30
|
+
- `Cargo.toml`: `[lib] name = "_rustwasgi"`, `crate-type = ["cdylib"]`;
|
|
31
|
+
`pyproject.toml` maps it via `[tool.maturin] module-name = "rustwasgi._rustwasgi"`.
|
|
32
|
+
Keep `#[pymodule] fn _rustwasgi` in sync with both.
|
|
33
|
+
- `src/`: `lib.rs` (`run` standalone + `run_worker` gunicorn entry),
|
|
34
|
+
`server.rs` (hyper), `asgi.rs` (bridge), `runtime.rs` (asyncio loop thread),
|
|
35
|
+
`lifespan.rs`, `ws.rs` (tungstenite), `socket.rs` (inherited FDs), `cli.rs`.
|
|
36
|
+
Don't collapse into one file.
|
|
37
|
+
- `python/rustwasgi/`: `__init__.py`, `__main__.py` (standalone CLI +
|
|
38
|
+
`RUSTWASGI_*` env), `gunicorn.py` (worker; Gunicorn owns master duties).
|
|
39
|
+
- `examples/`: FastAPI/WS/lifespan/pure-ASGI/ASGI2/factory apps for tests.
|
|
40
|
+
- `.venv/` and `/target` are gitignored; never commit them or `*.so`.
|
|
41
|
+
|
|
42
|
+
## Hard-earned gotchas (perf work added more — profile before optimizing)
|
|
43
|
+
|
|
44
|
+
- Perf workflow: `bench/bench.py` (JSON + medians) → per-thread `/proc` CPU
|
|
45
|
+
→ `RUSTWASGI_PROFILE=1` phase table → change ONE thing → re-bench. Past
|
|
46
|
+
wins: `TCP_NODELAY` on accept (42ms floor), merging 6 GIL attaches into 1
|
|
47
|
+
(+40% RPS), RAII drain guard (IncompleteRead). Past non-issues: accept
|
|
48
|
+
skew without `--reuse-port` (environmental), single-loader caps above
|
|
49
|
+
~3500 rps (use parallel loader processes), `RUSTWASGI_THREADS` 2≈8 here.
|
|
50
|
+
- `/proc/PID/status` switch counters can be stale in containers; sum
|
|
51
|
+
`/proc/PID/task/*/status` instead. No ptrace here (py-spy/gdb attach fail).
|
|
52
|
+
- `loop.call_soon_threadsafe(put, msg)` — pass `(put, msg)`, NOT `(put, (msg,))`
|
|
53
|
+
(PyO3 turns the Rust 1-tuple into a Python tuple and the app gets garbage).
|
|
54
|
+
(Legacy note: direct threadsafe calls are gone from the request path;
|
|
55
|
+
only `runtime.rs` loop-stop keeps one, allowlisted in `test_bridge.py`.)
|
|
56
|
+
- Lifespan handshake: race `from_app.get()` vs app-exit vs timeout (no
|
|
57
|
+
polling); never print tracebacks for "unsupported" (pure-HTTP apps raise
|
|
58
|
+
by design — one line). Always `cancel()` timed-out futures or the
|
|
59
|
+
loop logs "Task was destroyed but it is pending" at close.
|
|
60
|
+
- Tokio listeners must be created inside the serving runtime's context
|
|
61
|
+
(`rt.enter()` for inherited FDs; `block_on(bind)` for standalone).
|
|
62
|
+
- Dropping a multi-thread Tokio runtime waits on spawned tasks: the signal
|
|
63
|
+
watcher must exit via a done-channel, plus `rt.shutdown_timeout(5)`.
|
|
64
|
+
- PyO3 0.29: `Bound::cast` (not `downcast`), `Py::bind` returns `&Bound`
|
|
65
|
+
(no `Option<Py>`/struct `Clone` without GIL — write manual impls),
|
|
66
|
+
`py.detach` closures need owned `Send` captures, `http_body_util::channel`
|
|
67
|
+
needs `features = ["channel"]` and `Channel::new(buffer)`.
|
|
68
|
+
- pyo3-async-runtimes 0.29: `into_future_with_locals(&locals, coro)` is
|
|
69
|
+
runtime--agnostic (oneshot/waker, await on OUR runtime); `future_into_py*`
|
|
70
|
+
spawns on the GLOBAL runtime, so call `init_with_runtime(leaked ours)` once
|
|
71
|
+
post-fork. `TaskLocals::new(loop)` (not `get_current_locals`, which reads
|
|
72
|
+
the calling thread). Returned futures are `impl Future + Send` — `Box::pin`.
|
|
73
|
+
`block_on` with GIL held starves the loop thread (same as join).
|
|
74
|
+
- hyper 1.x: `serve_connection(...).with_upgrades()` required for WS;
|
|
75
|
+
`Upgraded` only impls hyper I/O traits — `src/ws.rs` has the Tokio adapter.
|
|
76
|
+
- Gunicorn contract (v26 source of truth in `.venv/.../gunicorn/workers/`):
|
|
77
|
+
subclass `base.Worker`, override `load_wsgi`/`run`, call `notify()` every
|
|
78
|
+
`timeout` seconds from Rust, mirror `alive`, count for `max_requests`,
|
|
79
|
+
`dup()` listener FDs (never steal them), return from `run()` to recycle.
|