While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
427 lines
21 KiB
Python
427 lines
21 KiB
Python
"""A Connect's sequence says whether it repeats the one a session accepted, and
|
|
the handshake alone seeds the watermark that orders delivery."""
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from dataclasses import replace
|
|
|
|
from cultnet_py.transport import (
|
|
CultNetRudpPacketType,
|
|
CultNetRudpSendOptions,
|
|
CultNetRudpSession,
|
|
CultNetRudpSessionOptions,
|
|
CultNetRudpSocketMode,
|
|
CultNetRudpSocketTransportOptions,
|
|
)
|
|
|
|
CONNECTION_ID = 0x10203090
|
|
|
|
|
|
def session(initial_sequence: int) -> CultNetRudpSession:
|
|
return CultNetRudpSession(CultNetRudpSessionOptions(connection_id=CONNECTION_ID, initial_sequence=initial_sequence))
|
|
|
|
|
|
def send(from_session: CultNetRudpSession, text: str, *, ordered: bool = True, channel: str = "schema"):
|
|
return from_session.send(channel, text.encode(), CultNetRudpSendOptions(reliable=True, ordered=ordered))
|
|
|
|
|
|
def names(result) -> list[str]:
|
|
return [frame.payload.decode() for frame in result.delivered]
|
|
|
|
|
|
def handshake(client: CultNetRudpSession, server: CultNetRudpSession):
|
|
connect = client.create_connect(0)
|
|
accept = server.accept_connect(connect, 0)
|
|
client.receive(accept, 0)
|
|
assert client.connected and server.connected
|
|
return connect, accept
|
|
|
|
|
|
class CultNetRudpConnectGenerationTests(unittest.TestCase):
|
|
def test_a_reconnect_on_the_same_session_is_not_stranded_by_a_write_the_ended_session_owed(self) -> None:
|
|
client, server = session(1), session(500)
|
|
handshake(client, server)
|
|
send(server, "lost")
|
|
server.create_disconnect(b"gone")
|
|
|
|
client.receive(server.accept_connect(client.create_connect(1), 1), 1)
|
|
self.assertEqual(names(client.receive(send(server, "after"), 2)), ["after"])
|
|
|
|
def test_a_server_that_accepts_a_new_client_forgets_the_old_clients_sequences(self) -> None:
|
|
old_client, server = session(50), session(500)
|
|
handshake(old_client, server)
|
|
for name in ("c1", "c2", "c3"):
|
|
self.assertEqual(names(server.receive(send(old_client, name), 1)), [name])
|
|
server.create_disconnect(b"restart")
|
|
|
|
new_client = session(2)
|
|
new_client.receive(server.accept_connect(new_client.create_connect(2), 2), 2)
|
|
self.assertEqual(names(server.receive(send(new_client, "fresh"), 3)), ["fresh"])
|
|
|
|
def test_a_retransmitted_connect_after_data_has_flowed_does_not_reset_the_session(self) -> None:
|
|
client, server = session(1), session(500)
|
|
connect, _ = handshake(client, server)
|
|
frame = send(client, "once")
|
|
self.assertEqual(names(server.receive(frame, 1)), ["once"])
|
|
|
|
self.assertTrue(server.connect_repeats(connect))
|
|
self.assertEqual(server.accept_connect(connect, 2).packet_type, CultNetRudpPacketType.ACK)
|
|
self.assertEqual(server.outstanding_reliable_packet_count, 0, "a repeat queued something")
|
|
self.assertEqual(
|
|
names(server.receive(frame, 3)), [], "a retransmitted Connect made the server forget what it had delivered"
|
|
)
|
|
|
|
def test_a_connect_with_another_sequence_is_not_a_repeat_whatever_it_carries(self) -> None:
|
|
server = session(500)
|
|
connect = session(1).create_connect(0, b"same payload")
|
|
server.accept_connect(connect, 0)
|
|
self.assertTrue(server.connect_repeats(connect))
|
|
|
|
self.assertFalse(server.connect_repeats(session(9).create_connect(0, b"same payload")))
|
|
self.assertFalse(server.connect_repeats(replace(connect, packet_type=CultNetRudpPacketType.PING)))
|
|
server.create_disconnect()
|
|
self.assertFalse(server.connect_repeats(connect), "an ended session repeats nothing")
|
|
|
|
def test_an_accept_that_does_not_name_the_pending_connect_is_ignored(self) -> None:
|
|
client, server = session(10), session(500)
|
|
connect = client.create_connect(0)
|
|
accept = server.accept_connect(connect, 0)
|
|
|
|
client.receive(replace(accept, ack=connect.sequence + 50, ack_mask=0), 1)
|
|
self.assertFalse(client.connected, "a stale Accept connected the session")
|
|
self.assertEqual(client.pending_reliable_sequences, (connect.sequence,))
|
|
|
|
client.receive(replace(accept, ack=connect.sequence + 3, ack_mask=1 << 2), 2)
|
|
self.assertTrue(client.connected, "the mask names the Connect")
|
|
|
|
def test_an_accept_after_a_local_disconnect_does_not_reconnect_the_session(self) -> None:
|
|
client, server = session(1), session(500)
|
|
_, accept = handshake(client, server)
|
|
client.create_disconnect(b"bye")
|
|
self.assertFalse(client.connected)
|
|
|
|
client.receive(accept, 5)
|
|
self.assertFalse(client.connected, "a late Accept revived an ended session")
|
|
|
|
def test_a_reconnect_forgets_what_the_old_server_sent(self) -> None:
|
|
client, old_server = session(1), session(500)
|
|
handshake(client, old_server)
|
|
for name in ("d1", "d2", "d3"):
|
|
self.assertEqual(names(client.receive(send(old_server, name), 1)), [name])
|
|
|
|
new_server = session(501)
|
|
client.receive(new_server.accept_connect(client.create_connect(2), 2), 2)
|
|
self.assertEqual(names(client.receive(send(new_server, "fresh"), 3)), ["fresh"])
|
|
|
|
def test_an_accept_for_an_abandoned_connect_does_not_connect_the_session(self) -> None:
|
|
client, server = session(1), session(500)
|
|
accept = server.accept_connect(client.create_connect(0), 0)
|
|
client.create_disconnect(b"never mind")
|
|
|
|
client.receive(accept, 1)
|
|
self.assertFalse(client.connected, "an Accept revived an abandoned Connect")
|
|
|
|
def test_a_full_queue_owed_to_a_vanished_peer_does_not_refuse_a_new_generation(self) -> None:
|
|
def bounded(initial_sequence: int) -> CultNetRudpSession:
|
|
return CultNetRudpSession(
|
|
CultNetRudpSessionOptions(
|
|
connection_id=CONNECTION_ID, initial_sequence=initial_sequence, max_pending_reliable_packets=1
|
|
)
|
|
)
|
|
|
|
server = bounded(500)
|
|
server.accept_connect(session(1).create_connect(0), 0)
|
|
self.assertEqual(server.outstanding_reliable_packet_count, 1, "the Accept is owed and never acknowledged")
|
|
following = session(9).create_connect(1)
|
|
server.accept_connect(following, 1)
|
|
self.assertTrue(server.connect_repeats(following))
|
|
|
|
client = bounded(1)
|
|
client.create_connect(0)
|
|
client.create_connect(1)
|
|
self.assertEqual(client.outstanding_reliable_packet_count, 1, "only the new Connect is owed")
|
|
|
|
def test_a_duplicate_accept_does_not_reseed_the_watermark(self) -> None:
|
|
client, server = session(1), session(500)
|
|
_, accept = handshake(client, server)
|
|
s1, s2 = send(server, "s1"), send(server, "s2")
|
|
client.receive(replace(accept, sequence=s2.sequence), 1)
|
|
self.assertEqual(names(client.receive(s2, 2)), [], "s1 is still missing")
|
|
self.assertEqual(names(client.receive(s1, 3)), ["s1", "s2"])
|
|
|
|
def test_reliable_data_before_the_accept_is_neither_delivered_nor_acknowledged(self) -> None:
|
|
client, server = session(1), session(500)
|
|
accept = server.accept_connect(client.create_connect(0), 0)
|
|
a, b = send(server, "A"), send(server, "B")
|
|
|
|
for early in (b, a):
|
|
self.assertEqual(names(client.receive(early, 1)), [])
|
|
ack = client.create_ack_for_received(early.sequence)
|
|
self.assertEqual((ack.ack, ack.ack_mask), (0, 0), "an unhandled packet was acknowledged")
|
|
client.receive(accept, 2)
|
|
self.assertTrue(client.connected)
|
|
self.assertEqual(server.outstanding_reliable_packet_count, 3, "nothing was acknowledged")
|
|
|
|
self.assertEqual(names(client.receive(a, 3)), ["A"])
|
|
self.assertEqual(names(client.receive(b, 3)), ["B"])
|
|
|
|
def test_ordered_frames_after_the_accept_are_delivered_in_order_whatever_arrives_first(self) -> None:
|
|
client, server = session(1), session(500)
|
|
handshake(client, server)
|
|
a, b = send(server, "A"), send(server, "B")
|
|
self.assertEqual(names(client.receive(b, 1)), [])
|
|
self.assertEqual(names(client.receive(a, 2)), ["A", "B"])
|
|
|
|
def test_an_unordered_reliable_frame_is_delivered_on_receipt_never_held_behind_a_gap(self) -> None:
|
|
client, server = session(1), session(500)
|
|
handshake(client, server)
|
|
send(server, "lost")
|
|
unordered = send(server, "now", ordered=False, channel="media")
|
|
ordered = send(server, "later")
|
|
self.assertEqual(names(client.receive(unordered, 1)), ["now"])
|
|
self.assertEqual(names(client.receive(ordered, 2)), [], "the ordered frame waits for the gap")
|
|
|
|
def test_a_peer_disconnect_does_not_forget_what_the_peer_sent(self) -> None:
|
|
client, server = session(1), session(500)
|
|
handshake(client, server)
|
|
s1, s2 = send(client, "s1"), send(client, "s2")
|
|
self.assertEqual(names(server.receive(s2, 1)), [], "s1 is missing, so s2 is held")
|
|
|
|
server.receive(client.create_disconnect(b"bye"), 2)
|
|
self.assertEqual(names(server.receive(s1, 3)), ["s1", "s2"], "the Disconnect forgot the held frame")
|
|
self.assertEqual(names(server.receive(s2, 4)), [], "the Disconnect forgot what was received")
|
|
|
|
def test_sessions_from_one_options_object_each_draw_their_own_initial_sequence(self) -> None:
|
|
self.assertIsNone(CultNetRudpSessionOptions(connection_id=1).initial_sequence)
|
|
self.assertIsNone(
|
|
CultNetRudpSocketTransportOptions(
|
|
runtime_id="r", socket=None, mode=CultNetRudpSocketMode.CLIENT, connection_id=1
|
|
).initial_sequence
|
|
)
|
|
options = CultNetRudpSessionOptions(connection_id=1)
|
|
firsts = {CultNetRudpSession(options).create_connect(0).sequence for _ in range(64)}
|
|
self.assertGreater(len(firsts), 32, "a default session does not draw its own sequence")
|
|
self.assertTrue(all(1 <= value < 2**31 for value in firsts), "a draw left [1, 2^31)")
|
|
|
|
# Ack Cut 1d: a frame from an earlier generation is a duplicate, a delayed
|
|
# Connect from an earlier attempt is stale, a Connect from a new endpoint is
|
|
# a new client.
|
|
|
|
@staticmethod
|
|
def names_sequence(ack, sequence: int) -> bool:
|
|
return ack.ack == sequence or any(
|
|
ack.ack_mask & (1 << bit) and ack.ack > bit and ack.ack - bit - 1 == sequence for bit in range(32)
|
|
)
|
|
|
|
def test_a_frame_from_the_clients_earlier_generation_is_acknowledged_not_delivered_again(self) -> None:
|
|
client, server = session(10), session(500)
|
|
handshake(client, server)
|
|
old = send(client, "old")
|
|
self.assertEqual(names(server.receive(old, 1)), ["old"])
|
|
|
|
connect = client.create_connect(2)
|
|
client.receive(server.accept_connect(connect, 2), 2)
|
|
self.assertLess(old.sequence, connect.sequence)
|
|
self.assertEqual(names(server.receive(old, 3)), [], "an earlier generation's frame was delivered again")
|
|
self.assertTrue(self.names_sequence(server.create_ack_for_received(old.sequence), old.sequence))
|
|
self.assertEqual(names(server.receive(send(client, "fresh"), 4)), ["fresh"])
|
|
|
|
def test_a_frame_from_the_servers_earlier_generation_is_acknowledged_not_delivered_again(self) -> None:
|
|
client, server = session(10), session(500)
|
|
handshake(client, server)
|
|
old = send(server, "old")
|
|
self.assertEqual(names(client.receive(old, 1)), ["old"])
|
|
|
|
client.receive(server.accept_connect(client.create_connect(2), 2), 2)
|
|
self.assertEqual(names(client.receive(old, 3)), [], "an earlier generation's frame was delivered again")
|
|
self.assertTrue(self.names_sequence(client.create_ack_for_received(old.sequence), old.sequence))
|
|
self.assertEqual(names(client.receive(send(server, "fresh"), 4)), ["fresh"])
|
|
|
|
def test_a_duplicate_below_the_receive_window_is_acknowledged_by_name(self) -> None:
|
|
client, server = session(10), session(500)
|
|
handshake(client, server)
|
|
first = send(client, "first")
|
|
self.assertEqual(names(server.receive(first, 1)), ["first"])
|
|
for _ in range(4_200):
|
|
server.receive(send(client, "x"), 1)
|
|
client.receive(server.create_ack(), 1)
|
|
self.assertEqual(names(server.receive(first, 2)), [])
|
|
self.assertEqual(
|
|
server.create_ack_for_received(first.sequence).ack,
|
|
first.sequence,
|
|
"a duplicate below the window was not acknowledged by name",
|
|
)
|
|
|
|
def test_a_delayed_connect_from_an_earlier_attempt_does_not_strand_the_client(self) -> None:
|
|
client, server = session(10), session(500)
|
|
earlier = client.create_connect(0)
|
|
retried = client.create_connect(300)
|
|
accept = server.accept_connect(retried, 301)
|
|
client.receive(accept, 302)
|
|
server.receive(client.create_ack_for_received(accept.sequence), 302)
|
|
self.assertTrue(client.connected)
|
|
a = send(client, "a")
|
|
self.assertEqual(names(server.receive(a, 303)), ["a"])
|
|
client.receive(server.create_ack_for_received(a.sequence), 303)
|
|
|
|
self.assertTrue(server.connect_repeats(earlier), "the delayed Connect starts nothing")
|
|
reply = server.accept_connect(earlier, 304)
|
|
self.assertEqual(reply.packet_type, CultNetRudpPacketType.ACK)
|
|
client.receive(reply, 305)
|
|
self.assertTrue(client.connected)
|
|
|
|
b = send(client, "b")
|
|
self.assertEqual(names(server.receive(b, 306)), ["b"], "the delayed Connect reset the server")
|
|
client.receive(server.create_ack_for_received(b.sequence), 307)
|
|
self.assertNotIn(b.sequence, client.pending_reliable_sequences)
|
|
|
|
def test_a_repeated_connect_refreshes_liveness_and_a_stale_one_does_not(self) -> None:
|
|
client, server = session(10), session(500)
|
|
earlier = client.create_connect(0)
|
|
current = client.create_connect(1)
|
|
server.accept_connect(current, 0)
|
|
|
|
server.accept_connect(current, 900)
|
|
self.assertFalse(server.check_timeout(1_000, 500), "a repeated Connect did not refresh liveness")
|
|
|
|
server.accept_connect(earlier, 1_400)
|
|
self.assertTrue(server.check_timeout(1_600, 500), "a stale Connect refreshed liveness")
|
|
|
|
def test_stale_connects_are_recognised_by_serial_arithmetic(self) -> None:
|
|
server = session(500)
|
|
server.accept_connect(session(3).create_connect(0), 0)
|
|
|
|
def connect(sequence: int):
|
|
return session(sequence).create_connect(0)
|
|
|
|
self.assertTrue(server.connect_repeats(connect(2**32 - 2)), "just before the wrap")
|
|
self.assertFalse(server.connect_repeats(connect(4)), "just after")
|
|
self.assertFalse(server.connect_repeats(connect(3 + 4_096)), "far after")
|
|
self.assertTrue(server.connect_repeats(connect(2**32 - 4_092)), "the window's edge")
|
|
self.assertFalse(server.connect_repeats(connect(2**32 - 4_093)), "past the window")
|
|
|
|
def test_a_restarted_client_whose_first_sequence_is_stale_connects_after_redrawing(self) -> None:
|
|
server, old = session(500), session(1_000)
|
|
old_accept = server.accept_connect(old.create_connect(0), 0)
|
|
old.receive(old_accept, 0)
|
|
|
|
restarted = session(900)
|
|
first = restarted.create_connect(0, b"join")
|
|
# The old client has not acknowledged its Accept yet, so that is the
|
|
# reply: it names the old Connect, not this one.
|
|
early = server.accept_connect(first, 1)
|
|
self.assertEqual(early.packet_type, CultNetRudpPacketType.ACCEPT)
|
|
restarted.receive(early, 1)
|
|
self.assertFalse(restarted.connected, "an Accept for another Connect connected the client")
|
|
server.receive(old.create_ack_for_received(old_accept.sequence), 1)
|
|
reply = server.accept_connect(first, 2)
|
|
self.assertEqual(reply.packet_type, CultNetRudpPacketType.ACK)
|
|
restarted.receive(reply, 2)
|
|
self.assertFalse(restarted.connected)
|
|
|
|
retransmitted = restarted.due_resends(1_000)
|
|
self.assertEqual(len(retransmitted), 1)
|
|
self.assertEqual(retransmitted[0].sequence, first.sequence, "the attempt is still young")
|
|
|
|
fresh = restarted.due_resends(3_500)
|
|
self.assertEqual(len(fresh), 1)
|
|
self.assertEqual(fresh[0].packet_type, CultNetRudpPacketType.CONNECT)
|
|
self.assertNotEqual(fresh[0].sequence, first.sequence, "the same Connect was retransmitted for ever")
|
|
self.assertEqual(fresh[0].payload, b"join", "the fresh attempt lost the Connect payload")
|
|
|
|
accept = server.accept_connect(fresh[0], 3_500)
|
|
self.assertEqual(accept.packet_type, CultNetRudpPacketType.ACCEPT)
|
|
restarted.receive(accept, 3_500)
|
|
self.assertTrue(restarted.connected)
|
|
self.assertEqual(names(server.receive(send(restarted, "hello"), 3_501)), ["hello"])
|
|
|
|
def test_an_answered_connect_is_never_replaced_by_the_attempt_timeout(self) -> None:
|
|
client, server = session(10), session(500)
|
|
connect, _ = handshake(client, server)
|
|
resent = client.due_resends(60_000)
|
|
self.assertTrue(
|
|
all(p.packet_type != CultNetRudpPacketType.CONNECT or p.sequence == connect.sequence for p in resent),
|
|
"a connected client started a new attempt",
|
|
)
|
|
self.assertTrue(client.connected)
|
|
|
|
def test_an_accepted_peer_that_goes_silent_times_out(self) -> None:
|
|
server = session(500)
|
|
server.accept_connect(session(10).create_connect(0), 0)
|
|
self.assertTrue(server.check_timeout(100_000, 1_000))
|
|
|
|
|
|
# Ack Cut 1d, batch 2: the connect-attempt timeout.
|
|
|
|
def test_an_ack_naming_the_pending_connect_does_not_retire_it(self) -> None:
|
|
server, old = session(500), session(1)
|
|
old_accept = server.accept_connect(old.create_connect(0), 0)
|
|
old.receive(old_accept, 0)
|
|
server.receive(old.create_ack_for_received(old_accept.sequence), 0)
|
|
|
|
restarted = session(1)
|
|
first = restarted.create_connect(0, b"join")
|
|
reply = server.accept_connect(first, 1)
|
|
self.assertEqual(reply.packet_type, CultNetRudpPacketType.ACK)
|
|
self.assertTrue(self.names_sequence(reply, first.sequence), "the Ack must name the pending Connect for this test to bite")
|
|
restarted.receive(reply, 1)
|
|
self.assertFalse(restarted.connected)
|
|
self.assertEqual(restarted.pending_reliable_sequences, (first.sequence,), "an Ack retired the Connect")
|
|
|
|
fresh = restarted.due_resends(3_000)
|
|
self.assertEqual(len(fresh), 1, "the client waits for ever with nothing to resend")
|
|
self.assertEqual(fresh[0].payload, b"join")
|
|
accept = server.accept_connect(fresh[0], 3_000)
|
|
self.assertEqual(accept.packet_type, CultNetRudpPacketType.ACCEPT)
|
|
restarted.receive(accept, 3_000)
|
|
self.assertTrue(restarted.connected)
|
|
|
|
def test_a_late_copy_of_an_abandoned_connect_is_stale_once_its_replacement_is_accepted(self) -> None:
|
|
server, client = session(500), session(10)
|
|
abandoned = client.create_connect(0)
|
|
fresh = client.due_resends(3_000)
|
|
self.assertEqual(len(fresh), 1)
|
|
self.assertEqual(fresh[0].sequence, abandoned.sequence + 4_095)
|
|
accept = server.accept_connect(fresh[0], 3_001)
|
|
client.receive(accept, 3_002)
|
|
server.receive(client.create_ack_for_received(accept.sequence), 3_002)
|
|
self.assertTrue(client.connected)
|
|
a = send(client, "a")
|
|
self.assertEqual(names(server.receive(a, 3_003)), ["a"])
|
|
client.receive(server.create_ack_for_received(a.sequence), 3_003)
|
|
|
|
self.assertTrue(server.connect_repeats(abandoned), "the late copy would restart the server")
|
|
reply = server.accept_connect(abandoned, 3_004)
|
|
self.assertEqual(reply.packet_type, CultNetRudpPacketType.ACK)
|
|
client.receive(reply, 3_005)
|
|
self.assertTrue(client.connected)
|
|
self.assertEqual(names(server.receive(send(client, "b"), 3_006)), ["b"])
|
|
|
|
def test_a_server_sitting_at_the_jump_is_left_by_the_next_attempt(self) -> None:
|
|
server, old = session(500), session(5_095)
|
|
old_accept = server.accept_connect(old.create_connect(0), 0)
|
|
old.receive(old_accept, 0)
|
|
server.receive(old.create_ack_for_received(old_accept.sequence), 0)
|
|
|
|
restarted = session(1_000)
|
|
first = restarted.create_connect(0, b"join")
|
|
restarted.receive(server.accept_connect(first, 1), 1)
|
|
self.assertFalse(restarted.connected)
|
|
|
|
second = restarted.due_resends(3_000)[0]
|
|
self.assertEqual(second.sequence, 5_095, "the jump lands on the server's generation")
|
|
reply = server.accept_connect(second, 3_001)
|
|
self.assertEqual(reply.packet_type, CultNetRudpPacketType.ACK)
|
|
restarted.receive(reply, 3_001)
|
|
self.assertFalse(restarted.connected)
|
|
|
|
third = restarted.due_resends(6_000)[0]
|
|
self.assertEqual(third.sequence, 5_095 + 4_095)
|
|
accept = server.accept_connect(third, 6_001)
|
|
self.assertEqual(accept.packet_type, CultNetRudpPacketType.ACCEPT)
|
|
restarted.receive(accept, 6_001)
|
|
self.assertTrue(restarted.connected)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|