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.
Files changed (87) hide show
  1. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/PKG-INFO +7 -4
  2. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/README.md +1 -1
  3. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/pyproject.toml +12 -3
  4. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/listeners/udp.py +238 -0
  5. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/congestion/__init__.py +4 -0
  6. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/congestion/pacer.py +70 -0
  7. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/congestion/runtime.py +221 -0
  8. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/base.py +42 -4
  9. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/core.py +36 -2
  10. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/imports.py +11 -2
  11. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/model.py +1 -0
  12. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/receive.py +24 -1
  13. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/connection/recovery_policy.py +61 -0
  14. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/runtime.py +22 -22
  15. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/send.py +198 -7
  16. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/crypto/aes.py +13 -0
  17. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/crypto/gcm.py +34 -0
  18. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/flow.py +35 -8
  19. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/recovery/__init__.py +27 -0
  20. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/recovery/congestion.py +83 -0
  21. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/recovery/core.py +80 -74
  22. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/recovery/evidence.py +3 -2
  23. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/recovery/model.py +1 -0
  24. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports/quic/recovery/policy.py +118 -0
  25. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/codec.py +21 -0
  26. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/constants.py +1 -0
  27. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/frames.py +13 -0
  28. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/labels.py +6 -0
  29. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/manager.py +7 -1
  30. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports.egg-info/PKG-INFO +7 -4
  31. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports.egg-info/SOURCES.txt +6 -0
  32. tigrcorn_transports-0.3.18.dev3/src/tigrcorn_transports.egg-info/requires.txt +5 -0
  33. tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports/listeners/udp.py +0 -82
  34. tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports/quic/crypto/aes.py +0 -134
  35. tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports/quic/crypto/gcm.py +0 -100
  36. tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports/quic/recovery/__init__.py +0 -14
  37. tigrcorn_transports-0.3.18.dev2/src/tigrcorn_transports.egg-info/requires.txt +0 -2
  38. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/LICENSE +0 -0
  39. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/setup.cfg +0 -0
  40. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/__init__.py +0 -0
  41. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/base.py +0 -0
  42. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/inproc/__init__.py +0 -0
  43. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/inproc/channel.py +0 -0
  44. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/__init__.py +0 -0
  45. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/base.py +0 -0
  46. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/inproc.py +0 -0
  47. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/pipe.py +0 -0
  48. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/registry.py +0 -0
  49. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/tcp.py +0 -0
  50. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/listeners/unix.py +0 -0
  51. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/pipe/__init__.py +0 -0
  52. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/pipe/connection.py +0 -0
  53. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/py.typed +0 -0
  54. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/__init__.py +0 -0
  55. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/__init__.py +0 -0
  56. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/decode.py +0 -0
  57. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/connection/loss_api.py +0 -0
  58. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/crypto/__init__.py +0 -0
  59. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/crypto/keys.py +0 -0
  60. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/crypto/packets.py +0 -0
  61. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/datagrams.py +0 -0
  62. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/handshake.py +0 -0
  63. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/packets.py +0 -0
  64. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/scheduler.py +0 -0
  65. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/security/__init__.py +0 -0
  66. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/security/certification.py +0 -0
  67. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/security/helpers.py +0 -0
  68. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/security/runtime.py +0 -0
  69. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/__init__.py +0 -0
  70. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/streams/state.py +0 -0
  71. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/quic/tls_adapter.py +0 -0
  72. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/registry.py +0 -0
  73. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/__init__.py +0 -0
  74. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/accept.py +0 -0
  75. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/connection.py +0 -0
  76. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/reader.py +0 -0
  77. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/socketopts.py +0 -0
  78. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/tls.py +0 -0
  79. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/tcp/writer.py +0 -0
  80. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/udp/__init__.py +0 -0
  81. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/udp/endpoint.py +0 -0
  82. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/udp/packet.py +0 -0
  83. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/udp/socketopts.py +0 -0
  84. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/unix/__init__.py +0 -0
  85. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports/unix/connection.py +0 -0
  86. {tigrcorn_transports-0.3.18.dev2 → tigrcorn_transports-0.3.18.dev3}/src/tigrcorn_transports.egg-info/dependency_links.txt +0 -0
  87. {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.dev2
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: tigrcorn-core==0.3.18.dev2
190
- Requires-Dist: tigrcorn-config==0.3.18.dev2
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.dev2"
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
- "tigrcorn-core==0.3.18.dev2",
33
- "tigrcorn-config==0.3.18.dev2",
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,4 @@
1
+ from .pacer import QuicPacer
2
+ from .runtime import QuicCongestionRuntime
3
+
4
+ __all__ = ["QuicCongestionRuntime", "QuicPacer"]
@@ -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
+ )