httpunk 0.4.2__tar.gz → 0.4.4__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.
- {httpunk-0.4.2 → httpunk-0.4.4}/Cargo.lock +42 -1
- {httpunk-0.4.2 → httpunk-0.4.4}/Cargo.toml +4 -7
- {httpunk-0.4.2 → httpunk-0.4.4}/PKG-INFO +2 -2
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/_backend/asyncio.py +13 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/_backend/tonio.py +94 -114
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/_common.py +0 -10
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h1/client.py +4 -1
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h1/server.py +8 -7
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h2/connection.py +11 -10
- {httpunk-0.4.2 → httpunk-0.4.4}/pyproject.toml +1 -1
- {httpunk-0.4.2 → httpunk-0.4.4}/src/lib.rs +4 -0
- httpunk-0.4.4/tests/_transport.py +24 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_asyncio_backend.py +40 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_asyncio_e2e.py +4 -3
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_asyncio_protocol.py +2 -1
- httpunk-0.4.4/tests/test_backend.py +430 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h1_server.py +2 -3
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_errors.py +10 -9
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_util_auto.py +2 -3
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_util_graceful.py +2 -3
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_util_tls.py +42 -0
- httpunk-0.4.2/tests/_transport.py +0 -24
- httpunk-0.4.2/tests/test_backend.py +0 -270
- {httpunk-0.4.2 → httpunk-0.4.4}/LICENSE +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/README.md +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/Cargo.toml +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/LICENSE +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/codec/error.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/codec/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/error.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/ext.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/data.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/go_away.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/head.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/headers.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/ping.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/priority.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/reason.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/reset.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/settings.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/util.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/frame/window_update.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/hpack/header.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/hpack/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/hpack/table.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/lib.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/proto/error.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/proto/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/Cargo.toml +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/LICENSE +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/body/length.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/body/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/cfg.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/common/date.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/common/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/error.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/ext/informational.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/ext/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/headers.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/lib.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/proto/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper/src/trace.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper-util/Cargo.toml +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper-util/LICENSE +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/crates/vendor-hyper-util/src/lib.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/__init__.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/_backend/__init__.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/_httpunk.pyi +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/asyncio.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/exceptions.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h1/__init__.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h1/connection.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h1/share.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h2/__init__.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h2/client.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h2/server.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h2/share.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/h2/stream.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/http.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/py.typed +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/types.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/util/__init__.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/util/auto.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/util/client.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/util/graceful.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/util/pool.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/httpunk/util/proxy.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/errors.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h1/codec.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h1/conn.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h1/errors.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h1/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h2/codec.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h2/conn.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h2/errors.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h2/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h2/reason.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h2/settings.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/h2/streams.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/http/mod.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/proxy.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/src/py.rs +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/_client.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/conftest.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_asyncio_client_protocol.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_error_retention.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h1_codec.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h1_conn.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h1_decode.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_codec.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_conn.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_flow.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_flow_control.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_ready.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_server.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_settings.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_state.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_h2_write_batch.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_headermap.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_util_connect.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_util_pool.py +0 -0
- {httpunk-0.4.2 → httpunk-0.4.4}/tests/test_util_proxy.py +0 -0
|
@@ -20,12 +20,28 @@ version = "1.12.1"
|
|
|
20
20
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
21
21
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|
22
22
|
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "cc"
|
|
25
|
+
version = "1.4.7"
|
|
26
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
27
|
+
checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
|
|
28
|
+
dependencies = [
|
|
29
|
+
"find-msvc-tools",
|
|
30
|
+
"shlex",
|
|
31
|
+
]
|
|
32
|
+
|
|
23
33
|
[[package]]
|
|
24
34
|
name = "equivalent"
|
|
25
35
|
version = "1.0.2"
|
|
26
36
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
27
37
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
28
38
|
|
|
39
|
+
[[package]]
|
|
40
|
+
name = "find-msvc-tools"
|
|
41
|
+
version = "0.1.13"
|
|
42
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
43
|
+
checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
|
|
44
|
+
|
|
29
45
|
[[package]]
|
|
30
46
|
name = "fnv"
|
|
31
47
|
version = "1.0.7"
|
|
@@ -118,12 +134,13 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|
|
118
134
|
|
|
119
135
|
[[package]]
|
|
120
136
|
name = "httpunk"
|
|
121
|
-
version = "0.4.
|
|
137
|
+
version = "0.4.4"
|
|
122
138
|
dependencies = [
|
|
123
139
|
"bytes",
|
|
124
140
|
"h2",
|
|
125
141
|
"http",
|
|
126
142
|
"hyper",
|
|
143
|
+
"mimalloc",
|
|
127
144
|
"pyo3",
|
|
128
145
|
"vendor-h2",
|
|
129
146
|
"vendor-hyper",
|
|
@@ -179,6 +196,24 @@ version = "0.2.189"
|
|
|
179
196
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
180
197
|
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|
181
198
|
|
|
199
|
+
[[package]]
|
|
200
|
+
name = "libmimalloc-sys"
|
|
201
|
+
version = "0.1.49"
|
|
202
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
203
|
+
checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
|
|
204
|
+
dependencies = [
|
|
205
|
+
"cc",
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
[[package]]
|
|
209
|
+
name = "mimalloc"
|
|
210
|
+
version = "0.1.52"
|
|
211
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
212
|
+
checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
|
|
213
|
+
dependencies = [
|
|
214
|
+
"libmimalloc-sys",
|
|
215
|
+
]
|
|
216
|
+
|
|
182
217
|
[[package]]
|
|
183
218
|
name = "once_cell"
|
|
184
219
|
version = "1.21.4"
|
|
@@ -278,6 +313,12 @@ dependencies = [
|
|
|
278
313
|
"proc-macro2",
|
|
279
314
|
]
|
|
280
315
|
|
|
316
|
+
[[package]]
|
|
317
|
+
name = "shlex"
|
|
318
|
+
version = "2.0.1"
|
|
319
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
320
|
+
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
321
|
+
|
|
281
322
|
[[package]]
|
|
282
323
|
name = "slab"
|
|
283
324
|
version = "0.4.12"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "httpunk"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.4"
|
|
4
4
|
description = "The Rust HTTP library for Python"
|
|
5
5
|
authors = [
|
|
6
6
|
"Giovanni Barillari <g@baro.dev>",
|
|
@@ -39,22 +39,19 @@ name = "_httpunk"
|
|
|
39
39
|
crate-type = ["cdylib"]
|
|
40
40
|
|
|
41
41
|
[features]
|
|
42
|
+
mimalloc = ["dep:mimalloc"]
|
|
42
43
|
# Never enabled. Exists only so the optional crates.io `h2`/`hyper` deps are
|
|
43
44
|
# referenced (no "unused optional dependency"), keeping them in Cargo.lock for
|
|
44
45
|
# advisory scanning while they stay uncompiled. See the dependency comments.
|
|
45
46
|
_audit = ["dep:h2", "dep:hyper"]
|
|
46
47
|
|
|
47
48
|
[dependencies]
|
|
48
|
-
pyo3 = { version = "=0.29", features = ["extension-module", "generate-import-lib"] }
|
|
49
|
-
# Used by the PyO3 layer (src/py): `http::HeaderMap` (the HeaderMap wrapper) and
|
|
50
|
-
# `bytes` buffers in the codecs.
|
|
51
49
|
bytes = "1"
|
|
52
50
|
http = "1.1"
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
mimalloc = { version = "0.1.49", default-features = false, features = ["local_dynamic_tls"], optional = true }
|
|
52
|
+
pyo3 = { version = "=0.29", features = ["extension-module", "generate-import-lib"] }
|
|
55
53
|
vendor-h2 = { path = "crates/vendor-h2" }
|
|
56
54
|
vendor-hyper = { path = "crates/vendor-hyper", features = ["client", "server", "http1"] }
|
|
57
|
-
# Vendored hyper-util proxy matcher (pure sans-IO selection logic; `from_system` off).
|
|
58
55
|
vendor-hyper-util = { path = "crates/vendor-hyper-util" }
|
|
59
56
|
# NOT compiled: the crates.io `h2`/`hyper` — vendored into crates/vendor-*/ (see
|
|
60
57
|
# THIRD-PARTY.md). Optional + gated behind the never-enabled `_audit` feature so
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpunk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
|
20
20
|
Classifier: Programming Language :: Python
|
|
21
21
|
Classifier: Programming Language :: Rust
|
|
22
22
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
-
Requires-Dist: tonio~=0.
|
|
23
|
+
Requires-Dist: tonio~=0.10.0 ; python_full_version >= '3.14' and extra == 'tonio'
|
|
24
24
|
Provides-Extra: tonio
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Summary: The Rust HTTP library for Python
|
|
@@ -445,6 +445,19 @@ class AsyncioBackend:
|
|
|
445
445
|
await asyncio.gather(*losers, return_exceptions=True)
|
|
446
446
|
return winner.result()
|
|
447
447
|
|
|
448
|
+
async def select_events(self, *events):
|
|
449
|
+
"""Resume once ANY of `events` is set (see the tonio twin): one `asyncio.wait`
|
|
450
|
+
over the events' `wait()` futures, the losers cancelled and drained on the way
|
|
451
|
+
out — also when this wait is itself cancelled. No verdict: the caller reads the
|
|
452
|
+
events' flags."""
|
|
453
|
+
waits = [asyncio.ensure_future(e.wait()) for e in events]
|
|
454
|
+
try:
|
|
455
|
+
await asyncio.wait(waits, return_when=asyncio.FIRST_COMPLETED)
|
|
456
|
+
finally:
|
|
457
|
+
for w in waits:
|
|
458
|
+
w.cancel()
|
|
459
|
+
await asyncio.gather(*waits, return_exceptions=True)
|
|
460
|
+
|
|
448
461
|
def queue(self):
|
|
449
462
|
"""An unbounded queue as `(sender, receiver)`: `sender.send(x)` is sync;
|
|
450
463
|
`await receiver.receive()` yields items in order."""
|
|
@@ -7,8 +7,8 @@ across coroutines — so a future asyncio/trio backend is a drop-in replacement.
|
|
|
7
7
|
|
|
8
8
|
import ssl as _ssl
|
|
9
9
|
|
|
10
|
-
from tonio import colored as _colored
|
|
11
|
-
from tonio.
|
|
10
|
+
from tonio import Waiter as _Waiter, colored as _colored
|
|
11
|
+
from tonio._tonio import get_runtime as _get_runtime # private: the runtime's µs clock (`_clock`), see `bounded_reader`
|
|
12
12
|
from tonio.colored.net import SocketStream as _SocketStream, open_tcp_stream as _open_tcp_stream
|
|
13
13
|
from tonio.colored.net.tls import TLSStream as _TLSStream, open_tls_over_tcp_stream as _open_tls_over_tcp_stream
|
|
14
14
|
from tonio.colored.sync import Lock as _Lock, Semaphore as _Semaphore
|
|
@@ -100,140 +100,114 @@ class TonioBackend:
|
|
|
100
100
|
return tls, tls._ssl.selected_alpn_protocol()
|
|
101
101
|
|
|
102
102
|
def receive_nowait(self, transport, max_bytes=65536):
|
|
103
|
-
"""A synchronous, non-blocking read: whatever bytes are immediately
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
so unread ciphertext on the socket reads as "nothing ready"; the drain
|
|
123
|
-
then closes rather than reuses, which matches hyper's cheap-drain-or-close.
|
|
124
|
-
EOF is never reported for TLS either — it is only knowable by decrypting.)
|
|
125
|
-
- A peek beside a parked plain-socket reader is safe: both are one `recv`
|
|
126
|
-
syscall on the same non-blocking socket, the bytes go to exactly one of
|
|
127
|
-
them, and the loser's `EAGAIN` sends it back to waiting (tonio `recv`)."""
|
|
128
|
-
if isinstance(transport, _TLSStream): # peek only already-decrypted plaintext
|
|
129
|
-
ssl_obj = transport._ssl
|
|
130
|
-
with ssl_obj._lock: # tonio `_SSLProxy._lock`: the SSLObject's one lock
|
|
131
|
-
pending = ssl_obj._inner.pending()
|
|
132
|
-
return ssl_obj._inner.read(min(max_bytes, pending)) if pending else None
|
|
133
|
-
try:
|
|
134
|
-
return transport.socket._sock.recv(max_bytes) # b"" only at EOF
|
|
135
|
-
except (BlockingIOError, InterruptedError):
|
|
136
|
-
return None
|
|
103
|
+
"""A synchronous, non-blocking read: whatever bytes are immediately available
|
|
104
|
+
without suspending, `b""` at EOF, or `None` if nothing is ready right now. The
|
|
105
|
+
readiness primitive hyper's server drain (`poll_read_body` inside
|
|
106
|
+
`poll_drain_or_close_read`) relies on. EOF and not-ready are distinct.
|
|
107
|
+
|
|
108
|
+
tonio's own no-wait read on both streams (`receive_some_nowait`: `NotReady` is
|
|
109
|
+
this seam's `None`). On a plain socket it is one `recv` (EAGAIN clears the
|
|
110
|
+
readiness bits, as tonio's `recv` does). Over TLS it decrypts what the socket
|
|
111
|
+
already holds — feeds the ingress BIO without suspending — the way hyper's read
|
|
112
|
+
over rustls does, so the server's drain finds a small unread body instead of
|
|
113
|
+
closing; a close_notify already in the socket is `b""`, an abrupt close raises
|
|
114
|
+
`ResourceBroken` (a `broken_transport_errors` shape, like the plain socket's
|
|
115
|
+
`ConnectionResetError`). Beside a parked reader: on a plain socket both are one
|
|
116
|
+
`recv` on the same non-blocking socket and the bytes go to exactly one of them;
|
|
117
|
+
over TLS the parked reader holds the receive lock, so the peek answers `None`
|
|
118
|
+
whatever the socket holds — hyper's `require_empty_read` looks at its `read_buf`
|
|
119
|
+
the same way, and ciphertext not yet decrypted is invisible to it too."""
|
|
120
|
+
data = transport.receive_some_nowait(max_bytes)
|
|
121
|
+
return None if data is transport.NotReady else data
|
|
137
122
|
|
|
138
123
|
def bounded_reader(self, transport):
|
|
139
124
|
"""Chosen ONCE per connection: how `transport`'s reads are bounded by a deadline —
|
|
140
125
|
the h1 server's head-read deadline (hyper `header_read_timeout`) riding the read
|
|
141
126
|
itself, so no task is spawned per head and no parked read is ever cancelled.
|
|
142
127
|
Returns `async read(max_bytes, deadline) -> bytes | b"" (EOF) | None (expired)`;
|
|
143
|
-
`deadline` is an instant on this backend's `monotonic` clock
|
|
144
|
-
is fixed for the connection's life, so the dispatch
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
`deadline` is an instant on this backend's `monotonic` clock (seconds, the seam's
|
|
129
|
+
unit). The transport's kind is fixed for the connection's life, so the dispatch
|
|
130
|
+
happens here, never per read, over bound methods resolved here.
|
|
131
|
+
|
|
132
|
+
Both readers are tonio's own `wait_readable(timeout)` loop inlined over the
|
|
133
|
+
stream's no-wait read and its readiness waiter — the same primitives, without the
|
|
134
|
+
coroutine frame per park and without the seconds -> µs -> seconds round trip: the
|
|
135
|
+
deadline is converted once per read, and each park compares it with the runtime's
|
|
136
|
+
µs clock (`_clock`, read off the runtime cached at bind time rather than through
|
|
137
|
+
`get_runtime()` on every pass; private API — tonio and httpunk share an author).
|
|
147
138
|
|
|
148
139
|
Bytes first, hyper's order (`poll_read_head`: `parse` -> `poll_read`, and the timer
|
|
149
|
-
is polled only once that returned Pending): every pass tries the
|
|
150
|
-
looks at the clock,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
- **
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
140
|
+
is polled only once that returned Pending): every pass tries the read before it
|
|
141
|
+
looks at the clock, so a wake that finds bytes returns them, whatever the timer
|
|
142
|
+
did. The readiness bits are stale-set after any successful read (nothing but a
|
|
143
|
+
failed syscall clears them), exactly as tokio's readiness word is when hyper's
|
|
144
|
+
`poll_read` runs, so both pay one `EAGAIN` probe per head; only on `NotReady`
|
|
145
|
+
(which cleared the bits) is the remaining time computed. Past the deadline that is
|
|
146
|
+
the answer at once — hyper's Pending followed by a ready timer poll
|
|
147
|
+
(`new_header_timeout`) — and no zero-length timer is ever armed. Otherwise the
|
|
148
|
+
waiter puts the readiness wait and the timer on ONE suspension, and after the wake
|
|
149
|
+
the readiness question is asked BEFORE the next read: `None` = the bits are set
|
|
150
|
+
(read now), a waiter = none are, so the timer fired (a stale reader slot, overwritten
|
|
151
|
+
by the next arm, harmless by tonio's contract). That question is not optional: the
|
|
152
|
+
socket's `_io_clear_r` is tick-guarded — it clears the bits only if no readiness
|
|
153
|
+
edge landed since an arm last SAW them set — so a wake followed straight by a read
|
|
154
|
+
leaves the edge unobserved, the next head's `EAGAIN` clear is ignored, and that head
|
|
155
|
+
pays a second syscall (measured: 3 reads and 2 arms per request instead of 2 and 1).
|
|
156
|
+
Every arm is computed from the absolute deadline, so a wake that finds nothing to
|
|
157
|
+
read re-arms for what is left.
|
|
158
|
+
|
|
159
|
+
- **Plain socket**: `waiter_readable(µs)` is the socket's own arm: `None` when the
|
|
160
|
+
bits landed between the probe and the arm (read now), else the waiter; bare, it
|
|
161
|
+
is the readiness question.
|
|
162
|
+
- **TLS (`TLSStream`)**: `watch_readable()` holds the receive lock across the arm
|
|
163
|
+
and the park (as tonio's `wait_readable` does); its waiter is `None` when
|
|
164
|
+
plaintext is already decoded, the raw socket's arm otherwise, and the lock's own
|
|
165
|
+
hand-off when a parked reader holds it; `ready()` is the question (plaintext
|
|
166
|
+
pending, or the socket's bare arm). A "not ready" wake loops back to the clock
|
|
167
|
+
rather than returning: after a lock hand-off nothing was armed, so it is not a
|
|
168
|
+
verdict there (that hand-off never happens in httpunk — one reader per connection
|
|
169
|
+
— but the loop stays exact). `receive_some_nowait` decrypts.
|
|
172
170
|
A transport is one of tonio's two streams — a `TLSStream` or a `SocketStream` —
|
|
173
171
|
and nothing else; anything else fails here, at setup, not on a read."""
|
|
172
|
+
runtime = _get_runtime() # one runtime per process, bound once per connection
|
|
174
173
|
if isinstance(transport, _TLSStream):
|
|
175
|
-
return self._tls_bounded_reader(transport)
|
|
176
|
-
return self._socket_bounded_reader(transport
|
|
174
|
+
return self._tls_bounded_reader(transport, runtime)
|
|
175
|
+
return self._socket_bounded_reader(transport, runtime)
|
|
177
176
|
|
|
178
177
|
@staticmethod
|
|
179
|
-
def _socket_bounded_reader(
|
|
180
|
-
|
|
178
|
+
def _socket_bounded_reader(stream, runtime):
|
|
179
|
+
nowait, arm, not_ready = stream.receive_some_nowait, stream.waiter_readable, stream.NotReady
|
|
181
180
|
|
|
182
181
|
async def read(max_bytes, deadline):
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
continue
|
|
188
|
-
except BlockingIOError:
|
|
189
|
-
clear() # EAGAIN: the bits were stale (or a spurious wake) — now the timer
|
|
190
|
-
micros = round((deadline - _now()) * 1_000_000)
|
|
191
|
-
if micros <= 0:
|
|
182
|
+
deadline_micros = int(deadline * 1_000_000) # the seam's seconds -> the runtime clock's µs, once per read
|
|
183
|
+
while (data := nowait(max_bytes)) is not_ready: # bytes first; EAGAIN clears the bits
|
|
184
|
+
remaining = deadline_micros - runtime._clock
|
|
185
|
+
if remaining <= 0:
|
|
192
186
|
return None # nothing readable and the deadline passed: hyper's timer poll is ready
|
|
193
|
-
waiter
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return None
|
|
187
|
+
if (waiter := arm(remaining)) is not None: # None: bits landed between the probe and the arm
|
|
188
|
+
await waiter
|
|
189
|
+
if arm() is not None: # the readiness question: no bits = the timer woke us
|
|
190
|
+
return None
|
|
191
|
+
return data
|
|
199
192
|
|
|
200
193
|
return read
|
|
201
194
|
|
|
202
195
|
@staticmethod
|
|
203
|
-
def _tls_bounded_reader(stream):
|
|
204
|
-
|
|
205
|
-
raw_read = TonioBackend._socket_bounded_reader(stream.transport.socket)
|
|
196
|
+
def _tls_bounded_reader(stream, runtime):
|
|
197
|
+
nowait, watch, not_ready = stream.receive_some_nowait, stream.watch_readable, stream.NotReady
|
|
206
198
|
|
|
207
199
|
async def read(max_bytes, deadline):
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
recv_count = stream._recv_count
|
|
220
|
-
async with stream._lock_recv:
|
|
221
|
-
if recv_count == stream._recv_count: # nobody fed the BIO meanwhile
|
|
222
|
-
data = await raw_read(65536, deadline)
|
|
223
|
-
if data is None:
|
|
224
|
-
return None
|
|
225
|
-
if not data:
|
|
226
|
-
ssl_obj._ingress_write_eof()
|
|
227
|
-
else:
|
|
228
|
-
stream._recv_est_size = max(stream._recv_est_size, len(data))
|
|
229
|
-
ssl_obj._ingress_write(data)
|
|
230
|
-
stream._recv_count += 1
|
|
231
|
-
if not want_read:
|
|
232
|
-
return ret
|
|
233
|
-
except ResourceBroken as exc:
|
|
234
|
-
if stream._compat_https and _is_eof(exc.__cause__):
|
|
235
|
-
return b""
|
|
236
|
-
raise
|
|
200
|
+
deadline_micros = int(deadline * 1_000_000)
|
|
201
|
+
while (data := nowait(max_bytes)) is not_ready:
|
|
202
|
+
remaining = deadline_micros - runtime._clock
|
|
203
|
+
if remaining <= 0:
|
|
204
|
+
return None
|
|
205
|
+
with watch() as watcher: # the receive lock is held across the park, as tonio's own wait
|
|
206
|
+
if (waiter := watcher.waiter(remaining)) is not None:
|
|
207
|
+
await waiter
|
|
208
|
+
if not watcher.ready(): # the readiness question: nothing readable, back to the clock
|
|
209
|
+
continue
|
|
210
|
+
return data
|
|
237
211
|
|
|
238
212
|
return read
|
|
239
213
|
|
|
@@ -294,6 +268,12 @@ class TonioBackend:
|
|
|
294
268
|
spawn_without_results = staticmethod(_colored.spawn.without_results)
|
|
295
269
|
|
|
296
270
|
select = staticmethod(_colored.select)
|
|
271
|
+
# `await select_events(*events)`: resume once ANY of the events is set — ONE suspension
|
|
272
|
+
# of the calling task (tonio's merged waiter), no wrapper tasks, no scope, nothing to
|
|
273
|
+
# cancel; returns no verdict, the caller reads the events' flags. The race of "pump
|
|
274
|
+
# done" against "peer gone" in `_send_async_body` (h1 and h2): hyper's
|
|
275
|
+
# `PipeToSendStream` polling `poll_reset` beside the body future, in one task.
|
|
276
|
+
select_events = staticmethod(_Waiter.any)
|
|
297
277
|
scope = staticmethod(_colored.scope)
|
|
298
278
|
lock = _Lock
|
|
299
279
|
event = _colored.Event
|
|
@@ -44,16 +44,6 @@ async def read_all(aiter):
|
|
|
44
44
|
return b"".join([chunk async for chunk in aiter])
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
PUMP_DONE = object() # sentinel: the async-body pump finished (`event_result`)
|
|
48
|
-
PUMP_ABANDONED = object() # sentinel: the peer abandoned the exchange first (`event_result`)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
async def event_result(evt, value):
|
|
52
|
-
"""Await `evt`, then return `value` — a labelled racer for a one-shot `select`."""
|
|
53
|
-
await evt.wait()
|
|
54
|
-
return value
|
|
55
|
-
|
|
56
|
-
|
|
57
47
|
async def aclose_body(body):
|
|
58
48
|
"""Deterministically close an async body once its pump is done with it — tonio
|
|
59
49
|
finalizes nothing, so an async generator left unclosed never runs its cleanup."""
|
|
@@ -276,7 +276,10 @@ class Connection(H1Framing, H1ClientState):
|
|
|
276
276
|
# gets bytes delivered before the request treats them as the violation they
|
|
277
277
|
# are — the watcher through the idle rules, since the exchange is not yet
|
|
278
278
|
# active — so the outcome is the same either way; a plain-socket `recv` is
|
|
279
|
-
# one syscall.
|
|
279
|
+
# one syscall. Over TLS the parked watcher holds the receive lock, so the peek
|
|
280
|
+
# sees nothing while it is parked: bytes still ciphertext in the socket reach
|
|
281
|
+
# the watcher's read instead — hyper's `read_buf` over rustls holds decrypted
|
|
282
|
+
# bytes only, and its next `poll_read_head` parses the rest as the response too.)
|
|
280
283
|
transport = self.transport_ref()
|
|
281
284
|
pending = self.backend.receive_nowait(transport, _READ_SIZE) if transport is not None else b""
|
|
282
285
|
if pending:
|
|
@@ -38,7 +38,7 @@ from collections.abc import AsyncIterator, Awaitable
|
|
|
38
38
|
from typing import TYPE_CHECKING, Any
|
|
39
39
|
|
|
40
40
|
from .. import _backend
|
|
41
|
-
from .._common import
|
|
41
|
+
from .._common import BaseServer, aclose_body, read_all
|
|
42
42
|
from .._httpunk import (
|
|
43
43
|
H1_NEXT_CLOSE,
|
|
44
44
|
H1_NEXT_DRAIN,
|
|
@@ -623,25 +623,26 @@ class ServerConnection(H1Framing, H1ServerState):
|
|
|
623
623
|
connection (so `mid_message_detect_eof`) while the body's next chunk is pending,
|
|
624
624
|
so a parked producer (SSE, long poll) does not hold a dead exchange. Same shape as
|
|
625
625
|
the h2 driver's `_send_async_body`: ONE pump task per response; this caller waits
|
|
626
|
-
|
|
626
|
+
ONCE, one suspension on both events (`select_events`: hyper's `PipeToSendStream`
|
|
627
|
+
polls `poll_reset` beside the body future in one task — no racer tasks, no scope
|
|
628
|
+
to cancel; a `select` here measured 5 µs and a 100 ms p99 tail per response), and
|
|
629
|
+
the flags afterwards are the verdict. On the FIN the pump is CANCELLED (hyper
|
|
627
630
|
drops the body future) at its suspension — the app's `await`, or, h1 having no
|
|
628
631
|
write pump, possibly inside a chunk write: harmless, this connection is closed
|
|
629
632
|
right below and a truncated body is what the client gets regardless (hyper:
|
|
630
633
|
`IncompleteMessage`, io dropped). Also cancelled if this task is cancelled or
|
|
631
634
|
unwinds: neither backend cancels scope children on a body exception, and joining
|
|
632
|
-
a parked pump would hang. (A per-chunk
|
|
635
|
+
a parked pump would hang. (A per-chunk wait measured 4-5x slower.)"""
|
|
633
636
|
await self._arm_watcher(req) # the FIN is consumable now: make sure a read is parked
|
|
634
637
|
done, box = self.backend.event(), []
|
|
635
638
|
abandoned = False
|
|
636
639
|
async with self.backend.scope() as scope:
|
|
637
640
|
scope.spawn(self._pump_async_body(body, trailers, done, box))
|
|
638
641
|
try:
|
|
639
|
-
|
|
640
|
-
event_result(done, PUMP_DONE), event_result(req._peer_closed_evt, PUMP_ABANDONED)
|
|
641
|
-
)
|
|
642
|
+
await self.backend.select_events(done, req._peer_closed_evt)
|
|
642
643
|
# The event also marks the window's close (`_close_window`), which cannot
|
|
643
644
|
# precede this response's own completion — the state's flag is the verdict.
|
|
644
|
-
abandoned =
|
|
645
|
+
abandoned = not done.is_set() and self.peer_closed_flag(req._seq)
|
|
645
646
|
finally:
|
|
646
647
|
if not done.is_set():
|
|
647
648
|
scope.cancel() # leave with the pump gone, whatever ended the wait
|
|
@@ -25,7 +25,7 @@ crates/vendor-h2), paths relative to its `src/`.
|
|
|
25
25
|
import contextlib
|
|
26
26
|
|
|
27
27
|
from .. import _backend
|
|
28
|
-
from .._common import
|
|
28
|
+
from .._common import aclose_body, aiter_body
|
|
29
29
|
from .._httpunk import (
|
|
30
30
|
H2_FLAG_CAPACITY,
|
|
31
31
|
H2_FLAG_CONN_DONE,
|
|
@@ -433,21 +433,22 @@ class H2ConnectionBase(H2Streams):
|
|
|
433
433
|
async def _send_async_body(self, st, body, trailers):
|
|
434
434
|
"""Stream an ASYNC body and fail fast on a peer reset — hyper's `PipeToSendStream`
|
|
435
435
|
(proto/h2/mod.rs), which polls `poll_reset` while it waits for the body's next
|
|
436
|
-
chunk. ONE pump task per response queues each chunk; this caller
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
436
|
+
chunk — in ONE task. ONE pump task per response queues each chunk; this caller
|
|
437
|
+
waits ONCE, one suspension on both events (`select_events`: no racer tasks, no
|
|
438
|
+
scope to cancel), for "pump done" or "stream reset", and the flags afterwards
|
|
439
|
+
are the verdict. On a reset the pump is CANCELLED (hyper dropping the body
|
|
440
|
+
future) at the app's own `await` or the flow-control wait — DATA is queued for
|
|
441
|
+
the write pump, never written by this task, so no write is interrupted. The
|
|
442
|
+
scope exit does not wait for a CANCELLED child to unwind, so wait for the pump's
|
|
443
|
+
own `done` (set in its `finally`, after the producer's cleanup ran)."""
|
|
443
444
|
backend = self.backend
|
|
444
445
|
done, box = backend.event(), []
|
|
445
446
|
abandoned = False
|
|
446
447
|
async with backend.scope() as scope:
|
|
447
448
|
scope.spawn(self._pump_async_body(st, body, trailers, done, box))
|
|
448
449
|
try:
|
|
449
|
-
|
|
450
|
-
abandoned =
|
|
450
|
+
await backend.select_events(done, st.reset_evt)
|
|
451
|
+
abandoned = not done.is_set() # the wait ended, the pump did not: the reset did
|
|
451
452
|
finally:
|
|
452
453
|
if not done.is_set():
|
|
453
454
|
scope.cancel() # leave with the pump gone, whatever ended the wait
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Test-side stand-in for the piece of tonio's `SocketStream` the backend's bounded
|
|
2
|
+
reader binds at connection setup (`TonioBackend.bounded_reader`): the no-wait read
|
|
3
|
+
`receive_some_nowait(n)` -> bytes | b"" | `NotReady`, and `waiter_readable(micros)` ->
|
|
4
|
+
None (readable now) or a waiter to park on. A stub transport mixes this in and plugs its
|
|
5
|
+
buffer in through three sync hooks: `_readable()`, `_recv_now(n)` (raises
|
|
6
|
+
`BlockingIOError` when nothing is there), and `_park(timeout_micros)` -> an awaitable
|
|
7
|
+
that resumes when bytes/EOF may have landed or the timer fired (never called by a stub
|
|
8
|
+
that is always readable)."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StubStream:
|
|
12
|
+
class NotReady: # the stream's own sentinel, compared by identity (`transport.NotReady`)
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def receive_some_nowait(self, max_bytes=65536):
|
|
16
|
+
try:
|
|
17
|
+
return self._recv_now(max_bytes)
|
|
18
|
+
except BlockingIOError:
|
|
19
|
+
return self.NotReady
|
|
20
|
+
|
|
21
|
+
def waiter_readable(self, timeout=None):
|
|
22
|
+
if self._readable():
|
|
23
|
+
return None
|
|
24
|
+
return self._park(timeout)
|
|
@@ -246,6 +246,46 @@ async def test_select_cancels_racers_when_itself_cancelled():
|
|
|
246
246
|
assert set(cancelled) == {"x", "y"}
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
@pytest.mark.asyncio
|
|
250
|
+
async def test_select_events_resumes_on_either_event_without_a_verdict():
|
|
251
|
+
# One wait on both events (the `_send_async_body` race): resumes once ANY is set,
|
|
252
|
+
# at once when one already is, returns nothing — the flags are the answer.
|
|
253
|
+
backend = AsyncioBackend()
|
|
254
|
+
a, b = backend.event(), backend.event()
|
|
255
|
+
a.set()
|
|
256
|
+
assert await backend.select_events(a, b) is None
|
|
257
|
+
a, b = backend.event(), backend.event()
|
|
258
|
+
loop = asyncio.get_running_loop()
|
|
259
|
+
loop.call_soon(b.set)
|
|
260
|
+
await backend.select_events(a, b)
|
|
261
|
+
assert not a.is_set() and b.is_set()
|
|
262
|
+
b.clear()
|
|
263
|
+
loop.call_soon(b.set) # the events are reusable after a clear
|
|
264
|
+
await backend.select_events(a, b)
|
|
265
|
+
assert b.is_set()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@pytest.mark.asyncio
|
|
269
|
+
async def test_select_events_leaves_no_waiter_behind():
|
|
270
|
+
# The loser's wait is cancelled and drained: no task is left parked on the event, and
|
|
271
|
+
# cancelling the wait itself drains both.
|
|
272
|
+
backend = AsyncioBackend()
|
|
273
|
+
a, b = backend.event(), backend.event()
|
|
274
|
+
before = len(asyncio.all_tasks())
|
|
275
|
+
asyncio.get_running_loop().call_soon(a.set)
|
|
276
|
+
await backend.select_events(a, b)
|
|
277
|
+
assert len(asyncio.all_tasks()) == before # the loser's wait task is gone
|
|
278
|
+
a, b = backend.event(), backend.event()
|
|
279
|
+
sel = asyncio.ensure_future(backend.select_events(a, b))
|
|
280
|
+
for _ in range(3):
|
|
281
|
+
await asyncio.sleep(0) # both waits parked
|
|
282
|
+
sel.cancel()
|
|
283
|
+
with pytest.raises(asyncio.CancelledError):
|
|
284
|
+
await sel
|
|
285
|
+
await asyncio.sleep(0)
|
|
286
|
+
assert len(asyncio.all_tasks()) == before
|
|
287
|
+
|
|
288
|
+
|
|
249
289
|
@pytest.mark.asyncio
|
|
250
290
|
async def test_scope_spawn_cancel_and_join():
|
|
251
291
|
backend = AsyncioBackend()
|