loopmini 0.1.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.
- loopmini-0.1.1/.github/workflows/ci.yml +70 -0
- loopmini-0.1.1/.gitignore +16 -0
- loopmini-0.1.1/Cargo.lock +271 -0
- loopmini-0.1.1/Cargo.toml +23 -0
- loopmini-0.1.1/DEV.md +204 -0
- loopmini-0.1.1/LICENSE +202 -0
- loopmini-0.1.1/PKG-INFO +67 -0
- loopmini-0.1.1/README.md +38 -0
- loopmini-0.1.1/_config.yml +1 -0
- loopmini-0.1.1/_layouts/default.html +52 -0
- loopmini-0.1.1/pyproject.toml +47 -0
- loopmini-0.1.1/python/loopmini/__init__.py +4 -0
- loopmini-0.1.1/python/loopmini/loop.py +603 -0
- loopmini-0.1.1/python/loopmini/subproc.py +114 -0
- loopmini-0.1.1/python/loopmini/transports.py +481 -0
- loopmini-0.1.1/src/lib.rs +16 -0
- loopmini-0.1.1/src/pyreactor.rs +81 -0
- loopmini-0.1.1/src/reactor.rs +178 -0
- loopmini-0.1.1/src/tokio_core.rs +118 -0
- loopmini-0.1.1/tests/oracle_util.py +128 -0
- loopmini-0.1.1/tests/test_bench.py +59 -0
- loopmini-0.1.1/tests/test_loop.py +343 -0
- loopmini-0.1.1/tests/test_oracle.py +65 -0
- loopmini-0.1.1/tests/test_soak.py +110 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ['v*']
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v7
|
|
14
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
15
|
+
- uses: actions/setup-python@v7
|
|
16
|
+
with:
|
|
17
|
+
python-version: '3.12'
|
|
18
|
+
- run: pip install -e '.[dev]'
|
|
19
|
+
- run: pytest -q
|
|
20
|
+
|
|
21
|
+
build:
|
|
22
|
+
needs: test
|
|
23
|
+
strategy:
|
|
24
|
+
matrix:
|
|
25
|
+
os: [ubuntu-latest, macos-latest]
|
|
26
|
+
runs-on: ${{ matrix.os }}
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v7
|
|
29
|
+
- uses: PyO3/maturin-action@v1
|
|
30
|
+
with:
|
|
31
|
+
args: --release --out dist -i python3.10 -i python3.11 -i python3.12 -i python3.13
|
|
32
|
+
manylinux: auto
|
|
33
|
+
- uses: actions/upload-artifact@v7
|
|
34
|
+
with:
|
|
35
|
+
name: wheels-${{ matrix.os }}
|
|
36
|
+
path: dist
|
|
37
|
+
|
|
38
|
+
sdist:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v7
|
|
42
|
+
- uses: PyO3/maturin-action@v1
|
|
43
|
+
with:
|
|
44
|
+
command: sdist
|
|
45
|
+
args: -o dist
|
|
46
|
+
- uses: actions/upload-artifact@v7
|
|
47
|
+
with:
|
|
48
|
+
name: wheels-sdist
|
|
49
|
+
path: dist
|
|
50
|
+
|
|
51
|
+
publish:
|
|
52
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
53
|
+
needs: [build, sdist]
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
permissions:
|
|
56
|
+
id-token: write
|
|
57
|
+
contents: write
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v7
|
|
60
|
+
- uses: actions/download-artifact@v8
|
|
61
|
+
with:
|
|
62
|
+
path: dist
|
|
63
|
+
merge-multiple: true
|
|
64
|
+
- uses: softprops/action-gh-release@v3
|
|
65
|
+
with:
|
|
66
|
+
files: dist/*
|
|
67
|
+
generate_release_notes: true
|
|
68
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
with:
|
|
70
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "bitflags"
|
|
7
|
+
version = "2.13.1"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "cfg-if"
|
|
13
|
+
version = "1.0.4"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "concurrent-queue"
|
|
19
|
+
version = "2.5.0"
|
|
20
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
21
|
+
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
|
22
|
+
dependencies = [
|
|
23
|
+
"crossbeam-utils",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[[package]]
|
|
27
|
+
name = "crossbeam-utils"
|
|
28
|
+
version = "0.8.22"
|
|
29
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
30
|
+
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
|
31
|
+
|
|
32
|
+
[[package]]
|
|
33
|
+
name = "errno"
|
|
34
|
+
version = "0.3.14"
|
|
35
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
36
|
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
|
37
|
+
dependencies = [
|
|
38
|
+
"libc",
|
|
39
|
+
"windows-sys",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[[package]]
|
|
43
|
+
name = "heck"
|
|
44
|
+
version = "0.5.0"
|
|
45
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
46
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
47
|
+
|
|
48
|
+
[[package]]
|
|
49
|
+
name = "hermit-abi"
|
|
50
|
+
version = "0.5.2"
|
|
51
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
52
|
+
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
|
53
|
+
|
|
54
|
+
[[package]]
|
|
55
|
+
name = "libc"
|
|
56
|
+
version = "0.2.189"
|
|
57
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
58
|
+
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|
59
|
+
|
|
60
|
+
[[package]]
|
|
61
|
+
name = "linux-raw-sys"
|
|
62
|
+
version = "0.12.1"
|
|
63
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
64
|
+
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|
65
|
+
|
|
66
|
+
[[package]]
|
|
67
|
+
name = "loopmini"
|
|
68
|
+
version = "0.1.1"
|
|
69
|
+
dependencies = [
|
|
70
|
+
"polling",
|
|
71
|
+
"pyo3",
|
|
72
|
+
"tokio",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[[package]]
|
|
76
|
+
name = "mio"
|
|
77
|
+
version = "1.2.2"
|
|
78
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
79
|
+
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
|
80
|
+
dependencies = [
|
|
81
|
+
"libc",
|
|
82
|
+
"wasi",
|
|
83
|
+
"windows-sys",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[[package]]
|
|
87
|
+
name = "once_cell"
|
|
88
|
+
version = "1.21.4"
|
|
89
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
90
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
91
|
+
|
|
92
|
+
[[package]]
|
|
93
|
+
name = "pin-project-lite"
|
|
94
|
+
version = "0.2.17"
|
|
95
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
96
|
+
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
|
97
|
+
|
|
98
|
+
[[package]]
|
|
99
|
+
name = "polling"
|
|
100
|
+
version = "3.11.0"
|
|
101
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
102
|
+
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
|
103
|
+
dependencies = [
|
|
104
|
+
"cfg-if",
|
|
105
|
+
"concurrent-queue",
|
|
106
|
+
"hermit-abi",
|
|
107
|
+
"pin-project-lite",
|
|
108
|
+
"rustix",
|
|
109
|
+
"windows-sys",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
[[package]]
|
|
113
|
+
name = "portable-atomic"
|
|
114
|
+
version = "1.15.0"
|
|
115
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
116
|
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|
117
|
+
|
|
118
|
+
[[package]]
|
|
119
|
+
name = "proc-macro2"
|
|
120
|
+
version = "1.0.107"
|
|
121
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
122
|
+
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
|
123
|
+
dependencies = [
|
|
124
|
+
"unicode-ident",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[[package]]
|
|
128
|
+
name = "pyo3"
|
|
129
|
+
version = "0.29.2"
|
|
130
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
131
|
+
checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
|
|
132
|
+
dependencies = [
|
|
133
|
+
"libc",
|
|
134
|
+
"once_cell",
|
|
135
|
+
"portable-atomic",
|
|
136
|
+
"pyo3-build-config",
|
|
137
|
+
"pyo3-ffi",
|
|
138
|
+
"pyo3-macros",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
[[package]]
|
|
142
|
+
name = "pyo3-build-config"
|
|
143
|
+
version = "0.29.2"
|
|
144
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
145
|
+
checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
|
|
146
|
+
dependencies = [
|
|
147
|
+
"target-lexicon",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
[[package]]
|
|
151
|
+
name = "pyo3-ffi"
|
|
152
|
+
version = "0.29.2"
|
|
153
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
154
|
+
checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
|
|
155
|
+
dependencies = [
|
|
156
|
+
"libc",
|
|
157
|
+
"pyo3-build-config",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
[[package]]
|
|
161
|
+
name = "pyo3-macros"
|
|
162
|
+
version = "0.29.2"
|
|
163
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
164
|
+
checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
|
|
165
|
+
dependencies = [
|
|
166
|
+
"proc-macro2",
|
|
167
|
+
"pyo3-macros-backend",
|
|
168
|
+
"quote",
|
|
169
|
+
"syn",
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
[[package]]
|
|
173
|
+
name = "pyo3-macros-backend"
|
|
174
|
+
version = "0.29.2"
|
|
175
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
176
|
+
checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
|
|
177
|
+
dependencies = [
|
|
178
|
+
"heck",
|
|
179
|
+
"proc-macro2",
|
|
180
|
+
"quote",
|
|
181
|
+
"syn",
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
[[package]]
|
|
185
|
+
name = "quote"
|
|
186
|
+
version = "1.0.47"
|
|
187
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
188
|
+
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
|
189
|
+
dependencies = [
|
|
190
|
+
"proc-macro2",
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
[[package]]
|
|
194
|
+
name = "rustix"
|
|
195
|
+
version = "1.1.4"
|
|
196
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
197
|
+
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
|
198
|
+
dependencies = [
|
|
199
|
+
"bitflags",
|
|
200
|
+
"errno",
|
|
201
|
+
"libc",
|
|
202
|
+
"linux-raw-sys",
|
|
203
|
+
"windows-sys",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
[[package]]
|
|
207
|
+
name = "socket2"
|
|
208
|
+
version = "0.6.5"
|
|
209
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
210
|
+
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
|
211
|
+
dependencies = [
|
|
212
|
+
"libc",
|
|
213
|
+
"windows-sys",
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
[[package]]
|
|
217
|
+
name = "syn"
|
|
218
|
+
version = "2.0.119"
|
|
219
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
220
|
+
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
|
221
|
+
dependencies = [
|
|
222
|
+
"proc-macro2",
|
|
223
|
+
"quote",
|
|
224
|
+
"unicode-ident",
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
[[package]]
|
|
228
|
+
name = "target-lexicon"
|
|
229
|
+
version = "0.13.5"
|
|
230
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
231
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
232
|
+
|
|
233
|
+
[[package]]
|
|
234
|
+
name = "tokio"
|
|
235
|
+
version = "1.53.1"
|
|
236
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
237
|
+
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
|
238
|
+
dependencies = [
|
|
239
|
+
"libc",
|
|
240
|
+
"mio",
|
|
241
|
+
"pin-project-lite",
|
|
242
|
+
"socket2",
|
|
243
|
+
"windows-sys",
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
[[package]]
|
|
247
|
+
name = "unicode-ident"
|
|
248
|
+
version = "1.0.24"
|
|
249
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
250
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
251
|
+
|
|
252
|
+
[[package]]
|
|
253
|
+
name = "wasi"
|
|
254
|
+
version = "0.11.1+wasi-snapshot-preview1"
|
|
255
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
256
|
+
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
|
257
|
+
|
|
258
|
+
[[package]]
|
|
259
|
+
name = "windows-link"
|
|
260
|
+
version = "0.2.1"
|
|
261
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
262
|
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
|
263
|
+
|
|
264
|
+
[[package]]
|
|
265
|
+
name = "windows-sys"
|
|
266
|
+
version = "0.61.2"
|
|
267
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
268
|
+
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
|
269
|
+
dependencies = [
|
|
270
|
+
"windows-link",
|
|
271
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "loopmini"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
license = "Apache-2.0"
|
|
6
|
+
description = "Rust-backed asyncio event loop"
|
|
7
|
+
repository = "https://github.com/AnswerDotAI/loopmini"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
|
|
10
|
+
[lib]
|
|
11
|
+
name = "loopmini"
|
|
12
|
+
crate-type = ["cdylib", "rlib"]
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
pyo3 = { version = ">=0.28", features = ["py-clone"] }
|
|
16
|
+
polling = "3"
|
|
17
|
+
tokio = { version = "1", features = ["rt", "time", "net"] }
|
|
18
|
+
|
|
19
|
+
[dev-dependencies]
|
|
20
|
+
tokio = { version = "1", features = ["rt-multi-thread"] }
|
|
21
|
+
|
|
22
|
+
[features]
|
|
23
|
+
extension-module = ["pyo3/extension-module"]
|
loopmini-0.1.1/DEV.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Developer guide
|
|
2
|
+
|
|
3
|
+
## Why this exists
|
|
4
|
+
|
|
5
|
+
Kernmini's Rust engine (see kernmini `meta/ROUGH.md`, "Separate project idea: a
|
|
6
|
+
Tokio-backed asyncio loop" and the 2026-08-28 review) needs Python kernels to
|
|
7
|
+
run arbitrary user asyncio code. loopmini tests whether a Rust reactor can host
|
|
8
|
+
that loop with full asyncio compatibility. The compatibility bar is the full
|
|
9
|
+
public asyncio surface, because solveit users run arbitrary packages; the
|
|
10
|
+
strategy for meeting it is maximal reuse of CPython's own asyncio machinery,
|
|
11
|
+
with Rust owning only what Python cannot express well.
|
|
12
|
+
|
|
13
|
+
## Crate/Python split
|
|
14
|
+
|
|
15
|
+
A key goal is that Rust crates and PyO3 wrappers share code: the same reactor
|
|
16
|
+
must be drivable by a pure-Rust kernel (as kernmini's native crate will be).
|
|
17
|
+
So the crate has three layers:
|
|
18
|
+
|
|
19
|
+
- `src/reactor.rs`: `Reactor<H>`, a PyO3-free, Tokio-free core generic over
|
|
20
|
+
the handle type. Ready queue, timer map, fd interest map (oneshot `polling`
|
|
21
|
+
sources, re-armed on delivery), thread-safe `schedule_ts` + notify, and the
|
|
22
|
+
turn phases: `next_timeout` / `poll` / `process` / `take_batch` /
|
|
23
|
+
`requeue_front`. `poll` holds no locks, so a driver may block in it with the
|
|
24
|
+
GIL released. The crate builds as an rlib, so a Rust consumer instantiates
|
|
25
|
+
`Reactor<RustHandle>` directly.
|
|
26
|
+
- `src/tokio_core.rs`: the reactor hosted on a Tokio current-thread runtime.
|
|
27
|
+
Tokio waits on the kqueue's own fd (a kqueue is pollable) and a zero-timeout
|
|
28
|
+
drain then collects events, so the kqueue-native level/oneshot semantics
|
|
29
|
+
that asyncio's `add_reader` contract needs survive Tokio's edge-triggered
|
|
30
|
+
driver. Rust futures spawned on the runtime advance during every blocking
|
|
31
|
+
poll, with the GIL released: the shared-reactor path for kernmini's engine.
|
|
32
|
+
Zero-timeout turns skip the runtime entirely, because entering it rounds the
|
|
33
|
+
wait up to the timer driver's ~1ms tick.
|
|
34
|
+
- `src/pyreactor.rs`: `PyReactor`, the Python-facing pyclass: the scheduling
|
|
35
|
+
and readiness methods plus the canonical dispatch loop (`check_signals` each
|
|
36
|
+
turn, `py.detach` around `poll`, EINTR retry, and the injected-exception
|
|
37
|
+
requeue rule, which must exist exactly once). `loopmini._core` registers it
|
|
38
|
+
on an owned runtime; kernmini's Python feature compiles the same struct and constructs
|
|
39
|
+
it with `PyReactor::with_handle` on kernmini's runtime. Rust has no stable
|
|
40
|
+
dylib ABI, so runtimes and reactors never cross extension boundaries: each
|
|
41
|
+
extension compiles the crate in, and the Python-visible reactor methods are
|
|
42
|
+
the only shared surface. `Loop(reactor=...)` accepts such a foreign reactor.
|
|
43
|
+
- `src/lib.rs`: module registration and the public re-exports (`ReactorCore`,
|
|
44
|
+
`TokioCore`, `PyReactor`) for embedding crates.
|
|
45
|
+
|
|
46
|
+
Python (`python/loopmini/loop.py`) implements `asyncio.AbstractEventLoop` by
|
|
47
|
+
delegating scheduling to the reactor and reusing stock `Handle`, `TimerHandle`,
|
|
48
|
+
`Task`, `Future`, and `wrap_future`. That reuse is a design decision, not a
|
|
49
|
+
shortcut: it buys exact contextvars, cancellation, and introspection semantics
|
|
50
|
+
and removes the most version-sensitive surface. `run_forever` on the main
|
|
51
|
+
thread routes signals through `signal.set_wakeup_fd` into an fd the reactor
|
|
52
|
+
watches, matching `BaseSelectorEventLoop`.
|
|
53
|
+
|
|
54
|
+
## Implemented surface
|
|
55
|
+
|
|
56
|
+
Beyond the scheduling core: TCP transports (`transports.py`: `SockTransport`
|
|
57
|
+
speaking both `Protocol` and `BufferedProtocol`, with `TCP_NODELAY` set as the
|
|
58
|
+
standard loop does, and `MiniServer` with the accept loop),
|
|
59
|
+
`create_connection`/`create_server` and their Unix-socket twins,
|
|
60
|
+
`connect_accepted_socket`, UDP via `DatagramTransport` and
|
|
61
|
+
`create_datagram_endpoint`, `start_tls` (including the gh-142352
|
|
62
|
+
buffered-StreamReader move), TLS client and server via stock `asyncio.sslproto`
|
|
63
|
+
over our transports, `getaddrinfo`/`getnameinfo` through the executor, and
|
|
64
|
+
`add_signal_handler`/`remove_signal_handler` dispatched from the
|
|
65
|
+
`set_wakeup_fd` socketpair (main-thread loops only, as in the standard loop),
|
|
66
|
+
pipe transports (`connect_read_pipe`/`connect_write_pipe`), and
|
|
67
|
+
`subprocess_exec`/`subprocess_shell` (`subproc.py`: Popen spawned in the
|
|
68
|
+
executor since fork/exec blocks, one reaper thread per child as in asyncio's
|
|
69
|
+
default ThreadedChildWatcher, and pipe transports over the child's fds).
|
|
70
|
+
|
|
71
|
+
Contracts learned from the oracles, kept working by them: `remove_reader`
|
|
72
|
+
cancels the stored `Handle`, so an already-queued readiness callback for a
|
|
73
|
+
removed fd never fires (anyio's futures assume it); `connection_lost` is
|
|
74
|
+
scheduled exactly once however `close()`, `abort()`, and fatal errors overlap
|
|
75
|
+
(anyio's `aclose` does close-then-abort, and a missed schedule leaks the
|
|
76
|
+
socket); a cancelled connect attempt closes its socket; and a raised
|
|
77
|
+
`create_connection` error must not keep a referrer to the attempt-error list.
|
|
78
|
+
`MiniServer` mirrors 3.13 `Server` exactly (test_server pins it):
|
|
79
|
+
`wait_closed()` returns only once the server is closed *and* the last client
|
|
80
|
+
transport is gone, `close_clients()`/`abort_clients()` act on a `WeakSet` of
|
|
81
|
+
attached transports, a cancelled `serve_forever` closes the server and its
|
|
82
|
+
clients before re-raising, and a Unix socket path is unlinked at close only
|
|
83
|
+
when its inode still matches the one bound (`cleanup_socket=False` disables).
|
|
84
|
+
A cancelled timer leaves the reactor's timer map at once: `schedule_at`
|
|
85
|
+
returns a `(deadline µs, seq)` key, the `TimerHandle` subclass carries it, and
|
|
86
|
+
`_timer_handle_cancelled` removes the entry. Retention until the original
|
|
87
|
+
deadline would grow memory for an hour per cancelled `wait_for(..., 3600)`.
|
|
88
|
+
From the aiohttp suite: `loop.time()` must be anchored to `time.monotonic`
|
|
89
|
+
(connectors compare the two directly), so the reactor clock carries a
|
|
90
|
+
constant offset captured at loop creation; and a transport must not hold an
|
|
91
|
+
explicit `memoryview` export of its write buffer across a `send` that can
|
|
92
|
+
raise, because the raised exception's traceback keeps the export alive and
|
|
93
|
+
the buffer can then never be resized (send the bytearray itself, as the
|
|
94
|
+
standard loop does).
|
|
95
|
+
|
|
96
|
+
## Deliberately not implemented yet
|
|
97
|
+
|
|
98
|
+
Sendfile and Windows.
|
|
99
|
+
`AbstractEventLoop` raises `NotImplementedError` for these, which is the
|
|
100
|
+
honest signal while the compatibility ladder is climbed. Next rungs and their
|
|
101
|
+
oracles (anyio/aiohttp/httpx/websockets test suites) are listed in kernmini
|
|
102
|
+
`meta/ROUGH.md`.
|
|
103
|
+
|
|
104
|
+
## Tests
|
|
105
|
+
|
|
106
|
+
The tiers, so the inner loop stays seconds:
|
|
107
|
+
|
|
108
|
+
- `pytest -q` per change (~2s).
|
|
109
|
+
- `pytest -m oracle -n auto` when a feature lands, not per edit.
|
|
110
|
+
- `pytest -m bench -s` and `pytest -m soak -s` only when performance or
|
|
111
|
+
stability is the question.
|
|
112
|
+
- `chkstyle` once, at the final PR stage, never per edit.
|
|
113
|
+
|
|
114
|
+
`pytest -q`. Ten integration stories, deliberately few: scheduling/tasks/
|
|
115
|
+
threads (timers, contextvars, gather, TaskGroup, timeout, cancellation,
|
|
116
|
+
cross-thread wakeup, to_thread), socket I/O through the reactor (accept/
|
|
117
|
+
connect/backpressure on a 5MB payload), asyncio streams echo with drain
|
|
118
|
+
backpressure, a background task surviving between `run_until_complete` calls
|
|
119
|
+
(the kernel-persistence story), KeyboardInterrupt injection with the loop
|
|
120
|
+
reused afterwards, uvicorn serving an ASGI app fetched by urllib and by
|
|
121
|
+
httpx-over-anyio, TLS echo against a throwaway openssl cert, a subprocess
|
|
122
|
+
round-trip (exec and shell, streams and communicate), and the
|
|
123
|
+
interrupt-torture story: window-scoped `PyThreadState_SetAsyncExc` injection
|
|
124
|
+
under stream/timer/cross-thread load, mirroring kernmini's
|
|
125
|
+
`sync_execution_context` contract (0.5s by default; `LOOPMINI_TORTURE_SECONDS`
|
|
126
|
+
extends it), and the KI-at-`_run`-entry orphan repro that pins the
|
|
127
|
+
traceback-depth requeue rule. Rust-side unit tests should exist only for
|
|
128
|
+
reactor invariants Python stories cannot reach.
|
|
129
|
+
|
|
130
|
+
Two CPython 3.13 facts the torture test depends on, discovered the hard way:
|
|
131
|
+
an async-injected exception raised at an eval-breaker check escapes the
|
|
132
|
+
raising frame even past a same-frame `try/except` (open regression
|
|
133
|
+
[gh-139622](https://github.com/python/cpython/issues/139622); 3.12 and 3.14
|
|
134
|
+
unaffected), so the guard must sit in a parent frame; and a busy window
|
|
135
|
+
shorter than the GIL switch interval (5ms) is invisible to a sampling thread,
|
|
136
|
+
because the GIL is only ever released while the window is closed.
|
|
137
|
+
|
|
138
|
+
## The compatibility oracles
|
|
139
|
+
|
|
140
|
+
Four external suites are the conformance measure, run in-repo through
|
|
141
|
+
`tests/test_oracle.py` (marked `oracle`, deselected by default):
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pytest -m oracle -n auto # all four suites, one worker per module
|
|
145
|
+
pytest -m oracle -k uvloop # one suite; add "and test_tcp" etc. for one module
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`tests/oracle_util.py` fetches and caches what each suite needs under
|
|
149
|
+
`~/.cache/loopmini-oracle` (override with `LOOPMINI_ORACLE_CACHE`):
|
|
150
|
+
|
|
151
|
+
- CPython's own test_asyncio (uvloop's strategy). This uv-managed Python ships
|
|
152
|
+
without the stdlib `test` package, so the matching source tarball is fetched
|
|
153
|
+
and its functional modules run under a loopmini event-loop policy. Suites
|
|
154
|
+
asserting standard-loop internals (base_events, selector_events,
|
|
155
|
+
unix_events) are not fair oracles and are excluded.
|
|
156
|
+
- anyio's test suite, via its sanctioned alternative-loop mechanism: the sdist
|
|
157
|
+
matching the installed version is unpacked and a loopmini entry added to
|
|
158
|
+
`asyncio_params` in its conftest. Runs as a subprocess; requires the anyio
|
|
159
|
+
test deps (`trustme`, `blockbuster`) in the venv. blockbuster's
|
|
160
|
+
blocking-call detector allowlists Popen's blocking os.read only under
|
|
161
|
+
`asyncio/base_events.py`, which is one reason the Popen spawn goes through
|
|
162
|
+
the executor.
|
|
163
|
+
- uvloop's test suite, from a source clone (`LOOPMINI_UVLOOP_REPO`, default
|
|
164
|
+
`~/aai-ws/links/uvloop`): its loop-parameterized test classes are cloned
|
|
165
|
+
onto loopmini with `implementation='asyncio'`, so branchy tests take the
|
|
166
|
+
standard-loop expectation paths. `test_tcp` needs pyOpenSSL and is skipped
|
|
167
|
+
without it.
|
|
168
|
+
- aiohttp's test suite: the sdist is unpacked, uvloop is stubbed to loopmini in
|
|
169
|
+
its conftest so `--aiohttp-loop=uvloop` selects it, and its blockbuster
|
|
170
|
+
fixture gains an allowlist entry for `MiniServer.close` (the same
|
|
171
|
+
unlink-if-unchanged stat asyncio's `_stop_serving` is allowlisted for).
|
|
172
|
+
Runs the pure-Python aiohttp (`AIOHTTP_NO_EXTENSIONS=1`), with a private
|
|
173
|
+
`--basetemp` because its permission tests leave chmod-000 dirs that
|
|
174
|
+
pytest's numbered-dir sweeper cannot remove, fatal under aiohttp's
|
|
175
|
+
`filterwarnings = error`. Deselected, with reasons in `test_oracle.py`:
|
|
176
|
+
tests that fail identically on the standard loop in this venv, and one
|
|
177
|
+
test that mock-patches `loop.time()`, which only steers loops whose timer
|
|
178
|
+
arithmetic reads Python-level time each turn (real uvloop fails it too).
|
|
179
|
+
|
|
180
|
+
Status on 2026-08-28: all green; 25 oracle tests, the first three suites in
|
|
181
|
+
~17s under xdist and aiohttp's ~4,200 tests in a further ~2 minutes.
|
|
182
|
+
|
|
183
|
+
## The soak gate
|
|
184
|
+
|
|
185
|
+
`pytest -m soak -s` runs a kernel-shaped workload on one loopmini loop for
|
|
186
|
+
`LOOPMINI_SOAK_SECONDS` (default 30): a fasthtml app on uvicorn under
|
|
187
|
+
threaded httpx load, a websocket echo server with a bot-style client, a
|
|
188
|
+
housekeeping tick, and periodic cells submitted from another thread, as a
|
|
189
|
+
kernel would. It asserts zero errors, bounded fd growth, and that every
|
|
190
|
+
component made progress. It watches stability, not speed.
|
|
191
|
+
|
|
192
|
+
## Benchmarks
|
|
193
|
+
|
|
194
|
+
`pytest -m bench -s` prints loopmini vs the standard loop on loop-bound
|
|
195
|
+
microbenchmarks; informational, never gating. Numbers on 2026-08-28 (M-series
|
|
196
|
+
macOS): call_soon 1.10x, sleep0 1.12x, tcp_echo 1.10x, task spawn 2.48x
|
|
197
|
+
(stdlib=1.0). The spawn gap is per-task PyO3 boundary crossings (two to three
|
|
198
|
+
`schedule` calls per task lifecycle at ~0.5µs each); loop startup is at parity.
|
|
199
|
+
Closing it would need handles represented Rust-side, which is later-rung work.
|
|
200
|
+
|
|
201
|
+
## Style and releases
|
|
202
|
+
|
|
203
|
+
fastai style (`chkstyle` before committing). Releases via fastship; the tree
|
|
204
|
+
carries the next version.
|