cultmesh-py: RUDP option rules apply only to a RUDP server; the daemon reports a refused limit as a usage error
The RUDP option rules refused a TCP-only server (enable_rudp=False) for RUDP limits it never uses. Rust checks those options only for the RUDP server they configure, so Python now checks rudp_resend_delay_ms, rudp_max_pending_reliable_packets, rudp_max_fragment_bytes and the empty snapshot's fragment rule only when enable_rudp is true. The snapshot limits still apply to every server. The daemon turned --max-snapshot-documents 0 into a traceback. The server still owns the rule; the daemon reports its refusal through parser.error: exit status 2 and the rule named, no traceback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
699432128b
commit
ead1a25aff
3 changed files with 43 additions and 25 deletions
|
|
@ -50,7 +50,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
|
||||
stop = threading.Event()
|
||||
install_signal_handlers(stop)
|
||||
server = start_server(args)
|
||||
server = start_server(args, parser)
|
||||
try:
|
||||
ready = daemon_ready_document(server)
|
||||
publish_ready(ready, ready_file=args.ready_file)
|
||||
|
|
@ -61,7 +61,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
return 0
|
||||
|
||||
|
||||
def start_server(args: argparse.Namespace) -> CultMeshLocalServer:
|
||||
def start_server(args: argparse.Namespace, parser: argparse.ArgumentParser) -> CultMeshLocalServer:
|
||||
node = CultMesh.create_node(
|
||||
args.cache_file,
|
||||
runtime_id=args.runtime_id,
|
||||
|
|
@ -98,17 +98,21 @@ def start_server(args: argparse.Namespace) -> CultMeshLocalServer:
|
|||
quorum_ratio=args.simulation_quorum_ratio,
|
||||
)
|
||||
)
|
||||
server = CultMesh.serve_node(
|
||||
node,
|
||||
verse_catalog=verse_catalog,
|
||||
peer_catalog=peer_catalog,
|
||||
observation_hub=observation_hub,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
display_name=args.display_name,
|
||||
max_snapshot_documents=args.max_snapshot_documents,
|
||||
max_snapshot_bytes=args.max_snapshot_bytes,
|
||||
)
|
||||
try:
|
||||
server = CultMesh.serve_node(
|
||||
node,
|
||||
verse_catalog=verse_catalog,
|
||||
peer_catalog=peer_catalog,
|
||||
observation_hub=observation_hub,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
display_name=args.display_name,
|
||||
max_snapshot_documents=args.max_snapshot_documents,
|
||||
max_snapshot_bytes=args.max_snapshot_bytes,
|
||||
)
|
||||
except ValueError as error:
|
||||
# The server owns its option rules; a limit it refuses is a usage error, not a crash.
|
||||
parser.error(str(error))
|
||||
advertise_self(server, verse_catalog, peer_catalog, args)
|
||||
return server
|
||||
|
||||
|
|
|
|||
|
|
@ -105,23 +105,25 @@ class CultMeshLocalServer:
|
|||
# The Rust document server's option rules (validate_options), for each option this
|
||||
# server shares with it: every limit is greater than zero, and the limits leave room for
|
||||
# the smallest response the server sends, an empty snapshot. A server that could answer
|
||||
# nothing is misconfigured.
|
||||
# nothing is misconfigured. The RUDP options are checked only when RUDP is served, as
|
||||
# Rust checks them only for the RUDP server they configure.
|
||||
if self.max_snapshot_bytes is not None and self.max_snapshot_bytes <= 0:
|
||||
raise ValueError("max_snapshot_bytes must be greater than zero")
|
||||
if self.max_snapshot_documents is not None and self.max_snapshot_documents <= 0:
|
||||
raise ValueError("max_snapshot_documents must be greater than zero")
|
||||
if self.rudp_resend_delay_ms <= 0:
|
||||
raise ValueError("rudp_resend_delay_ms must be greater than zero")
|
||||
if self.rudp_max_pending_reliable_packets is not None and self.rudp_max_pending_reliable_packets <= 0:
|
||||
raise ValueError("rudp_max_pending_reliable_packets must be greater than zero")
|
||||
if self.rudp_max_fragment_bytes <= 0:
|
||||
raise ValueError("rudp_max_fragment_bytes must be greater than zero")
|
||||
if self.enable_rudp:
|
||||
if self.rudp_resend_delay_ms <= 0:
|
||||
raise ValueError("rudp_resend_delay_ms must be greater than zero")
|
||||
if self.rudp_max_pending_reliable_packets is not None and self.rudp_max_pending_reliable_packets <= 0:
|
||||
raise ValueError("rudp_max_pending_reliable_packets must be greater than zero")
|
||||
if self.rudp_max_fragment_bytes <= 0:
|
||||
raise ValueError("rudp_max_fragment_bytes must be greater than zero")
|
||||
empty = _snapshot_response_bytes(
|
||||
CultNetRawSnapshotResponse(message_id=_SHORTEST_SERVED_MESSAGE_ID, documents=()).to_wire()
|
||||
)
|
||||
if self.max_snapshot_bytes is not None and empty > self.max_snapshot_bytes:
|
||||
raise ValueError("max_snapshot_bytes cannot hold an empty snapshot response")
|
||||
if -(-empty // self.rudp_max_fragment_bytes) > self._max_rudp_reply_fragments():
|
||||
if self.enable_rudp and -(-empty // self.rudp_max_fragment_bytes) > self._max_rudp_reply_fragments():
|
||||
raise ValueError(
|
||||
"rudp_max_fragment_bytes and rudp_max_pending_reliable_packets cannot carry an empty "
|
||||
"snapshot response"
|
||||
|
|
|
|||
|
|
@ -3722,8 +3722,8 @@ class CultMeshTests(unittest.TestCase):
|
|||
|
||||
def test_cultmesh_daemon_refuses_a_snapshot_document_limit_of_zero_at_startup(self) -> None:
|
||||
# A limit of zero is a server that could answer no snapshot, which the Rust document
|
||||
# server's options also refuse; the daemon passes the flag to the server, and exits
|
||||
# naming the rule instead of serving. The limit's enforcement as a peer error is pinned
|
||||
# server's options also refuse; the daemon passes the flag to the server, and exits with
|
||||
# a usage error naming the rule instead of serving. The limit's enforcement as a peer error is pinned
|
||||
# in process by test_cultmesh_local_server_rejects_oversized_snapshot_responses.
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
ready_path = Path(temp) / "ready.json"
|
||||
|
|
@ -3755,8 +3755,10 @@ class CultMeshTests(unittest.TestCase):
|
|||
env=env,
|
||||
timeout=30,
|
||||
)
|
||||
self.assertNotEqual(completed.returncode, 0)
|
||||
self.assertIn("max_snapshot_documents must be greater than zero", completed.stderr)
|
||||
# A usage error from argparse: exit status 2, the rule named, no traceback.
|
||||
self.assertEqual(completed.returncode, 2, completed.stderr)
|
||||
self.assertIn("error: max_snapshot_documents must be greater than zero", completed.stderr)
|
||||
self.assertNotIn("Traceback", completed.stderr)
|
||||
self.assertFalse(ready_path.exists())
|
||||
|
||||
def test_cultmesh_daemon_serves_opt_in_simulation_observations(self) -> None:
|
||||
|
|
@ -4719,6 +4721,16 @@ class CultMeshTests(unittest.TestCase):
|
|||
accepted(rudp_resend_delay_ms=1)
|
||||
self.assertIn("greater than zero", refused(rudp_max_fragment_bytes=0))
|
||||
self.assertIn("greater than zero", refused(rudp_max_pending_reliable_packets=0))
|
||||
# The RUDP options configure only the RUDP server: without one, none of them is checked.
|
||||
accepted(enable_rudp=False, rudp_max_fragment_bytes=1, rudp_max_pending_reliable_packets=16)
|
||||
accepted(
|
||||
enable_rudp=False,
|
||||
rudp_max_fragment_bytes=0,
|
||||
rudp_max_pending_reliable_packets=0,
|
||||
rudp_resend_delay_ms=0,
|
||||
)
|
||||
self.assertIn("greater than zero", refused(enable_rudp=False, max_snapshot_documents=0))
|
||||
self.assertIn("cannot hold an empty snapshot response", refused(enable_rudp=False, max_snapshot_bytes=empty - 1))
|
||||
|
||||
# The configuration at the bound does answer: an empty snapshot in one-byte fragments,
|
||||
# exactly as many as the queue holds.
|
||||
|
|
|
|||
Loading…
Reference in a new issue