freastal 0.0.1__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.
- freastal-0.0.1/LICENSE +21 -0
- freastal-0.0.1/PKG-INFO +139 -0
- freastal-0.0.1/README.md +124 -0
- freastal-0.0.1/freastal/__init__.py +126 -0
- freastal-0.0.1/freastal/_asgi_protocol.py +42 -0
- freastal-0.0.1/freastal/src/asgi.c +411 -0
- freastal-0.0.1/freastal/src/freastalmodule.c +128 -0
- freastal-0.0.1/freastal/src/server.c +681 -0
- freastal-0.0.1/freastal/src/tls.c +56 -0
- freastal-0.0.1/freastal/src/wsgi.c +473 -0
- freastal-0.0.1/freastal.egg-info/PKG-INFO +139 -0
- freastal-0.0.1/freastal.egg-info/SOURCES.txt +24 -0
- freastal-0.0.1/freastal.egg-info/dependency_links.txt +1 -0
- freastal-0.0.1/freastal.egg-info/top_level.txt +1 -0
- freastal-0.0.1/pyproject.toml +65 -0
- freastal-0.0.1/setup.cfg +4 -0
- freastal-0.0.1/setup.py +218 -0
- freastal-0.0.1/tests/test_asgi.py +14 -0
- freastal-0.0.1/tests/test_server.py +52 -0
- freastal-0.0.1/tests/test_wsgi.py +1 -0
- freastal-0.0.1/vendor/picohttpparser/picohttpparser.c +707 -0
- freastal-0.0.1/vendor/picotls/lib/asn1.c +309 -0
- freastal-0.0.1/vendor/picotls/lib/hpke.c +263 -0
- freastal-0.0.1/vendor/picotls/lib/openssl.c +2704 -0
- freastal-0.0.1/vendor/picotls/lib/pembase64.c +370 -0
- freastal-0.0.1/vendor/picotls/lib/picotls.c +7326 -0
freastal-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Bylund
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
freastal-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: freastal
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Fast libuv + picohttpparser WSGI/ASGI server (Irish: freastal — service)
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
8
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
9
|
+
Classifier: Operating System :: MacOS
|
|
10
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# freastal
|
|
17
|
+
|
|
18
|
+
A fast WSGI/ASGI server for Python, built as a C extension on top of [libuv](https://libuv.org/) and [picohttpparser](https://github.com/h2o/picohttpparser). Optional TLS 1.3 via [picotls](https://github.com/h2o/picotls).
|
|
19
|
+
|
|
20
|
+
*Freastal* (IPA: /ˈfʲɾʲasˠtəl/) is Irish Gaelic for "service."
|
|
21
|
+
|
|
22
|
+
## Performance
|
|
23
|
+
|
|
24
|
+
Benchmarked against gunicorn+uvicorn (the most common production Python stack) as baseline. 30-second runs, `wrk -t4 -c40`, 4 worker processes, ARM64 Linux.
|
|
25
|
+
|
|
26
|
+
### 500B response
|
|
27
|
+
|
|
28
|
+
| Server | Protocol | Req/s | p50 | p99 | vs baseline |
|
|
29
|
+
|--------|----------|------:|----:|----:|------------:|
|
|
30
|
+
| **gunicorn+uvicorn** | **ASGI** | **~225k** | **156µs** | **476µs** | **1.00×** |
|
|
31
|
+
| bjoern | WSGI | ~370k | 90µs | 390µs | 1.65× |
|
|
32
|
+
| freastal | WSGI | ~424k | 78µs | 312µs | 1.88× |
|
|
33
|
+
| freastal | ASGI | ~408k | 81µs | 317µs | 1.81× |
|
|
34
|
+
| freastal | TLS 1.3 | ~421k | 78µs | 342µs | 1.87× |
|
|
35
|
+
|
|
36
|
+
### 12KB response
|
|
37
|
+
|
|
38
|
+
| Server | Protocol | Req/s | p50 | p99 | vs baseline |
|
|
39
|
+
|--------|----------|------:|----:|----:|------------:|
|
|
40
|
+
| **gunicorn+uvicorn** | **ASGI** | **~201k** | **173µs** | **524µs** | **1.00×** |
|
|
41
|
+
| bjoern | WSGI | ~293k | 120µs | 360µs | 1.46× |
|
|
42
|
+
| freastal | WSGI | ~299k | 114µs | 391µs | 1.49× |
|
|
43
|
+
| freastal | ASGI | ~295k | 115µs | 409µs | 1.47× |
|
|
44
|
+
| freastal | TLS 1.3 | ~279k | 121µs | 555µs | 1.39× |
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
Pre-built wheels for Linux (x86\_64, aarch64) and macOS (arm64, x86\_64) are available on PyPI:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install freastal
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Building from source requires libuv ≥ 1.44, a C compiler, and (optionally) OpenSSL for TLS support. See [Building from source](#building-from-source).
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
### WSGI
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import freastal
|
|
62
|
+
|
|
63
|
+
def app(environ, start_response):
|
|
64
|
+
body = b"Hello, world!"
|
|
65
|
+
start_response("200 OK", [("Content-Type", "text/plain")])
|
|
66
|
+
return [body]
|
|
67
|
+
|
|
68
|
+
freastal.serve(app, host="0.0.0.0", port=8000, workers=4)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### ASGI
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import freastal
|
|
75
|
+
|
|
76
|
+
async def app(scope, receive, send):
|
|
77
|
+
await send({"type": "http.response.start", "status": 200,
|
|
78
|
+
"headers": [[b"content-type", b"text/plain"]]})
|
|
79
|
+
await send({"type": "http.response.body", "body": b"Hello, world!"})
|
|
80
|
+
|
|
81
|
+
freastal.serve_asgi(app, host="0.0.0.0", port=8000, workers=4)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### TLS 1.3
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
freastal.serve(app, host="0.0.0.0", port=8000, workers=4,
|
|
88
|
+
certfile="/path/to/cert.pem", keyfile="/path/to/key.pem")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
TLS requires OpenSSL headers at build time. Wheels published to PyPI include TLS support.
|
|
92
|
+
|
|
93
|
+
## Architecture
|
|
94
|
+
|
|
95
|
+
- **libuv** — cross-platform event loop; io\_uring-ready on Linux (libuv ≥ 1.45 batches syscalls automatically)
|
|
96
|
+
- **picohttpparser** — SSE4.2/NEON SIMD HTTP/1.1 parser from the h2o project; vendored
|
|
97
|
+
- **picotls** — TLS 1.3 library from the h2o project; vendored, gated by `FREASTAL_TLS`
|
|
98
|
+
- **io_uring fixed-buffer path** (Linux, optional) — when built with `liburing`, responses > 4 KB are copied into pre-registered kernel buffers and sent with `io_uring_prep_write_fixed`, eliminating per-write `get_user_pages()` overhead. libuv ≥ 1.45 also transparently batches `accept`/`read`/`write` via io_uring regardless of this flag.
|
|
99
|
+
- Single `uv_write` per response — headers and body sent together, no extra copy
|
|
100
|
+
- HTTP/1.1 keep-alive: connections re-armed in-place without close/reopen; `TCP_NODELAY` set on every accepted socket
|
|
101
|
+
- Slab allocator for per-connection state — no per-request malloc on the hot path
|
|
102
|
+
- Pre-interned Python strings for all WSGI/ASGI environ keys
|
|
103
|
+
- GIL released for the duration of the libuv event loop; acquired only when calling the WSGI/ASGI application and touching Python response objects
|
|
104
|
+
- `SO_REUSEPORT` (`UV_TCP_REUSEPORT`) for kernel-level load balancing across worker processes
|
|
105
|
+
|
|
106
|
+
**Multi-process model:** `workers=N` forks N independent OS processes, each with its own libuv loop and Python interpreter (and therefore its own GIL). The kernel distributes incoming connections across workers via `SO_REUSEPORT`.
|
|
107
|
+
|
|
108
|
+
**ASGI event loop bridge (libuv ↔ asyncio):**
|
|
109
|
+
|
|
110
|
+
freastal runs asyncio inside the libuv event loop rather than the other way around. A `uv_check_t` steps asyncio after each I/O poll; a `uv_poll_t` on asyncio's selector fd wakes libuv when external async I/O (database calls, aiohttp, etc.) completes.
|
|
111
|
+
|
|
112
|
+
## Building from source
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# macOS
|
|
116
|
+
brew install libuv openssl@3
|
|
117
|
+
pip install freastal --no-binary freastal
|
|
118
|
+
|
|
119
|
+
# Debian/Ubuntu
|
|
120
|
+
apt-get install libuv1-dev libssl-dev
|
|
121
|
+
pip install freastal --no-binary freastal
|
|
122
|
+
|
|
123
|
+
# Debian/Ubuntu with io_uring fixed-buffer path (Linux ≥ 5.6)
|
|
124
|
+
apt-get install libuv1-dev libssl-dev liburing-dev
|
|
125
|
+
pip install freastal --no-binary freastal
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
picohttpparser and picotls are vendored — no extra steps required.
|
|
129
|
+
|
|
130
|
+
## Requirements
|
|
131
|
+
|
|
132
|
+
- Python ≥ 3.10
|
|
133
|
+
- Linux or macOS
|
|
134
|
+
- libuv ≥ 1.44 (shared library, found via pkg-config or standard include paths)
|
|
135
|
+
- OpenSSL (optional, for TLS 1.3)
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
freastal-0.0.1/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# freastal
|
|
2
|
+
|
|
3
|
+
A fast WSGI/ASGI server for Python, built as a C extension on top of [libuv](https://libuv.org/) and [picohttpparser](https://github.com/h2o/picohttpparser). Optional TLS 1.3 via [picotls](https://github.com/h2o/picotls).
|
|
4
|
+
|
|
5
|
+
*Freastal* (IPA: /ˈfʲɾʲasˠtəl/) is Irish Gaelic for "service."
|
|
6
|
+
|
|
7
|
+
## Performance
|
|
8
|
+
|
|
9
|
+
Benchmarked against gunicorn+uvicorn (the most common production Python stack) as baseline. 30-second runs, `wrk -t4 -c40`, 4 worker processes, ARM64 Linux.
|
|
10
|
+
|
|
11
|
+
### 500B response
|
|
12
|
+
|
|
13
|
+
| Server | Protocol | Req/s | p50 | p99 | vs baseline |
|
|
14
|
+
|--------|----------|------:|----:|----:|------------:|
|
|
15
|
+
| **gunicorn+uvicorn** | **ASGI** | **~225k** | **156µs** | **476µs** | **1.00×** |
|
|
16
|
+
| bjoern | WSGI | ~370k | 90µs | 390µs | 1.65× |
|
|
17
|
+
| freastal | WSGI | ~424k | 78µs | 312µs | 1.88× |
|
|
18
|
+
| freastal | ASGI | ~408k | 81µs | 317µs | 1.81× |
|
|
19
|
+
| freastal | TLS 1.3 | ~421k | 78µs | 342µs | 1.87× |
|
|
20
|
+
|
|
21
|
+
### 12KB response
|
|
22
|
+
|
|
23
|
+
| Server | Protocol | Req/s | p50 | p99 | vs baseline |
|
|
24
|
+
|--------|----------|------:|----:|----:|------------:|
|
|
25
|
+
| **gunicorn+uvicorn** | **ASGI** | **~201k** | **173µs** | **524µs** | **1.00×** |
|
|
26
|
+
| bjoern | WSGI | ~293k | 120µs | 360µs | 1.46× |
|
|
27
|
+
| freastal | WSGI | ~299k | 114µs | 391µs | 1.49× |
|
|
28
|
+
| freastal | ASGI | ~295k | 115µs | 409µs | 1.47× |
|
|
29
|
+
| freastal | TLS 1.3 | ~279k | 121µs | 555µs | 1.39× |
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Pre-built wheels for Linux (x86\_64, aarch64) and macOS (arm64, x86\_64) are available on PyPI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install freastal
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Building from source requires libuv ≥ 1.44, a C compiler, and (optionally) OpenSSL for TLS support. See [Building from source](#building-from-source).
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### WSGI
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import freastal
|
|
47
|
+
|
|
48
|
+
def app(environ, start_response):
|
|
49
|
+
body = b"Hello, world!"
|
|
50
|
+
start_response("200 OK", [("Content-Type", "text/plain")])
|
|
51
|
+
return [body]
|
|
52
|
+
|
|
53
|
+
freastal.serve(app, host="0.0.0.0", port=8000, workers=4)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### ASGI
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import freastal
|
|
60
|
+
|
|
61
|
+
async def app(scope, receive, send):
|
|
62
|
+
await send({"type": "http.response.start", "status": 200,
|
|
63
|
+
"headers": [[b"content-type", b"text/plain"]]})
|
|
64
|
+
await send({"type": "http.response.body", "body": b"Hello, world!"})
|
|
65
|
+
|
|
66
|
+
freastal.serve_asgi(app, host="0.0.0.0", port=8000, workers=4)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### TLS 1.3
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
freastal.serve(app, host="0.0.0.0", port=8000, workers=4,
|
|
73
|
+
certfile="/path/to/cert.pem", keyfile="/path/to/key.pem")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
TLS requires OpenSSL headers at build time. Wheels published to PyPI include TLS support.
|
|
77
|
+
|
|
78
|
+
## Architecture
|
|
79
|
+
|
|
80
|
+
- **libuv** — cross-platform event loop; io\_uring-ready on Linux (libuv ≥ 1.45 batches syscalls automatically)
|
|
81
|
+
- **picohttpparser** — SSE4.2/NEON SIMD HTTP/1.1 parser from the h2o project; vendored
|
|
82
|
+
- **picotls** — TLS 1.3 library from the h2o project; vendored, gated by `FREASTAL_TLS`
|
|
83
|
+
- **io_uring fixed-buffer path** (Linux, optional) — when built with `liburing`, responses > 4 KB are copied into pre-registered kernel buffers and sent with `io_uring_prep_write_fixed`, eliminating per-write `get_user_pages()` overhead. libuv ≥ 1.45 also transparently batches `accept`/`read`/`write` via io_uring regardless of this flag.
|
|
84
|
+
- Single `uv_write` per response — headers and body sent together, no extra copy
|
|
85
|
+
- HTTP/1.1 keep-alive: connections re-armed in-place without close/reopen; `TCP_NODELAY` set on every accepted socket
|
|
86
|
+
- Slab allocator for per-connection state — no per-request malloc on the hot path
|
|
87
|
+
- Pre-interned Python strings for all WSGI/ASGI environ keys
|
|
88
|
+
- GIL released for the duration of the libuv event loop; acquired only when calling the WSGI/ASGI application and touching Python response objects
|
|
89
|
+
- `SO_REUSEPORT` (`UV_TCP_REUSEPORT`) for kernel-level load balancing across worker processes
|
|
90
|
+
|
|
91
|
+
**Multi-process model:** `workers=N` forks N independent OS processes, each with its own libuv loop and Python interpreter (and therefore its own GIL). The kernel distributes incoming connections across workers via `SO_REUSEPORT`.
|
|
92
|
+
|
|
93
|
+
**ASGI event loop bridge (libuv ↔ asyncio):**
|
|
94
|
+
|
|
95
|
+
freastal runs asyncio inside the libuv event loop rather than the other way around. A `uv_check_t` steps asyncio after each I/O poll; a `uv_poll_t` on asyncio's selector fd wakes libuv when external async I/O (database calls, aiohttp, etc.) completes.
|
|
96
|
+
|
|
97
|
+
## Building from source
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# macOS
|
|
101
|
+
brew install libuv openssl@3
|
|
102
|
+
pip install freastal --no-binary freastal
|
|
103
|
+
|
|
104
|
+
# Debian/Ubuntu
|
|
105
|
+
apt-get install libuv1-dev libssl-dev
|
|
106
|
+
pip install freastal --no-binary freastal
|
|
107
|
+
|
|
108
|
+
# Debian/Ubuntu with io_uring fixed-buffer path (Linux ≥ 5.6)
|
|
109
|
+
apt-get install libuv1-dev libssl-dev liburing-dev
|
|
110
|
+
pip install freastal --no-binary freastal
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
picohttpparser and picotls are vendored — no extra steps required.
|
|
114
|
+
|
|
115
|
+
## Requirements
|
|
116
|
+
|
|
117
|
+
- Python ≥ 3.10
|
|
118
|
+
- Linux or macOS
|
|
119
|
+
- libuv ≥ 1.44 (shared library, found via pkg-config or standard include paths)
|
|
120
|
+
- OpenSSL (optional, for TLS 1.3)
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""freastal – libuv + picohttpparser WSGI/ASGI server."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import sys
|
|
7
|
+
import multiprocessing
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from ._freastal import (
|
|
11
|
+
serve as _serve_single,
|
|
12
|
+
serve_asgi as _serve_asgi_single,
|
|
13
|
+
__version__,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = ["serve", "serve_asgi", "__version__"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def serve(
|
|
20
|
+
app,
|
|
21
|
+
host="0.0.0.0",
|
|
22
|
+
port=8000,
|
|
23
|
+
workers=1,
|
|
24
|
+
reuse_port=True,
|
|
25
|
+
certfile=None,
|
|
26
|
+
keyfile=None,
|
|
27
|
+
):
|
|
28
|
+
"""Start freastal.
|
|
29
|
+
|
|
30
|
+
With workers=1 (default) runs in-process.
|
|
31
|
+
With workers>1 forks worker processes, each binding with SO_REUSEPORT
|
|
32
|
+
so the kernel load-balances connections across them.
|
|
33
|
+
Pass certfile and keyfile (PEM paths) to enable TLS 1.3 (requires picotls).
|
|
34
|
+
"""
|
|
35
|
+
if workers <= 1:
|
|
36
|
+
_serve_single(
|
|
37
|
+
app,
|
|
38
|
+
host=host,
|
|
39
|
+
port=port,
|
|
40
|
+
reuse_port=reuse_port,
|
|
41
|
+
certfile=certfile,
|
|
42
|
+
keyfile=keyfile,
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
processes = []
|
|
47
|
+
|
|
48
|
+
def _worker(worker_id):
|
|
49
|
+
print(f"[freastal] worker {worker_id} pid={os.getpid()} starting", flush=True)
|
|
50
|
+
try:
|
|
51
|
+
_serve_single(
|
|
52
|
+
app,
|
|
53
|
+
host=host,
|
|
54
|
+
port=port,
|
|
55
|
+
reuse_port=True,
|
|
56
|
+
certfile=certfile,
|
|
57
|
+
keyfile=keyfile,
|
|
58
|
+
)
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def _shutdown(sig, frame):
|
|
63
|
+
for p in processes:
|
|
64
|
+
p.terminate()
|
|
65
|
+
for p in processes:
|
|
66
|
+
p.join(timeout=5)
|
|
67
|
+
sys.exit(0)
|
|
68
|
+
|
|
69
|
+
signal.signal(signal.SIGINT, _shutdown)
|
|
70
|
+
signal.signal(signal.SIGTERM, _shutdown)
|
|
71
|
+
|
|
72
|
+
for i in range(workers):
|
|
73
|
+
p = multiprocessing.Process(target=_worker, args=(i + 1,), daemon=True)
|
|
74
|
+
p.start()
|
|
75
|
+
processes.append(p)
|
|
76
|
+
|
|
77
|
+
for p in processes:
|
|
78
|
+
p.join()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def serve_asgi(app, host="0.0.0.0", port=8000, workers=1, reuse_port=True):
|
|
82
|
+
"""Start freastal in ASGI mode.
|
|
83
|
+
|
|
84
|
+
With workers=1 runs in-process.
|
|
85
|
+
With workers>1 forks worker processes using SO_REUSEPORT.
|
|
86
|
+
Each worker creates its own asyncio event loop.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def _run_single():
|
|
90
|
+
loop = asyncio.new_event_loop()
|
|
91
|
+
asyncio.set_event_loop(loop)
|
|
92
|
+
_serve_asgi_single(app, loop, host=host, port=port, reuse_port=reuse_port)
|
|
93
|
+
|
|
94
|
+
if workers <= 1:
|
|
95
|
+
_run_single()
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
processes = []
|
|
99
|
+
|
|
100
|
+
def _worker(worker_id):
|
|
101
|
+
print(
|
|
102
|
+
f"[freastal] ASGI worker {worker_id} pid={os.getpid()} starting", flush=True
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
_run_single()
|
|
106
|
+
except KeyboardInterrupt:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def _shutdown(sig, frame):
|
|
110
|
+
for p in processes:
|
|
111
|
+
p.terminate()
|
|
112
|
+
for p in processes:
|
|
113
|
+
p.join(timeout=5)
|
|
114
|
+
sys.exit(0)
|
|
115
|
+
|
|
116
|
+
signal.signal(signal.SIGINT, _shutdown)
|
|
117
|
+
signal.signal(signal.SIGTERM, _shutdown)
|
|
118
|
+
|
|
119
|
+
for i in range(workers):
|
|
120
|
+
p = multiprocessing.Process(target=_worker, args=(i + 1,), daemon=True)
|
|
121
|
+
p.start()
|
|
122
|
+
processes.append(p)
|
|
123
|
+
time.sleep(0.05)
|
|
124
|
+
|
|
125
|
+
for p in processes:
|
|
126
|
+
p.join()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Pure-Python ASGI protocol bridge for freastal.
|
|
2
|
+
|
|
3
|
+
run_asgi_request() is called from C (asgi_dispatch) once per HTTP request.
|
|
4
|
+
It creates the receive/send coroutines and schedules the ASGI app as an
|
|
5
|
+
asyncio Task on the loop that freastal is stepping from its uv_check_t callback.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._freastal import asgi_send_response
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_asgi_request(loop, app, scope, body, capsule):
|
|
12
|
+
"""Schedule `app(scope, receive, send)` as a Task and return it.
|
|
13
|
+
|
|
14
|
+
The Task runs inside the asyncio loop that freastal drives from
|
|
15
|
+
uv_check_t / uv_poll_t callbacks. Because receive() returns immediately
|
|
16
|
+
and send() calls back into C synchronously, a simple ASGI app completes
|
|
17
|
+
in one _run_once() step with no event-loop round-trips.
|
|
18
|
+
|
|
19
|
+
Apps that do real async I/O (await aiohttp.get(...), await db.query(...))
|
|
20
|
+
work normally: their futures are resolved by asyncio's selector, which
|
|
21
|
+
freastal monitors via a uv_poll_t on asyncio's selector fd.
|
|
22
|
+
"""
|
|
23
|
+
status_cell = [None]
|
|
24
|
+
headers_cell = [None]
|
|
25
|
+
|
|
26
|
+
async def receive():
|
|
27
|
+
return {"type": "http.request", "body": body, "more_body": False}
|
|
28
|
+
|
|
29
|
+
async def send(event):
|
|
30
|
+
t = event["type"]
|
|
31
|
+
if t == "http.response.start":
|
|
32
|
+
status_cell[0] = event["status"]
|
|
33
|
+
headers_cell[0] = list(event.get("headers", []))
|
|
34
|
+
elif t == "http.response.body":
|
|
35
|
+
asgi_send_response(
|
|
36
|
+
capsule,
|
|
37
|
+
status_cell[0],
|
|
38
|
+
headers_cell[0],
|
|
39
|
+
event.get("body", b""),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return loop.create_task(app(scope, receive, send))
|