websocket-rs 0.7.2__tar.gz → 0.7.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.
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/.github/workflows/test.yml +4 -1
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/CHANGELOG.md +32 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/Cargo.lock +1 -1
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/Cargo.toml +1 -1
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/PKG-INFO +1 -1
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/README.md +22 -7
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/pyproject.toml +1 -1
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/src/native_client.rs +90 -53
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/src/sync_client.rs +241 -52
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/tests/diagnose_server_pattern.py +1 -3
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/tests/test_compatibility.py +70 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/tests/test_native_features.py +1 -1
- websocket_rs-0.7.4/tests/test_timeout_and_errors.py +477 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/websocket_rs/__init__.py +4 -1
- websocket_rs-0.7.2/tests/test_timeout_and_errors.py +0 -235
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/.github/pull_request_template.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/.github/workflows/pr.yml +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/.github/workflows/release.yml +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/.gitignore +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/LICENSE +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/Makefile +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/README.pypi.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/README.zh-TW.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/docs/API.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/docs/BENCHMARKS.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/docs/BENCHMARKS.zh-TW.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/docs/CONTRIBUTING.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/docs/MIGRATION.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/docs/OPTIMIZATION_RESEARCH.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/docs/PUBLISHING.md +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/monitor_and_merge.sh +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/pytest.ini +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/scripts/build-pgo.sh +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/scripts/pre-check.sh +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/scripts/prepare-release.sh +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/scripts/tag-release.sh +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/scripts/test.sh +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/src/async_client.rs +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/src/bin/ws_echo_fastws.rs +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/src/bin/ws_echo_server.rs +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/src/bin/ws_echo_server_tls.rs +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/src/lib.rs +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/tests/bench_socks5_handshake.py +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/tests/benchmark_picows_parity.py +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/tests/benchmark_three_servers.py +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/tests/benchmark_tls_parity.py +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/tests/perf_pipelined_64k.py +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/uv.lock +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/websocket_rs/__init__.pyi +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/websocket_rs/async_client.pyi +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/websocket_rs/native_client.pyi +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/websocket_rs/py.typed +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/websocket_rs/sync/__init__.pyi +0 -0
- {websocket_rs-0.7.2 → websocket_rs-0.7.4}/websocket_rs/sync/client.pyi +0 -0
|
@@ -55,7 +55,10 @@ jobs:
|
|
|
55
55
|
uv venv --python ${{ matrix.python-version }}
|
|
56
56
|
|
|
57
57
|
- name: Install dependencies with uv
|
|
58
|
-
|
|
58
|
+
# uvloop has no Windows wheels; without it the uvloop half of the
|
|
59
|
+
# peer-close matrix silently drops, and uvloop+wss is a combination
|
|
60
|
+
# that actually regressed once.
|
|
61
|
+
run: uv pip install maturin pytest websockets "uvloop; sys_platform != 'win32'"
|
|
59
62
|
|
|
60
63
|
- name: Build
|
|
61
64
|
run: uv run maturin develop --release
|
|
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.7.4] - 2026-07-29
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`__version__` reported 0.7.1 (#TBD)**: the Python package hard-coded a version literal that was never bumped, so `websocket_rs.__version__` read `0.7.1` through the 0.7.2 and 0.7.3 releases while the installed distribution said otherwise. It now derives from the compiled module's `CARGO_PKG_VERSION`, leaving `Cargo.toml` as the single place a version is written by hand (`pyproject.toml` still mirrors it for the build backend).
|
|
13
|
+
|
|
14
|
+
- **`eof_received` on the native protocol (#TBD)**: `NativeClient` implements asyncio's protocol callbacks by hand — it is a pyclass and inherits nothing from `asyncio.Protocol`, so the base class's default was never available — and `eof_received` was missing. When the peer half-closes, the transport calls it unguarded and the resulting `AttributeError` was raised inside asyncio's own callback: CPython routes it into `_fatal_error` ("Fatal error: protocol.eof_received() call failed."), so it surfaced as a connection error rather than a warning.
|
|
15
|
+
|
|
16
|
+
Affects every combination except plain TCP under uvloop, whose `_on_eof` is the only call site that guards with `try/except AttributeError`. Both TLS stacks (CPython `sslproto._call_eof_received`, uvloop `sslproto.pyx` `_call_eof_received`) call it directly, so `wss://` was affected under both event loops. Client-closes-first tests never reach this path, which is why it went unnoticed.
|
|
17
|
+
|
|
18
|
+
`NativeClientBuffered` inherits the fix. The regression test lives in `tests/test_timeout_and_errors.py` (one of the two files CI actually runs) and covers the loop × transport matrix; CI now installs uvloop everywhere it has wheels, so `uvloop + wss` is exercised there too. After the peer closes, a second `recv()` must fail immediately rather than hang to the timeout, which is what proves `connection_lost` still fails pending receives. The TLS cases mint a self-signed cert into a pytest temp dir.
|
|
19
|
+
|
|
20
|
+
## [0.7.3] - 2026-07-24
|
|
21
|
+
|
|
22
|
+
### Performance
|
|
23
|
+
|
|
24
|
+
- **Sync receive without the intermediate copy (#29)**: tungstenite's owned `Bytes` payload now moves through the synchronous receive path instead of being copied into an intermediate `Vec<u8>`. Measured medians on an idle host: plain 1 MiB +9.1%, TLS 1 MiB +27.9%.
|
|
25
|
+
|
|
26
|
+
- **Sync send borrows exact `bytes` (#30)**: exact CPython `bytes` payloads are sent through an owner-backed `Bytes` with no extraction copy; `bytearray`, `memoryview`, other buffer-protocol objects, and `bytes` subclasses keep the existing copy path. Pooled 7-round median: plain 1 MiB +7.2%.
|
|
27
|
+
|
|
28
|
+
- **Interned Future-delivery identifiers (#31)**: `done` / `set_result` lookups in the hot async receive path use interned strings. Merged under the documented micro-optimization exception — the +2% performance gate was **not** met (~+1% on TLS 256 B, 7/7 positive rounds); this entry must not be cited as a perf-gate pass.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Interrupted socket I/O in the sync client (#34)**: blocking read/write/flush now follow PEP 475 — on `EINTR` the client checks pending Python signals first (so Ctrl-C still raises `KeyboardInterrupt`) and then retries, and an overall receive deadline is enforced across retries so `receive_timeout` is never extended by signal storms. Previously any signal delivered during a blocking `recv()` surfaced `RuntimeError: Interrupted system call`.
|
|
33
|
+
|
|
34
|
+
### Internal
|
|
35
|
+
|
|
36
|
+
- **Owner invariants compiler-enforced (#32)**: the sync-send `PyBytes` owner moved into a private module with a single constructor, making its safety invariants unconstructible-wrong; mutate-after-send tripwire tests added for mutable input types.
|
|
37
|
+
|
|
38
|
+
- **Peer-close unified behind one seam (#33)**: close parsing and effects flow through `parse_close_payload` + `begin_peer_close` / `apply_peer_close` on every path (slow-path event sink, all three fast-path adapters, `connection_lost`, client `close()`). The remaining future-failure and transport calls made under an active `RefCell` borrow were eliminated — the same reentrancy discipline the 0.7.2 event sink introduced — and `handle_close_frame` / `fail_all_pending` were deleted outright.
|
|
39
|
+
|
|
8
40
|
## [0.7.2] - 2026-07-24
|
|
9
41
|
|
|
10
42
|
### Fixed
|
|
@@ -15,7 +15,7 @@ Fastest Python WebSocket client on plain TCP — wins **12/12** benchmark cells
|
|
|
15
15
|
| Plain Python script / CLI tool (no asyncio) | **ws-rs sync** | picows is async-only; 1.3–7× over websocket-client (size-dependent) |
|
|
16
16
|
| asyncio app, sequential RR ≤ 8 KB | **ws-rs sync** via `to_thread` | +13–28% over picows; sync bypasses asyncio overhead |
|
|
17
17
|
| asyncio app, pipelined / streaming | **ws-rs async** | mean RPS +14–21% over picows on best path |
|
|
18
|
-
| asyncio app, ≥ 1 MB payloads | **ws-rs async**
|
|
18
|
+
| asyncio app, ≥ 1 MB payloads | **ws-rs async** | +15–20% over picows (RR, two servers, 7-round median; v0.7.3 zero-copy recv) |
|
|
19
19
|
| RR latency-sensitive, payload ≥ 16 KB | **picows** | 3–5 µs lower per-message RTT (Cython cdef vs PyO3) |
|
|
20
20
|
| Cross-network (real server, real RTT) | **Either** | network RT dominates; library choice < 1% of total latency |
|
|
21
21
|
|
|
@@ -34,10 +34,26 @@ Neutral Rust echo server (tokio-tungstenite), pinned cores, uvloop, 1 s discarde
|
|
|
34
34
|
| 256 B | **15.0k** | 12.8k | 13.5k | 12.0k | 9.9k | 11.7k |
|
|
35
35
|
| 8 KB | **15.1k** | 13.4k | 13.2k | 11.8k | 9.6k | 10.1k |
|
|
36
36
|
| 100 KB | **10.8k** | 10.3k | 10.5k | 9.4k | 8.1k | 4.5k |
|
|
37
|
-
| 1 MB | 4.0k | **4.2k** |
|
|
37
|
+
| 1 MB | 4.0k | **4.2k** | 3.4k | 3.4k | 3.0k | 509 |
|
|
38
|
+
|
|
39
|
+
> The legacy multi-client table above predates picows 2.1.1 and the v0.7.3 zero-copy path; the async-vs-picows section below supersedes its `ws-rs async` / `picows` columns. Its 1 MB `picows` cell is corrected here (was tied at 4.2k, now 3.4k on picows 2.1.1); the aiohttp / websockets / websocket-client columns are the older single-round figures pending a decision-grade re-measure.
|
|
38
40
|
|
|
39
41
|
ws-rs wins or ties **12/12 cells** across three server architectures (tokio-tungstenite, fastwebsockets, picows-server).
|
|
40
42
|
|
|
43
|
+
#### Apples-to-apples: async vs picows (v0.7.3, decision-grade)
|
|
44
|
+
|
|
45
|
+
Both are `asyncio.Protocol` peers, so this is the fair head-to-head. Measured against **picows 2.1.1** (latest) on an AMD Ryzen 9 9950X / WSL2, single-connection plain-TCP RR loopback, one discarded warm-up round + **7 interleaved scored rounds**, pooled median. Cross-checked on two independent servers so the result isn't tied to one server implementation:
|
|
46
|
+
|
|
47
|
+
| Payload | tokio-tungstenite server | picows-exact-echo server | verdict |
|
|
48
|
+
|---------|---:|---:|---|
|
|
49
|
+
| 256 B | 100.4% | ~parity | tie |
|
|
50
|
+
| 8 KB | 102.5% | 102.4% | +2.4% |
|
|
51
|
+
| 100 KB | 102.6% | 100.7% | ~parity–small win |
|
|
52
|
+
| 1 MB | **120.0%** | **115.5%** | **+15–20%** |
|
|
53
|
+
| **geomean** | **+6.1%** | **+5.2%** | ws-rs ahead |
|
|
54
|
+
|
|
55
|
+
The 1 MB win is the direct payoff of the v0.7.3 zero-copy receive path. Scope: these numbers are specific to this machine and a request-response workload — they do **not** cover TLS, pipelining, multiple concurrent connections, or streaming throughput.
|
|
56
|
+
|
|
41
57
|
### Request-Response Throughput — TLS / wss:// (rustls)
|
|
42
58
|
|
|
43
59
|
| Payload | **ws-rs sync** | ws-rs async | picows | aiohttp | websockets | websocket-client |
|
|
@@ -55,12 +71,11 @@ Sync wins TLS 256 B–100 KB by 30–60%. At 1 MB picows leads by ~7%; ws-rs tie
|
|
|
55
71
|
|
|
56
72
|
📊 **[Full benchmarks — all 3 servers, TCP & TLS, latency distributions, compression](docs/BENCHMARKS.md)** | 📝 **[Optimization Research](docs/OPTIMIZATION_RESEARCH.md)**
|
|
57
73
|
|
|
58
|
-
## ✨ What's New in v0.7.
|
|
74
|
+
## ✨ What's New in v0.7.3
|
|
59
75
|
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
62
|
-
- **
|
|
63
|
-
- **Internals**: the buffered state machine is now a pure sans-I/O `ProtocolCore` with an allocation-free event sink, and the three fast-path frame walks share one monomorphized scanner — receive-path performance held or improved (several parity cells +3–11%).
|
|
76
|
+
- **Faster sync client**: both directions of the synchronous client drop their per-message payload copy — large-message throughput up 7–28% (plain 1 MiB recv +9.1%, TLS 1 MiB recv +27.9%, plain 1 MiB send +7.2%; medians on an idle host).
|
|
77
|
+
- **Signal-safe sync I/O**: blocking read/write now follow PEP 475 — signals during a blocking `recv()` no longer surface `Interrupted system call` errors, Ctrl-C still raises `KeyboardInterrupt`, and `receive_timeout` deadlines hold across retries.
|
|
78
|
+
- **Internals**: peer-close handling unified behind one seam with all transport effects outside internal borrows; the sync-send buffer owner's safety invariants are now compiler-enforced.
|
|
64
79
|
|
|
65
80
|
Full details in [CHANGELOG.md](CHANGELOG.md).
|
|
66
81
|
|
|
@@ -159,6 +159,13 @@ fn parse_header(buf: &[u8]) -> Option<(bool, bool, u8, usize, usize)> {
|
|
|
159
159
|
Some((fin, rsv1, opcode, plen, hdr))
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/// Close-frame payload → (code, reason). Empty/short payloads carry neither.
|
|
163
|
+
fn parse_close_payload(payload: &[u8]) -> (Option<u16>, Option<String>) {
|
|
164
|
+
let code = (payload.len() >= 2).then(|| u16::from_be_bytes([payload[0], payload[1]]));
|
|
165
|
+
let reason = (payload.len() > 2).then(|| String::from_utf8_lossy(&payload[2..]).into_owned());
|
|
166
|
+
(code, reason)
|
|
167
|
+
}
|
|
168
|
+
|
|
162
169
|
struct FastFrame<'a> {
|
|
163
170
|
opcode: u8,
|
|
164
171
|
payload: &'a [u8],
|
|
@@ -350,10 +357,7 @@ impl ProtocolCore<'_> {
|
|
|
350
357
|
OP_CLOSE => {
|
|
351
358
|
self.buf.advance(hdr);
|
|
352
359
|
let payload = self.buf.split_to(plen);
|
|
353
|
-
let code =
|
|
354
|
-
(payload.len() >= 2).then(|| u16::from_be_bytes([payload[0], payload[1]]));
|
|
355
|
-
let reason = (payload.len() > 2)
|
|
356
|
-
.then(|| String::from_utf8_lossy(&payload[2..]).into_owned());
|
|
360
|
+
let (code, reason) = parse_close_payload(&payload);
|
|
357
361
|
return Ok(Some(ProtocolEvent::Close { code, reason }));
|
|
358
362
|
}
|
|
359
363
|
OP_PING => {
|
|
@@ -1025,11 +1029,28 @@ impl NativeClient {
|
|
|
1025
1029
|
Ok(())
|
|
1026
1030
|
}
|
|
1027
1031
|
|
|
1032
|
+
/// The peer half-closed (TCP FIN, or TLS close_notify). No further frame can
|
|
1033
|
+
/// arrive, so let the transport close itself and let `connection_lost` do the
|
|
1034
|
+
/// cleanup: returning true would mean "the protocol keeps this open", which is
|
|
1035
|
+
/// wrong for a WebSocket client and is ignored under TLS anyway.
|
|
1036
|
+
///
|
|
1037
|
+
/// asyncio.Protocol supplies a default, but this class is a pyclass and is not
|
|
1038
|
+
/// a subclass of it, so the method has to exist here. Both call sites are
|
|
1039
|
+
/// unconditional and unguarded (`sslproto._call_eof_received`,
|
|
1040
|
+
/// `_SelectorSocketTransport._read_ready__data_received`), and the TLS one
|
|
1041
|
+
/// routes a missing attribute into `_fatal_error`.
|
|
1042
|
+
fn eof_received(&self) -> bool {
|
|
1043
|
+
false
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1028
1046
|
fn connection_lost(&self, py: Python<'_>, _exc: Py<PyAny>) {
|
|
1029
|
-
let
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1047
|
+
let pending = {
|
|
1048
|
+
let mut state = self.state.borrow_mut();
|
|
1049
|
+
state.closed = true;
|
|
1050
|
+
state.transport = None;
|
|
1051
|
+
std::mem::take(&mut state.pending_recv)
|
|
1052
|
+
};
|
|
1053
|
+
Self::fail_pending(py, pending, "Connection lost");
|
|
1033
1054
|
}
|
|
1034
1055
|
|
|
1035
1056
|
// ---- User-facing API ----
|
|
@@ -1347,8 +1368,9 @@ impl NativeClient {
|
|
|
1347
1368
|
state.create_future = None;
|
|
1348
1369
|
state.wait_for = None;
|
|
1349
1370
|
let write_queue = std::mem::take(&mut state.write_queue);
|
|
1350
|
-
|
|
1371
|
+
let pending = std::mem::take(&mut state.pending_recv);
|
|
1351
1372
|
drop(state);
|
|
1373
|
+
Self::fail_pending(py, pending, "Connection closed by client");
|
|
1352
1374
|
// All mutex-guarded references are gone; drop pending writes and then
|
|
1353
1375
|
// issue the close frame + transport.close() on the surviving transport ref.
|
|
1354
1376
|
drop(write_queue);
|
|
@@ -1388,6 +1410,7 @@ impl NativeClient {
|
|
|
1388
1410
|
self.data_received_inner(py, data)?;
|
|
1389
1411
|
return Ok((data.len(), None));
|
|
1390
1412
|
}
|
|
1413
|
+
let mut close_effects = None;
|
|
1391
1414
|
let outcome = walk_frames(data, |frame| -> PyResult<VisitOutcome> {
|
|
1392
1415
|
match frame.opcode {
|
|
1393
1416
|
OP_TEXT | OP_BINARY => {
|
|
@@ -1397,14 +1420,18 @@ impl NativeClient {
|
|
|
1397
1420
|
Self::deliver_message(py, &mut state, msg)?;
|
|
1398
1421
|
}
|
|
1399
1422
|
OP_CLOSE => {
|
|
1423
|
+
let (code, reason) = parse_close_payload(frame.payload);
|
|
1400
1424
|
let mut state = self.state.borrow_mut();
|
|
1401
|
-
Self::
|
|
1425
|
+
close_effects = Some(Self::begin_peer_close(py, &mut state, code, reason));
|
|
1402
1426
|
return Ok(VisitOutcome::Stop);
|
|
1403
1427
|
}
|
|
1404
1428
|
_ => {}
|
|
1405
1429
|
}
|
|
1406
1430
|
Ok(VisitOutcome::Continue)
|
|
1407
1431
|
})?;
|
|
1432
|
+
if let Some((pending, transport)) = close_effects {
|
|
1433
|
+
Self::apply_peer_close(py, pending, transport);
|
|
1434
|
+
}
|
|
1408
1435
|
match outcome {
|
|
1409
1436
|
ScanOutcome::Exhausted { consumed } | ScanOutcome::Stopped { consumed } => {
|
|
1410
1437
|
Ok((consumed, None))
|
|
@@ -1429,6 +1456,7 @@ impl NativeClient {
|
|
|
1429
1456
|
};
|
|
1430
1457
|
if can_fast_path {
|
|
1431
1458
|
let mut state = self.state.borrow_mut();
|
|
1459
|
+
let mut close_effects = None;
|
|
1432
1460
|
let outcome = walk_frames(data, |frame| -> PyResult<VisitOutcome> {
|
|
1433
1461
|
match frame.opcode {
|
|
1434
1462
|
OP_TEXT | OP_BINARY => {
|
|
@@ -1447,7 +1475,8 @@ impl NativeClient {
|
|
|
1447
1475
|
Self::deliver_message(py, &mut state, msg)?;
|
|
1448
1476
|
}
|
|
1449
1477
|
OP_CLOSE => {
|
|
1450
|
-
|
|
1478
|
+
let (code, reason) = parse_close_payload(frame.payload);
|
|
1479
|
+
close_effects = Some(Self::begin_peer_close(py, &mut state, code, reason));
|
|
1451
1480
|
return Ok(VisitOutcome::Stop);
|
|
1452
1481
|
}
|
|
1453
1482
|
_ => {}
|
|
@@ -1455,7 +1484,13 @@ impl NativeClient {
|
|
|
1455
1484
|
Ok(VisitOutcome::Continue)
|
|
1456
1485
|
})?;
|
|
1457
1486
|
let consumed = match outcome {
|
|
1458
|
-
ScanOutcome::Stopped { .. } =>
|
|
1487
|
+
ScanOutcome::Stopped { .. } => {
|
|
1488
|
+
drop(state);
|
|
1489
|
+
if let Some((pending, transport)) = close_effects {
|
|
1490
|
+
Self::apply_peer_close(py, pending, transport);
|
|
1491
|
+
}
|
|
1492
|
+
return Ok(());
|
|
1493
|
+
}
|
|
1459
1494
|
ScanOutcome::Exhausted { consumed } if consumed == data.len() => return Ok(()),
|
|
1460
1495
|
ScanOutcome::Exhausted { consumed }
|
|
1461
1496
|
| ScanOutcome::Partial { consumed, .. }
|
|
@@ -1483,6 +1518,7 @@ impl NativeClient {
|
|
|
1483
1518
|
};
|
|
1484
1519
|
if can_fast_path {
|
|
1485
1520
|
let mut state = self.state.borrow_mut();
|
|
1521
|
+
let mut close_effects = None;
|
|
1486
1522
|
let outcome = walk_frames(data, |frame| -> PyResult<VisitOutcome> {
|
|
1487
1523
|
match frame.opcode {
|
|
1488
1524
|
OP_TEXT | OP_BINARY => {
|
|
@@ -1491,7 +1527,8 @@ impl NativeClient {
|
|
|
1491
1527
|
Self::deliver_message(py, &mut state, msg)?;
|
|
1492
1528
|
}
|
|
1493
1529
|
OP_CLOSE => {
|
|
1494
|
-
|
|
1530
|
+
let (code, reason) = parse_close_payload(frame.payload);
|
|
1531
|
+
close_effects = Some(Self::begin_peer_close(py, &mut state, code, reason));
|
|
1495
1532
|
return Ok(VisitOutcome::Stop);
|
|
1496
1533
|
}
|
|
1497
1534
|
_ => {}
|
|
@@ -1499,7 +1536,13 @@ impl NativeClient {
|
|
|
1499
1536
|
Ok(VisitOutcome::Continue)
|
|
1500
1537
|
})?;
|
|
1501
1538
|
let consumed = match outcome {
|
|
1502
|
-
ScanOutcome::Stopped { .. } =>
|
|
1539
|
+
ScanOutcome::Stopped { .. } => {
|
|
1540
|
+
drop(state);
|
|
1541
|
+
if let Some((pending, transport)) = close_effects {
|
|
1542
|
+
Self::apply_peer_close(py, pending, transport);
|
|
1543
|
+
}
|
|
1544
|
+
return Ok(());
|
|
1545
|
+
}
|
|
1503
1546
|
ScanOutcome::Exhausted { consumed } if consumed == data.len() => return Ok(()),
|
|
1504
1547
|
ScanOutcome::Exhausted { consumed }
|
|
1505
1548
|
| ScanOutcome::Partial { consumed, .. }
|
|
@@ -1736,19 +1779,9 @@ impl NativeClient {
|
|
|
1736
1779
|
ProtocolEvent::Close { code, reason } => {
|
|
1737
1780
|
let (pending, transport) = {
|
|
1738
1781
|
let mut state = self.state.borrow_mut();
|
|
1739
|
-
|
|
1740
|
-
state.close_code = code;
|
|
1741
|
-
state.close_reason = reason;
|
|
1742
|
-
}
|
|
1743
|
-
state.closed = true;
|
|
1744
|
-
let pending = std::mem::take(&mut state.pending_recv);
|
|
1745
|
-
let transport = state.transport.as_ref().map(|t| t.clone_ref(py));
|
|
1746
|
-
(pending, transport)
|
|
1782
|
+
Self::begin_peer_close(py, &mut state, code, reason)
|
|
1747
1783
|
};
|
|
1748
|
-
Self::
|
|
1749
|
-
if let Some(transport) = transport {
|
|
1750
|
-
let _ = transport.bind(py).call_method0("close");
|
|
1751
|
-
}
|
|
1784
|
+
Self::apply_peer_close(py, pending, transport);
|
|
1752
1785
|
Ok(EventFlow::Stop)
|
|
1753
1786
|
}
|
|
1754
1787
|
ProtocolEvent::ProtocolError(reason) => {
|
|
@@ -1799,8 +1832,12 @@ impl NativeClient {
|
|
|
1799
1832
|
}
|
|
1800
1833
|
if let Some(fut) = state.pending_recv.pop_front() {
|
|
1801
1834
|
let fb = fut.bind(py);
|
|
1802
|
-
if !fb
|
|
1803
|
-
|
|
1835
|
+
if !fb
|
|
1836
|
+
.call_method0(pyo3::intern!(py, "done"))?
|
|
1837
|
+
.extract::<bool>()
|
|
1838
|
+
.unwrap_or(false)
|
|
1839
|
+
{
|
|
1840
|
+
fb.call_method1(pyo3::intern!(py, "set_result"), (msg,))?;
|
|
1804
1841
|
}
|
|
1805
1842
|
} else {
|
|
1806
1843
|
state.backlog.push_back(msg);
|
|
@@ -1808,33 +1845,33 @@ impl NativeClient {
|
|
|
1808
1845
|
Ok(())
|
|
1809
1846
|
}
|
|
1810
1847
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1848
|
+
/// Record a peer-initiated close in `state` and hand back the effects the
|
|
1849
|
+
/// caller must apply AFTER releasing the State borrow — the same
|
|
1850
|
+
/// reentrancy discipline as the slow-path event sink.
|
|
1851
|
+
fn begin_peer_close(
|
|
1852
|
+
py: Python<'_>,
|
|
1853
|
+
state: &mut State,
|
|
1854
|
+
code: Option<u16>,
|
|
1855
|
+
reason: Option<String>,
|
|
1856
|
+
) -> (VecDeque<Py<PyAny>>, Option<Py<PyAny>>) {
|
|
1857
|
+
if code.is_some() {
|
|
1858
|
+
state.close_code = code;
|
|
1859
|
+
state.close_reason = reason;
|
|
1816
1860
|
}
|
|
1817
1861
|
state.closed = true;
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
.unwrap_or(true)
|
|
1832
|
-
{
|
|
1833
|
-
let _ = fb.call_method1(
|
|
1834
|
-
"set_exception",
|
|
1835
|
-
(PyConnectionError::new_err(msg.to_string()),),
|
|
1836
|
-
);
|
|
1837
|
-
}
|
|
1862
|
+
let pending = std::mem::take(&mut state.pending_recv);
|
|
1863
|
+
let transport = state.transport.as_ref().map(|t| t.clone_ref(py));
|
|
1864
|
+
(pending, transport)
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
fn apply_peer_close(
|
|
1868
|
+
py: Python<'_>,
|
|
1869
|
+
pending: VecDeque<Py<PyAny>>,
|
|
1870
|
+
transport: Option<Py<PyAny>>,
|
|
1871
|
+
) {
|
|
1872
|
+
Self::fail_pending(py, pending, "Connection closed by peer");
|
|
1873
|
+
if let Some(transport) = transport {
|
|
1874
|
+
let _ = transport.bind(py).call_method0("close");
|
|
1838
1875
|
}
|
|
1839
1876
|
}
|
|
1840
1877
|
}
|