cultnet-py: an unsendable goodbye names the failure by a fixed code
end_unsendable_session put the OSError's text on the wire, which is the platform's wording and can quote an address. send_error_code names a permanent failure EMSGSIZE, EINVAL or EAFNOSUPPORT (Winsock equivalents included), anything else UNKNOWN, as the Rust and TypeScript goodbyes do; is_permanent_send_error is now "has a known code". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a71f62e997
commit
c3cc2b49c4
3 changed files with 38 additions and 13 deletions
|
|
@ -124,6 +124,7 @@ from .transport import (
|
|||
compute_reconnect_delay_ms,
|
||||
create_reconnect_policy,
|
||||
is_permanent_send_error,
|
||||
send_error_code,
|
||||
create_rudp_transport_profile,
|
||||
create_tcp_framed_transport_profile,
|
||||
decode_rudp_packet,
|
||||
|
|
@ -211,6 +212,7 @@ __all__ = [
|
|||
"CultNetRudpSendOptions",
|
||||
"CultNetRudpSession",
|
||||
"is_permanent_send_error",
|
||||
"send_error_code",
|
||||
"CultNetRudpSessionOptions",
|
||||
"CultNetRudpSocketMode",
|
||||
"CultNetRudpReconnectLoop",
|
||||
|
|
|
|||
|
|
@ -438,18 +438,28 @@ def create_rudp_transport_profile(
|
|||
|
||||
UNSENDABLE_PACKET_REASON = "packet could not be sent"
|
||||
|
||||
_PERMANENT_SEND_ERRNOS = frozenset(
|
||||
code
|
||||
for code in (
|
||||
errno.EMSGSIZE,
|
||||
errno.EINVAL,
|
||||
errno.EAFNOSUPPORT,
|
||||
getattr(errno, "WSAEMSGSIZE", None),
|
||||
getattr(errno, "WSAEINVAL", None),
|
||||
getattr(errno, "WSAEAFNOSUPPORT", None),
|
||||
# The fixed name of each permanent send failure, keyed by errno, Winsock equivalents included.
|
||||
_PERMANENT_SEND_ERROR_CODES = {
|
||||
code: name
|
||||
for code, name in (
|
||||
(errno.EMSGSIZE, "EMSGSIZE"),
|
||||
(errno.EINVAL, "EINVAL"),
|
||||
(errno.EAFNOSUPPORT, "EAFNOSUPPORT"),
|
||||
(getattr(errno, "WSAEMSGSIZE", None), "EMSGSIZE"),
|
||||
(getattr(errno, "WSAEINVAL", None), "EINVAL"),
|
||||
(getattr(errno, "WSAEAFNOSUPPORT", None), "EAFNOSUPPORT"),
|
||||
)
|
||||
if code is not None
|
||||
)
|
||||
}
|
||||
_UNKNOWN_SEND_ERROR = "UNKNOWN"
|
||||
|
||||
|
||||
def send_error_code(error: OSError) -> str:
|
||||
"""The fixed name of a failed send's cause: EMSGSIZE, EINVAL or EAFNOSUPPORT for a
|
||||
failure that can never pass (`is_permanent_send_error`), UNKNOWN for anything else. An
|
||||
unsendable session's goodbye carries this name, never the error's own text, which is the
|
||||
platform's wording and can quote an address."""
|
||||
return _PERMANENT_SEND_ERROR_CODES.get(error.errno, _UNKNOWN_SEND_ERROR)
|
||||
|
||||
|
||||
def is_permanent_send_error(error: OSError) -> bool:
|
||||
|
|
@ -459,7 +469,7 @@ def is_permanent_send_error(error: OSError) -> bool:
|
|||
Winsock equivalents. Everything else (no route, full buffers, firewall drops, a refusal
|
||||
reported for an earlier datagram, an interrupted call) is a property of the path or the
|
||||
moment and may pass, so it is a lost datagram, not a reason to end a session."""
|
||||
return error.errno in _PERMANENT_SEND_ERRNOS
|
||||
return send_error_code(error) != _UNKNOWN_SEND_ERROR
|
||||
|
||||
|
||||
class CultNetRudpSession:
|
||||
|
|
@ -872,9 +882,10 @@ class CultNetRudpSession:
|
|||
"""Ends a session that owes its peer a packet that can never be sent as built
|
||||
(see `is_permanent_send_error`). Resending it would fail forever, and dropping it
|
||||
would leave a reliable sequence the peer waits on for good, so the session cannot
|
||||
be kept. Returns the goodbye for the peer; its reason names the error."""
|
||||
be kept. Returns the goodbye for the peer; its reason names the failure by its fixed
|
||||
name (`send_error_code`), never the error's text."""
|
||||
self.reset_peer_state()
|
||||
return self.create_disconnect(f"{UNSENDABLE_PACKET_REASON}: {error}".encode("utf-8"))
|
||||
return self.create_disconnect(f"{UNSENDABLE_PACKET_REASON}: {send_error_code(error)}".encode("utf-8"))
|
||||
|
||||
def create_disconnect(self, reason: bytes = b"") -> CultNetRudpPacket:
|
||||
self._end_session()
|
||||
|
|
|
|||
|
|
@ -901,6 +901,18 @@ class CultNetTests(unittest.TestCase):
|
|||
self.assertFalse(is_permanent_send_error(OSError(code, "transient")), errno.errorcode[code])
|
||||
self.assertFalse(is_permanent_send_error(OSError("no errno")))
|
||||
|
||||
def test_cultnet_rudp_an_unsendable_goodbye_names_the_failure_by_a_fixed_code(self) -> None:
|
||||
import errno
|
||||
|
||||
from cultnet_py import send_error_code
|
||||
|
||||
for code, name in ((errno.EMSGSIZE, "EMSGSIZE"), (errno.EINVAL, "EINVAL"), (errno.EAFNOSUPPORT, "EAFNOSUPPORT")):
|
||||
self.assertEqual(send_error_code(OSError(code, "CANARY-7f3a")), name)
|
||||
self.assertEqual(send_error_code(OSError(errno.ECONNREFUSED, "CANARY-7f3a")), "UNKNOWN")
|
||||
session = CultNetRudpSession(CultNetRudpSessionOptions(connection_id=1))
|
||||
goodbye = session.end_unsendable_session(OSError(errno.EMSGSIZE, "Message too long CANARY-7f3a"))
|
||||
self.assertEqual(bytes(goodbye.payload), b"packet could not be sent: EMSGSIZE")
|
||||
|
||||
# The sender's flow window: a reliable packet goes on the wire only while its sequence is at most 1,023 above
|
||||
# the lowest unacked one and the payload above that sequence stays within 4 MiB.
|
||||
FLOW_CONNECTION_ID = 0x464C4F57
|
||||
|
|
|
|||
Loading…
Reference in a new issue