Skip to content

Commit 64fac8b

Browse files
committed
Added more demos
1 parent 977eba3 commit 64fac8b

10 files changed

Lines changed: 1050 additions & 11 deletions

File tree

docs/source/rfc-gap-analysis.rst

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ Cross-RFC snapshot
3131
| RFC 9621 | partial | Core architecture is now recognizably aligned: event-driven |
3232
| | | API, message-oriented transfer, connection groups, shared |
3333
| | | connection contexts, cached state, and monitoring snapshots |
34-
| | | all exist. The main remaining gaps are depth: richer |
35-
| | | monitoring semantics and broader use of cached state and |
36-
| | | policy during ongoing connection management. |
34+
| | | all exist. Shared contexts now also track object lifecycle, |
35+
| | | recent operational events, health summaries, adaptive |
36+
| | | policy signals, and monitoring subscriptions. The main |
37+
| | | remaining gaps are breadth and external policy sources |
38+
| | | rather than missing architectural structure. |
3739
+----------+-------------+-------------------------------------------------------------+
3840
| RFC 9622 | partial | The repo now has a large subset of the abstract API: |
3941
| | | preconnections, listeners, connections, groups, |
@@ -69,11 +71,15 @@ RFC 9621 checklist
6971
+---------------------------------------------+-------------+--------------------------------------------------------------+
7072
| Selection between equivalent protocol | partial | Property-driven selection, cached protocol/path history, |
7173
| stacks | | and dynamic policy inputs exist; the eligible transport set |
72-
| | | is still relatively small and some policy is heuristic. |
74+
| | | is still relatively small and some policy is still |
75+
| | | heuristic/backend-limited. |
7376
+---------------------------------------------+-------------+--------------------------------------------------------------+
74-
| Monitoring support | partial | Event history, read-only properties, connection-context |
75-
| | | snapshots, and re-establishment advice are exposed, but the |
76-
| | | RFC's broader monitoring intent is not fully covered. |
77+
| Monitoring support | partial | Event history, read-only properties, shared monitoring |
78+
| | | snapshots, recent context-level events, lifecycle counts, |
79+
| | | health summaries, adaptive policy views, monitoring |
80+
| | | subscriptions, and re-establishment advice are exposed. |
81+
| | | The RFC's broader monitoring intent is still only partially |
82+
| | | covered. |
7783
+---------------------------------------------+-------------+--------------------------------------------------------------+
7884
| Preestablishment and establishment actions | partial | ``Initiate``, ``InitiateWithSend``, ``Listen``, and |
7985
| | | ``Rendezvous`` all exist, though rendezvous is still a |
@@ -91,7 +97,8 @@ RFC 9621 checklist
9197
| | | and broader transport diversity are still limited. |
9298
+---------------------------------------------+-------------+--------------------------------------------------------------+
9399
| Separating connection contexts | implemented | Shared ``ConnectionContext`` objects now exist and can be |
94-
| | | cloned/forked to isolate cached state between groups. |
100+
| | | cloned/forked to isolate cached state between groups, while |
101+
| | | preserving shared monitoring/accounting where appropriate. |
95102
+---------------------------------------------+-------------+--------------------------------------------------------------+
96103

97104
RFC 9622 checklist

examples/demo_suite/README.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# PyTAPS Feature Demos
2+
3+
These demos exercise the newer RFC-facing PyTAPS features: candidate selection,
4+
message contexts, batching, expiration, monitoring subscriptions, adaptive
5+
policy snapshots, re-establishment guidance, rendezvous, and multicast.
6+
7+
Run all commands from the repository root with the virtualenv active:
8+
9+
```bash
10+
source .venv/bin/activate
11+
```
12+
13+
## 1. Feature Echo Between Local And Linode
14+
15+
On the Linode, open the demo port in the firewall/security group, then run:
16+
17+
```bash
18+
./.venv/bin/python examples/demo_suite/featureServer.py \
19+
--local-address 0.0.0.0 \
20+
--port 7777 \
21+
--transport auto
22+
```
23+
24+
On the local machine, replace `LINODE_IP` with the Linode's public address:
25+
26+
```bash
27+
./.venv/bin/python examples/demo_suite/featureClient.py \
28+
--remote-address LINODE_IP \
29+
--port 7777 \
30+
--transport auto \
31+
--payload "hello from local" \
32+
--batch-size 3 \
33+
--degrade-path
34+
```
35+
36+
What to look for:
37+
38+
- the selected protocol in the client `ready` log
39+
- `sent`, `expired`, and receive metadata logs
40+
- monitoring subscription updates on both sides
41+
- `reestablishment advice` after `--degrade-path`
42+
43+
## 2. Force UDP Message Semantics
44+
45+
On the Linode:
46+
47+
```bash
48+
./.venv/bin/python examples/demo_suite/featureServer.py \
49+
--local-address 0.0.0.0 \
50+
--port 7778 \
51+
--transport udp
52+
```
53+
54+
On the local machine:
55+
56+
```bash
57+
./.venv/bin/python examples/demo_suite/featureClient.py \
58+
--remote-address LINODE_IP \
59+
--port 7778 \
60+
--transport udp \
61+
--payload "udp message demo" \
62+
--batch-size 2
63+
```
64+
65+
This path exercises `safelyReplayable`, datagram message boundaries, and the
66+
UDP send/receive path.
67+
68+
## 3. Rendezvous
69+
70+
Rendezvous needs both hosts to be able to connect to each other on the selected
71+
port. This is easiest with public IPv6 or with explicit firewall/NAT forwarding
72+
on both sides.
73+
74+
On the Linode:
75+
76+
```bash
77+
./.venv/bin/python examples/demo_suite/rendezvousPeer.py \
78+
--local-address LINODE_IP \
79+
--local-port 7788 \
80+
--remote-address LOCAL_REACHABLE_IP \
81+
--remote-port 7788 \
82+
--payload "hello from linode"
83+
```
84+
85+
On the local machine:
86+
87+
```bash
88+
./.venv/bin/python examples/demo_suite/rendezvousPeer.py \
89+
--local-address LOCAL_REACHABLE_IP \
90+
--local-port 7788 \
91+
--remote-address LINODE_IP \
92+
--remote-port 7788 \
93+
--payload "hello from local"
94+
```
95+
96+
What to look for:
97+
98+
- `rendezvous done` logs
99+
- `RendezvousResult` completion state
100+
- passive-side `connection_received` logs
101+
- shared monitoring updates
102+
103+
## 4. Multicast Send/Receive
104+
105+
The multicast demos live in `examples/multicast_example` and use PyTAPS on both
106+
the sender and receiver paths.
107+
108+
Receiver:
109+
110+
```bash
111+
./.venv/bin/python examples/multicast_example/multicastReceiver.py \
112+
--group ff3e::8000:1234 \
113+
--source SOURCE_IP \
114+
--port 5001 \
115+
--interface-address RECEIVER_INTERFACE_IP
116+
```
117+
118+
Sender:
119+
120+
```bash
121+
./.venv/bin/python examples/multicast_example/multicastSender.py \
122+
--group ff3e::8000:1234 \
123+
--port 5001 \
124+
--payload hello-v6 \
125+
--count 5 \
126+
--interval-ms 100 \
127+
--source SOURCE_IP \
128+
--interface-address SENDER_INTERFACE_IP
129+
```
130+
131+
Install the optional multicast bindings first if needed:
132+
133+
```bash
134+
python -m pip install -e /Users/mfranke/Devtools/Multicast/mcrx-core/mcrx-core-py
135+
python -m pip install -e /Users/mfranke/Devtools/Multicast/mctx-core/mctx-core-py
136+
```
137+
138+
## 5. Optional QUIC
139+
140+
If `aioquic` is installed, the `auto` demos include QUIC stream candidates.
141+
Without `aioquic`, QUIC candidates are skipped cleanly and the demos continue
142+
with the available transports.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import argparse
2+
import asyncio
3+
import sys
4+
from pathlib import Path
5+
6+
ROOT = Path(__file__).resolve().parents[2]
7+
if str(ROOT) not in sys.path:
8+
sys.path.insert(0, str(ROOT))
9+
10+
import pytaps as taps # noqa: E402
11+
12+
13+
logger = taps.setup_logger("TAPS Feature Client", "yellow")
14+
15+
16+
def build_remote_endpoint(args):
17+
remote = taps.RemoteEndpoint()
18+
if args.remote_address:
19+
remote.with_address(args.remote_address)
20+
else:
21+
remote.with_hostname(args.remote_host)
22+
remote.with_port(args.port)
23+
return remote
24+
25+
26+
def build_local_endpoint(args):
27+
if not args.local_address and not args.interface and args.local_port is None:
28+
return None
29+
local = taps.LocalEndpoint()
30+
if args.local_address:
31+
local.with_address(args.local_address)
32+
if args.interface:
33+
local.with_interface(args.interface)
34+
if args.local_port is not None:
35+
local.with_port(args.local_port)
36+
return local
37+
38+
39+
def build_transport_properties(args):
40+
properties = taps.TransportProperties()
41+
properties.ignore("congestionControl")
42+
properties.ignore("preserveOrder")
43+
properties.prefer("multistreaming")
44+
properties.prefer("zeroRttMsg")
45+
properties.set_property("connPriority", args.priority)
46+
if args.transport == "udp":
47+
properties.prohibit("reliability")
48+
properties.require("preserveMsgBoundaries")
49+
elif args.transport == "tcp":
50+
properties.require("reliability")
51+
properties.ignore("preserveMsgBoundaries")
52+
else:
53+
properties.ignore("reliability")
54+
return properties
55+
56+
57+
class FeatureClient:
58+
def __init__(self):
59+
self.connection = None
60+
self.received = []
61+
62+
async def handle_monitoring_update(self, update):
63+
health = update["snapshot"]["healthSummary"]
64+
logger.info(
65+
"monitor trigger=%s severity=%s guidance=%s",
66+
update["trigger"],
67+
health["severity"],
68+
update["snapshot"]["operationalGuidance"],
69+
)
70+
71+
async def handle_sent(self, context, connection):
72+
logger.info("sent message_id=%s props=%s", context.message_id, context.get_properties())
73+
74+
async def handle_send_error(self, context, reason, connection):
75+
logger.warning("send error message_id=%s reason=%s", context.message_id, reason)
76+
77+
async def handle_expired(self, context, connection):
78+
logger.info("expired message_id=%s lifetime=%s", context.message_id, context.lifetime)
79+
80+
async def handle_received(self, data, context, connection):
81+
logger.info(
82+
"received echo bytes=%s seq=%s props=%s",
83+
len(data),
84+
context.receive_sequence,
85+
context.get_properties(),
86+
)
87+
self.received.append(data)
88+
89+
async def handle_reestablishment_suggested(self, advice, candidates, connection):
90+
logger.info("reestablishment advice=%s candidate_count=%s", advice, len(candidates))
91+
92+
async def main(self, args):
93+
remote = build_remote_endpoint(args)
94+
local = build_local_endpoint(args)
95+
properties = build_transport_properties(args)
96+
preconnection = taps.Preconnection(
97+
local_endpoint=local,
98+
remote_endpoint=remote,
99+
transport_properties=properties,
100+
)
101+
preconnection.subscribe_monitoring(self.handle_monitoring_update)
102+
preconnection.set_address_family_policy(args.prefer_family, preference_adjustment=2)
103+
if args.avoid_protocol:
104+
preconnection.set_protocol_policy(
105+
args.avoid_protocol,
106+
available=True,
107+
preference_adjustment=-4,
108+
racing_cooldown=10,
109+
)
110+
if args.alternate_remote:
111+
preconnection.note_alternate_remote(
112+
args.remote_address or args.remote_host,
113+
args.alternate_remote,
114+
protocol="quic",
115+
)
116+
117+
first_context = taps.MessageContext(
118+
priority=10,
119+
safely_replayable=True,
120+
lifetime=args.lifetime,
121+
final=False,
122+
)
123+
self.connection = await preconnection.initiate_with_send(
124+
args.payload,
125+
first_context,
126+
)
127+
await self.connection.wait_ready(timeout=args.timeout)
128+
self.connection.on_sent(self.handle_sent)
129+
self.connection.on_send_error(self.handle_send_error)
130+
self.connection.on_expired(self.handle_expired)
131+
self.connection.on_received(self.handle_received)
132+
self.connection.on_reestablishment_suggested(self.handle_reestablishment_suggested)
133+
self.connection.subscribe_monitoring(self.handle_monitoring_update)
134+
135+
logger.info(
136+
"ready protocol=%s read_only=%s",
137+
self.connection.protocol,
138+
self.connection.get_properties()["readOnly"],
139+
)
140+
141+
await self.connection.receive(min_incomplete_length=1, max_length=4096, timeout=args.timeout)
142+
143+
batch = []
144+
for idx in range(args.batch_size):
145+
context = self.connection.new_message_context(
146+
msgPriority=idx,
147+
safelyReplayable=self.connection.protocol == "udp",
148+
final=False,
149+
)
150+
batch.append((f"{args.payload}-{idx}".encode(), context, True))
151+
await self.connection.send_batch(batch)
152+
153+
expired = self.connection.new_message_context(
154+
msgLifetime=0,
155+
safelyReplayable=self.connection.protocol == "udp",
156+
final=False,
157+
)
158+
await self.connection.send(b"this message should expire", expired)
159+
160+
if args.degrade_path:
161+
self.connection.note_soft_error(
162+
"demo path degradation",
163+
penalty=4,
164+
lifetime=120,
165+
)
166+
167+
await asyncio.sleep(args.settle_time)
168+
snapshot = self.connection.get_monitoring_snapshot()
169+
logger.info("monitoring snapshot=%s", snapshot)
170+
self.connection.close()
171+
await self.connection.wait_closed(timeout=args.timeout)
172+
173+
174+
def parse_args():
175+
parser = argparse.ArgumentParser(
176+
description="PyTAPS feature demo client for racing, messages, and monitoring."
177+
)
178+
parser.add_argument("--remote-host", default="localhost")
179+
parser.add_argument("--remote-address", default=None)
180+
parser.add_argument("--port", type=int, default=7777)
181+
parser.add_argument("--local-address", default=None)
182+
parser.add_argument("--local-port", type=int, default=None)
183+
parser.add_argument("--interface", default=None)
184+
parser.add_argument("--payload", default="hello taps")
185+
parser.add_argument("--batch-size", type=int, default=3)
186+
parser.add_argument("--priority", type=int, default=50)
187+
parser.add_argument("--lifetime", type=float, default=None)
188+
parser.add_argument("--timeout", type=float, default=5.0)
189+
parser.add_argument("--settle-time", type=float, default=0.25)
190+
parser.add_argument("--prefer-family", choices=["ipv4", "ipv6"], default="ipv6")
191+
parser.add_argument("--avoid-protocol", default=None)
192+
parser.add_argument("--alternate-remote", default=None)
193+
parser.add_argument("--degrade-path", action="store_true")
194+
parser.add_argument(
195+
"--transport",
196+
choices=["auto", "tcp", "udp"],
197+
default="auto",
198+
)
199+
return parser.parse_args()
200+
201+
202+
if __name__ == "__main__":
203+
asyncio.run(FeatureClient().main(parse_args()))

0 commit comments

Comments
 (0)