pyronova 2.0.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.
- pyronova-2.0.0/.claude/commands/test.md +10 -0
- pyronova-2.0.0/.claude/projects/-Users-hucao-projects-pyre/memory/MEMORY.md +1 -0
- pyronova-2.0.0/.claude/projects/-Users-hucao-projects-pyre/memory/feedback_y_hash_fix.md +16 -0
- pyronova-2.0.0/.github/workflows/ci.yml +108 -0
- pyronova-2.0.0/.github/workflows/release.yml +87 -0
- pyronova-2.0.0/.gitignore +12 -0
- pyronova-2.0.0/CHANGELOG.md +369 -0
- pyronova-2.0.0/CLAUDE.md +138 -0
- pyronova-2.0.0/Cargo.lock +2806 -0
- pyronova-2.0.0/Cargo.toml +77 -0
- pyronova-2.0.0/LICENSE +199 -0
- pyronova-2.0.0/PKG-INFO +804 -0
- pyronova-2.0.0/README.md +777 -0
- pyronova-2.0.0/ROADMAP.md +421 -0
- pyronova-2.0.0/arena_submission/Dockerfile +45 -0
- pyronova-2.0.0/arena_submission/README.md +55 -0
- pyronova-2.0.0/arena_submission/app.py +235 -0
- pyronova-2.0.0/arena_submission/launcher.py +108 -0
- pyronova-2.0.0/arena_submission/meta.json +23 -0
- pyronova-2.0.0/benchmarks/BENCH_PLAN.en.md +154 -0
- pyronova-2.0.0/benchmarks/BENCH_PLAN.md +154 -0
- pyronova-2.0.0/benchmarks/baseline.json +9 -0
- pyronova-2.0.0/benchmarks/bench.py +36 -0
- pyronova-2.0.0/benchmarks/bench_compression.py +85 -0
- pyronova-2.0.0/benchmarks/bench_gil.py +46 -0
- pyronova-2.0.0/benchmarks/bench_plaintext.py +65 -0
- pyronova-2.0.0/benchmarks/bench_subinterp.py +28 -0
- pyronova-2.0.0/benchmarks/benchmark-1-reason.en.md +96 -0
- pyronova-2.0.0/benchmarks/benchmark-1-reason.md +96 -0
- pyronova-2.0.0/benchmarks/benchmark-10-final-verdict.en.md +87 -0
- pyronova-2.0.0/benchmarks/benchmark-10-final-verdict.md +87 -0
- pyronova-2.0.0/benchmarks/benchmark-11-vs-fastapi.md +68 -0
- pyronova-2.0.0/benchmarks/benchmark-12-stability.md +155 -0
- pyronova-2.0.0/benchmarks/benchmark-13-logging.en.md +104 -0
- pyronova-2.0.0/benchmarks/benchmark-13-logging.md +104 -0
- pyronova-2.0.0/benchmarks/benchmark-14-channel-dispatch.en.md +131 -0
- pyronova-2.0.0/benchmarks/benchmark-14-channel-dispatch.md +131 -0
- pyronova-2.0.0/benchmarks/benchmark-14-linux.en.md +245 -0
- pyronova-2.0.0/benchmarks/benchmark-14-linux.md +245 -0
- pyronova-2.0.0/benchmarks/benchmark-15-vs-actix-compression.md +299 -0
- pyronova-2.0.0/benchmarks/benchmark-16-v1.6-regression-audit.md +169 -0
- pyronova-2.0.0/benchmarks/benchmark-17-arena-head-to-head.md +231 -0
- pyronova-2.0.0/benchmarks/benchmark-2-results.en.md +59 -0
- pyronova-2.0.0/benchmarks/benchmark-2-results.md +59 -0
- pyronova-2.0.0/benchmarks/benchmark-3-full-stack.en.md +41 -0
- pyronova-2.0.0/benchmarks/benchmark-3-full-stack.md +41 -0
- pyronova-2.0.0/benchmarks/benchmark-4-features.en.md +30 -0
- pyronova-2.0.0/benchmarks/benchmark-4-features.md +30 -0
- pyronova-2.0.0/benchmarks/benchmark-5-refactor.en.md +46 -0
- pyronova-2.0.0/benchmarks/benchmark-5-refactor.md +46 -0
- pyronova-2.0.0/benchmarks/benchmark-6-full-suite.en.md +84 -0
- pyronova-2.0.0/benchmarks/benchmark-6-full-suite.md +84 -0
- pyronova-2.0.0/benchmarks/benchmark-7-shared-state.en.md +106 -0
- pyronova-2.0.0/benchmarks/benchmark-7-shared-state.md +108 -0
- pyronova-2.0.0/benchmarks/benchmark-8-sse-regression.en.md +77 -0
- pyronova-2.0.0/benchmarks/benchmark-8-sse-regression.md +78 -0
- pyronova-2.0.0/benchmarks/benchmark-9-gil-memory.en.md +105 -0
- pyronova-2.0.0/benchmarks/benchmark-9-gil-memory.md +99 -0
- pyronova-2.0.0/benchmarks/fastapi_bench_app.py +53 -0
- pyronova-2.0.0/benchmarks/mem_bench.sh +80 -0
- pyronova-2.0.0/benchmarks/pyronova_bench_app.py +56 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_113354/raw.json +1306 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_113354/summary.md +43 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_130737/raw.json +1306 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_130737/summary.md +43 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_140616/raw.json +1306 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_140616/summary.md +43 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_144558/charts/latency.png +0 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_144558/charts/pyre_vs_robyn.png +0 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_144558/charts/throughput.png +0 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_144558/raw.json +1306 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_144558/summary.md +43 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_161056/charts/latency.png +0 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_161056/charts/pyre_vs_robyn.png +0 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_161056/charts/throughput.png +0 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_161056/raw.json +1630 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_161056/summary.md +43 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_171330/raw.json +1630 -0
- pyronova-2.0.0/benchmarks/results/2026-03-24_171330/summary.md +43 -0
- pyronova-2.0.0/benchmarks/robyn_app.py +57 -0
- pyronova-2.0.0/benchmarks/run_bench.sh +66 -0
- pyronova-2.0.0/benchmarks/run_comparison.sh +99 -0
- pyronova-2.0.0/benchmarks/run_compression_bench.sh +96 -0
- pyronova-2.0.0/benchmarks/run_full_bench.sh +281 -0
- pyronova-2.0.0/benchmarks/run_logging_bench.sh +176 -0
- pyronova-2.0.0/benchmarks/run_vs_robyn.sh +208 -0
- pyronova-2.0.0/benchmarks/suite/charts.py +186 -0
- pyronova-2.0.0/benchmarks/suite/payloads/large.json +1 -0
- pyronova-2.0.0/benchmarks/suite/payloads/medium.json +1 -0
- pyronova-2.0.0/benchmarks/suite/payloads/small.json +1 -0
- pyronova-2.0.0/benchmarks/suite/post.lua +12 -0
- pyronova-2.0.0/benchmarks/suite/runner.py +384 -0
- pyronova-2.0.0/benchmarks/suite/servers/pyronova_gil.py +79 -0
- pyronova-2.0.0/benchmarks/suite/servers/pyronova_hybrid.py +77 -0
- pyronova-2.0.0/benchmarks/suite/servers/pyronova_subinterp.py +72 -0
- pyronova-2.0.0/benchmarks/suite/servers/robyn_server.py +77 -0
- pyronova-2.0.0/docs/advisor-triage-2026-04-19.md +52 -0
- pyronova-2.0.0/docs/developer-experience.md +293 -0
- pyronova-2.0.0/docs/dual-engine-design.md +81 -0
- pyronova-2.0.0/docs/features.md +170 -0
- pyronova-2.0.0/docs/gc-optimization-guide.md +108 -0
- pyronova-2.0.0/docs/gil-monitor-design.md +301 -0
- pyronova-2.0.0/docs/http-arena-preflight.md +138 -0
- pyronova-2.0.0/docs/logging-design.en.md +201 -0
- pyronova-2.0.0/docs/logging-design.md +201 -0
- pyronova-2.0.0/docs/memory-leak-investigation-2026-04-19.md +575 -0
- pyronova-2.0.0/docs/phase-7.2-async-bridge.md +165 -0
- pyronova-2.0.0/docs/positioning-and-roadmap.md +167 -0
- pyronova-2.0.0/docs/release-pipeline.md +140 -0
- pyronova-2.0.0/docs/rpc-engine-design.md +113 -0
- pyronova-2.0.0/docs/subinterp-c-extension-compat.md +140 -0
- pyronova-2.0.0/docs/subinterp-safe-ecosystem.md +224 -0
- pyronova-2.0.0/docs/why-not-multiprocess.md +145 -0
- pyronova-2.0.0/docs/zero-copy-design.md +82 -0
- pyronova-2.0.0/examples/README.md +39 -0
- pyronova-2.0.0/examples/ai_agent_server.py +179 -0
- pyronova-2.0.0/examples/fullstack_api.py +325 -0
- pyronova-2.0.0/examples/hello.py +77 -0
- pyronova-2.0.0/examples/hello_subinterp.py +33 -0
- pyronova-2.0.0/examples/sse_demo.py +50 -0
- pyronova-2.0.0/examples/trading_api.py +274 -0
- pyronova-2.0.0/examples/websocket_demo.py +41 -0
- pyronova-2.0.0/justfile +265 -0
- pyronova-2.0.0/pyproject.toml +49 -0
- pyronova-2.0.0/python/pyronova/__init__.py +34 -0
- pyronova-2.0.0/python/pyronova/__main__.py +5 -0
- pyronova-2.0.0/python/pyronova/_async_engine.py +146 -0
- pyronova-2.0.0/python/pyronova/_bootstrap.py +384 -0
- pyronova-2.0.0/python/pyronova/app.py +950 -0
- pyronova-2.0.0/python/pyronova/cli.py +161 -0
- pyronova-2.0.0/python/pyronova/config.py +66 -0
- pyronova-2.0.0/python/pyronova/context.py +87 -0
- pyronova-2.0.0/python/pyronova/cookies.py +125 -0
- pyronova-2.0.0/python/pyronova/crud.py +223 -0
- pyronova-2.0.0/python/pyronova/db.py +50 -0
- pyronova-2.0.0/python/pyronova/engine.pyi +78 -0
- pyronova-2.0.0/python/pyronova/health.py +97 -0
- pyronova-2.0.0/python/pyronova/mcp.py +313 -0
- pyronova-2.0.0/python/pyronova/observability.py +171 -0
- pyronova-2.0.0/python/pyronova/rpc.py +150 -0
- pyronova-2.0.0/python/pyronova/testing.py +286 -0
- pyronova-2.0.0/python/pyronova/uploads.py +118 -0
- pyronova-2.0.0/src/app.rs +921 -0
- pyronova-2.0.0/src/body_stream.rs +177 -0
- pyronova-2.0.0/src/compression.rs +463 -0
- pyronova-2.0.0/src/db.rs +664 -0
- pyronova-2.0.0/src/handlers.rs +954 -0
- pyronova-2.0.0/src/interp.rs +2449 -0
- pyronova-2.0.0/src/json.rs +348 -0
- pyronova-2.0.0/src/leak_detect.rs +195 -0
- pyronova-2.0.0/src/lib.rs +78 -0
- pyronova-2.0.0/src/logging.rs +175 -0
- pyronova-2.0.0/src/monitor.rs +213 -0
- pyronova-2.0.0/src/pyronova_request_type.rs +423 -0
- pyronova-2.0.0/src/response.rs +336 -0
- pyronova-2.0.0/src/router.rs +152 -0
- pyronova-2.0.0/src/state.rs +137 -0
- pyronova-2.0.0/src/static_fs.rs +351 -0
- pyronova-2.0.0/src/stream.rs +119 -0
- pyronova-2.0.0/src/tls.rs +139 -0
- pyronova-2.0.0/src/types.rs +353 -0
- pyronova-2.0.0/src/websocket.rs +362 -0
- pyronova-2.0.0/tests/conftest.py +138 -0
- pyronova-2.0.0/tests/e2e/README.md +21 -0
- pyronova-2.0.0/tests/e2e/ws_binary_client.py +35 -0
- pyronova-2.0.0/tests/e2e/ws_binary_server.py +23 -0
- pyronova-2.0.0/tests/test_admission_control.py +77 -0
- pyronova-2.0.0/tests/test_async.py +29 -0
- pyronova-2.0.0/tests/test_async_bridge.py +28 -0
- pyronova-2.0.0/tests/test_async_isolation.py +96 -0
- pyronova-2.0.0/tests/test_async_shutdown.py +44 -0
- pyronova-2.0.0/tests/test_async_timeout.py +137 -0
- pyronova-2.0.0/tests/test_c_extensions.py +42 -0
- pyronova-2.0.0/tests/test_capi_hygiene.py +357 -0
- pyronova-2.0.0/tests/test_cli.py +157 -0
- pyronova-2.0.0/tests/test_client_ip.py +106 -0
- pyronova-2.0.0/tests/test_compression.py +186 -0
- pyronova-2.0.0/tests/test_config_context.py +128 -0
- pyronova-2.0.0/tests/test_cookies_e2e.py +52 -0
- pyronova-2.0.0/tests/test_cookies_unit.py +76 -0
- pyronova-2.0.0/tests/test_cors_404.py +101 -0
- pyronova-2.0.0/tests/test_cors_e2e.py +69 -0
- pyronova-2.0.0/tests/test_crud.py +192 -0
- pyronova-2.0.0/tests/test_db_pg.py +332 -0
- pyronova-2.0.0/tests/test_env_var_worker.py +46 -0
- pyronova-2.0.0/tests/test_fast_response.py +120 -0
- pyronova-2.0.0/tests/test_ffi_panic_safety.py +52 -0
- pyronova-2.0.0/tests/test_fixes.py +34 -0
- pyronova-2.0.0/tests/test_health.py +165 -0
- pyronova-2.0.0/tests/test_hybrid.py +42 -0
- pyronova-2.0.0/tests/test_json_serialization.py +649 -0
- pyronova-2.0.0/tests/test_lifecycle.py +163 -0
- pyronova-2.0.0/tests/test_logging.py +233 -0
- pyronova-2.0.0/tests/test_mcp.py +133 -0
- pyronova-2.0.0/tests/test_middleware_hooks.py +200 -0
- pyronova-2.0.0/tests/test_observability.py +245 -0
- pyronova-2.0.0/tests/test_passive_gil_metrics.py +83 -0
- pyronova-2.0.0/tests/test_pyerr_no_stderr.py +126 -0
- pyronova-2.0.0/tests/test_request_fields.py +260 -0
- pyronova-2.0.0/tests/test_routing_e2e.py +101 -0
- pyronova-2.0.0/tests/test_rpc.py +104 -0
- pyronova-2.0.0/tests/test_shared_state.py +95 -0
- pyronova-2.0.0/tests/test_static_files.py +153 -0
- pyronova-2.0.0/tests/test_stream_backpressure.py +69 -0
- pyronova-2.0.0/tests/test_stream_lifecycle.py +128 -0
- pyronova-2.0.0/tests/test_subinterp_features.py +278 -0
- pyronova-2.0.0/tests/test_subinterp_hooks.py +32 -0
- pyronova-2.0.0/tests/test_subinterp_memory_regression.py +509 -0
- pyronova-2.0.0/tests/test_subinterp_timeout.py +112 -0
- pyronova-2.0.0/tests/test_testclient.py +145 -0
- pyronova-2.0.0/tests/test_testclient_v2.py +238 -0
- pyronova-2.0.0/tests/test_tls.py +156 -0
- pyronova-2.0.0/tests/test_tls_slowloris.py +30 -0
- pyronova-2.0.0/tests/test_upload_streaming.py +141 -0
- pyronova-2.0.0/tests/test_uploads_e2e.py +57 -0
- pyronova-2.0.0/tests/test_websocket.py +113 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Run all Pyre tests: Rust compile check + Python unit + integration.
|
|
2
|
+
|
|
3
|
+
Steps:
|
|
4
|
+
1. `cargo test` — Rust compilation check (catches type errors, unused imports, etc.)
|
|
5
|
+
2. `source .venv/bin/activate && maturin develop --release` — build Python extension
|
|
6
|
+
3. `pytest tests/test_mcp.py tests/test_cookies_unit.py tests/test_testclient.py tests/test_rpc.py tests/test_static_files.py tests/test_websocket.py tests/test_async_isolation.py tests/test_logging.py -v` — pytest unit tests
|
|
7
|
+
4. `python tests/test_all_features.py` — integration tests (starts real servers, 22 end-to-end tests)
|
|
8
|
+
5. Report a summary table of results (pass/fail counts per stage)
|
|
9
|
+
|
|
10
|
+
If any test fails, investigate the failure and report what went wrong. Do NOT auto-fix — just report.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- [feedback_y_hash_fix.md](feedback_y_hash_fix.md) — Phase 7.2: use y# not z# for bytes in PyArg_ParseTuple
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Phase 7.2 FFI format string fix
|
|
3
|
+
description: When implementing pyre_send_cfunc, use y# (not z#) for Python bytes args. z# is for str/None only.
|
|
4
|
+
type: feedback
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
When implementing Phase 7.2 async bridge (`pyre_send_cfunc` in interp.rs):
|
|
8
|
+
- Use `c"nKHsy#"` format string for `PyArg_ParseTuple` (not `z#z#`)
|
|
9
|
+
- `y#` = bytes, `z#` = str/None, `s` = str
|
|
10
|
+
- Don't pass `&mut body_len` twice (pointer overwrite bug)
|
|
11
|
+
- Always `PyErr_Print()` on parse failure, never silently return null
|
|
12
|
+
- Consider replacing global `Mutex<HashMap>` with `OnceLock<Vec<WorkerState>>` for zero-lock at 200k+ QPS
|
|
13
|
+
|
|
14
|
+
**Why:** Python side sends `json.dumps(res).encode('utf-8')` which is `bytes`, not `str`. Wrong format causes silent parse failure → tx.send never fires → request hangs → Tokio pool exhaustion.
|
|
15
|
+
|
|
16
|
+
**How to apply:** When writing `pyre_send_cfunc` for Phase 7.2 implementation.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
CARGO_TERM_COLOR: always
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
# ── Build + unit tests ────────────────────────────────────────────
|
|
14
|
+
test-unit:
|
|
15
|
+
name: Unit tests (Python ${{ matrix.python-version }})
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.13", "3.14"]
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
allow-prereleases: true
|
|
27
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
28
|
+
- uses: Swatinem/rust-cache@v2
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: |
|
|
32
|
+
python -m venv .venv
|
|
33
|
+
source .venv/bin/activate
|
|
34
|
+
pip install maturin pytest pytest-asyncio websockets httpx pydantic numpy msgpack
|
|
35
|
+
|
|
36
|
+
- name: cargo test (Rust compile check)
|
|
37
|
+
env:
|
|
38
|
+
PYO3_PYTHON: ${{ env.pythonLocation }}/bin/python
|
|
39
|
+
run: cargo test
|
|
40
|
+
|
|
41
|
+
- name: Build (release)
|
|
42
|
+
run: |
|
|
43
|
+
source .venv/bin/activate
|
|
44
|
+
maturin develop --release
|
|
45
|
+
|
|
46
|
+
- name: Run pytest (unit)
|
|
47
|
+
run: |
|
|
48
|
+
source .venv/bin/activate
|
|
49
|
+
pytest tests/test_mcp.py tests/test_cookies_unit.py tests/test_testclient.py tests/test_rpc.py tests/test_static_files.py tests/test_websocket.py tests/test_async_isolation.py tests/test_logging.py -v
|
|
50
|
+
|
|
51
|
+
# ── Integration tests ─────────────────────────────────────────────
|
|
52
|
+
test-integration:
|
|
53
|
+
name: Integration tests (Python ${{ matrix.python-version }})
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
needs: test-unit
|
|
56
|
+
strategy:
|
|
57
|
+
fail-fast: false
|
|
58
|
+
matrix:
|
|
59
|
+
python-version: ["3.13", "3.14"]
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v4
|
|
62
|
+
- uses: actions/setup-python@v5
|
|
63
|
+
with:
|
|
64
|
+
python-version: ${{ matrix.python-version }}
|
|
65
|
+
allow-prereleases: true
|
|
66
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
67
|
+
- uses: Swatinem/rust-cache@v2
|
|
68
|
+
|
|
69
|
+
- name: Install dependencies
|
|
70
|
+
run: |
|
|
71
|
+
python -m venv .venv
|
|
72
|
+
source .venv/bin/activate
|
|
73
|
+
pip install maturin pytest pytest-asyncio websockets httpx pydantic numpy msgpack
|
|
74
|
+
|
|
75
|
+
- name: Build (release)
|
|
76
|
+
run: |
|
|
77
|
+
source .venv/bin/activate
|
|
78
|
+
maturin develop --release
|
|
79
|
+
|
|
80
|
+
- name: Integration — full pytest suite (GIL + sub-interp)
|
|
81
|
+
run: |
|
|
82
|
+
source .venv/bin/activate
|
|
83
|
+
# E2E drivers live under tests/e2e/ (not collected by pytest).
|
|
84
|
+
pytest tests/ -q --ignore=tests/e2e
|
|
85
|
+
|
|
86
|
+
# ── Rust lint (blocking) ──────────────────────────────────────────
|
|
87
|
+
rust-lint:
|
|
88
|
+
name: Rust lint
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v4
|
|
92
|
+
# pyo3-ffi gates PyThreadState_GetUnchecked on cfg(Py_3_13); Pyronova
|
|
93
|
+
# requires it. Without pinning an interpreter, the runner's
|
|
94
|
+
# default python3 (typically 3.11/3.12) is picked up and clippy
|
|
95
|
+
# fails with E0425 on a missing ffi symbol.
|
|
96
|
+
- uses: actions/setup-python@v5
|
|
97
|
+
with:
|
|
98
|
+
python-version: "3.13"
|
|
99
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
100
|
+
with:
|
|
101
|
+
components: clippy, rustfmt
|
|
102
|
+
- uses: Swatinem/rust-cache@v2
|
|
103
|
+
- name: cargo fmt
|
|
104
|
+
run: cargo fmt --check
|
|
105
|
+
- name: cargo clippy
|
|
106
|
+
env:
|
|
107
|
+
PYO3_PYTHON: ${{ env.pythonLocation }}/bin/python
|
|
108
|
+
run: cargo clippy --all-targets -- -D warnings
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
CARGO_TERM_COLOR: always
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
# ── Build wheels ────────────────────────────────────────────────────
|
|
16
|
+
build:
|
|
17
|
+
name: Build (${{ matrix.target }})
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
include:
|
|
23
|
+
# Linux x86_64 (manylinux via Docker)
|
|
24
|
+
- os: ubuntu-latest
|
|
25
|
+
target: x86_64-unknown-linux-gnu
|
|
26
|
+
# macOS ARM (Apple Silicon)
|
|
27
|
+
- os: macos-14
|
|
28
|
+
target: aarch64-apple-darwin
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
|
|
32
|
+
- uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.13"
|
|
35
|
+
|
|
36
|
+
- name: Build wheels
|
|
37
|
+
uses: PyO3/maturin-action@v1
|
|
38
|
+
with:
|
|
39
|
+
target: ${{ matrix.target }}
|
|
40
|
+
args: --release --out dist -i python3.13
|
|
41
|
+
manylinux: auto
|
|
42
|
+
|
|
43
|
+
- uses: actions/upload-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: wheels-${{ matrix.target }}
|
|
46
|
+
path: dist/
|
|
47
|
+
|
|
48
|
+
# ── Build source distribution ───────────────────────────────────────
|
|
49
|
+
sdist:
|
|
50
|
+
name: Build sdist
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
|
|
55
|
+
- name: Build sdist
|
|
56
|
+
uses: PyO3/maturin-action@v1
|
|
57
|
+
with:
|
|
58
|
+
command: sdist
|
|
59
|
+
args: --out dist
|
|
60
|
+
|
|
61
|
+
- uses: actions/upload-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: wheels-sdist
|
|
64
|
+
path: dist/
|
|
65
|
+
|
|
66
|
+
# ── Publish to PyPI ─────────────────────────────────────────────────
|
|
67
|
+
publish:
|
|
68
|
+
name: Publish to PyPI
|
|
69
|
+
needs: [build, sdist]
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
environment:
|
|
72
|
+
name: pypi
|
|
73
|
+
url: https://pypi.org/p/pyronova
|
|
74
|
+
permissions:
|
|
75
|
+
id-token: write
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/download-artifact@v4
|
|
78
|
+
with:
|
|
79
|
+
pattern: wheels-*
|
|
80
|
+
merge-multiple: true
|
|
81
|
+
path: dist/
|
|
82
|
+
|
|
83
|
+
- name: Publish to PyPI
|
|
84
|
+
uses: PyO3/maturin-action@v1
|
|
85
|
+
with:
|
|
86
|
+
command: upload
|
|
87
|
+
args: --non-interactive --skip-existing dist/*
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v2.0.0 (2026-04-21) — Rename to Pyronova
|
|
4
|
+
|
|
5
|
+
Project rename — `pyreframework` → `pyronova`, brand `Pyre` → `Pyronova`.
|
|
6
|
+
Clean break, no alias. All downstream users must update imports. This is
|
|
7
|
+
a rename-only release — no behavioral changes.
|
|
8
|
+
|
|
9
|
+
### Why
|
|
10
|
+
|
|
11
|
+
"Pyre" collided with Meta's widely-used Pyre type checker. After a round
|
|
12
|
+
of name-collision research (PyPI availability + GitHub hit count), we
|
|
13
|
+
settled on **Pyronova** — retains the fire theme, encodes "Python +
|
|
14
|
+
Rust + nova (explosive newness)", single GitHub collision, free on
|
|
15
|
+
PyPI. See the rename PR description for the full candidate shortlist.
|
|
16
|
+
|
|
17
|
+
### Breaking — import surface
|
|
18
|
+
|
|
19
|
+
- PyPI package: `pip install pyronova` (was `pyreframework`).
|
|
20
|
+
- Main class: `from pyronova import Pyronova` (was `from pyreframework
|
|
21
|
+
import Pyre`).
|
|
22
|
+
- Type names switch to unprefixed ("FastAPI style"): `Request`,
|
|
23
|
+
`Response`, `WebSocket`, `Stream`, `BodyStream`, `RPCClient`,
|
|
24
|
+
`Settings`. The `PyreRequest` / `PyreResponse` / … prefixed forms
|
|
25
|
+
are gone.
|
|
26
|
+
- CLI: `pyronova run/dev/routes` (was `pyre ...`).
|
|
27
|
+
- Env vars: `PYRONOVA_HOST`, `PYRONOVA_PORT`, `PYRONOVA_LOG`,
|
|
28
|
+
`PYRONOVA_WORKERS`, `PYRONOVA_IO_WORKERS`, `PYRONOVA_TLS_CERT`,
|
|
29
|
+
`PYRONOVA_TLS_KEY`, `PYRONOVA_RELOAD`, `PYRONOVA_LOG_LEVEL`,
|
|
30
|
+
`PYRONOVA_TEST_PG_DSN`.
|
|
31
|
+
- Rust crate: `pyronova-engine`, lib name `pyronova_engine` (was
|
|
32
|
+
`pyreframework-engine` / `pyreframework_engine`).
|
|
33
|
+
- Rust FFI symbols injected into sub-interpreter globals: `_pyronova_recv`,
|
|
34
|
+
`_pyronova_send`, `_pyronova_emit_log`, `_pyronova_pool_id`.
|
|
35
|
+
- Log target names: `pyronova::server`, `pyronova::access`,
|
|
36
|
+
`pyronova::app`.
|
|
37
|
+
|
|
38
|
+
### Migration
|
|
39
|
+
|
|
40
|
+
Mechanical — two find-and-replace passes handle the common shape:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git grep -l pyreframework | xargs sed -i 's/pyreframework/pyronova/g'
|
|
44
|
+
git grep -l '\bPyre\b' | xargs sed -i 's/\bPyre\b/Pyronova/g'
|
|
45
|
+
# Type names lost their prefix — handle per-class:
|
|
46
|
+
git grep -l PyreRequest | xargs sed -i 's/PyreRequest/Request/g'
|
|
47
|
+
git grep -l PyreResponse | xargs sed -i 's/PyreResponse/Response/g'
|
|
48
|
+
# …and the same for WebSocket, Stream, BodyStream, RPCClient, Settings.
|
|
49
|
+
# Env vars:
|
|
50
|
+
git grep -l PYRE_ | xargs sed -i 's/PYRE_/PYRONOVA_/g'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Historical release notes, benchmark reports, and incident post-mortems
|
|
54
|
+
under `benchmarks/benchmark-*.md`, `docs/memory-leak-investigation-*.md`,
|
|
55
|
+
`docs/advisor-triage-*.md` were deliberately left unrenamed — the
|
|
56
|
+
framework was called Pyre when those events happened, and rewriting the
|
|
57
|
+
history is dishonest.
|
|
58
|
+
|
|
59
|
+
## v1.5.0 (2026-04-19)
|
|
60
|
+
|
|
61
|
+
Memory-leak root cause fix + hardening pass. Minor-bump because Python
|
|
62
|
+
3.13+ is now required (dropped 3.10-3.12 support, see "Breaking").
|
|
63
|
+
|
|
64
|
+
### Headline — sub-interpreter memory leak closed
|
|
65
|
+
|
|
66
|
+
Pyre sub-interpreters had a long-standing unbounded RSS growth under
|
|
67
|
+
sustained load (~128 B/req at 400k rps → OOM in ~10 min). Root cause
|
|
68
|
+
isolated via a pure-C reproducer to cross-thread `PyThreadState` reuse:
|
|
69
|
+
`SubInterpreterWorker::new` ran on the main thread, `Py_NewInterpreterFromConfig`
|
|
70
|
+
bound the tstate to that OS thread, then the worker pthread attach/detached
|
|
71
|
+
that tstate on every request. CPython's per-OS-thread tstate bookkeeping
|
|
72
|
+
accumulates when the attaching thread differs from the creator — measured
|
|
73
|
+
at ~1 KB per iteration in isolation.
|
|
74
|
+
|
|
75
|
+
Fix (`fc45a7f`): new `rebind_tstate_to_current_thread` helper that each
|
|
76
|
+
worker calls on entry. Creates a fresh tstate via `PyThreadState_New(interp)`
|
|
77
|
+
bound to the worker's OS thread, swaps it in, and disposes of the creator
|
|
78
|
+
tstate. All subsequent request dispatch runs against the thread-local
|
|
79
|
+
tstate. **Measured result**: 73.8M requests @ 410k rps over 180s = total
|
|
80
|
+
RSS growth of **4 MB** (0.057 B/req, below /proc sampling noise).
|
|
81
|
+
|
|
82
|
+
Side effect: Python `__del__` / `tp_finalize` now fires correctly in
|
|
83
|
+
sub-interp handlers (previously silently broken, was xfail'd).
|
|
84
|
+
|
|
85
|
+
### Architecture — raw C-API `_PyreRequest` type
|
|
86
|
+
|
|
87
|
+
Replaced the Python-defined `_PyreRequest` class with a custom heap
|
|
88
|
+
type built via `PyType_FromSpec` + `PyMemberDef` in
|
|
89
|
+
`src/pyre_request_type.rs`. Custom `tp_dealloc` synchronously DECREFs
|
|
90
|
+
all seven slot fields. pyo3's `#[pyclass]` can't be used — pyo3 0.28
|
|
91
|
+
hard-rejects sub-interpreters.
|
|
92
|
+
|
|
93
|
+
Two invariants learned the hard way:
|
|
94
|
+
|
|
95
|
+
- **No `Py_TPFLAGS_BASETYPE`**: Python subclassing triggers CPython's
|
|
96
|
+
`subtype_dealloc` fallback which silently bypasses our `tp_dealloc`.
|
|
97
|
+
Helper methods (`.text()`, `.json()`, `.body`, `.query_params`) are
|
|
98
|
+
monkey-patched onto the heap type at sub-interp init instead.
|
|
99
|
+
- **No `Py_TPFLAGS_HAVE_GC`**: empirically made per-request leak 5×
|
|
100
|
+
worse. Our workload has no cycles; GC tracking costs without benefit.
|
|
101
|
+
|
|
102
|
+
### Correctness & hygiene — 22 fixes triaged from adversarial review
|
|
103
|
+
|
|
104
|
+
Full triage record (31 claims reviewed, 21 real / 10 rejected) lives in
|
|
105
|
+
`docs/advisor-triage-2026-04-19.md`.
|
|
106
|
+
|
|
107
|
+
C-API hygiene: `PyDict_Next` + `PyObject_Str` re-entrancy in
|
|
108
|
+
`parse_sky_response`; `PyObject_IsInstance == 1` now handles `-1`
|
|
109
|
+
(error); `py_str_dict` clears pending exceptions on OOM; `PyDict_SetItem`
|
|
110
|
+
return value checked; `_PyreRequest.__init__` path type-checks dict
|
|
111
|
+
slots via `PyDict_Check` before `PyDict_Clear`.
|
|
112
|
+
|
|
113
|
+
Lifecycle: `LoopGuard` drop-after-`Py_Finalize` segfault fixed via
|
|
114
|
+
`std::mem::forget`; Hyper graceful shutdown via `TaskTracker` waits
|
|
115
|
+
up to 30s for in-flight connections before runtime drop (was
|
|
116
|
+
RST-on-shutdown); `InterpreterPool::drop` bounds worker-thread join
|
|
117
|
+
at 5s; `spawn_rss_sampler` JoinHandle now joined on shutdown;
|
|
118
|
+
`WORKER_STATES` `OnceLock` → `RwLock<Vec>` so repeated `app.run()`
|
|
119
|
+
gets fresh channels.
|
|
120
|
+
|
|
121
|
+
WebSocket: async WS handlers now driven via `asyncio.run` (was
|
|
122
|
+
silently dropped); explicit `drop(handler)` under GIL.
|
|
123
|
+
|
|
124
|
+
Routing: path params URL-decoded (`/user/john%20doe` → `"john doe"`);
|
|
125
|
+
async middleware coroutines driven through `resolve_coroutine` in
|
|
126
|
+
both sub-interp and GIL-mode paths; CORS `origin="*"` + credentials
|
|
127
|
+
emits W3C-violation warning.
|
|
128
|
+
|
|
129
|
+
Static files: `canonicalize → File::open` TOCTOU closed via
|
|
130
|
+
`O_NOFOLLOW` on Unix (refuses post-check symlink swap).
|
|
131
|
+
|
|
132
|
+
### Performance
|
|
133
|
+
|
|
134
|
+
Awaitable detection in `resolve_coroutine` moved from
|
|
135
|
+
`PyObject_HasAttrString(obj, "__await__")` (μs per call — interns the
|
|
136
|
+
string, walks the MRO, runs descriptor protocol) to a direct
|
|
137
|
+
`Py_TYPE(obj)->tp_as_async->am_await` pointer probe (ns, L1-resident).
|
|
138
|
+
|
|
139
|
+
### Benchmark
|
|
140
|
+
|
|
141
|
+
Bench targets split cleanly. `benchmarks/bench_plaintext.py` (new,
|
|
142
|
+
feature-light) is the target for `just bench-record` / `bench-compare`.
|
|
143
|
+
`examples/hello.py` (restored to v1.4.0 content) is the feature demo,
|
|
144
|
+
run via `just bench-features`. `just bench-tfb-plaintext` runs
|
|
145
|
+
TechEmpower-style `wrk -t8 -c256 -d15s --pipeline 16`.
|
|
146
|
+
|
|
147
|
+
Numbers (AMD Ryzen 7 7840HS, 8C/16T, Python 3.14.4, performance
|
|
148
|
+
governor, wrk 4.1.0):
|
|
149
|
+
|
|
150
|
+
| Workload | Config | Req/s |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| Plaintext baseline | `wrk -t4 -c100 -d10s` | **422,976** |
|
|
153
|
+
| Feature demo | `wrk -t4 -c100 -d10s` on `hello.py` | 381,123 |
|
|
154
|
+
| **TFB Plaintext** | `wrk -t8 -c256 -d15s --pipeline 16` | **902,213** |
|
|
155
|
+
| TFB JSON (`/hello/{name}`) | `wrk -t8 -c512 -d15s` | ~536,000 |
|
|
156
|
+
|
|
157
|
+
Plaintext baseline vs v1.4.0's published 419,730 on the same machine:
|
|
158
|
+
**+0.8% — zero regression**. The ~10% gap on `hello.py` reflects the
|
|
159
|
+
added cost of async-correct middleware + access log (intentional
|
|
160
|
+
hygiene tax, opt-in).
|
|
161
|
+
|
|
162
|
+
### Tests
|
|
163
|
+
|
|
164
|
+
- `test_sustained_concurrent_load_no_leak` — 12s soak, fails if RSS
|
|
165
|
+
grows >15 MB. Regression guard for the tstate fix.
|
|
166
|
+
- `test_subinterp_python_finalizers_fire` — xfail removed.
|
|
167
|
+
- `test_capi_hygiene.py` — 5 tests (reentrancy, malformed init,
|
|
168
|
+
URL-decode, async hook, instancecheck raise).
|
|
169
|
+
- `test_static_symlink_out_of_root_refused` — O_NOFOLLOW regression.
|
|
170
|
+
- `worker_states_can_be_reinstalled` — Rust unit test for hot-reload
|
|
171
|
+
of sub-interp pool.
|
|
172
|
+
- `TestClient(port=None)` auto-allocates port (new).
|
|
173
|
+
- Port collision fixes (19878, 19883).
|
|
174
|
+
|
|
175
|
+
Total: 235 passed / 2 full-suite runs / 0 flakes / 0 regressions.
|
|
176
|
+
|
|
177
|
+
### Breaking
|
|
178
|
+
|
|
179
|
+
- **`requires-python = ">=3.13"`** (was `>=3.10`). Users on 3.10-3.12
|
|
180
|
+
should stay on v1.4.x.
|
|
181
|
+
|
|
182
|
+
### Deferred to future releases
|
|
183
|
+
|
|
184
|
+
- SSE dedicated asyncio background thread.
|
|
185
|
+
- `response_map` active-sweep GC for deadlocked-worker orphans.
|
|
186
|
+
- PyBuffer-based zero-copy body write on the send path.
|
|
187
|
+
- `query_params_all()` additive API for HPP-correct multivalue access.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## v1.4.5 (2026-04-19)
|
|
192
|
+
|
|
193
|
+
Security + correctness hardening from an adversarial review pass. 23 fixes
|
|
194
|
+
(6 critical + 17 error).
|
|
195
|
+
|
|
196
|
+
**Same-day same-machine benchmark** (AMD 7840HS, `wrk -t4 -c100 -d10s`
|
|
197
|
+
on `examples/hello.py` hybrid mode):
|
|
198
|
+
|
|
199
|
+
| Build | Requests/sec (avg of 3) |
|
|
200
|
+
|---|---|
|
|
201
|
+
| d0ce481 (v1.4.4 pre-hotfix) | ~260k |
|
|
202
|
+
| v1.4.5 (this release) | ~320k |
|
|
203
|
+
|
|
204
|
+
No regression; observed a small net gain within noise. Both runs are
|
|
205
|
+
below the 419k historical baseline from v1.4.0's benchmark day — the
|
|
206
|
+
machine is in a different thermal / load state today, not a code
|
|
207
|
+
change. The relative comparison is what matters and it is clean.
|
|
208
|
+
|
|
209
|
+
### Critical (already shipped in hotfix)
|
|
210
|
+
- `accept()` loop classifies errno (EMFILE / ENFILE / ENOBUFS / ENOMEM)
|
|
211
|
+
and backs off — was 100% CPU spin on FD exhaustion
|
|
212
|
+
- Sub-interpreter RAII: `SubInterpreterWorker::new` no longer leaks the
|
|
213
|
+
sub-interp on any of 5+ fallible init steps
|
|
214
|
+
- WebSocket: `py_handle.join()` moved off the Tokio worker pool;
|
|
215
|
+
`recv/recv_bytes/recv_message` release the GIL via `py.detach()`
|
|
216
|
+
- MCP: reject non-object JSON-RPC payloads with -32600 instead of
|
|
217
|
+
crashing through `AttributeError`
|
|
218
|
+
|
|
219
|
+
### Security
|
|
220
|
+
- Cookies: reject CRLF / NUL in name / value / domain / path / expires
|
|
221
|
+
(HTTP Response Splitting)
|
|
222
|
+
- Error responses: `serde_json` for `{"error": msg}` — hand-rolled
|
|
223
|
+
escape handled only `"`, leaving backslash / control-char injection
|
|
224
|
+
open
|
|
225
|
+
- Static files: open-once design removes a TOCTOU where a rename
|
|
226
|
+
between `metadata()` and `read()` bypassed the `MAX_STATIC_FILE_BYTES`
|
|
227
|
+
cap; `.take(cap)` adds belt-and-braces
|
|
228
|
+
- `before_request` hook that raises now fails the request with 500
|
|
229
|
+
— previously fell through to the unprotected main handler, a
|
|
230
|
+
critical auth-bypass for deny-via-raise auth hooks
|
|
231
|
+
- Sub-interp path: CORS now applied on the `Err` branch so browsers
|
|
232
|
+
show the real 5xx instead of an opaque CORS error
|
|
233
|
+
- Interp FFI: `PyTuple_SetItem` never embeds a NULL — every leaf
|
|
234
|
+
allocation NULL-checked up front, partial failures cleaned up
|
|
235
|
+
atomically (was a latent segfault on OOM)
|
|
236
|
+
|
|
237
|
+
### Correctness
|
|
238
|
+
- `logging::init_logger`: `(writer, guard)` stored atomically in
|
|
239
|
+
`OnceLock<LoggerState>` — previous design could drop the guard of
|
|
240
|
+
whichever caller won `try_init()`, silently killing the log thread
|
|
241
|
+
- `router::lookup` uppercases the method to match `insert` — HTTP is
|
|
242
|
+
case-insensitive per RFC 9110 §9.1, lowercase / `Get` was silently
|
|
243
|
+
missing routes
|
|
244
|
+
- `SharedState::incr` raises `TypeError` / `OverflowError` instead of
|
|
245
|
+
silently resetting non-numeric values to 0
|
|
246
|
+
- Bounded channels: `PyreStream` (1024) + WebSocket outgoing (1024)
|
|
247
|
+
with `try_send` — unbounded was an OOM DoS under slow-client
|
|
248
|
+
backpressure
|
|
249
|
+
- `handlers::handle_request` error path: full PyErr logged server-side
|
|
250
|
+
(`e.display(py)` + `tracing::error!`); client gets a generic "handler
|
|
251
|
+
error" instead of a leaked one-line repr
|
|
252
|
+
|
|
253
|
+
### Python
|
|
254
|
+
- `rpc.py` / `_async_engine.py`: `log.exception` before the error
|
|
255
|
+
envelope so server-side stack traces survive RPC / async failures
|
|
256
|
+
- `_bootstrap.py` `PyreRustHandler.emit`: `self.handleError(record)`
|
|
257
|
+
instead of `pass` — stdlib logging's standard "I failed to log" hook
|
|
258
|
+
- `mcp._extract_schema`: `typing.get_type_hints(fn)` so tools defined
|
|
259
|
+
in modules with `from __future__ import annotations` don't silently
|
|
260
|
+
regress to "string" for every argument
|
|
261
|
+
- `UploadFile`: `@dataclass(frozen=True, slots=True)` — shares memory
|
|
262
|
+
with the raw multipart buffer, mutation would corrupt replay
|
|
263
|
+
|
|
264
|
+
## v1.4.0 (2026-04-01)
|
|
265
|
+
|
|
266
|
+
### Performance — Linux 42万 QPS
|
|
267
|
+
- **SO_REUSEPORT multi-accept** — N=io_workers 个独立 accept loop,Linux 内核级四元组哈希负载均衡,macOS 自动降级为 1
|
|
268
|
+
- **M:N scheduling** — `io_workers` (Tokio I/O threads) 和 `workers` (Python sub-interpreters) 独立配置,解耦网络层与计算层
|
|
269
|
+
- **LTO fat + codegen-units=1** — 编译期全局优化,+4% JSON/params 路由
|
|
270
|
+
- **TCP_QUICKACK** — Linux 禁用延迟 ACK,降低首字节延迟
|
|
271
|
+
- **Headers OnceLock lazy view** — 不访问 headers 时零开销,延迟转换
|
|
272
|
+
- **serde_json + pythonize** — Rust 侧 JSON 序列化,替代 Python json.loads
|
|
273
|
+
- **SharedState Bytes** — 零拷贝 clone
|
|
274
|
+
- **Arc\<str\> method/path** — 请求路径零分配
|
|
275
|
+
- **IpAddr lazy eval** — 不访问时不解析
|
|
276
|
+
- **Bytes zero-copy body** — 请求体零拷贝
|
|
277
|
+
- **mimalloc global allocator** — 高并发分配性能
|
|
278
|
+
|
|
279
|
+
### Features
|
|
280
|
+
- `io_workers` parameter — `app.run(workers=24, io_workers=16)` 或 `PYRE_IO_WORKERS=16`
|
|
281
|
+
- `client_ip` — 请求客户端 IP 地址
|
|
282
|
+
- Lifecycle hooks — `on_startup` / `on_shutdown`
|
|
283
|
+
- Zero-cost logging — Rust tracing engine + Python→Rust FFI bridge, OFF 级别原子跳过
|
|
284
|
+
|
|
285
|
+
### Benchmarks (Linux, AMD Ryzen 7 7840HS 8C/16T)
|
|
286
|
+
- **GET /: 420k req/s** (P99 571μs) — vs macOS v1.2.0 214k (+96%)
|
|
287
|
+
- **300s sustained: 401k req/s**, 1.2 亿请求, 0 错误, 内存仅 +27 MB
|
|
288
|
+
- **vs Robyn: 14-16x faster** across all routes
|
|
289
|
+
|
|
290
|
+
## v1.3.0 (2026-03-31)
|
|
291
|
+
|
|
292
|
+
### Features
|
|
293
|
+
- **Zero-cost logging system** — Rust `tracing` + `EnvFilter`, three targets, Python logging bridge via C-FFI
|
|
294
|
+
- **client_ip** property on PyreRequest
|
|
295
|
+
- **on_startup / on_shutdown** lifecycle hooks
|
|
296
|
+
|
|
297
|
+
### Performance
|
|
298
|
+
- IpAddr lazy evaluation
|
|
299
|
+
- Bytes zero-copy request body
|
|
300
|
+
- Arc\<str\> method/path to eliminate allocations
|
|
301
|
+
- Vec params (from HashMap)
|
|
302
|
+
- Zero-allocation hook iteration
|
|
303
|
+
- Sync Python log level with Rust EnvFilter
|
|
304
|
+
|
|
305
|
+
### Docs
|
|
306
|
+
- Sub-interpreter C extension compatibility guide (30/30 libs confirmed)
|
|
307
|
+
- English translations for all benchmark reports
|
|
308
|
+
|
|
309
|
+
## v1.2.0 (2026-03-25)
|
|
310
|
+
|
|
311
|
+
### Features
|
|
312
|
+
- **Dual async/sync worker pool** — `async def` handlers auto-route to asyncio event loops, `def` handlers to sync sub-interpreters. Zero config, zero performance loss.
|
|
313
|
+
- **Native async bridge (C-FFI)** — `pyre_recv`/`pyre_send` release GIL during channel wait, enabling true async in sub-interpreters.
|
|
314
|
+
- **MCP Server** — JSON-RPC 2.0 with `@app.mcp.tool()`, `@app.mcp.resource()`, `@app.mcp.prompt()` decorators.
|
|
315
|
+
- **MsgPack RPC** — `@app.rpc()` with content negotiation (MsgPack/JSON) + `PyreRPCClient` magic client.
|
|
316
|
+
- **SSE Streaming** — `PyreStream` with mpsc channel, returned directly from handlers.
|
|
317
|
+
- **SharedState** — Cross-worker `app.state` backed by `Arc<DashMap>`, nanosecond latency.
|
|
318
|
+
- **GIL Watchdog** — Monitor GIL contention, hold time, queue depth, memory RSS.
|
|
319
|
+
- **Backpressure** — Bounded channels with `try_send()`, returns 503 on overload.
|
|
320
|
+
- **Request timeout** — 30s zombie reaper in sub-interpreter mode (504 Gateway Timeout).
|
|
321
|
+
- **mimalloc** — Global allocator for high-concurrency allocation performance.
|
|
322
|
+
- **Hybrid dispatch** — `gil=True` routes auto-dispatch to main interpreter for C extension compatibility.
|
|
323
|
+
|
|
324
|
+
### Code Quality
|
|
325
|
+
- Extracted bootstrap script from Rust string to `python/pyreframework/_bootstrap.py` (`include_str!`).
|
|
326
|
+
- Removed dead `filter_script_ast` code.
|
|
327
|
+
- Moved CORS/logging from global statics to per-instance `PyreApp` fields.
|
|
328
|
+
- Added `debug_assert!(PyGILState_Check())` in `PyObjRef::Drop`.
|
|
329
|
+
- Full `cargo fmt` + zero clippy warnings.
|
|
330
|
+
- Migrated deprecated PyO3 `downcast` → `cast` calls.
|
|
331
|
+
|
|
332
|
+
### Bug Fixes
|
|
333
|
+
- **Fixed segfault on Ctrl+C** — `InterpreterPool::Drop` now joins worker threads before `Py_Finalize`.
|
|
334
|
+
- **Fixed KeyboardInterrupt noise** — Guard `signal.signal()` for main thread only.
|
|
335
|
+
- Hot reload fallback skips `.venv`/`node_modules`/`__pycache__`.
|
|
336
|
+
|
|
337
|
+
### Testing
|
|
338
|
+
- 21 Rust unit tests (response builders, MIME detection, header extraction, query params).
|
|
339
|
+
- 54 Python pytest tests (MCP, cookies, TestClient, RPC, static files, WebSocket, async isolation, logging).
|
|
340
|
+
- 22 integration tests (GIL + sub-interp modes, all features end-to-end).
|
|
341
|
+
- 5-minute stability benchmark: 64M requests, zero memory leaks, zero crashes.
|
|
342
|
+
|
|
343
|
+
### CI/CD
|
|
344
|
+
- GitHub Actions: cargo test → pytest → integration tests on Python 3.13/3.14.
|
|
345
|
+
- Blocking `cargo fmt --check` + `cargo clippy -- -D warnings`.
|
|
346
|
+
|
|
347
|
+
## v1.1.0 (2026-03-24)
|
|
348
|
+
|
|
349
|
+
### Features
|
|
350
|
+
- WebSocket support (text + binary) via tokio-tungstenite.
|
|
351
|
+
- Cookie utilities (`get_cookie`, `set_cookie`, `delete_cookie`).
|
|
352
|
+
- Multipart file upload parser.
|
|
353
|
+
- Redirect helper.
|
|
354
|
+
- TestClient for testing without a running server.
|
|
355
|
+
- Env var configuration (`PYRE_HOST`, `PYRE_PORT`, `PYRE_WORKERS`, `PYRE_LOG`).
|
|
356
|
+
- Hot reload (`reload=True` or `PYRE_RELOAD=1`).
|
|
357
|
+
|
|
358
|
+
## v1.0.0 (2026-03-23)
|
|
359
|
+
|
|
360
|
+
### Initial Release
|
|
361
|
+
- Rust core with Tokio + Hyper HTTP server.
|
|
362
|
+
- Per-Interpreter GIL (PEP 684) sub-interpreter pool.
|
|
363
|
+
- Decorator routing (`@app.get`, `@app.post`, etc.).
|
|
364
|
+
- Path params, query params, JSON parsing.
|
|
365
|
+
- CORS middleware.
|
|
366
|
+
- Static file serving with MIME detection + path traversal protection.
|
|
367
|
+
- Pydantic validation via `model=` parameter.
|
|
368
|
+
- Before/after request hooks.
|
|
369
|
+
- Graceful shutdown via Ctrl+C.
|