tigrcorn-transports 0.3.18.dev2__tar.gz → 0.3.18.dev3__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.
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/PKG-INFO +7 -4
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/README.md +1 -1
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/pyproject.toml +12 -3
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/listeners/udp.py +238 -0
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/congestion/__init__.py +4 -0
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/congestion/pacer.py +70 -0
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/congestion/runtime.py +221 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/base.py +42 -4
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/core.py +36 -2
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/imports.py +11 -2
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/model.py +1 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/receive.py +24 -1
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/connection/recovery_policy.py +61 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/runtime.py +22 -22
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/send.py +198 -7
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/crypto/aes.py +13 -0
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/crypto/gcm.py +34 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/flow.py +35 -8
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/recovery/__init__.py +27 -0
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/recovery/congestion.py +83 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/recovery/core.py +80 -74
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/recovery/evidence.py +3 -2
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/recovery/model.py +1 -0
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/recovery/policy.py +118 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/codec.py +21 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/constants.py +1 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/frames.py +13 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/labels.py +6 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/manager.py +7 -1
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports.egg-info/PKG-INFO +7 -4
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports.egg-info/SOURCES.txt +6 -0
- tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports.egg-info/requires.txt +5 -0
- tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports/listeners/udp.py +0 -82
- tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports/quic/crypto/aes.py +0 -134
- tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports/quic/crypto/gcm.py +0 -100
- tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports/quic/recovery/__init__.py +0 -14
- tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports.egg-info/requires.txt +0 -2
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/LICENSE +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/setup.cfg +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/base.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/inproc/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/inproc/channel.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/base.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/inproc.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/pipe.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/registry.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/tcp.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/unix.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/pipe/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/pipe/connection.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/py.typed +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/decode.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/loss_api.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/crypto/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/crypto/keys.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/crypto/packets.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/datagrams.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/handshake.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/packets.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/scheduler.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/security/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/security/certification.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/security/helpers.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/security/runtime.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/state.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/tls_adapter.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/registry.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/accept.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/connection.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/reader.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/socketopts.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/tls.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/writer.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/udp/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/udp/endpoint.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/udp/packet.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/udp/socketopts.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/unix/__init__.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/unix/connection.py +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports.egg-info/dependency_links.txt +0 -0
- {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrcorn-transports
|
|
3
|
-
Version: 0.3.18.
|
|
3
|
+
Version: 0.3.18.dev3
|
|
4
4
|
Summary: TCP, UDP, Unix socket, pipe, in-process, listener, and QUIC transport primitives for the Tigrcorn server stack.
|
|
5
5
|
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
6
6
|
License: Apache License
|
|
@@ -186,8 +186,11 @@ Classifier: Typing :: Typed
|
|
|
186
186
|
Requires-Python: <3.15,>=3.10
|
|
187
187
|
Description-Content-Type: text/markdown
|
|
188
188
|
License-File: LICENSE
|
|
189
|
-
Requires-Dist:
|
|
190
|
-
Requires-Dist: tigrcorn-
|
|
189
|
+
Requires-Dist: cryptography>=46.0.0
|
|
190
|
+
Requires-Dist: tigrcorn-core==0.3.18.dev3
|
|
191
|
+
Requires-Dist: tigrcorn-config==0.3.18.dev3
|
|
192
|
+
Requires-Dist: tigrcorn-quic-cc==0.3.18.dev3
|
|
193
|
+
Requires-Dist: tigrcorn-quic-cc-reno==0.3.18.dev3
|
|
191
194
|
Dynamic: license-file
|
|
192
195
|
|
|
193
196
|
<div align="center">
|
|
@@ -277,7 +280,7 @@ The package exposes its supported public surface through the <code>tigrcorn_tran
|
|
|
277
280
|
|
|
278
281
|
## Package Graph
|
|
279
282
|
|
|
280
|
-
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
283
|
+
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
281
284
|
|
|
282
285
|
## Best Practices
|
|
283
286
|
|
|
@@ -85,7 +85,7 @@ The package exposes its supported public surface through the <code>tigrcorn_tran
|
|
|
85
85
|
|
|
86
86
|
## Package Graph
|
|
87
87
|
|
|
88
|
-
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
88
|
+
[tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
|
|
89
89
|
|
|
90
90
|
## Best Practices
|
|
91
91
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tigrcorn-transports"
|
|
7
|
-
version = "0.3.18.
|
|
7
|
+
version = "0.3.18.dev3"
|
|
8
8
|
description = "TCP, UDP, Unix socket, pipe, in-process, listener, and QUIC transport primitives for the Tigrcorn server stack."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10,<3.15"
|
|
@@ -29,10 +29,19 @@ classifiers = [
|
|
|
29
29
|
"Typing :: Typed",
|
|
30
30
|
]
|
|
31
31
|
dependencies = [
|
|
32
|
-
"
|
|
33
|
-
"tigrcorn-
|
|
32
|
+
"cryptography>=46.0.0",
|
|
33
|
+
"tigrcorn-core==0.3.18.dev3",
|
|
34
|
+
"tigrcorn-config==0.3.18.dev3",
|
|
35
|
+
"tigrcorn-quic-cc==0.3.18.dev3",
|
|
36
|
+
"tigrcorn-quic-cc-reno==0.3.18.dev3",
|
|
34
37
|
]
|
|
35
38
|
|
|
39
|
+
[tool.uv.sources]
|
|
40
|
+
tigrcorn-core = { workspace = true }
|
|
41
|
+
tigrcorn-config = { workspace = true }
|
|
42
|
+
tigrcorn-quic-cc = { workspace = true }
|
|
43
|
+
tigrcorn-quic-cc-reno = { workspace = true }
|
|
44
|
+
|
|
36
45
|
[tool.setuptools]
|
|
37
46
|
package-dir = {"" = "src"}
|
|
38
47
|
license-files = ["LICENSE"]
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import socket
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
from tigrcorn_transports.udp.endpoint import UDPEndpoint
|
|
10
|
+
from tigrcorn_transports.udp.packet import UDPPacket
|
|
11
|
+
from tigrcorn_transports.udp.socketopts import configure_udp_socket
|
|
12
|
+
|
|
13
|
+
from .base import BaseListener
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("tigrcorn")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _UDPProtocol(asyncio.DatagramProtocol):
|
|
19
|
+
_PEER_STARTUP_PRIORITY_SECONDS = 4.0
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
callback: Callable[..., Awaitable[None] | None],
|
|
24
|
+
*,
|
|
25
|
+
dispatch_workers: int = 4,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.callback = callback
|
|
28
|
+
# One worker is reserved for QUIC long-header traffic. A minimum of
|
|
29
|
+
# two workers ensures bulk media can never occupy the handshake lane.
|
|
30
|
+
self.dispatch_workers = max(2, dispatch_workers)
|
|
31
|
+
self.transport: asyncio.DatagramTransport | None = None
|
|
32
|
+
self.endpoint: UDPEndpoint | None = None
|
|
33
|
+
self.tasks: set[asyncio.Task[None]] = set()
|
|
34
|
+
self.urgent_queue: asyncio.Queue[tuple[int, UDPPacket]] = asyncio.Queue()
|
|
35
|
+
self.normal_queue: asyncio.Queue[tuple[int, UDPPacket]] = asyncio.Queue()
|
|
36
|
+
self._peer_priority_until: dict[tuple[str, int], float] = {}
|
|
37
|
+
self._sequence = 0
|
|
38
|
+
|
|
39
|
+
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
|
40
|
+
self.transport = transport # runtime transport provided by asyncio
|
|
41
|
+
sockname = transport.get_extra_info("sockname")
|
|
42
|
+
sock = transport.get_extra_info("socket")
|
|
43
|
+
if sock is not None:
|
|
44
|
+
configure_udp_socket(sock)
|
|
45
|
+
self.endpoint = UDPEndpoint(transport=transport, local_addr=sockname)
|
|
46
|
+
queues = [
|
|
47
|
+
self.urgent_queue,
|
|
48
|
+
*([self.normal_queue] * (self.dispatch_workers - 1)),
|
|
49
|
+
]
|
|
50
|
+
for index, queue in enumerate(queues):
|
|
51
|
+
task = asyncio.create_task(
|
|
52
|
+
self._dispatch(queue, urgent=queue is self.urgent_queue),
|
|
53
|
+
name=f"tigrcorn-udp-dispatch-{index}",
|
|
54
|
+
)
|
|
55
|
+
self.tasks.add(task)
|
|
56
|
+
task.add_done_callback(self.tasks.discard)
|
|
57
|
+
|
|
58
|
+
def datagram_received(self, data: bytes, addr) -> None: # type: ignore[override]
|
|
59
|
+
if self.endpoint is None:
|
|
60
|
+
return
|
|
61
|
+
packet = UDPPacket(data=data, addr=addr)
|
|
62
|
+
self._sequence += 1
|
|
63
|
+
loop_time = asyncio.get_running_loop().time()
|
|
64
|
+
is_long_header = bool(data and data[0] & 0x80)
|
|
65
|
+
if is_long_header:
|
|
66
|
+
self._peer_priority_until[addr] = (
|
|
67
|
+
loop_time + self._PEER_STARTUP_PRIORITY_SECONDS
|
|
68
|
+
)
|
|
69
|
+
priority_until = self._peer_priority_until.get(addr, 0.0)
|
|
70
|
+
if priority_until and loop_time >= priority_until:
|
|
71
|
+
self._peer_priority_until.pop(addr, None)
|
|
72
|
+
priority_until = 0.0
|
|
73
|
+
if self._sequence % 256 == 0:
|
|
74
|
+
self._peer_priority_until = {
|
|
75
|
+
peer: deadline
|
|
76
|
+
for peer, deadline in self._peer_priority_until.items()
|
|
77
|
+
if loop_time < deadline
|
|
78
|
+
}
|
|
79
|
+
queue = (
|
|
80
|
+
self.urgent_queue
|
|
81
|
+
if is_long_header or priority_until
|
|
82
|
+
else self.normal_queue
|
|
83
|
+
)
|
|
84
|
+
queue.put_nowait((self._sequence, packet))
|
|
85
|
+
|
|
86
|
+
async def _dispatch(
|
|
87
|
+
self,
|
|
88
|
+
queue: asyncio.Queue[tuple[int, UDPPacket]],
|
|
89
|
+
*,
|
|
90
|
+
urgent: bool,
|
|
91
|
+
) -> None:
|
|
92
|
+
while True:
|
|
93
|
+
_sequence, packet = await queue.get()
|
|
94
|
+
try:
|
|
95
|
+
if not urgent:
|
|
96
|
+
# queue.get() and a lightweight callback can both finish
|
|
97
|
+
# synchronously while media keeps this queue non-empty.
|
|
98
|
+
# Yield so the reserved long-header worker can service an
|
|
99
|
+
# Initial already captured by the batched socket reader.
|
|
100
|
+
await asyncio.sleep(0)
|
|
101
|
+
if self.endpoint is None:
|
|
102
|
+
continue
|
|
103
|
+
result = self.callback(packet, self.endpoint)
|
|
104
|
+
if inspect.isawaitable(result):
|
|
105
|
+
await result
|
|
106
|
+
except asyncio.CancelledError:
|
|
107
|
+
raise
|
|
108
|
+
# A bad application callback must not permanently reduce the fixed
|
|
109
|
+
# dispatcher pool or stop later QUIC handshakes from being served.
|
|
110
|
+
except Exception as exc: # noqa: BLE001
|
|
111
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
112
|
+
{
|
|
113
|
+
"message": "Tigrcorn UDP datagram callback failed",
|
|
114
|
+
"exception": exc,
|
|
115
|
+
"protocol": self,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
finally:
|
|
119
|
+
queue.task_done()
|
|
120
|
+
|
|
121
|
+
def connection_lost(self, exc: Exception | None) -> None:
|
|
122
|
+
for task in list(self.tasks):
|
|
123
|
+
task.cancel()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class _BatchedUDPReader:
|
|
127
|
+
"""Drain multiple queued datagrams per selector readiness callback."""
|
|
128
|
+
|
|
129
|
+
_MAX_BATCH_DATAGRAMS = 256
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
loop: asyncio.AbstractEventLoop,
|
|
134
|
+
protocol: _UDPProtocol,
|
|
135
|
+
sock: socket.socket,
|
|
136
|
+
) -> None:
|
|
137
|
+
self.loop = loop
|
|
138
|
+
self.protocol = protocol
|
|
139
|
+
self.sock = sock
|
|
140
|
+
|
|
141
|
+
def start(self) -> None:
|
|
142
|
+
self.loop.add_reader(self.sock.fileno(), self._read_ready)
|
|
143
|
+
|
|
144
|
+
def _read_ready(self) -> None:
|
|
145
|
+
for _ in range(self._MAX_BATCH_DATAGRAMS):
|
|
146
|
+
try:
|
|
147
|
+
data, addr = self.sock.recvfrom(65536)
|
|
148
|
+
except (BlockingIOError, InterruptedError):
|
|
149
|
+
return
|
|
150
|
+
except OSError as exc:
|
|
151
|
+
self.loop.call_exception_handler(
|
|
152
|
+
{
|
|
153
|
+
"message": "Tigrcorn UDP batch receive failed",
|
|
154
|
+
"exception": exc,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
return
|
|
158
|
+
self.protocol.datagram_received(data, addr)
|
|
159
|
+
|
|
160
|
+
def close(self) -> None:
|
|
161
|
+
self.loop.remove_reader(self.sock.fileno())
|
|
162
|
+
self.sock.close()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class UDPListener(BaseListener):
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
host: str,
|
|
169
|
+
port: int,
|
|
170
|
+
*,
|
|
171
|
+
reuse_port: bool = False,
|
|
172
|
+
fd: int | None = None,
|
|
173
|
+
sock: socket.socket | None = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
self.host = host
|
|
176
|
+
self.port = port
|
|
177
|
+
self.reuse_port = reuse_port
|
|
178
|
+
self.fd = fd
|
|
179
|
+
self.sock = sock
|
|
180
|
+
self.transport: asyncio.DatagramTransport | None = None
|
|
181
|
+
self.protocol: _UDPProtocol | None = None
|
|
182
|
+
self.batch_reader: _BatchedUDPReader | None = None
|
|
183
|
+
|
|
184
|
+
def _get_socket(self) -> socket.socket | None:
|
|
185
|
+
if self.sock is not None:
|
|
186
|
+
return self.sock
|
|
187
|
+
if self.fd is None:
|
|
188
|
+
return None
|
|
189
|
+
sock = socket.socket(fileno=self.fd)
|
|
190
|
+
sock.setblocking(False)
|
|
191
|
+
configure_udp_socket(sock)
|
|
192
|
+
self.sock = sock
|
|
193
|
+
return sock
|
|
194
|
+
|
|
195
|
+
async def start(self, client_connected_cb):
|
|
196
|
+
loop = asyncio.get_running_loop()
|
|
197
|
+
existing_sock = self._get_socket()
|
|
198
|
+
transport, protocol = await loop.create_datagram_endpoint(
|
|
199
|
+
lambda: _UDPProtocol(client_connected_cb),
|
|
200
|
+
local_addr=None if existing_sock is not None else (self.host, self.port),
|
|
201
|
+
reuse_port=self.reuse_port if existing_sock is None else None,
|
|
202
|
+
sock=existing_sock,
|
|
203
|
+
)
|
|
204
|
+
self.transport = transport
|
|
205
|
+
self.protocol = protocol
|
|
206
|
+
transport_socket = transport.get_extra_info("socket")
|
|
207
|
+
if transport_socket is not None and hasattr(transport, "pause_reading"):
|
|
208
|
+
duplicate = transport_socket.dup()
|
|
209
|
+
duplicate.setblocking(False)
|
|
210
|
+
reader = _BatchedUDPReader(loop, protocol, duplicate)
|
|
211
|
+
paused = False
|
|
212
|
+
try:
|
|
213
|
+
transport.pause_reading()
|
|
214
|
+
paused = True
|
|
215
|
+
reader.start()
|
|
216
|
+
except (NotImplementedError, AttributeError, OSError):
|
|
217
|
+
reader.sock.close()
|
|
218
|
+
if paused:
|
|
219
|
+
try:
|
|
220
|
+
transport.resume_reading()
|
|
221
|
+
except (NotImplementedError, AttributeError, OSError):
|
|
222
|
+
pass
|
|
223
|
+
logger.info("UDP listener using asyncio single-datagram receive path")
|
|
224
|
+
else:
|
|
225
|
+
self.batch_reader = reader
|
|
226
|
+
logger.info(
|
|
227
|
+
"UDP listener using bounded batch receive path batch_size=%d",
|
|
228
|
+
reader._MAX_BATCH_DATAGRAMS,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
async def close(self) -> None:
|
|
232
|
+
if self.batch_reader is not None:
|
|
233
|
+
self.batch_reader.close()
|
|
234
|
+
self.batch_reader = None
|
|
235
|
+
if self.transport is not None:
|
|
236
|
+
self.transport.close()
|
|
237
|
+
self.transport = None
|
|
238
|
+
self.protocol = None
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class QuicPacer:
|
|
5
|
+
"""Transport-owned token bucket enforcing provider pacing limits."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, *, initial_budget: float) -> None:
|
|
8
|
+
self.budget = max(0.0, float(initial_budget))
|
|
9
|
+
self._last_update: float | None = None
|
|
10
|
+
|
|
11
|
+
def refresh(self, *, now: float, pacing_rate: float, congestion_window: int) -> None:
|
|
12
|
+
if self._last_update is None:
|
|
13
|
+
self._last_update = now
|
|
14
|
+
self.budget = min(float(congestion_window), self.budget)
|
|
15
|
+
return
|
|
16
|
+
elapsed = max(0.0, now - self._last_update)
|
|
17
|
+
self._last_update = now
|
|
18
|
+
self.budget = min(
|
|
19
|
+
float(congestion_window),
|
|
20
|
+
self.budget + elapsed * pacing_rate,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def time_until_send(
|
|
24
|
+
self,
|
|
25
|
+
amount: int,
|
|
26
|
+
*,
|
|
27
|
+
now: float,
|
|
28
|
+
pacing_rate: float,
|
|
29
|
+
congestion_window: int,
|
|
30
|
+
) -> float | None:
|
|
31
|
+
self.refresh(
|
|
32
|
+
now=now,
|
|
33
|
+
pacing_rate=pacing_rate,
|
|
34
|
+
congestion_window=congestion_window,
|
|
35
|
+
)
|
|
36
|
+
if float(amount) <= self.budget:
|
|
37
|
+
return 0.0
|
|
38
|
+
if pacing_rate <= 0:
|
|
39
|
+
return None
|
|
40
|
+
return max(0.0, (float(amount) - self.budget) / pacing_rate)
|
|
41
|
+
|
|
42
|
+
def spend(
|
|
43
|
+
self,
|
|
44
|
+
amount: int,
|
|
45
|
+
*,
|
|
46
|
+
now: float,
|
|
47
|
+
pacing_rate: float,
|
|
48
|
+
congestion_window: int,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.refresh(
|
|
51
|
+
now=now,
|
|
52
|
+
pacing_rate=pacing_rate,
|
|
53
|
+
congestion_window=congestion_window,
|
|
54
|
+
)
|
|
55
|
+
self.budget = max(0.0, self.budget - float(amount))
|
|
56
|
+
|
|
57
|
+
def refund(
|
|
58
|
+
self,
|
|
59
|
+
amount: int,
|
|
60
|
+
*,
|
|
61
|
+
now: float,
|
|
62
|
+
pacing_rate: float,
|
|
63
|
+
congestion_window: int,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.refresh(
|
|
66
|
+
now=now,
|
|
67
|
+
pacing_rate=pacing_rate,
|
|
68
|
+
congestion_window=congestion_window,
|
|
69
|
+
)
|
|
70
|
+
self.budget = min(float(congestion_window), self.budget + float(amount))
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
|
|
6
|
+
from tigrcorn_quic_cc import (
|
|
7
|
+
AckReceived,
|
|
8
|
+
CongestionController,
|
|
9
|
+
CongestionControllerFactory,
|
|
10
|
+
ControllerContext,
|
|
11
|
+
ControllerSnapshot,
|
|
12
|
+
EcnFeedback,
|
|
13
|
+
MtuUpdated,
|
|
14
|
+
PacketSent,
|
|
15
|
+
PacketsLost,
|
|
16
|
+
PersistentCongestion,
|
|
17
|
+
SendLimits,
|
|
18
|
+
validate_factory,
|
|
19
|
+
validate_send_limits,
|
|
20
|
+
)
|
|
21
|
+
from tigrcorn_quic_cc_reno import factory as reno_factory
|
|
22
|
+
|
|
23
|
+
from .pacer import QuicPacer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class QuicCongestionRuntime:
|
|
27
|
+
"""Validates provider policy and enforces it through a transport pacer."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
max_datagram_size: int,
|
|
33
|
+
factory: CongestionControllerFactory | None = None,
|
|
34
|
+
options: Mapping[str, object] | None = None,
|
|
35
|
+
options_validated: bool = False,
|
|
36
|
+
clock: Callable[[], float] | None = None,
|
|
37
|
+
max_ack_delay: float = 0.025,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.clock = clock or time.monotonic
|
|
40
|
+
self.factory = validate_factory(factory or reno_factory)
|
|
41
|
+
self.options = (
|
|
42
|
+
dict(options or {})
|
|
43
|
+
if options_validated
|
|
44
|
+
else self.factory.validate_options(options or {})
|
|
45
|
+
)
|
|
46
|
+
self.max_datagram_size = max_datagram_size
|
|
47
|
+
self.controller: CongestionController = self.factory.create(
|
|
48
|
+
ControllerContext(
|
|
49
|
+
max_datagram_size=max_datagram_size,
|
|
50
|
+
max_ack_delay=max_ack_delay,
|
|
51
|
+
),
|
|
52
|
+
self.options,
|
|
53
|
+
clock=self.clock,
|
|
54
|
+
)
|
|
55
|
+
initial = self._validated_limits(self.clock())
|
|
56
|
+
self.pacer = QuicPacer(initial_budget=float(initial.congestion_window))
|
|
57
|
+
self.failed = False
|
|
58
|
+
self.failure_reason: str | None = None
|
|
59
|
+
self._compat_window_override: int | None = None
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def metadata(self):
|
|
63
|
+
return self.factory.metadata
|
|
64
|
+
|
|
65
|
+
def _fail(self, exc: BaseException) -> None:
|
|
66
|
+
self.failed = True
|
|
67
|
+
self.failure_reason = type(exc).__name__
|
|
68
|
+
|
|
69
|
+
def _validated_limits(self, now: float) -> SendLimits:
|
|
70
|
+
return validate_send_limits(
|
|
71
|
+
self.controller.send_limits(now),
|
|
72
|
+
max_datagram_size=self.max_datagram_size,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def limits(self, *, now: float | None = None) -> SendLimits | None:
|
|
76
|
+
if self.failed:
|
|
77
|
+
return None
|
|
78
|
+
at = self.clock() if now is None else now
|
|
79
|
+
try:
|
|
80
|
+
limits = self._validated_limits(at)
|
|
81
|
+
except Exception as exc: # noqa: BLE001 - provider boundary must fail closed
|
|
82
|
+
self._fail(exc)
|
|
83
|
+
return None
|
|
84
|
+
if self._compat_window_override is None:
|
|
85
|
+
return limits
|
|
86
|
+
return SendLimits(
|
|
87
|
+
congestion_window=max(0, self._compat_window_override),
|
|
88
|
+
pacing_rate=limits.pacing_rate,
|
|
89
|
+
send_quantum=min(limits.send_quantum, max(1, self._compat_window_override)),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def congestion_window(self) -> int:
|
|
94
|
+
limits = self.limits()
|
|
95
|
+
return 0 if limits is None else limits.congestion_window
|
|
96
|
+
|
|
97
|
+
@congestion_window.setter
|
|
98
|
+
def congestion_window(self, value: int) -> None:
|
|
99
|
+
self._compat_window_override = int(value)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def pacing_rate(self) -> float:
|
|
103
|
+
limits = self.limits()
|
|
104
|
+
return 0.0 if limits is None else limits.pacing_rate
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def pacing_budget(self) -> float:
|
|
108
|
+
return self.pacer.budget
|
|
109
|
+
|
|
110
|
+
@pacing_budget.setter
|
|
111
|
+
def pacing_budget(self, value: float) -> None:
|
|
112
|
+
self.pacer.budget = max(0.0, float(value))
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def ssthresh(self) -> int:
|
|
116
|
+
if self.failed:
|
|
117
|
+
return 2**31 - 1
|
|
118
|
+
try:
|
|
119
|
+
value = self.controller.snapshot().measurements.get("ssthresh")
|
|
120
|
+
except Exception as exc: # noqa: BLE001 - provider boundary must fail closed
|
|
121
|
+
self._fail(exc)
|
|
122
|
+
return 2**31 - 1
|
|
123
|
+
return int(value) if isinstance(value, int) else 2**31 - 1
|
|
124
|
+
|
|
125
|
+
def _dispatch(self, method: str, event: object) -> None:
|
|
126
|
+
if self.failed:
|
|
127
|
+
return
|
|
128
|
+
try:
|
|
129
|
+
getattr(self.controller, method)(event)
|
|
130
|
+
except Exception as exc: # noqa: BLE001 - provider boundary must fail closed
|
|
131
|
+
self._fail(exc)
|
|
132
|
+
|
|
133
|
+
def on_packet_sent(self, event: PacketSent) -> None:
|
|
134
|
+
self._dispatch("on_packet_sent", event)
|
|
135
|
+
|
|
136
|
+
def on_ack_received(self, event: AckReceived) -> None:
|
|
137
|
+
self._dispatch("on_ack_received", event)
|
|
138
|
+
|
|
139
|
+
def on_packets_lost(self, event: PacketsLost) -> None:
|
|
140
|
+
self._dispatch("on_packets_lost", event)
|
|
141
|
+
|
|
142
|
+
def on_persistent_congestion(self, event: PersistentCongestion) -> None:
|
|
143
|
+
self._dispatch("on_persistent_congestion", event)
|
|
144
|
+
|
|
145
|
+
def on_ecn_feedback(self, event: EcnFeedback) -> None:
|
|
146
|
+
self._dispatch("on_ecn_feedback", event)
|
|
147
|
+
|
|
148
|
+
def update_mtu(self, max_datagram_size: int, *, now: float | None = None) -> None:
|
|
149
|
+
self.max_datagram_size = max_datagram_size
|
|
150
|
+
self._dispatch(
|
|
151
|
+
"on_mtu_updated",
|
|
152
|
+
MtuUpdated(
|
|
153
|
+
now=self.clock() if now is None else now,
|
|
154
|
+
max_datagram_size=max_datagram_size,
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def can_send(self, amount: int, *, bytes_in_flight: int, now: float) -> bool:
|
|
159
|
+
limits = self.limits(now=now)
|
|
160
|
+
if limits is None or bytes_in_flight + amount > limits.congestion_window:
|
|
161
|
+
return False
|
|
162
|
+
return self.pacer.time_until_send(
|
|
163
|
+
amount,
|
|
164
|
+
now=now,
|
|
165
|
+
pacing_rate=limits.pacing_rate,
|
|
166
|
+
congestion_window=limits.congestion_window,
|
|
167
|
+
) == 0.0
|
|
168
|
+
|
|
169
|
+
def time_until_send(
|
|
170
|
+
self,
|
|
171
|
+
amount: int,
|
|
172
|
+
*,
|
|
173
|
+
bytes_in_flight: int,
|
|
174
|
+
now: float,
|
|
175
|
+
) -> float | None:
|
|
176
|
+
limits = self.limits(now=now)
|
|
177
|
+
if limits is None or bytes_in_flight + amount > limits.congestion_window:
|
|
178
|
+
return None
|
|
179
|
+
return self.pacer.time_until_send(
|
|
180
|
+
amount,
|
|
181
|
+
now=now,
|
|
182
|
+
pacing_rate=limits.pacing_rate,
|
|
183
|
+
congestion_window=limits.congestion_window,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def spend(self, amount: int, *, now: float) -> None:
|
|
187
|
+
limits = self.limits(now=now)
|
|
188
|
+
if limits is None:
|
|
189
|
+
return
|
|
190
|
+
self.pacer.spend(
|
|
191
|
+
amount,
|
|
192
|
+
now=now,
|
|
193
|
+
pacing_rate=limits.pacing_rate,
|
|
194
|
+
congestion_window=limits.congestion_window,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def refund(self, amount: int, *, now: float) -> None:
|
|
198
|
+
limits = self.limits(now=now)
|
|
199
|
+
if limits is None:
|
|
200
|
+
return
|
|
201
|
+
self.pacer.refund(
|
|
202
|
+
amount,
|
|
203
|
+
now=now,
|
|
204
|
+
pacing_rate=limits.pacing_rate,
|
|
205
|
+
congestion_window=limits.congestion_window,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def snapshot(self) -> ControllerSnapshot:
|
|
209
|
+
if not self.failed:
|
|
210
|
+
try:
|
|
211
|
+
return self.controller.snapshot()
|
|
212
|
+
except Exception as exc: # noqa: BLE001 - provider boundary must fail closed
|
|
213
|
+
self._fail(exc)
|
|
214
|
+
return ControllerSnapshot(
|
|
215
|
+
algorithm=self.metadata.algorithm_id,
|
|
216
|
+
mode="failed_closed",
|
|
217
|
+
congestion_window=0,
|
|
218
|
+
pacing_rate=0.0,
|
|
219
|
+
send_quantum=0,
|
|
220
|
+
measurements={"failure_reason": self.failure_reason},
|
|
221
|
+
)
|