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.
Files changed (39) hide show
  1. rustwasgi-0.1.0/.github/workflows/CI.yml +186 -0
  2. rustwasgi-0.1.0/.gitignore +72 -0
  3. rustwasgi-0.1.0/AGENTS.md +79 -0
  4. rustwasgi-0.1.0/Cargo.lock +919 -0
  5. rustwasgi-0.1.0/Cargo.toml +23 -0
  6. rustwasgi-0.1.0/PKG-INFO +13 -0
  7. rustwasgi-0.1.0/README.md +360 -0
  8. rustwasgi-0.1.0/bench/bench.py +436 -0
  9. rustwasgi-0.1.0/deploy/rustwasgi.service +23 -0
  10. rustwasgi-0.1.0/docs/report.md +146 -0
  11. rustwasgi-0.1.0/examples/asgi2_app.py +27 -0
  12. rustwasgi-0.1.0/examples/asgi_app.py +41 -0
  13. rustwasgi-0.1.0/examples/bench_app.py +17 -0
  14. rustwasgi-0.1.0/examples/factory_app.py +13 -0
  15. rustwasgi-0.1.0/examples/fastapi_app.py +49 -0
  16. rustwasgi-0.1.0/examples/fastapi_lifespan.py +37 -0
  17. rustwasgi-0.1.0/examples/lifespan_state_app.py +31 -0
  18. rustwasgi-0.1.0/examples/websocket_app.py +30 -0
  19. rustwasgi-0.1.0/pyproject.toml +28 -0
  20. rustwasgi-0.1.0/python/rustwasgi/__init__.py +15 -0
  21. rustwasgi-0.1.0/python/rustwasgi/__main__.py +63 -0
  22. rustwasgi-0.1.0/python/rustwasgi/gunicorn.py +167 -0
  23. rustwasgi-0.1.0/src/asgi.rs +726 -0
  24. rustwasgi-0.1.0/src/cli.rs +67 -0
  25. rustwasgi-0.1.0/src/lib.rs +495 -0
  26. rustwasgi-0.1.0/src/lifespan.rs +306 -0
  27. rustwasgi-0.1.0/src/metrics.rs +191 -0
  28. rustwasgi-0.1.0/src/runtime.rs +182 -0
  29. rustwasgi-0.1.0/src/server.rs +1164 -0
  30. rustwasgi-0.1.0/src/socket.rs +158 -0
  31. rustwasgi-0.1.0/src/ws.rs +663 -0
  32. rustwasgi-0.1.0/tests/_util.py +173 -0
  33. rustwasgi-0.1.0/tests/test_asgi.py +173 -0
  34. rustwasgi-0.1.0/tests/test_bridge.py +73 -0
  35. rustwasgi-0.1.0/tests/test_fastapi.py +97 -0
  36. rustwasgi-0.1.0/tests/test_gunicorn.py +215 -0
  37. rustwasgi-0.1.0/tests/test_lifespan.py +52 -0
  38. rustwasgi-0.1.0/tests/test_websocket.py +65 -0
  39. 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.