Type Python CultNet peer errors
This commit is contained in:
parent
a3ccaa0290
commit
28dbb37dc6
4 changed files with 95 additions and 5 deletions
|
|
@ -15,7 +15,10 @@ CultCache, CultNet, and CultMesh. It is evidence, not confetti.
|
|||
shard-log membership before stamping shard cursor metadata.
|
||||
- Python CultNet raw client:
|
||||
Fetches schema catalogs, raw snapshots, shard catalogs, shard logs, and live
|
||||
database subscription changes.
|
||||
database subscription changes. Request-response reads and database
|
||||
subscription streams raise typed `CultNetPeerError` values for
|
||||
`cultnet.error.v0` peer responses while preserving the raw error document for
|
||||
callers that need the wire evidence.
|
||||
- Python CultNet raw snapshot:
|
||||
Provides typed `CultNetRawSnapshotResponse` and `CultNetRawDocumentRecord`
|
||||
helpers plus `CultNetRawClient.fetch_snapshot_response(...)` for inspecting
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .client import CultNetDatabaseSubscription, CultNetRawClient
|
||||
from .client import CultNetDatabaseSubscription, CultNetPeerError, CultNetRawClient
|
||||
from .framing import decode_frame, encode_frame, read_frame, write_frame
|
||||
from .messages import (
|
||||
CultNetMessage,
|
||||
|
|
@ -68,6 +68,7 @@ __all__ = [
|
|||
"CultNetWitnessArtifactBundle",
|
||||
"CultNetDatabaseSubscription",
|
||||
"CultNetDatabaseChange",
|
||||
"CultNetPeerError",
|
||||
"CultNetRawClient",
|
||||
"CultNetSchemaCatalog",
|
||||
"CultNetSchemaDescriptor",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ from .snapshot import CultNetRawSnapshotResponse
|
|||
from .subscription import CultNetDatabaseChange
|
||||
|
||||
|
||||
class CultNetPeerError(RuntimeError):
|
||||
def __init__(self, response: dict[str, Any]) -> None:
|
||||
self.response = dict(response)
|
||||
self.peer_error = str(response.get("error") or "CultNet peer returned an error")
|
||||
super().__init__(self.peer_error)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CultNetRawClient:
|
||||
host: str
|
||||
|
|
@ -49,8 +56,7 @@ class CultNetRawClient:
|
|||
raise ValueError("CultNet response must be a MessagePack map")
|
||||
schema_version = response.get("schemaVersion")
|
||||
if schema_version != expected_schema_version:
|
||||
if schema_version == "cultnet.error.v0":
|
||||
raise ValueError(str(response.get("error") or "CultNet peer returned an error"))
|
||||
_raise_peer_error(response)
|
||||
raise ValueError(f"Expected {expected_schema_version}, received {schema_version!r}")
|
||||
return response
|
||||
|
||||
|
|
@ -296,6 +302,7 @@ class CultNetDatabaseSubscription:
|
|||
if not isinstance(response, dict):
|
||||
raise ValueError("CultNet subscription response must be a MessagePack map")
|
||||
schema_version = response.get("schemaVersion")
|
||||
_raise_peer_error(response)
|
||||
if schema_version not in {"cultnet.snapshot_response_raw.v0", "cultnet.database_change_raw.v0"}:
|
||||
raise ValueError(f"Unexpected CultNet subscription message {schema_version!r}")
|
||||
return response
|
||||
|
|
@ -325,3 +332,8 @@ class CultNetDatabaseSubscription:
|
|||
while max_messages is None or received < max_messages:
|
||||
yield self.read_next_change()
|
||||
received += 1
|
||||
|
||||
|
||||
def _raise_peer_error(response: dict[str, Any]) -> None:
|
||||
if response.get("schemaVersion") == "cultnet.error.v0":
|
||||
raise CultNetPeerError(response)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from cultnet_py import (
|
|||
CultNetSchemaWriteForwarder,
|
||||
CultNetSchemaShardLogFetcher,
|
||||
CultNetSchemaShardSnapshotFetcher,
|
||||
CultNetPeerError,
|
||||
CultNetShardReplicator,
|
||||
CultNetShardReplicatorOptions,
|
||||
CultNetShardMutationLogStore,
|
||||
|
|
@ -621,6 +622,43 @@ class CultCacheTests(unittest.TestCase):
|
|||
"cultnet.shard_log_request.v0",
|
||||
])
|
||||
|
||||
def test_cultnet_raw_client_raises_typed_peer_error_response(self) -> None:
|
||||
import msgpack # type: ignore
|
||||
from cultnet_py import read_frame, write_frame
|
||||
|
||||
ready = threading.Event()
|
||||
port_holder: list[int] = []
|
||||
|
||||
def serve_error() -> None:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind(("127.0.0.1", 0))
|
||||
port_holder.append(server.getsockname()[1])
|
||||
server.listen(1)
|
||||
ready.set()
|
||||
connection, _ = server.accept()
|
||||
with connection:
|
||||
stream = connection.makefile("rwb")
|
||||
read_frame(stream)
|
||||
write_frame(stream, msgpack.packb({
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"messageId": "error-1",
|
||||
"error": "Snapshot document limit exceeded",
|
||||
}, use_bin_type=True))
|
||||
stream.flush()
|
||||
|
||||
thread = threading.Thread(target=serve_error, daemon=True)
|
||||
thread.start()
|
||||
self.assertTrue(ready.wait(2.0))
|
||||
|
||||
client = CultNetRawClient("127.0.0.1", port_holder[0], timeout_seconds=2.0)
|
||||
with self.assertRaisesRegex(CultNetPeerError, "Snapshot document limit exceeded") as raised:
|
||||
client.fetch_snapshot()
|
||||
|
||||
self.assertEqual(raised.exception.peer_error, "Snapshot document limit exceeded")
|
||||
self.assertEqual(raised.exception.response["schemaVersion"], "cultnet.error.v0")
|
||||
thread.join(2.0)
|
||||
|
||||
def test_cultnet_database_subscription_reads_snapshot_and_change(self) -> None:
|
||||
import msgpack # type: ignore
|
||||
from cultnet_py import read_frame, write_frame
|
||||
|
|
@ -713,6 +751,42 @@ class CultCacheTests(unittest.TestCase):
|
|||
"cultnet.database_unsubscribe.v0",
|
||||
])
|
||||
|
||||
def test_cultnet_database_subscription_raises_typed_peer_error(self) -> None:
|
||||
import msgpack # type: ignore
|
||||
from cultnet_py import read_frame, write_frame
|
||||
|
||||
ready = threading.Event()
|
||||
port_holder: list[int] = []
|
||||
|
||||
def serve_subscription_error() -> None:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind(("127.0.0.1", 0))
|
||||
port_holder.append(server.getsockname()[1])
|
||||
server.listen(1)
|
||||
ready.set()
|
||||
connection, _ = server.accept()
|
||||
with connection:
|
||||
stream = connection.makefile("rwb")
|
||||
read_frame(stream)
|
||||
write_frame(stream, msgpack.packb({
|
||||
"schemaVersion": "cultnet.error.v0",
|
||||
"messageId": "subscription-error",
|
||||
"error": "subscription rejected",
|
||||
}, use_bin_type=True))
|
||||
stream.flush()
|
||||
|
||||
thread = threading.Thread(target=serve_subscription_error, daemon=True)
|
||||
thread.start()
|
||||
self.assertTrue(ready.wait(2.0))
|
||||
|
||||
client = CultNetRawClient("127.0.0.1", port_holder[0], timeout_seconds=2.0)
|
||||
with client.subscribe_database(subscription_id="sub-error") as subscription:
|
||||
with self.assertRaisesRegex(CultNetPeerError, "subscription rejected") as raised:
|
||||
subscription.read_next()
|
||||
self.assertEqual(raised.exception.response["messageId"], "subscription-error")
|
||||
thread.join(2.0)
|
||||
|
||||
def test_cultnet_replication_helpers_apply_raw_snapshot_and_shard_log(self) -> None:
|
||||
document = define_database_entry_type(
|
||||
"replica.item",
|
||||
|
|
@ -3076,7 +3150,7 @@ class CultCacheTests(unittest.TestCase):
|
|||
snapshot_request(schema_ids=["mesh.snapshot_limit_note.v1"]),
|
||||
expected_schema_version="cultnet.error.v0",
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "Snapshot document limit exceeded"):
|
||||
with self.assertRaisesRegex(CultNetPeerError, "Snapshot document limit exceeded"):
|
||||
raw_client.fetch_snapshot(schema_ids=["mesh.snapshot_limit_note.v1"])
|
||||
finally:
|
||||
server.stop()
|
||||
|
|
|
|||
Loading…
Reference in a new issue