1#![forbid(unsafe_code)]
4
5pub mod deployment;
6
7use std::collections::{BTreeMap, BTreeSet};
8
9use sectorsync_core::prelude::{
10 BarrierId, BarrierScope, BarrierState, CellCoord3, CellIndex, CellLoadSample, ClientId,
11 CommandEnvelope, CommandId, CommandIngress, CommandQueueError, CommandQueueMode, CommandQueues,
12 ComponentStore, EntityHandle, EntityId, EventQueueError, EventQueueLimits, EventQueues,
13 GatewayError, GatewaySessionTable, HandoffTransfer, HotspotDecision, HotspotPlanner,
14 HotspotSeverity, HotspotThresholds, NodeId, OwnerEpoch, PolicyTable, PushOutcome,
15 ReplicationBudget, ReplicationPlan, ReplicationPlanner, RuntimeBarrier, RuntimeUpgradeHook,
16 SnapshotVersion, SplitProposal, Station, StationError, StationEvent, StationId,
17 StationLoadSample, StationSnapshot, Tick, ViewerQuery, VisibilityFilter,
18};
19use sectorsync_transport::{
20 OutboundPacket, StationOutboundPacket, StationTransportReceiver, StationTransportSink,
21 TransportReceiver, TransportSink,
22};
23use sectorsync_wire::{
24 BarrierFrame, BinaryDecodeError, BinaryEncodeError, BinaryFrameDecoder, BinaryFrameEncoder,
25 CommandAckFrame, CommandDispatchFrame, CommandFrame, ComponentSelection, FrameDecoder,
26 FrameEncoder, ReplicationFrame, ReplicationFrameBuildStats, ReplicationFrameBuilder,
27 RuntimeFrame, StationEventFrame,
28};
29
30pub use deployment::{
31 DeploymentConfig, DeploymentError, DeploymentNodeRoute, DeploymentNodeState,
32 DeploymentRouteTable, DeploymentStationMove, DeploymentStationRoute, DeploymentStats,
33 GatewayDeliveryError, GatewayDeliveryRoute,
34};
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct ReplicationTransportConfig {
39 pub budget: ReplicationBudget,
41 pub send_empty_frames: bool,
43}
44
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
47pub struct ReplicationTransportStats {
48 pub viewers_planned: usize,
50 pub frames_skipped_empty: usize,
52 pub frames_sent: usize,
54 pub bytes_sent: usize,
56 pub entities_selected: usize,
58 pub entities_skipped_by_budget: usize,
60 pub entities_skipped_by_cadence: usize,
62 pub entities_encoded: usize,
64 pub components_encoded: usize,
66}
67
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub struct ReplicationTransportReport {
71 pub client_id: ClientId,
73 pub selected_entities: usize,
75 pub encoded_entities: usize,
77 pub encoded_components: usize,
79 pub estimated_plan_bytes: usize,
81 pub skipped_by_budget: usize,
83 pub skipped_by_cadence: usize,
85 pub bytes_sent: usize,
87 pub sent: bool,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
93pub enum ReplicationTransportError<E> {
94 Encode(BinaryEncodeError),
96 Transport(E),
98}
99
100impl<E: core::fmt::Display> core::fmt::Display for ReplicationTransportError<E> {
101 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102 match self {
103 Self::Encode(error) => write!(f, "{error}"),
104 Self::Transport(error) => write!(f, "{error}"),
105 }
106 }
107}
108
109impl<E> std::error::Error for ReplicationTransportError<E>
110where
111 E: std::error::Error + 'static,
112{
113 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
114 match self {
115 Self::Encode(error) => Some(error),
116 Self::Transport(error) => Some(error),
117 }
118 }
119}
120
121impl<E> From<BinaryEncodeError> for ReplicationTransportError<E> {
122 fn from(value: BinaryEncodeError) -> Self {
123 Self::Encode(value)
124 }
125}
126
127#[derive(Clone, Debug)]
129pub struct ReplicationTransportBridge {
130 config: ReplicationTransportConfig,
131 builder: ReplicationFrameBuilder,
132 stats: ReplicationTransportStats,
133}
134
135impl ReplicationTransportBridge {
136 pub const fn new(config: ReplicationTransportConfig, builder: ReplicationFrameBuilder) -> Self {
138 Self {
139 config,
140 builder,
141 stats: ReplicationTransportStats {
142 viewers_planned: 0,
143 frames_skipped_empty: 0,
144 frames_sent: 0,
145 bytes_sent: 0,
146 entities_selected: 0,
147 entities_skipped_by_budget: 0,
148 entities_skipped_by_cadence: 0,
149 entities_encoded: 0,
150 components_encoded: 0,
151 },
152 }
153 }
154
155 pub const fn config(&self) -> ReplicationTransportConfig {
157 self.config
158 }
159
160 pub const fn builder(&self) -> ReplicationFrameBuilder {
162 self.builder
163 }
164
165 pub const fn stats(&self) -> ReplicationTransportStats {
167 self.stats
168 }
169
170 #[allow(clippy::too_many_arguments)]
172 pub fn send_viewer<T, F>(
173 &mut self,
174 transport: &mut T,
175 station: &Station,
176 index: &CellIndex,
177 policies: &PolicyTable,
178 components: &ComponentStore,
179 selection: &ComponentSelection,
180 viewer: &ViewerQuery,
181 filter: &F,
182 ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
183 where
184 T: TransportSink,
185 F: VisibilityFilter,
186 {
187 let plan = ReplicationPlanner::plan_for_viewer(
188 station,
189 index,
190 policies,
191 viewer,
192 filter,
193 self.config.budget,
194 );
195 self.send_plan(
196 transport,
197 viewer.client_id,
198 station.tick(),
199 station,
200 components,
201 selection,
202 &plan,
203 )
204 }
205
206 #[allow(clippy::too_many_arguments)]
208 pub fn send_viewer_with_cadence<T, F, L>(
209 &mut self,
210 transport: &mut T,
211 station: &Station,
212 index: &CellIndex,
213 policies: &PolicyTable,
214 components: &ComponentStore,
215 selection: &ComponentSelection,
216 viewer: &ViewerQuery,
217 filter: &F,
218 last_sent: L,
219 ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
220 where
221 T: TransportSink,
222 F: VisibilityFilter,
223 L: Fn(EntityHandle) -> Option<Tick>,
224 {
225 let plan = ReplicationPlanner::plan_for_viewer_with_cadence(
226 station,
227 index,
228 policies,
229 viewer,
230 filter,
231 self.config.budget,
232 last_sent,
233 );
234 self.send_plan(
235 transport,
236 viewer.client_id,
237 station.tick(),
238 station,
239 components,
240 selection,
241 &plan,
242 )
243 }
244
245 #[allow(clippy::too_many_arguments)]
247 pub fn send_viewer_prioritized<T, F>(
248 &mut self,
249 transport: &mut T,
250 station: &Station,
251 index: &CellIndex,
252 policies: &PolicyTable,
253 components: &ComponentStore,
254 selection: &ComponentSelection,
255 viewer: &ViewerQuery,
256 filter: &F,
257 ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
258 where
259 T: TransportSink,
260 F: VisibilityFilter,
261 {
262 let plan = ReplicationPlanner::plan_for_viewer_prioritized(
263 station,
264 index,
265 policies,
266 viewer,
267 filter,
268 self.config.budget,
269 );
270 self.send_plan(
271 transport,
272 viewer.client_id,
273 station.tick(),
274 station,
275 components,
276 selection,
277 &plan,
278 )
279 }
280
281 #[allow(clippy::too_many_arguments)]
283 pub fn send_viewer_prioritized_with_cadence<T, F, L>(
284 &mut self,
285 transport: &mut T,
286 station: &Station,
287 index: &CellIndex,
288 policies: &PolicyTable,
289 components: &ComponentStore,
290 selection: &ComponentSelection,
291 viewer: &ViewerQuery,
292 filter: &F,
293 last_sent: L,
294 ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
295 where
296 T: TransportSink,
297 F: VisibilityFilter,
298 L: Fn(EntityHandle) -> Option<Tick>,
299 {
300 let plan = ReplicationPlanner::plan_for_viewer_prioritized_with_cadence(
301 station,
302 index,
303 policies,
304 viewer,
305 filter,
306 self.config.budget,
307 last_sent,
308 );
309 self.send_plan(
310 transport,
311 viewer.client_id,
312 station.tick(),
313 station,
314 components,
315 selection,
316 &plan,
317 )
318 }
319
320 #[allow(clippy::too_many_arguments)]
322 pub fn send_plan<T>(
323 &mut self,
324 transport: &mut T,
325 client_id: ClientId,
326 server_tick: Tick,
327 station: &Station,
328 components: &ComponentStore,
329 selection: &ComponentSelection,
330 plan: &ReplicationPlan,
331 ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
332 where
333 T: TransportSink,
334 {
335 self.stats.viewers_planned = self.stats.viewers_planned.saturating_add(1);
336 self.stats.entities_selected = self
337 .stats
338 .entities_selected
339 .saturating_add(plan.stats.selected);
340 self.stats.entities_skipped_by_budget = self
341 .stats
342 .entities_skipped_by_budget
343 .saturating_add(plan.stats.skipped_by_budget);
344 self.stats.entities_skipped_by_cadence = self
345 .stats
346 .entities_skipped_by_cadence
347 .saturating_add(plan.stats.skipped_by_cadence);
348
349 let build =
350 self.builder
351 .build(client_id, server_tick, station, plan, components, selection);
352 let build_stats = build.stats;
353 self.stats.entities_encoded = self
354 .stats
355 .entities_encoded
356 .saturating_add(build_stats.encoded_entities);
357 self.stats.components_encoded = self
358 .stats
359 .components_encoded
360 .saturating_add(build_stats.encoded_components);
361
362 if build.frame.entities.is_empty() && !self.config.send_empty_frames {
363 self.stats.frames_skipped_empty = self.stats.frames_skipped_empty.saturating_add(1);
364 return Ok(replication_report(client_id, plan, build_stats, 0, false));
365 }
366
367 let mut bytes = Vec::new();
368 BinaryFrameEncoder.encode_replication(&build.frame, &mut bytes)?;
369 let byte_len = bytes.len();
370 transport
371 .send(OutboundPacket { client_id, bytes })
372 .map_err(ReplicationTransportError::Transport)?;
373 self.stats.frames_sent = self.stats.frames_sent.saturating_add(1);
374 self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(byte_len);
375
376 Ok(replication_report(
377 client_id,
378 plan,
379 build_stats,
380 byte_len,
381 true,
382 ))
383 }
384}
385
386impl Default for ReplicationTransportBridge {
387 fn default() -> Self {
388 Self::new(
389 ReplicationTransportConfig::default(),
390 ReplicationFrameBuilder::default(),
391 )
392 }
393}
394
395fn replication_report(
396 client_id: ClientId,
397 plan: &ReplicationPlan,
398 build_stats: ReplicationFrameBuildStats,
399 bytes_sent: usize,
400 sent: bool,
401) -> ReplicationTransportReport {
402 ReplicationTransportReport {
403 client_id,
404 selected_entities: plan.stats.selected,
405 encoded_entities: build_stats.encoded_entities,
406 encoded_components: build_stats.encoded_components,
407 estimated_plan_bytes: plan.stats.estimated_bytes,
408 skipped_by_budget: plan.stats.skipped_by_budget,
409 skipped_by_cadence: plan.stats.skipped_by_cadence,
410 bytes_sent,
411 sent,
412 }
413}
414
415#[derive(Clone, Copy, Debug, PartialEq, Eq)]
417pub struct ReplicationReceiveConfig {
418 pub client_id: ClientId,
420 pub expected_source: Option<ClientId>,
422}
423
424impl ReplicationReceiveConfig {
425 pub const fn new(client_id: ClientId) -> Self {
427 Self {
428 client_id,
429 expected_source: None,
430 }
431 }
432
433 #[must_use]
435 pub const fn with_expected_source(mut self, source: ClientId) -> Self {
436 self.expected_source = Some(source);
437 self
438 }
439}
440
441#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
443pub struct ReplicationReceiveStats {
444 pub packets_received: usize,
446 pub bytes_received: usize,
448 pub frames_received: usize,
450 pub frames_rejected_decode: usize,
452 pub frames_rejected_unexpected: usize,
454 pub frames_rejected_source: usize,
456 pub frames_rejected_target: usize,
458 pub entities_received: usize,
460 pub components_received: usize,
462}
463
464#[derive(Clone, Debug, Default, PartialEq, Eq)]
466pub struct ReplicationReceivePump {
467 pub packets_received: usize,
469 pub bytes_received: usize,
471 pub frames: Vec<ReplicationFrame>,
473}
474
475impl ReplicationReceivePump {
476 pub fn frames_received(&self) -> usize {
478 self.frames.len()
479 }
480
481 pub fn entities_received(&self) -> usize {
483 self.frames.iter().map(|frame| frame.entities.len()).sum()
484 }
485
486 pub fn components_received(&self) -> usize {
488 self.frames
489 .iter()
490 .flat_map(|frame| &frame.entities)
491 .map(|entity| entity.components.len())
492 .sum()
493 }
494}
495
496#[derive(Clone, Debug, PartialEq, Eq)]
498pub enum ReplicationReceiveError<E> {
499 Transport(E),
501 Decode(BinaryDecodeError),
503 UnexpectedFrame,
505 SourceMismatch {
507 expected: ClientId,
509 actual: Option<ClientId>,
511 },
512 TargetMismatch {
514 expected: ClientId,
516 actual: ClientId,
518 },
519}
520
521impl<E: core::fmt::Display> core::fmt::Display for ReplicationReceiveError<E> {
522 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
523 match self {
524 Self::Transport(error) => write!(f, "{error}"),
525 Self::Decode(error) => write!(f, "{error}"),
526 Self::UnexpectedFrame => f.write_str("client packet was not a replication frame"),
527 Self::SourceMismatch { expected, actual } => write!(
528 f,
529 "replication source mismatch: expected {}, actual {:?}",
530 expected.get(),
531 actual.map(ClientId::get)
532 ),
533 Self::TargetMismatch { expected, actual } => write!(
534 f,
535 "replication target mismatch: expected {}, actual {}",
536 expected.get(),
537 actual.get()
538 ),
539 }
540 }
541}
542
543impl<E> std::error::Error for ReplicationReceiveError<E>
544where
545 E: std::error::Error + 'static,
546{
547 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
548 match self {
549 Self::Transport(error) => Some(error),
550 Self::Decode(error) => Some(error),
551 Self::UnexpectedFrame | Self::SourceMismatch { .. } | Self::TargetMismatch { .. } => {
552 None
553 }
554 }
555 }
556}
557
558impl<E> From<BinaryDecodeError> for ReplicationReceiveError<E> {
559 fn from(value: BinaryDecodeError) -> Self {
560 Self::Decode(value)
561 }
562}
563
564#[derive(Clone, Debug)]
566pub struct ReplicationReceiveBridge {
567 config: ReplicationReceiveConfig,
568 stats: ReplicationReceiveStats,
569}
570
571impl ReplicationReceiveBridge {
572 pub const fn new(config: ReplicationReceiveConfig) -> Self {
574 Self {
575 config,
576 stats: ReplicationReceiveStats {
577 packets_received: 0,
578 bytes_received: 0,
579 frames_received: 0,
580 frames_rejected_decode: 0,
581 frames_rejected_unexpected: 0,
582 frames_rejected_source: 0,
583 frames_rejected_target: 0,
584 entities_received: 0,
585 components_received: 0,
586 },
587 }
588 }
589
590 pub const fn config(&self) -> ReplicationReceiveConfig {
592 self.config
593 }
594
595 pub const fn stats(&self) -> ReplicationReceiveStats {
597 self.stats
598 }
599
600 pub fn pump<T>(
602 &mut self,
603 transport: &mut T,
604 max_packets: usize,
605 ) -> Result<ReplicationReceivePump, ReplicationReceiveError<T::Error>>
606 where
607 T: TransportReceiver,
608 {
609 let mut pump = ReplicationReceivePump::default();
610 for _ in 0..max_packets {
611 let Some(packet) = transport
612 .try_recv()
613 .map_err(ReplicationReceiveError::Transport)?
614 else {
615 break;
616 };
617 self.stats.packets_received = self.stats.packets_received.saturating_add(1);
618 self.stats.bytes_received =
619 self.stats.bytes_received.saturating_add(packet.bytes.len());
620 pump.packets_received = pump.packets_received.saturating_add(1);
621 pump.bytes_received = pump.bytes_received.saturating_add(packet.bytes.len());
622
623 if let Some(expected) = self.config.expected_source
624 && packet.client_id != Some(expected)
625 {
626 self.stats.frames_rejected_source =
627 self.stats.frames_rejected_source.saturating_add(1);
628 return Err(ReplicationReceiveError::SourceMismatch {
629 expected,
630 actual: packet.client_id,
631 });
632 }
633
634 let decoded = match BinaryFrameDecoder.decode(&packet.bytes) {
635 Ok(decoded) => decoded,
636 Err(error) => {
637 self.stats.frames_rejected_decode =
638 self.stats.frames_rejected_decode.saturating_add(1);
639 return Err(ReplicationReceiveError::Decode(error));
640 }
641 };
642 let RuntimeFrame::Replication(frame) = decoded else {
643 self.stats.frames_rejected_unexpected =
644 self.stats.frames_rejected_unexpected.saturating_add(1);
645 return Err(ReplicationReceiveError::UnexpectedFrame);
646 };
647 if frame.client_id != self.config.client_id {
648 self.stats.frames_rejected_target =
649 self.stats.frames_rejected_target.saturating_add(1);
650 return Err(ReplicationReceiveError::TargetMismatch {
651 expected: self.config.client_id,
652 actual: frame.client_id,
653 });
654 }
655
656 self.stats.frames_received = self.stats.frames_received.saturating_add(1);
657 self.stats.entities_received = self
658 .stats
659 .entities_received
660 .saturating_add(frame.entities.len());
661 let components = frame
662 .entities
663 .iter()
664 .map(|entity| entity.components.len())
665 .sum::<usize>();
666 self.stats.components_received =
667 self.stats.components_received.saturating_add(components);
668 pump.frames.push(frame);
669 }
670 Ok(pump)
671 }
672}
673
674#[derive(Clone, Copy, Debug, PartialEq, Eq)]
676pub struct ClientTransportConfig {
677 pub client_id: ClientId,
679 pub server_id: ClientId,
681 pub expected_source: Option<ClientId>,
683}
684
685impl ClientTransportConfig {
686 pub const fn new(client_id: ClientId, server_id: ClientId) -> Self {
688 Self {
689 client_id,
690 server_id,
691 expected_source: None,
692 }
693 }
694
695 #[must_use]
697 pub const fn with_expected_source(mut self, source: ClientId) -> Self {
698 self.expected_source = Some(source);
699 self
700 }
701}
702
703#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
705pub struct ClientTransportStats {
706 pub commands_sent: usize,
708 pub command_bytes_sent: usize,
710 pub packets_received: usize,
712 pub bytes_received: usize,
714 pub command_acks_received: usize,
716 pub replication_frames_received: usize,
718 pub barrier_frames_received: usize,
720 pub frames_rejected_decode: usize,
722 pub frames_rejected_unexpected: usize,
724 pub frames_rejected_source: usize,
726 pub frames_rejected_target: usize,
728 pub entities_received: usize,
730 pub components_received: usize,
732}
733
734#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
736pub struct ClientCommandSendReport {
737 pub command_id: CommandId,
739 pub bytes_sent: usize,
741}
742
743#[derive(Clone, Copy, Debug, PartialEq, Eq)]
745pub enum ClientInboundFrameKind {
746 CommandAck,
748 Replication,
750 Barrier,
752}
753
754#[derive(Clone, Debug, Default, PartialEq, Eq)]
756pub struct ClientTransportPump {
757 pub packets_received: usize,
759 pub bytes_received: usize,
761 pub command_acks: Vec<CommandAckFrame>,
763 pub replication_frames: Vec<ReplicationFrame>,
765 pub barriers: Vec<BarrierFrame>,
767}
768
769impl ClientTransportPump {
770 pub fn command_acks_received(&self) -> usize {
772 self.command_acks.len()
773 }
774
775 pub fn replication_frames_received(&self) -> usize {
777 self.replication_frames.len()
778 }
779
780 pub fn barrier_frames_received(&self) -> usize {
782 self.barriers.len()
783 }
784
785 pub fn entities_received(&self) -> usize {
787 self.replication_frames
788 .iter()
789 .map(|frame| frame.entities.len())
790 .sum()
791 }
792
793 pub fn components_received(&self) -> usize {
795 self.replication_frames
796 .iter()
797 .flat_map(|frame| &frame.entities)
798 .map(|entity| entity.components.len())
799 .sum()
800 }
801}
802
803#[derive(Clone, Debug, PartialEq, Eq)]
805pub enum ClientTransportBridgeError<E> {
806 CommandClientMismatch {
808 expected: ClientId,
810 actual: ClientId,
812 },
813 Encode(BinaryEncodeError),
815 Transport(E),
817 Decode(BinaryDecodeError),
819 UnexpectedFrame,
821 SourceMismatch {
823 expected: ClientId,
825 actual: Option<ClientId>,
827 },
828 TargetMismatch {
830 kind: ClientInboundFrameKind,
832 expected: ClientId,
834 actual: ClientId,
836 },
837}
838
839impl<E: core::fmt::Display> core::fmt::Display for ClientTransportBridgeError<E> {
840 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
841 match self {
842 Self::CommandClientMismatch { expected, actual } => write!(
843 f,
844 "command client mismatch: expected {}, actual {}",
845 expected.get(),
846 actual.get()
847 ),
848 Self::Encode(error) => write!(f, "{error}"),
849 Self::Transport(error) => write!(f, "{error}"),
850 Self::Decode(error) => write!(f, "{error}"),
851 Self::UnexpectedFrame => f.write_str("packet was not a client-bound frame"),
852 Self::SourceMismatch { expected, actual } => write!(
853 f,
854 "client packet source mismatch: expected {}, actual {:?}",
855 expected.get(),
856 actual.map(ClientId::get)
857 ),
858 Self::TargetMismatch {
859 kind,
860 expected,
861 actual,
862 } => write!(
863 f,
864 "client {:?} frame target mismatch: expected {}, actual {}",
865 kind,
866 expected.get(),
867 actual.get()
868 ),
869 }
870 }
871}
872
873impl<E> std::error::Error for ClientTransportBridgeError<E>
874where
875 E: std::error::Error + 'static,
876{
877 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
878 match self {
879 Self::Encode(error) => Some(error),
880 Self::Transport(error) => Some(error),
881 Self::Decode(error) => Some(error),
882 Self::CommandClientMismatch { .. }
883 | Self::UnexpectedFrame
884 | Self::SourceMismatch { .. }
885 | Self::TargetMismatch { .. } => None,
886 }
887 }
888}
889
890impl<E> From<BinaryEncodeError> for ClientTransportBridgeError<E> {
891 fn from(value: BinaryEncodeError) -> Self {
892 Self::Encode(value)
893 }
894}
895
896impl<E> From<BinaryDecodeError> for ClientTransportBridgeError<E> {
897 fn from(value: BinaryDecodeError) -> Self {
898 Self::Decode(value)
899 }
900}
901
902#[derive(Clone, Debug)]
904pub struct ClientTransportBridge {
905 config: ClientTransportConfig,
906 stats: ClientTransportStats,
907}
908
909impl ClientTransportBridge {
910 pub const fn new(config: ClientTransportConfig) -> Self {
912 Self {
913 config,
914 stats: ClientTransportStats {
915 commands_sent: 0,
916 command_bytes_sent: 0,
917 packets_received: 0,
918 bytes_received: 0,
919 command_acks_received: 0,
920 replication_frames_received: 0,
921 barrier_frames_received: 0,
922 frames_rejected_decode: 0,
923 frames_rejected_unexpected: 0,
924 frames_rejected_source: 0,
925 frames_rejected_target: 0,
926 entities_received: 0,
927 components_received: 0,
928 },
929 }
930 }
931
932 pub const fn config(&self) -> ClientTransportConfig {
934 self.config
935 }
936
937 pub const fn stats(&self) -> ClientTransportStats {
939 self.stats
940 }
941
942 pub fn send_command_frame<T>(
944 &mut self,
945 transport: &mut T,
946 frame: &CommandFrame,
947 ) -> Result<ClientCommandSendReport, ClientTransportBridgeError<T::Error>>
948 where
949 T: TransportSink,
950 {
951 if frame.client_id != self.config.client_id {
952 return Err(ClientTransportBridgeError::CommandClientMismatch {
953 expected: self.config.client_id,
954 actual: frame.client_id,
955 });
956 }
957
958 let mut bytes = Vec::new();
959 BinaryFrameEncoder.encode_command(frame, &mut bytes)?;
960 let bytes_sent = bytes.len();
961 transport
962 .send(OutboundPacket {
963 client_id: self.config.server_id,
964 bytes,
965 })
966 .map_err(ClientTransportBridgeError::Transport)?;
967 self.stats.commands_sent = self.stats.commands_sent.saturating_add(1);
968 self.stats.command_bytes_sent = self.stats.command_bytes_sent.saturating_add(bytes_sent);
969
970 Ok(ClientCommandSendReport {
971 command_id: frame.command_id,
972 bytes_sent,
973 })
974 }
975
976 pub fn pump<T>(
978 &mut self,
979 transport: &mut T,
980 max_packets: usize,
981 ) -> Result<ClientTransportPump, ClientTransportBridgeError<T::Error>>
982 where
983 T: TransportReceiver,
984 {
985 let mut pump = ClientTransportPump::default();
986 for _ in 0..max_packets {
987 let Some(packet) = transport
988 .try_recv()
989 .map_err(ClientTransportBridgeError::Transport)?
990 else {
991 break;
992 };
993 self.stats.packets_received = self.stats.packets_received.saturating_add(1);
994 self.stats.bytes_received =
995 self.stats.bytes_received.saturating_add(packet.bytes.len());
996 pump.packets_received = pump.packets_received.saturating_add(1);
997 pump.bytes_received = pump.bytes_received.saturating_add(packet.bytes.len());
998
999 if let Some(expected) = self.config.expected_source
1000 && packet.client_id != Some(expected)
1001 {
1002 self.stats.frames_rejected_source =
1003 self.stats.frames_rejected_source.saturating_add(1);
1004 return Err(ClientTransportBridgeError::SourceMismatch {
1005 expected,
1006 actual: packet.client_id,
1007 });
1008 }
1009
1010 let decoded = match BinaryFrameDecoder.decode(&packet.bytes) {
1011 Ok(decoded) => decoded,
1012 Err(error) => {
1013 self.stats.frames_rejected_decode =
1014 self.stats.frames_rejected_decode.saturating_add(1);
1015 return Err(ClientTransportBridgeError::Decode(error));
1016 }
1017 };
1018 match decoded {
1019 RuntimeFrame::CommandAck(frame) => {
1020 self.validate_client_target(
1021 ClientInboundFrameKind::CommandAck,
1022 frame.client_id,
1023 )?;
1024 self.stats.command_acks_received =
1025 self.stats.command_acks_received.saturating_add(1);
1026 pump.command_acks.push(frame);
1027 }
1028 RuntimeFrame::Replication(frame) => {
1029 self.validate_client_target(
1030 ClientInboundFrameKind::Replication,
1031 frame.client_id,
1032 )?;
1033 self.stats.replication_frames_received =
1034 self.stats.replication_frames_received.saturating_add(1);
1035 self.stats.entities_received = self
1036 .stats
1037 .entities_received
1038 .saturating_add(frame.entities.len());
1039 let components = frame
1040 .entities
1041 .iter()
1042 .map(|entity| entity.components.len())
1043 .sum::<usize>();
1044 self.stats.components_received =
1045 self.stats.components_received.saturating_add(components);
1046 pump.replication_frames.push(frame);
1047 }
1048 RuntimeFrame::Barrier(frame) => {
1049 self.validate_client_target(ClientInboundFrameKind::Barrier, frame.client_id)?;
1050 self.stats.barrier_frames_received =
1051 self.stats.barrier_frames_received.saturating_add(1);
1052 pump.barriers.push(frame);
1053 }
1054 RuntimeFrame::Command(_)
1055 | RuntimeFrame::CommandDispatch(_)
1056 | RuntimeFrame::StationEvent(_) => {
1057 self.stats.frames_rejected_unexpected =
1058 self.stats.frames_rejected_unexpected.saturating_add(1);
1059 return Err(ClientTransportBridgeError::UnexpectedFrame);
1060 }
1061 }
1062 }
1063 Ok(pump)
1064 }
1065
1066 fn validate_client_target<E>(
1067 &mut self,
1068 kind: ClientInboundFrameKind,
1069 actual: ClientId,
1070 ) -> Result<(), ClientTransportBridgeError<E>> {
1071 if actual == self.config.client_id {
1072 return Ok(());
1073 }
1074 self.stats.frames_rejected_target = self.stats.frames_rejected_target.saturating_add(1);
1075 Err(ClientTransportBridgeError::TargetMismatch {
1076 kind,
1077 expected: self.config.client_id,
1078 actual,
1079 })
1080 }
1081}
1082
1083#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1085pub struct BarrierTransportStats {
1086 pub notifications_sent: usize,
1088 pub clients_notified: usize,
1090 pub bytes_sent: usize,
1092}
1093
1094#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1096pub struct BarrierTransportReport {
1097 pub barrier_id: BarrierId,
1099 pub state: BarrierState,
1101 pub server_tick: Tick,
1103 pub clients_requested: usize,
1105 pub clients_sent: usize,
1107 pub bytes_sent: usize,
1109}
1110
1111#[derive(Clone, Debug, PartialEq, Eq)]
1113pub enum BarrierTransportError<E> {
1114 Encode(BinaryEncodeError),
1116 Transport(E),
1118}
1119
1120impl<E: core::fmt::Display> core::fmt::Display for BarrierTransportError<E> {
1121 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1122 match self {
1123 Self::Encode(error) => write!(f, "{error}"),
1124 Self::Transport(error) => write!(f, "{error}"),
1125 }
1126 }
1127}
1128
1129impl<E> std::error::Error for BarrierTransportError<E>
1130where
1131 E: std::error::Error + 'static,
1132{
1133 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1134 match self {
1135 Self::Encode(error) => Some(error),
1136 Self::Transport(error) => Some(error),
1137 }
1138 }
1139}
1140
1141impl<E> From<BinaryEncodeError> for BarrierTransportError<E> {
1142 fn from(value: BinaryEncodeError) -> Self {
1143 Self::Encode(value)
1144 }
1145}
1146
1147#[derive(Clone, Debug, Default)]
1149pub struct BarrierTransportBridge {
1150 stats: BarrierTransportStats,
1151}
1152
1153impl BarrierTransportBridge {
1154 pub const fn new() -> Self {
1156 Self {
1157 stats: BarrierTransportStats {
1158 notifications_sent: 0,
1159 clients_notified: 0,
1160 bytes_sent: 0,
1161 },
1162 }
1163 }
1164
1165 pub const fn stats(&self) -> BarrierTransportStats {
1167 self.stats
1168 }
1169
1170 pub fn send_state<T>(
1172 &mut self,
1173 transport: &mut T,
1174 client_id: ClientId,
1175 barrier_id: BarrierId,
1176 server_tick: Tick,
1177 state: BarrierState,
1178 ) -> Result<usize, BarrierTransportError<T::Error>>
1179 where
1180 T: TransportSink,
1181 {
1182 let frame = BarrierFrame {
1183 client_id,
1184 barrier_id,
1185 server_tick,
1186 state,
1187 };
1188 let mut bytes = Vec::new();
1189 BinaryFrameEncoder.encode_barrier(&frame, &mut bytes)?;
1190 let bytes_sent = bytes.len();
1191 transport
1192 .send(OutboundPacket { client_id, bytes })
1193 .map_err(BarrierTransportError::Transport)?;
1194 self.stats.notifications_sent = self.stats.notifications_sent.saturating_add(1);
1195 self.stats.clients_notified = self.stats.clients_notified.saturating_add(1);
1196 self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(bytes_sent);
1197 Ok(bytes_sent)
1198 }
1199
1200 pub fn send_barrier<T>(
1202 &mut self,
1203 transport: &mut T,
1204 client_id: ClientId,
1205 barrier: RuntimeBarrier,
1206 ) -> Result<usize, BarrierTransportError<T::Error>>
1207 where
1208 T: TransportSink,
1209 {
1210 self.send_state(
1211 transport,
1212 client_id,
1213 barrier.id,
1214 barrier.target_tick,
1215 barrier.state,
1216 )
1217 }
1218
1219 pub fn broadcast_state<T, I>(
1221 &mut self,
1222 transport: &mut T,
1223 clients: I,
1224 barrier_id: BarrierId,
1225 server_tick: Tick,
1226 state: BarrierState,
1227 ) -> Result<BarrierTransportReport, BarrierTransportError<T::Error>>
1228 where
1229 T: TransportSink,
1230 I: IntoIterator<Item = ClientId>,
1231 {
1232 let mut report = BarrierTransportReport {
1233 barrier_id,
1234 state,
1235 server_tick,
1236 clients_requested: 0,
1237 clients_sent: 0,
1238 bytes_sent: 0,
1239 };
1240 for client_id in clients {
1241 report.clients_requested = report.clients_requested.saturating_add(1);
1242 let bytes_sent =
1243 self.send_state(transport, client_id, barrier_id, server_tick, state)?;
1244 report.clients_sent = report.clients_sent.saturating_add(1);
1245 report.bytes_sent = report.bytes_sent.saturating_add(bytes_sent);
1246 }
1247 Ok(report)
1248 }
1249
1250 pub fn broadcast_barrier<T, I>(
1252 &mut self,
1253 transport: &mut T,
1254 clients: I,
1255 barrier: RuntimeBarrier,
1256 ) -> Result<BarrierTransportReport, BarrierTransportError<T::Error>>
1257 where
1258 T: TransportSink,
1259 I: IntoIterator<Item = ClientId>,
1260 {
1261 self.broadcast_state(
1262 transport,
1263 clients,
1264 barrier.id,
1265 barrier.target_tick,
1266 barrier.state,
1267 )
1268 }
1269}
1270
1271pub const GATEWAY_COMMAND_ACK_ACCEPTED: u16 = 0;
1273pub const GATEWAY_COMMAND_ACK_GATEWAY_REJECTED: u16 = 1;
1275pub const GATEWAY_COMMAND_ACK_RATE_LIMITED: u16 = 2;
1277pub const GATEWAY_COMMAND_ACK_REPLAY_OR_STALE: u16 = 3;
1279pub const GATEWAY_COMMAND_ACK_QUEUE_FULL: u16 = 4;
1281pub const GATEWAY_COMMAND_ACK_BARRIER_REJECTED: u16 = 5;
1283pub const GATEWAY_COMMAND_ACK_MISSING_QUEUE: u16 = 6;
1285pub const GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED: u16 = 7;
1287
1288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1290pub struct GatewayCommandPipelineConfig {
1291 pub ack_rejections: bool,
1293}
1294
1295impl Default for GatewayCommandPipelineConfig {
1296 fn default() -> Self {
1297 Self {
1298 ack_rejections: true,
1299 }
1300 }
1301}
1302
1303#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1305pub struct GatewayCommandPipelineStats {
1306 pub command_frames_decoded: usize,
1308 pub frames_rejected_decode: usize,
1310 pub frames_rejected_non_command: usize,
1312 pub commands_admitted: usize,
1314 pub commands_enqueued: usize,
1316 pub commands_rejected_gateway: usize,
1318 pub commands_rejected_queue: usize,
1320 pub commands_routed_deployment: usize,
1322 pub commands_rejected_deployment: usize,
1324 pub acks_encoded: usize,
1326}
1327
1328#[derive(Clone, Debug, PartialEq, Eq)]
1330pub enum GatewayCommandPipelineError {
1331 Decode(BinaryDecodeError),
1333 NonCommandFrame,
1335 Gateway(GatewayError),
1337 MissingQueue(StationId),
1339 Queue(CommandQueueError),
1341 Deployment(DeploymentError),
1343 Encode(BinaryEncodeError),
1345}
1346
1347impl core::fmt::Display for GatewayCommandPipelineError {
1348 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1349 match self {
1350 Self::Decode(error) => write!(f, "{error}"),
1351 Self::NonCommandFrame => f.write_str("gateway command pipeline expected command frame"),
1352 Self::Gateway(error) => write!(f, "{error}"),
1353 Self::MissingQueue(station_id) => write!(
1354 f,
1355 "gateway command route target station {} has no queue",
1356 station_id.get()
1357 ),
1358 Self::Queue(error) => write!(f, "{error}"),
1359 Self::Deployment(error) => write!(f, "{error}"),
1360 Self::Encode(error) => write!(f, "{error}"),
1361 }
1362 }
1363}
1364
1365impl std::error::Error for GatewayCommandPipelineError {
1366 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1367 match self {
1368 Self::Decode(error) => Some(error),
1369 Self::Gateway(error) => Some(error),
1370 Self::Queue(error) => Some(error),
1371 Self::Deployment(error) => Some(error),
1372 Self::Encode(error) => Some(error),
1373 Self::NonCommandFrame | Self::MissingQueue(_) => None,
1374 }
1375 }
1376}
1377
1378#[derive(Clone, Debug, Default, PartialEq, Eq)]
1380pub struct GatewayCommandPipelineReport {
1381 pub client_id: Option<ClientId>,
1383 pub command_id: Option<CommandId>,
1385 pub station_id: Option<StationId>,
1387 pub node_id: Option<NodeId>,
1389 pub delivery: Option<GatewayDeliveryRoute>,
1391 pub command: Option<CommandEnvelope>,
1393 pub accepted: bool,
1395 pub reason_code: u16,
1397 pub ack_bytes: Option<Vec<u8>>,
1399 pub error: Option<GatewayCommandPipelineError>,
1401}
1402
1403#[derive(Clone, Debug)]
1405pub struct GatewayCommandPipeline {
1406 config: GatewayCommandPipelineConfig,
1407 decoder: BinaryFrameDecoder,
1408 encoder: BinaryFrameEncoder,
1409 stats: GatewayCommandPipelineStats,
1410}
1411
1412impl GatewayCommandPipeline {
1413 pub fn new(config: GatewayCommandPipelineConfig) -> Self {
1415 Self {
1416 config,
1417 decoder: BinaryFrameDecoder,
1418 encoder: BinaryFrameEncoder,
1419 stats: GatewayCommandPipelineStats::default(),
1420 }
1421 }
1422
1423 pub const fn config(&self) -> GatewayCommandPipelineConfig {
1425 self.config
1426 }
1427
1428 pub const fn stats(&self) -> GatewayCommandPipelineStats {
1430 self.stats
1431 }
1432
1433 pub fn process(
1436 &mut self,
1437 gateway: &mut GatewaySessionTable,
1438 station_queues: &mut BTreeMap<StationId, CommandQueues>,
1439 input: &[u8],
1440 now: Tick,
1441 ingress: CommandIngress,
1442 ) -> GatewayCommandPipelineReport {
1443 let command_frame = match self.decode_command_frame(input) {
1444 Ok(command_frame) => command_frame,
1445 Err(error) => {
1446 return GatewayCommandPipelineReport {
1447 error: Some(error),
1448 ..GatewayCommandPipelineReport::default()
1449 };
1450 }
1451 };
1452
1453 self.process_command_frame(gateway, station_queues, command_frame, now, ingress)
1454 }
1455
1456 pub fn dispatch(
1459 &mut self,
1460 gateway: &mut GatewaySessionTable,
1461 deployment: &DeploymentRouteTable,
1462 input: &[u8],
1463 now: Tick,
1464 ) -> GatewayCommandPipelineReport {
1465 let command_frame = match self.decode_command_frame(input) {
1466 Ok(command_frame) => command_frame,
1467 Err(error) => {
1468 return GatewayCommandPipelineReport {
1469 error: Some(error),
1470 ..GatewayCommandPipelineReport::default()
1471 };
1472 }
1473 };
1474
1475 self.dispatch_command_frame(gateway, deployment, command_frame, now)
1476 }
1477
1478 fn decode_command_frame(
1479 &mut self,
1480 input: &[u8],
1481 ) -> Result<CommandFrame, GatewayCommandPipelineError> {
1482 let frame = match self.decoder.decode(input) {
1483 Ok(frame) => frame,
1484 Err(error) => {
1485 self.stats.frames_rejected_decode =
1486 self.stats.frames_rejected_decode.saturating_add(1);
1487 return Err(GatewayCommandPipelineError::Decode(error));
1488 }
1489 };
1490
1491 let RuntimeFrame::Command(command_frame) = frame else {
1492 self.stats.frames_rejected_non_command =
1493 self.stats.frames_rejected_non_command.saturating_add(1);
1494 return Err(GatewayCommandPipelineError::NonCommandFrame);
1495 };
1496 self.stats.command_frames_decoded = self.stats.command_frames_decoded.saturating_add(1);
1497
1498 Ok(command_frame)
1499 }
1500
1501 fn process_command_frame(
1502 &mut self,
1503 gateway: &mut GatewaySessionTable,
1504 station_queues: &mut BTreeMap<StationId, CommandQueues>,
1505 command_frame: CommandFrame,
1506 now: Tick,
1507 ingress: CommandIngress,
1508 ) -> GatewayCommandPipelineReport {
1509 let client_id = command_frame.client_id;
1510 let command_id = command_frame.command_id;
1511 let command = command_frame.into_envelope(now);
1512 let admission = match gateway.admit_command(&command) {
1513 Ok(admission) => {
1514 self.stats.commands_admitted = self.stats.commands_admitted.saturating_add(1);
1515 admission
1516 }
1517 Err(error) => {
1518 self.stats.commands_rejected_gateway =
1519 self.stats.commands_rejected_gateway.saturating_add(1);
1520 return self.rejected_report(
1521 client_id,
1522 command_id,
1523 None,
1524 now,
1525 gateway_reject_reason_code(error),
1526 GatewayCommandPipelineError::Gateway(error),
1527 );
1528 }
1529 };
1530
1531 let station_id = admission.route.station_id;
1532 let Some(queue) = station_queues.get_mut(&station_id) else {
1533 self.stats.commands_rejected_queue =
1534 self.stats.commands_rejected_queue.saturating_add(1);
1535 return self.rejected_report(
1536 client_id,
1537 command_id,
1538 Some(station_id),
1539 now,
1540 GATEWAY_COMMAND_ACK_MISSING_QUEUE,
1541 GatewayCommandPipelineError::MissingQueue(station_id),
1542 );
1543 };
1544
1545 if let Err(error) = queue.push(command, ingress) {
1546 self.stats.commands_rejected_queue =
1547 self.stats.commands_rejected_queue.saturating_add(1);
1548 return self.rejected_report(
1549 client_id,
1550 command_id,
1551 Some(station_id),
1552 now,
1553 queue_reject_reason_code(error),
1554 GatewayCommandPipelineError::Queue(error),
1555 );
1556 }
1557
1558 self.stats.commands_enqueued = self.stats.commands_enqueued.saturating_add(1);
1559 self.accepted_report(client_id, command_id, station_id, now)
1560 }
1561
1562 fn dispatch_command_frame(
1563 &mut self,
1564 gateway: &mut GatewaySessionTable,
1565 deployment: &DeploymentRouteTable,
1566 command_frame: CommandFrame,
1567 now: Tick,
1568 ) -> GatewayCommandPipelineReport {
1569 let client_id = command_frame.client_id;
1570 let command_id = command_frame.command_id;
1571 let command = command_frame.into_envelope(now);
1572 let admission = match gateway.admit_command(&command) {
1573 Ok(admission) => {
1574 self.stats.commands_admitted = self.stats.commands_admitted.saturating_add(1);
1575 admission
1576 }
1577 Err(error) => {
1578 self.stats.commands_rejected_gateway =
1579 self.stats.commands_rejected_gateway.saturating_add(1);
1580 return self.rejected_report(
1581 client_id,
1582 command_id,
1583 None,
1584 now,
1585 gateway_reject_reason_code(error),
1586 GatewayCommandPipelineError::Gateway(error),
1587 );
1588 }
1589 };
1590
1591 let delivery = match deployment.resolve_gateway_route(admission.route) {
1592 Ok(delivery) => {
1593 self.stats.commands_routed_deployment =
1594 self.stats.commands_routed_deployment.saturating_add(1);
1595 delivery
1596 }
1597 Err(error) => {
1598 self.stats.commands_rejected_deployment =
1599 self.stats.commands_rejected_deployment.saturating_add(1);
1600 return self.rejected_report(
1601 client_id,
1602 command_id,
1603 Some(admission.route.station_id),
1604 now,
1605 GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED,
1606 GatewayCommandPipelineError::Deployment(error),
1607 );
1608 }
1609 };
1610
1611 self.dispatch_report(command, delivery, now)
1612 }
1613
1614 fn accepted_report(
1615 &mut self,
1616 client_id: ClientId,
1617 command_id: CommandId,
1618 station_id: StationId,
1619 now: Tick,
1620 ) -> GatewayCommandPipelineReport {
1621 let ack = CommandAckFrame {
1622 client_id,
1623 command_id,
1624 server_tick: now,
1625 accepted: true,
1626 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1627 };
1628 match self.encode_ack(&ack) {
1629 Ok(ack_bytes) => GatewayCommandPipelineReport {
1630 client_id: Some(client_id),
1631 command_id: Some(command_id),
1632 station_id: Some(station_id),
1633 node_id: None,
1634 delivery: None,
1635 command: None,
1636 accepted: true,
1637 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1638 ack_bytes: Some(ack_bytes),
1639 error: None,
1640 },
1641 Err(error) => GatewayCommandPipelineReport {
1642 client_id: Some(client_id),
1643 command_id: Some(command_id),
1644 station_id: Some(station_id),
1645 node_id: None,
1646 delivery: None,
1647 command: None,
1648 accepted: false,
1649 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1650 ack_bytes: None,
1651 error: Some(GatewayCommandPipelineError::Encode(error)),
1652 },
1653 }
1654 }
1655
1656 fn dispatch_report(
1657 &mut self,
1658 command: CommandEnvelope,
1659 delivery: GatewayDeliveryRoute,
1660 now: Tick,
1661 ) -> GatewayCommandPipelineReport {
1662 let ack = CommandAckFrame {
1663 client_id: command.client_id,
1664 command_id: command.id,
1665 server_tick: now,
1666 accepted: true,
1667 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1668 };
1669 match self.encode_ack(&ack) {
1670 Ok(ack_bytes) => GatewayCommandPipelineReport {
1671 client_id: Some(command.client_id),
1672 command_id: Some(command.id),
1673 station_id: Some(delivery.station_id),
1674 node_id: Some(delivery.node_id),
1675 delivery: Some(delivery),
1676 command: Some(command),
1677 accepted: true,
1678 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1679 ack_bytes: Some(ack_bytes),
1680 error: None,
1681 },
1682 Err(error) => GatewayCommandPipelineReport {
1683 client_id: Some(command.client_id),
1684 command_id: Some(command.id),
1685 station_id: Some(delivery.station_id),
1686 node_id: Some(delivery.node_id),
1687 delivery: Some(delivery),
1688 command: None,
1689 accepted: false,
1690 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1691 ack_bytes: None,
1692 error: Some(GatewayCommandPipelineError::Encode(error)),
1693 },
1694 }
1695 }
1696
1697 fn rejected_report(
1698 &mut self,
1699 client_id: ClientId,
1700 command_id: CommandId,
1701 station_id: Option<StationId>,
1702 now: Tick,
1703 reason_code: u16,
1704 error: GatewayCommandPipelineError,
1705 ) -> GatewayCommandPipelineReport {
1706 let ack_bytes = if self.config.ack_rejections {
1707 let ack = CommandAckFrame {
1708 client_id,
1709 command_id,
1710 server_tick: now,
1711 accepted: false,
1712 reason_code,
1713 };
1714 match self.encode_ack(&ack) {
1715 Ok(bytes) => Some(bytes),
1716 Err(encode_error) => {
1717 return GatewayCommandPipelineReport {
1718 client_id: Some(client_id),
1719 command_id: Some(command_id),
1720 station_id,
1721 node_id: None,
1722 delivery: None,
1723 command: None,
1724 accepted: false,
1725 reason_code,
1726 ack_bytes: None,
1727 error: Some(GatewayCommandPipelineError::Encode(encode_error)),
1728 };
1729 }
1730 }
1731 } else {
1732 None
1733 };
1734
1735 GatewayCommandPipelineReport {
1736 client_id: Some(client_id),
1737 command_id: Some(command_id),
1738 station_id,
1739 node_id: None,
1740 delivery: None,
1741 command: None,
1742 accepted: false,
1743 reason_code,
1744 ack_bytes,
1745 error: Some(error),
1746 }
1747 }
1748
1749 fn encode_ack(&mut self, ack: &CommandAckFrame) -> Result<Vec<u8>, BinaryEncodeError> {
1750 let mut out = Vec::new();
1751 self.encoder.encode_command_ack(ack, &mut out)?;
1752 self.stats.acks_encoded = self.stats.acks_encoded.saturating_add(1);
1753 Ok(out)
1754 }
1755}
1756
1757impl Default for GatewayCommandPipeline {
1758 fn default() -> Self {
1759 Self::new(GatewayCommandPipelineConfig::default())
1760 }
1761}
1762
1763const fn gateway_reject_reason_code(error: GatewayError) -> u16 {
1764 match error {
1765 GatewayError::ReplayOrStale { .. } => GATEWAY_COMMAND_ACK_REPLAY_OR_STALE,
1766 GatewayError::RateLimited { .. } => GATEWAY_COMMAND_ACK_RATE_LIMITED,
1767 GatewayError::MissingSession(_)
1768 | GatewayError::SessionDisconnected { .. }
1769 | GatewayError::BadGeneration { .. }
1770 | GatewayError::CapacityFull { .. } => GATEWAY_COMMAND_ACK_GATEWAY_REJECTED,
1771 }
1772}
1773
1774const fn queue_reject_reason_code(error: CommandQueueError) -> u16 {
1775 match error {
1776 CommandQueueError::QueueFull(_) => GATEWAY_COMMAND_ACK_QUEUE_FULL,
1777 CommandQueueError::RejectedByBarrier(_) => GATEWAY_COMMAND_ACK_BARRIER_REJECTED,
1778 }
1779}
1780
1781#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1783pub struct GatewayClientTransportStats {
1784 pub packets_received: usize,
1786 pub bytes_received: usize,
1788 pub command_frames_received: usize,
1790 pub source_mismatches: usize,
1792 pub commands_accepted: usize,
1794 pub commands_rejected: usize,
1796 pub acks_sent: usize,
1798 pub ack_bytes_sent: usize,
1800}
1801
1802#[derive(Clone, Debug, Default, PartialEq, Eq)]
1804pub struct GatewayClientTransportPump {
1805 pub packets_received: usize,
1807 pub bytes_received: usize,
1809 pub reports: Vec<GatewayCommandPipelineReport>,
1811 pub acks_sent: usize,
1813 pub ack_bytes_sent: usize,
1815}
1816
1817impl GatewayClientTransportPump {
1818 pub fn commands_processed(&self) -> usize {
1820 self.reports.len()
1821 }
1822
1823 pub fn commands_accepted(&self) -> usize {
1825 self.reports.iter().filter(|report| report.accepted).count()
1826 }
1827
1828 pub fn commands_rejected(&self) -> usize {
1830 self.reports
1831 .iter()
1832 .filter(|report| !report.accepted)
1833 .count()
1834 }
1835}
1836
1837#[derive(Clone, Debug, PartialEq, Eq)]
1839pub enum GatewayClientTransportError<E> {
1840 Transport(E),
1842 Decode(BinaryDecodeError),
1844 NonCommandFrame,
1846 SourceMismatch {
1848 packet_client_id: ClientId,
1850 frame_client_id: ClientId,
1852 },
1853}
1854
1855impl<E: core::fmt::Display> core::fmt::Display for GatewayClientTransportError<E> {
1856 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1857 match self {
1858 Self::Transport(error) => write!(f, "{error}"),
1859 Self::Decode(error) => write!(f, "{error}"),
1860 Self::NonCommandFrame => f.write_str("gateway client transport expected command frame"),
1861 Self::SourceMismatch {
1862 packet_client_id,
1863 frame_client_id,
1864 } => write!(
1865 f,
1866 "gateway client source mismatch: packet {}, frame {}",
1867 packet_client_id.get(),
1868 frame_client_id.get()
1869 ),
1870 }
1871 }
1872}
1873
1874impl<E> std::error::Error for GatewayClientTransportError<E>
1875where
1876 E: std::error::Error + 'static,
1877{
1878 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1879 match self {
1880 Self::Transport(error) => Some(error),
1881 Self::Decode(error) => Some(error),
1882 Self::NonCommandFrame | Self::SourceMismatch { .. } => None,
1883 }
1884 }
1885}
1886
1887#[derive(Clone, Debug, Default)]
1889pub struct GatewayClientTransportBridge {
1890 stats: GatewayClientTransportStats,
1891}
1892
1893impl GatewayClientTransportBridge {
1894 pub const fn new() -> Self {
1896 Self {
1897 stats: GatewayClientTransportStats {
1898 packets_received: 0,
1899 bytes_received: 0,
1900 command_frames_received: 0,
1901 source_mismatches: 0,
1902 commands_accepted: 0,
1903 commands_rejected: 0,
1904 acks_sent: 0,
1905 ack_bytes_sent: 0,
1906 },
1907 }
1908 }
1909
1910 pub const fn stats(&self) -> GatewayClientTransportStats {
1912 self.stats
1913 }
1914
1915 #[allow(clippy::too_many_arguments)]
1918 pub fn pump_ingress<T, E>(
1919 &mut self,
1920 transport: &mut T,
1921 pipeline: &mut GatewayCommandPipeline,
1922 gateway: &mut GatewaySessionTable,
1923 station_queues: &mut BTreeMap<StationId, CommandQueues>,
1924 now: Tick,
1925 ingress: CommandIngress,
1926 max_packets: usize,
1927 ) -> Result<GatewayClientTransportPump, GatewayClientTransportError<E>>
1928 where
1929 T: TransportReceiver<Error = E> + TransportSink<Error = E>,
1930 {
1931 let mut pump = GatewayClientTransportPump::default();
1932 for _ in 0..max_packets {
1933 let Some(packet) = transport
1934 .try_recv()
1935 .map_err(GatewayClientTransportError::Transport)?
1936 else {
1937 break;
1938 };
1939 self.stats.packets_received = self.stats.packets_received.saturating_add(1);
1940 self.stats.bytes_received =
1941 self.stats.bytes_received.saturating_add(packet.bytes.len());
1942 pump.packets_received = pump.packets_received.saturating_add(1);
1943 pump.bytes_received = pump.bytes_received.saturating_add(packet.bytes.len());
1944
1945 let command_frame = match pipeline.decode_command_frame(&packet.bytes) {
1946 Ok(command_frame) => command_frame,
1947 Err(GatewayCommandPipelineError::Decode(error)) => {
1948 return Err(GatewayClientTransportError::Decode(error));
1949 }
1950 Err(GatewayCommandPipelineError::NonCommandFrame) => {
1951 return Err(GatewayClientTransportError::NonCommandFrame);
1952 }
1953 Err(error) => {
1954 unreachable!(
1955 "decode_command_frame only returns decode/non-command errors: {error}"
1956 );
1957 }
1958 };
1959 self.stats.command_frames_received =
1960 self.stats.command_frames_received.saturating_add(1);
1961
1962 if let Some(packet_client_id) = packet.client_id
1963 && packet_client_id != command_frame.client_id
1964 {
1965 self.stats.source_mismatches = self.stats.source_mismatches.saturating_add(1);
1966 return Err(GatewayClientTransportError::SourceMismatch {
1967 packet_client_id,
1968 frame_client_id: command_frame.client_id,
1969 });
1970 }
1971
1972 let ack_client_id = command_frame.client_id;
1973 let report = pipeline.process_command_frame(
1974 gateway,
1975 station_queues,
1976 command_frame,
1977 now,
1978 ingress,
1979 );
1980 if report.accepted {
1981 self.stats.commands_accepted = self.stats.commands_accepted.saturating_add(1);
1982 } else {
1983 self.stats.commands_rejected = self.stats.commands_rejected.saturating_add(1);
1984 }
1985
1986 if let Some(bytes) = &report.ack_bytes {
1987 let ack_len = bytes.len();
1988 transport
1989 .send(OutboundPacket {
1990 client_id: ack_client_id,
1991 bytes: bytes.clone(),
1992 })
1993 .map_err(GatewayClientTransportError::Transport)?;
1994 self.stats.acks_sent = self.stats.acks_sent.saturating_add(1);
1995 self.stats.ack_bytes_sent = self.stats.ack_bytes_sent.saturating_add(ack_len);
1996 pump.acks_sent = pump.acks_sent.saturating_add(1);
1997 pump.ack_bytes_sent = pump.ack_bytes_sent.saturating_add(ack_len);
1998 }
1999 pump.reports.push(report);
2000 }
2001 Ok(pump)
2002 }
2003}
2004
2005#[derive(Clone, Debug, Default)]
2007pub struct StationSet {
2008 stations: Vec<Station>,
2009}
2010
2011impl StationSet {
2012 pub fn push(&mut self, station: Station) {
2014 self.stations.push(station);
2015 }
2016
2017 pub fn get(&self, station_id: StationId) -> Option<&Station> {
2019 self.stations
2020 .iter()
2021 .find(|station| station.config().station_id == station_id)
2022 }
2023
2024 pub fn get_mut(&mut self, station_id: StationId) -> Option<&mut Station> {
2026 self.stations
2027 .iter_mut()
2028 .find(|station| station.config().station_id == station_id)
2029 }
2030
2031 pub fn get_pair_mut(
2033 &mut self,
2034 left_id: StationId,
2035 right_id: StationId,
2036 ) -> Option<(&mut Station, &mut Station)> {
2037 if left_id == right_id {
2038 return None;
2039 }
2040
2041 let left_index = self
2042 .stations
2043 .iter()
2044 .position(|station| station.config().station_id == left_id)?;
2045 let right_index = self
2046 .stations
2047 .iter()
2048 .position(|station| station.config().station_id == right_id)?;
2049
2050 if left_index < right_index {
2051 let (left, right) = self.stations.split_at_mut(right_index);
2052 Some((&mut left[left_index], &mut right[0]))
2053 } else {
2054 let (left, right) = self.stations.split_at_mut(left_index);
2055 Some((&mut right[0], &mut left[right_index]))
2056 }
2057 }
2058
2059 pub fn iter(&self) -> impl Iterator<Item = &Station> {
2061 self.stations.iter()
2062 }
2063
2064 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Station> {
2066 self.stations.iter_mut()
2067 }
2068
2069 pub fn station_ids_in_scope(&self, scope: BarrierScope) -> Vec<StationId> {
2071 self.stations
2072 .iter()
2073 .filter(|station| match scope {
2074 BarrierScope::Instance(instance_id) => station.config().instance_id == instance_id,
2075 BarrierScope::Station(station_id) => station.config().station_id == station_id,
2076 })
2077 .map(|station| station.config().station_id)
2078 .collect()
2079 }
2080
2081 pub fn len(&self) -> usize {
2083 self.stations.len()
2084 }
2085
2086 pub fn is_empty(&self) -> bool {
2088 self.stations.is_empty()
2089 }
2090}
2091
2092#[derive(Clone, Debug, Default)]
2094pub struct StationIndexSet {
2095 indexes: Vec<(StationId, CellIndex)>,
2096}
2097
2098impl StationIndexSet {
2099 pub fn insert(&mut self, station_id: StationId, index: CellIndex) {
2101 if let Some((_, existing)) = self.indexes.iter_mut().find(|(id, _)| *id == station_id) {
2102 *existing = index;
2103 } else {
2104 self.indexes.push((station_id, index));
2105 }
2106 }
2107
2108 pub fn get(&self, station_id: StationId) -> Option<&CellIndex> {
2110 self.indexes
2111 .iter()
2112 .find(|(id, _)| *id == station_id)
2113 .map(|(_, index)| index)
2114 }
2115
2116 pub fn get_mut(&mut self, station_id: StationId) -> Option<&mut CellIndex> {
2118 self.indexes
2119 .iter_mut()
2120 .find(|(id, _)| *id == station_id)
2121 .map(|(_, index)| index)
2122 }
2123
2124 pub fn get_pair_mut(
2126 &mut self,
2127 left_id: StationId,
2128 right_id: StationId,
2129 ) -> Option<(&mut CellIndex, &mut CellIndex)> {
2130 if left_id == right_id {
2131 return None;
2132 }
2133
2134 let left_index = self.indexes.iter().position(|(id, _)| *id == left_id)?;
2135 let right_index = self.indexes.iter().position(|(id, _)| *id == right_id)?;
2136
2137 if left_index < right_index {
2138 let (left, right) = self.indexes.split_at_mut(right_index);
2139 Some((&mut left[left_index].1, &mut right[0].1))
2140 } else {
2141 let (left, right) = self.indexes.split_at_mut(left_index);
2142 Some((&mut right[0].1, &mut left[right_index].1))
2143 }
2144 }
2145
2146 pub fn len(&self) -> usize {
2148 self.indexes.len()
2149 }
2150
2151 pub fn iter(&self) -> impl Iterator<Item = (StationId, &CellIndex)> {
2153 self.indexes
2154 .iter()
2155 .map(|(station_id, index)| (*station_id, index))
2156 }
2157
2158 pub fn is_empty(&self) -> bool {
2160 self.indexes.is_empty()
2161 }
2162}
2163
2164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2166pub struct StationLoadSamplerConfig {
2167 pub estimated_bytes_per_entity: usize,
2169 pub estimated_bytes_per_subscriber: usize,
2171 pub estimated_bytes_per_event: usize,
2173 pub tick_cost_per_owned_entity: u64,
2175 pub tick_cost_per_ghost_entity: u64,
2177 pub tick_cost_per_occupied_cell: u64,
2179 pub tick_cost_per_queued_event: u64,
2181}
2182
2183impl Default for StationLoadSamplerConfig {
2184 fn default() -> Self {
2185 Self {
2186 estimated_bytes_per_entity: 48,
2187 estimated_bytes_per_subscriber: 16,
2188 estimated_bytes_per_event: 32,
2189 tick_cost_per_owned_entity: 2,
2190 tick_cost_per_ghost_entity: 1,
2191 tick_cost_per_occupied_cell: 1,
2192 tick_cost_per_queued_event: 1,
2193 }
2194 }
2195}
2196
2197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2199pub struct StationLoadSampler {
2200 config: StationLoadSamplerConfig,
2201}
2202
2203impl StationLoadSampler {
2204 pub const fn new(config: StationLoadSamplerConfig) -> Self {
2206 Self { config }
2207 }
2208
2209 pub const fn config(&self) -> StationLoadSamplerConfig {
2211 self.config
2212 }
2213
2214 pub fn sample_station(
2217 &self,
2218 station: &Station,
2219 index: Option<&CellIndex>,
2220 queued_events: usize,
2221 subscribers: usize,
2222 ) -> StationLoadSample {
2223 let (owned_entities, ghost_entities) = count_station_roles(station);
2224 let cells = index
2225 .map(|index| self.sample_cells(station, index))
2226 .unwrap_or_default();
2227 StationLoadSample {
2228 station_id: station.config().station_id,
2229 owned_entities,
2230 ghost_entities,
2231 subscribers,
2232 queued_events,
2233 estimated_bytes: self.estimate_station_bytes(
2234 owned_entities,
2235 ghost_entities,
2236 subscribers,
2237 queued_events,
2238 ),
2239 tick_cost_units: self.estimate_tick_cost(
2240 owned_entities,
2241 ghost_entities,
2242 cells.len(),
2243 queued_events,
2244 ),
2245 cells,
2246 }
2247 }
2248
2249 pub fn sample_all(
2257 &self,
2258 stations: &StationSet,
2259 indexes: &StationIndexSet,
2260 router: &EventRouter,
2261 subscriber_counts: &[(StationId, usize)],
2262 ) -> Vec<StationLoadSample> {
2263 let subscribers_by_station = station_count_map(subscriber_counts);
2264 stations
2265 .iter()
2266 .map(|station| {
2267 let station_id = station.config().station_id;
2268 self.sample_station(
2269 station,
2270 indexes.get(station_id),
2271 router.queued_len(station_id).unwrap_or(0),
2272 subscribers_by_station
2273 .get(&station_id)
2274 .copied()
2275 .unwrap_or(0),
2276 )
2277 })
2278 .collect()
2279 }
2280
2281 fn sample_cells(&self, station: &Station, index: &CellIndex) -> Vec<CellLoadSample> {
2282 index
2283 .cell_occupancy()
2284 .into_iter()
2285 .map(|occupancy| {
2286 let mut owned_entities = 0usize;
2287 let mut ghost_entities = 0usize;
2288 for handle in index.handles_in_cell_slice(occupancy.cell) {
2289 if let Some(record) = station.get(*handle) {
2290 if record.is_owned() {
2291 owned_entities = owned_entities.saturating_add(1);
2292 } else {
2293 ghost_entities = ghost_entities.saturating_add(1);
2294 }
2295 }
2296 }
2297 let entities = owned_entities.saturating_add(ghost_entities);
2298 CellLoadSample {
2299 cell: occupancy.cell,
2300 owned_entities,
2301 ghost_entities,
2302 subscribers: 0,
2303 estimated_updates: entities,
2304 estimated_bytes: entities
2305 .saturating_mul(self.config.estimated_bytes_per_entity),
2306 tick_cost_units: self.estimate_tick_cost(owned_entities, ghost_entities, 1, 0),
2307 event_pressure: 0,
2308 }
2309 })
2310 .collect()
2311 }
2312
2313 fn estimate_station_bytes(
2314 &self,
2315 owned_entities: usize,
2316 ghost_entities: usize,
2317 subscribers: usize,
2318 queued_events: usize,
2319 ) -> usize {
2320 owned_entities
2321 .saturating_add(ghost_entities)
2322 .saturating_mul(self.config.estimated_bytes_per_entity)
2323 .saturating_add(subscribers.saturating_mul(self.config.estimated_bytes_per_subscriber))
2324 .saturating_add(queued_events.saturating_mul(self.config.estimated_bytes_per_event))
2325 }
2326
2327 fn estimate_tick_cost(
2328 &self,
2329 owned_entities: usize,
2330 ghost_entities: usize,
2331 occupied_cells: usize,
2332 queued_events: usize,
2333 ) -> u64 {
2334 (owned_entities as u64)
2335 .saturating_mul(self.config.tick_cost_per_owned_entity)
2336 .saturating_add(
2337 (ghost_entities as u64).saturating_mul(self.config.tick_cost_per_ghost_entity),
2338 )
2339 .saturating_add(
2340 (occupied_cells as u64).saturating_mul(self.config.tick_cost_per_occupied_cell),
2341 )
2342 .saturating_add(
2343 (queued_events as u64).saturating_mul(self.config.tick_cost_per_queued_event),
2344 )
2345 }
2346}
2347
2348impl Default for StationLoadSampler {
2349 fn default() -> Self {
2350 Self::new(StationLoadSamplerConfig::default())
2351 }
2352}
2353
2354fn count_station_roles(station: &Station) -> (usize, usize) {
2355 let mut owned_entities = 0usize;
2356 let mut ghost_entities = 0usize;
2357 for record in station.iter() {
2358 if record.is_owned() {
2359 owned_entities = owned_entities.saturating_add(1);
2360 } else {
2361 ghost_entities = ghost_entities.saturating_add(1);
2362 }
2363 }
2364 (owned_entities, ghost_entities)
2365}
2366
2367fn station_count_map(counts: &[(StationId, usize)]) -> BTreeMap<StationId, usize> {
2368 let mut map = BTreeMap::new();
2369 for (station_id, count) in counts {
2370 let entry = map.entry(*station_id).or_insert(0usize);
2371 *entry = entry.saturating_add(*count);
2372 }
2373 map
2374}
2375
2376#[derive(Clone, Debug, PartialEq)]
2378pub struct EntityMigrationReport {
2379 pub transfer: HandoffTransfer,
2381 pub source_ghost: EntityHandle,
2383 pub target_owner: EntityHandle,
2385}
2386
2387#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2389pub enum EntityMigrationError {
2390 SameSourceAndTarget(StationId),
2392 MissingSource(StationId),
2394 MissingTarget(StationId),
2396 Station(StationError),
2398}
2399
2400impl core::fmt::Display for EntityMigrationError {
2401 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2402 match self {
2403 Self::SameSourceAndTarget(id) => {
2404 write!(f, "source and target station are both {}", id.get())
2405 }
2406 Self::MissingSource(id) => write!(f, "source station {} is missing", id.get()),
2407 Self::MissingTarget(id) => write!(f, "target station {} is missing", id.get()),
2408 Self::Station(error) => write!(f, "{error}"),
2409 }
2410 }
2411}
2412
2413impl std::error::Error for EntityMigrationError {}
2414
2415impl From<StationError> for EntityMigrationError {
2416 fn from(value: StationError) -> Self {
2417 Self::Station(value)
2418 }
2419}
2420
2421#[derive(Clone, Copy, Debug, Default)]
2423pub struct EntityMigrationExecutor;
2424
2425impl EntityMigrationExecutor {
2426 pub fn migrate_entity(
2428 stations: &mut StationSet,
2429 entity_id: EntityId,
2430 source_station: StationId,
2431 target_station: StationId,
2432 ghost_ttl_ticks: u64,
2433 ) -> Result<EntityMigrationReport, EntityMigrationError> {
2434 if source_station == target_station {
2435 return Err(EntityMigrationError::SameSourceAndTarget(source_station));
2436 }
2437
2438 if stations.get(source_station).is_none() {
2439 return Err(EntityMigrationError::MissingSource(source_station));
2440 }
2441 if stations.get(target_station).is_none() {
2442 return Err(EntityMigrationError::MissingTarget(target_station));
2443 }
2444
2445 let (source, target) = stations
2446 .get_pair_mut(source_station, target_station)
2447 .expect("stations were checked above");
2448 let target_epoch = next_target_epoch(target);
2449 let source_ghost_expires_at =
2450 Tick::new(source.tick().get().saturating_add(ghost_ttl_ticks));
2451 let transfer = source.prepare_outgoing_handoff(
2452 entity_id,
2453 target_station,
2454 target_epoch,
2455 source_ghost_expires_at,
2456 )?;
2457 target.prewarm_handoff_ghost(&transfer)?;
2458 let target_owner = target.commit_incoming_handoff(transfer.clone())?;
2459 let source_ghost = source.commit_outgoing_handoff(&transfer)?;
2460
2461 Ok(EntityMigrationReport {
2462 transfer,
2463 source_ghost,
2464 target_owner,
2465 })
2466 }
2467}
2468
2469fn next_target_epoch(station: &mut Station) -> OwnerEpoch {
2470 station.next_owner_epoch()
2471}
2472
2473#[derive(Clone, Debug, Default, PartialEq, Eq)]
2475pub struct CellOwnershipTable {
2476 owners: BTreeMap<CellCoord3, StationId>,
2477}
2478
2479impl CellOwnershipTable {
2480 pub fn assign(&mut self, cell: CellCoord3, station_id: StationId) -> Option<StationId> {
2482 self.owners.insert(cell, station_id)
2483 }
2484
2485 pub fn owner_of(&self, cell: CellCoord3) -> Option<StationId> {
2487 self.owners.get(&cell).copied()
2488 }
2489
2490 pub fn apply_split(
2492 &mut self,
2493 proposal: &SplitProposal,
2494 target_station: StationId,
2495 ) -> CellOwnershipUpdate {
2496 let mut moved_cells = Vec::new();
2497 for cell in &proposal.cells_to_move {
2498 let previous = self.assign(*cell, target_station);
2499 if previous != Some(target_station) {
2500 moved_cells.push(*cell);
2501 }
2502 }
2503 CellOwnershipUpdate {
2504 source_station: proposal.source_station,
2505 target_station,
2506 moved_cells,
2507 }
2508 }
2509
2510 pub fn len(&self) -> usize {
2512 self.owners.len()
2513 }
2514
2515 pub fn is_empty(&self) -> bool {
2517 self.owners.is_empty()
2518 }
2519}
2520
2521#[derive(Clone, Debug, Default, PartialEq, Eq)]
2523pub struct CellOwnershipUpdate {
2524 pub source_station: StationId,
2526 pub target_station: StationId,
2528 pub moved_cells: Vec<CellCoord3>,
2530}
2531
2532#[derive(Clone, Debug, Default, PartialEq)]
2534pub struct CellMigrationReport {
2535 pub source_station: StationId,
2537 pub target_station: StationId,
2539 pub scanned_cells: Vec<CellCoord3>,
2541 pub entity_migrations: Vec<EntityMigrationReport>,
2543 pub skipped_missing_handles: usize,
2545 pub skipped_non_owned: usize,
2547 pub skipped_duplicate_entities: usize,
2549}
2550
2551#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2553pub enum CellMigrationError {
2554 Entity(EntityMigrationError),
2556 MissingTargetRecord(EntityId),
2558 MissingSourceRecord(EntityId),
2560}
2561
2562impl core::fmt::Display for CellMigrationError {
2563 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2564 match self {
2565 Self::Entity(error) => write!(f, "{error}"),
2566 Self::MissingTargetRecord(id) => {
2567 write!(f, "target owner record for entity {} is missing", id.get())
2568 }
2569 Self::MissingSourceRecord(id) => {
2570 write!(f, "source ghost record for entity {} is missing", id.get())
2571 }
2572 }
2573 }
2574}
2575
2576impl std::error::Error for CellMigrationError {}
2577
2578impl From<EntityMigrationError> for CellMigrationError {
2579 fn from(value: EntityMigrationError) -> Self {
2580 Self::Entity(value)
2581 }
2582}
2583
2584#[derive(Clone, Copy, Debug, Default)]
2586pub struct CellMigrationExecutor;
2587
2588impl CellMigrationExecutor {
2589 pub fn migrate_cells(
2591 stations: &mut StationSet,
2592 source_index: &mut CellIndex,
2593 target_index: &mut CellIndex,
2594 source_station: StationId,
2595 target_station: StationId,
2596 cells: &[CellCoord3],
2597 ghost_ttl_ticks: u64,
2598 ) -> Result<CellMigrationReport, CellMigrationError> {
2599 let mut report = CellMigrationReport {
2600 source_station,
2601 target_station,
2602 scanned_cells: cells.to_vec(),
2603 ..CellMigrationReport::default()
2604 };
2605 let mut seen_handles = BTreeSet::new();
2606 let mut entity_ids = Vec::new();
2607
2608 {
2609 let source = stations
2610 .get(source_station)
2611 .ok_or(EntityMigrationError::MissingSource(source_station))?;
2612 for cell in cells {
2613 for handle in source_index.handles_in_cell(*cell) {
2614 if !seen_handles.insert(handle) {
2615 report.skipped_duplicate_entities += 1;
2616 continue;
2617 }
2618 let Some(record) = source.get(handle) else {
2619 report.skipped_missing_handles += 1;
2620 continue;
2621 };
2622 if record.is_owned() {
2623 entity_ids.push(record.id);
2624 } else {
2625 report.skipped_non_owned += 1;
2626 }
2627 }
2628 }
2629 }
2630
2631 let mut seen_entities = BTreeSet::new();
2632 for entity_id in entity_ids {
2633 if !seen_entities.insert(entity_id) {
2634 report.skipped_duplicate_entities += 1;
2635 continue;
2636 }
2637 let migration = EntityMigrationExecutor::migrate_entity(
2638 stations,
2639 entity_id,
2640 source_station,
2641 target_station,
2642 ghost_ttl_ticks,
2643 )?;
2644
2645 {
2646 let target = stations
2647 .get(target_station)
2648 .ok_or(EntityMigrationError::MissingTarget(target_station))?;
2649 let target_record = target
2650 .get(migration.target_owner)
2651 .ok_or(CellMigrationError::MissingTargetRecord(entity_id))?;
2652 target_index.upsert(
2653 migration.target_owner,
2654 target_record.position,
2655 target_record.bounds,
2656 );
2657 }
2658
2659 {
2660 let source = stations
2661 .get(source_station)
2662 .ok_or(EntityMigrationError::MissingSource(source_station))?;
2663 let source_record = source
2664 .get(migration.source_ghost)
2665 .ok_or(CellMigrationError::MissingSourceRecord(entity_id))?;
2666 source_index.upsert(
2667 migration.source_ghost,
2668 source_record.position,
2669 source_record.bounds,
2670 );
2671 }
2672
2673 report.entity_migrations.push(migration);
2674 }
2675
2676 Ok(report)
2677 }
2678}
2679
2680#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2682pub struct SplitSchedulerConfig {
2683 pub thresholds: HotspotThresholds,
2685 pub max_actions_per_pass: usize,
2687 pub max_cells_per_action: usize,
2689 pub ghost_ttl_ticks: u64,
2691 pub min_score_improvement: u64,
2693 pub max_target_score_after_move: u64,
2695 pub split_cooldown_ticks: u64,
2697 pub allow_warm_targets: bool,
2699}
2700
2701impl Default for SplitSchedulerConfig {
2702 fn default() -> Self {
2703 Self {
2704 thresholds: HotspotThresholds::default(),
2705 max_actions_per_pass: 4,
2706 max_cells_per_action: 4,
2707 ghost_ttl_ticks: 4,
2708 min_score_improvement: 1,
2709 max_target_score_after_move: u64::MAX,
2710 split_cooldown_ticks: 0,
2711 allow_warm_targets: true,
2712 }
2713 }
2714}
2715
2716#[derive(Clone, Debug, PartialEq, Eq)]
2718pub struct SplitAction {
2719 pub source_station: StationId,
2721 pub target_station: StationId,
2723 pub proposal: SplitProposal,
2725 pub source_score: u64,
2727 pub target_score: u64,
2729 pub estimated_target_score_after_move: u64,
2731}
2732
2733#[derive(Clone, Debug, Default, PartialEq, Eq)]
2735pub struct SplitSchedule {
2736 pub decisions: Vec<HotspotDecision>,
2738 pub actions: Vec<SplitAction>,
2740 pub skipped_no_target: usize,
2742 pub skipped_no_cells: usize,
2744 pub skipped_cooldown: usize,
2746 pub skipped_target_severity: usize,
2748 pub skipped_target_capacity: usize,
2750 pub skipped_insufficient_improvement: usize,
2752}
2753
2754#[derive(Clone, Debug, Default, PartialEq, Eq)]
2756pub struct SplitSchedulerState {
2757 last_split_at: BTreeMap<StationId, Tick>,
2758}
2759
2760impl SplitSchedulerState {
2761 pub fn last_split_at(&self, station_id: StationId) -> Option<Tick> {
2763 self.last_split_at.get(&station_id).copied()
2764 }
2765
2766 pub fn record_action(&mut self, action: &SplitAction, tick: Tick) {
2768 self.last_split_at.insert(action.source_station, tick);
2769 }
2770
2771 pub fn record_schedule(&mut self, schedule: &SplitSchedule, tick: Tick) {
2773 for action in &schedule.actions {
2774 self.record_action(action, tick);
2775 }
2776 }
2777
2778 pub fn is_in_cooldown(
2780 &self,
2781 station_id: StationId,
2782 current_tick: Tick,
2783 cooldown_ticks: u64,
2784 ) -> bool {
2785 if cooldown_ticks == 0 {
2786 return false;
2787 }
2788 let Some(last_split) = self.last_split_at(station_id) else {
2789 return false;
2790 };
2791 current_tick.get().saturating_sub(last_split.get()) < cooldown_ticks
2792 }
2793}
2794
2795#[derive(Clone, Debug, Default, PartialEq)]
2797pub struct SplitScheduleExecutionReport {
2798 pub ownership_updates: Vec<CellOwnershipUpdate>,
2800 pub cell_migrations: Vec<CellMigrationReport>,
2802}
2803
2804#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2806pub enum SplitScheduleExecutionError {
2807 MissingSourceIndex(StationId),
2809 MissingTargetIndex(StationId),
2811 CellMigration(CellMigrationError),
2813}
2814
2815impl core::fmt::Display for SplitScheduleExecutionError {
2816 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2817 match self {
2818 Self::MissingSourceIndex(id) => write!(f, "source index {} is missing", id.get()),
2819 Self::MissingTargetIndex(id) => write!(f, "target index {} is missing", id.get()),
2820 Self::CellMigration(error) => write!(f, "{error}"),
2821 }
2822 }
2823}
2824
2825impl std::error::Error for SplitScheduleExecutionError {}
2826
2827impl From<CellMigrationError> for SplitScheduleExecutionError {
2828 fn from(value: CellMigrationError) -> Self {
2829 Self::CellMigration(value)
2830 }
2831}
2832
2833#[derive(Clone, Copy, Debug)]
2835pub struct SplitScheduler {
2836 pub config: SplitSchedulerConfig,
2838}
2839
2840impl SplitScheduler {
2841 pub const fn new(config: SplitSchedulerConfig) -> Self {
2843 Self { config }
2844 }
2845
2846 pub fn plan(&self, samples: &[StationLoadSample]) -> SplitSchedule {
2848 self.plan_with_state(samples, None, Tick::new(0))
2849 }
2850
2851 pub fn plan_with_state(
2853 &self,
2854 samples: &[StationLoadSample],
2855 state: Option<&SplitSchedulerState>,
2856 current_tick: Tick,
2857 ) -> SplitSchedule {
2858 let decisions = samples
2859 .iter()
2860 .map(|sample| HotspotPlanner::evaluate(sample, self.config.thresholds))
2861 .collect::<Vec<_>>();
2862 let mut schedule = SplitSchedule {
2863 decisions,
2864 ..SplitSchedule::default()
2865 };
2866
2867 for source in samples {
2868 if schedule.actions.len() >= self.config.max_actions_per_pass {
2869 break;
2870 }
2871 let Some(source_decision) = schedule
2872 .decisions
2873 .iter()
2874 .find(|decision| decision.station_id == source.station_id)
2875 else {
2876 continue;
2877 };
2878 if source_decision.severity != HotspotSeverity::Hot {
2879 continue;
2880 }
2881 if state.is_some_and(|state| {
2882 state.is_in_cooldown(
2883 source.station_id,
2884 current_tick,
2885 self.config.split_cooldown_ticks,
2886 )
2887 }) {
2888 schedule.skipped_cooldown += 1;
2889 continue;
2890 }
2891
2892 let proposal =
2893 HotspotPlanner::propose_cell_split(source, self.config.max_cells_per_action);
2894 if proposal.cells_to_move.is_empty() {
2895 schedule.skipped_no_cells += 1;
2896 continue;
2897 }
2898 let target_selection =
2899 select_split_target(source, &proposal, samples, &schedule.decisions, self.config);
2900 let Some(target) = target_selection.target else {
2901 if target_selection.considered_targets == 0 {
2902 schedule.skipped_no_target += 1;
2903 } else {
2904 schedule.skipped_target_severity +=
2905 usize::from(target_selection.rejected_by_severity > 0);
2906 schedule.skipped_target_capacity +=
2907 usize::from(target_selection.rejected_by_capacity > 0);
2908 schedule.skipped_insufficient_improvement +=
2909 usize::from(target_selection.rejected_by_improvement > 0);
2910 }
2911 continue;
2912 };
2913 let target_score = station_load_score(target);
2914 let estimated_target_score_after_move =
2915 target_score.saturating_add(proposal.moved_pressure_score);
2916 schedule.actions.push(SplitAction {
2917 source_station: source.station_id,
2918 target_station: target.station_id,
2919 proposal,
2920 source_score: station_load_score(source),
2921 target_score,
2922 estimated_target_score_after_move,
2923 });
2924 }
2925
2926 schedule
2927 }
2928
2929 pub fn execute(
2931 &self,
2932 schedule: &SplitSchedule,
2933 stations: &mut StationSet,
2934 indexes: &mut StationIndexSet,
2935 ownership: &mut CellOwnershipTable,
2936 ) -> Result<SplitScheduleExecutionReport, SplitScheduleExecutionError> {
2937 let mut report = SplitScheduleExecutionReport::default();
2938
2939 for action in &schedule.actions {
2940 if indexes.get(action.source_station).is_none() {
2941 return Err(SplitScheduleExecutionError::MissingSourceIndex(
2942 action.source_station,
2943 ));
2944 }
2945 if indexes.get(action.target_station).is_none() {
2946 return Err(SplitScheduleExecutionError::MissingTargetIndex(
2947 action.target_station,
2948 ));
2949 }
2950
2951 let update = ownership.apply_split(&action.proposal, action.target_station);
2952 let (source_index, target_index) = indexes
2953 .get_pair_mut(action.source_station, action.target_station)
2954 .expect("indexes were checked above");
2955 let migration = CellMigrationExecutor::migrate_cells(
2956 stations,
2957 source_index,
2958 target_index,
2959 action.source_station,
2960 action.target_station,
2961 &update.moved_cells,
2962 self.config.ghost_ttl_ticks,
2963 )?;
2964 report.ownership_updates.push(update);
2965 report.cell_migrations.push(migration);
2966 }
2967
2968 Ok(report)
2969 }
2970}
2971
2972impl Default for SplitScheduler {
2973 fn default() -> Self {
2974 Self::new(SplitSchedulerConfig::default())
2975 }
2976}
2977
2978#[derive(Clone, Copy, Debug, Default)]
2979struct SplitTargetSelection<'a> {
2980 target: Option<&'a StationLoadSample>,
2981 considered_targets: usize,
2982 rejected_by_severity: usize,
2983 rejected_by_capacity: usize,
2984 rejected_by_improvement: usize,
2985}
2986
2987fn select_split_target<'a>(
2988 source: &StationLoadSample,
2989 proposal: &SplitProposal,
2990 samples: &'a [StationLoadSample],
2991 decisions: &[HotspotDecision],
2992 config: SplitSchedulerConfig,
2993) -> SplitTargetSelection<'a> {
2994 let mut selection = SplitTargetSelection::default();
2995 let source_score = station_load_score(source);
2996
2997 for target in samples {
2998 if target.station_id == source.station_id {
2999 continue;
3000 }
3001 selection.considered_targets += 1;
3002
3003 let severity = decisions
3004 .iter()
3005 .find(|decision| decision.station_id == target.station_id)
3006 .map_or(HotspotSeverity::Normal, |decision| decision.severity);
3007 if severity == HotspotSeverity::Hot
3008 || (severity == HotspotSeverity::Warm && !config.allow_warm_targets)
3009 {
3010 selection.rejected_by_severity += 1;
3011 continue;
3012 }
3013
3014 let target_score = station_load_score(target);
3015 if source_score.saturating_sub(target_score) < config.min_score_improvement {
3016 selection.rejected_by_improvement += 1;
3017 continue;
3018 }
3019 if target_score.saturating_add(proposal.moved_pressure_score)
3020 > config.max_target_score_after_move
3021 {
3022 selection.rejected_by_capacity += 1;
3023 continue;
3024 }
3025
3026 let target_key = (
3027 severity_rank(severity),
3028 target_score,
3029 target.station_id.get(),
3030 );
3031 let current_key = selection.target.map(|current| {
3032 let current_severity = decisions
3033 .iter()
3034 .find(|decision| decision.station_id == current.station_id)
3035 .map_or(HotspotSeverity::Normal, |decision| decision.severity);
3036 (
3037 severity_rank(current_severity),
3038 station_load_score(current),
3039 current.station_id.get(),
3040 )
3041 });
3042 if current_key.is_none_or(|current_key| target_key < current_key) {
3043 selection.target = Some(target);
3044 }
3045 }
3046
3047 selection
3048}
3049
3050fn severity_rank(severity: HotspotSeverity) -> u8 {
3051 match severity {
3052 HotspotSeverity::Normal => 0,
3053 HotspotSeverity::Warm => 1,
3054 HotspotSeverity::Hot => 2,
3055 }
3056}
3057
3058fn station_load_score(sample: &StationLoadSample) -> u64 {
3059 (sample.total_entities() as u64)
3060 .saturating_mul(8)
3061 .saturating_add((sample.subscribers as u64).saturating_mul(4))
3062 .saturating_add(sample.queued_events as u64)
3063 .saturating_add((sample.estimated_bytes / 256) as u64)
3064 .saturating_add(sample.tick_cost_units)
3065}
3066
3067#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3069pub struct EventRouterStats {
3070 pub routed_events: usize,
3072 pub drained_events: usize,
3074 pub dropped_best_effort_events: usize,
3076}
3077
3078#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3080pub enum EventRouterError {
3081 MissingTarget(StationId),
3083 Queue(EventQueueError),
3085}
3086
3087impl core::fmt::Display for EventRouterError {
3088 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3089 match self {
3090 Self::MissingTarget(id) => write!(f, "event target station {} is missing", id.get()),
3091 Self::Queue(error) => write!(f, "{error}"),
3092 }
3093 }
3094}
3095
3096impl std::error::Error for EventRouterError {}
3097
3098impl From<EventQueueError> for EventRouterError {
3099 fn from(value: EventQueueError) -> Self {
3100 Self::Queue(value)
3101 }
3102}
3103
3104#[derive(Clone, Debug)]
3106pub struct EventRouter {
3107 limits: EventQueueLimits,
3108 queues: BTreeMap<StationId, EventQueues>,
3109 stats: EventRouterStats,
3110}
3111
3112impl EventRouter {
3113 pub fn new(limits: EventQueueLimits) -> Self {
3115 Self {
3116 limits,
3117 queues: BTreeMap::new(),
3118 stats: EventRouterStats::default(),
3119 }
3120 }
3121
3122 pub fn register_station(&mut self, station_id: StationId) {
3124 self.queues
3125 .entry(station_id)
3126 .or_insert_with(|| EventQueues::new(self.limits));
3127 }
3128
3129 pub fn register_stations(&mut self, stations: &StationSet) {
3131 for station in stations.iter() {
3132 self.register_station(station.config().station_id);
3133 }
3134 }
3135
3136 pub fn route(&mut self, event: StationEvent) -> Result<PushOutcome, EventRouterError> {
3138 let queue = self
3139 .queues
3140 .get_mut(&event.target)
3141 .ok_or(EventRouterError::MissingTarget(event.target))?;
3142 let outcome = queue.push(event)?;
3143 self.stats.routed_events += 1;
3144 if outcome == PushOutcome::DroppedOldestBestEffort {
3145 self.stats.dropped_best_effort_events += 1;
3146 }
3147 Ok(outcome)
3148 }
3149
3150 pub fn drain_ready(
3152 &mut self,
3153 station_id: StationId,
3154 current_tick: Tick,
3155 ) -> Result<Vec<StationEvent>, EventRouterError> {
3156 let queue = self
3157 .queues
3158 .get_mut(&station_id)
3159 .ok_or(EventRouterError::MissingTarget(station_id))?;
3160 let mut ready = Vec::new();
3161 let mut delayed = Vec::new();
3162
3163 while let Some(event) = queue.pop_next() {
3164 if event.target_tick <= current_tick {
3165 ready.push(event);
3166 } else {
3167 delayed.push(event);
3168 }
3169 }
3170
3171 for event in delayed {
3172 queue.push(event)?;
3173 }
3174 self.stats.drained_events += ready.len();
3175 Ok(ready)
3176 }
3177
3178 pub fn queued_len(&self, station_id: StationId) -> Option<usize> {
3180 self.queues.get(&station_id).map(EventQueues::len)
3181 }
3182
3183 pub const fn stats(&self) -> EventRouterStats {
3185 self.stats
3186 }
3187}
3188
3189impl Default for EventRouter {
3190 fn default() -> Self {
3191 Self::new(EventQueueLimits::default())
3192 }
3193}
3194
3195#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3197pub struct StationEventTransportStats {
3198 pub events_sent: usize,
3200 pub bytes_sent: usize,
3202 pub packets_received: usize,
3204 pub bytes_received: usize,
3206 pub events_routed: usize,
3208}
3209
3210#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3212pub struct StationEventPumpReport {
3213 pub target_station: StationId,
3215 pub packets_received: usize,
3217 pub bytes_received: usize,
3219 pub events_routed: usize,
3221}
3222
3223#[derive(Clone, Debug, PartialEq, Eq)]
3225pub enum StationEventTransportError<E> {
3226 Transport(E),
3228 Encode(BinaryEncodeError),
3230 Decode(BinaryDecodeError),
3232 UnexpectedFrame,
3234 EndpointMismatch {
3236 packet_source: StationId,
3238 packet_target: StationId,
3240 event_source: StationId,
3242 event_target: StationId,
3244 },
3245 Router(EventRouterError),
3247}
3248
3249impl<E: core::fmt::Display> core::fmt::Display for StationEventTransportError<E> {
3250 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3251 match self {
3252 Self::Transport(error) => write!(f, "{error}"),
3253 Self::Encode(error) => write!(f, "{error}"),
3254 Self::Decode(error) => write!(f, "{error}"),
3255 Self::UnexpectedFrame => f.write_str("station transport packet was not an event frame"),
3256 Self::EndpointMismatch {
3257 packet_source,
3258 packet_target,
3259 event_source,
3260 event_target,
3261 } => write!(
3262 f,
3263 "station event endpoint mismatch: packet {}->{}, event {}->{}",
3264 packet_source.get(),
3265 packet_target.get(),
3266 event_source.get(),
3267 event_target.get()
3268 ),
3269 Self::Router(error) => write!(f, "{error}"),
3270 }
3271 }
3272}
3273
3274impl<E> std::error::Error for StationEventTransportError<E>
3275where
3276 E: std::error::Error + 'static,
3277{
3278 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
3279 match self {
3280 Self::Transport(error) => Some(error),
3281 Self::Encode(error) => Some(error),
3282 Self::Decode(error) => Some(error),
3283 Self::UnexpectedFrame | Self::EndpointMismatch { .. } => None,
3284 Self::Router(error) => Some(error),
3285 }
3286 }
3287}
3288
3289impl<E> From<BinaryEncodeError> for StationEventTransportError<E> {
3290 fn from(value: BinaryEncodeError) -> Self {
3291 Self::Encode(value)
3292 }
3293}
3294
3295impl<E> From<BinaryDecodeError> for StationEventTransportError<E> {
3296 fn from(value: BinaryDecodeError) -> Self {
3297 Self::Decode(value)
3298 }
3299}
3300
3301impl<E> From<EventRouterError> for StationEventTransportError<E> {
3302 fn from(value: EventRouterError) -> Self {
3303 Self::Router(value)
3304 }
3305}
3306
3307#[derive(Clone, Debug, Default)]
3309pub struct StationEventTransportBridge {
3310 stats: StationEventTransportStats,
3311}
3312
3313impl StationEventTransportBridge {
3314 pub const fn stats(&self) -> StationEventTransportStats {
3316 self.stats
3317 }
3318
3319 pub fn send_event<T>(
3321 &mut self,
3322 transport: &mut T,
3323 event: &StationEvent,
3324 ) -> Result<(), StationEventTransportError<T::Error>>
3325 where
3326 T: StationTransportSink,
3327 {
3328 let frame = StationEventFrame::from_event(event);
3329 let mut bytes = Vec::with_capacity(64);
3330 BinaryFrameEncoder.encode_station_event(&frame, &mut bytes)?;
3331 let byte_len = bytes.len();
3332 transport
3333 .send_station(StationOutboundPacket {
3334 source_station: event.source,
3335 target_station: event.target,
3336 bytes,
3337 })
3338 .map_err(StationEventTransportError::Transport)?;
3339 self.stats.events_sent = self.stats.events_sent.saturating_add(1);
3340 self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(byte_len);
3341 Ok(())
3342 }
3343
3344 pub fn pump_target<T>(
3347 &mut self,
3348 transport: &mut T,
3349 router: &mut EventRouter,
3350 target_station: StationId,
3351 max_packets: usize,
3352 ) -> Result<StationEventPumpReport, StationEventTransportError<T::Error>>
3353 where
3354 T: StationTransportReceiver,
3355 {
3356 let mut report = StationEventPumpReport {
3357 target_station,
3358 ..StationEventPumpReport::default()
3359 };
3360 for _ in 0..max_packets {
3361 let Some(packet) = transport
3362 .try_recv_station(target_station)
3363 .map_err(StationEventTransportError::Transport)?
3364 else {
3365 break;
3366 };
3367 report.packets_received = report.packets_received.saturating_add(1);
3368 report.bytes_received = report.bytes_received.saturating_add(packet.bytes.len());
3369
3370 let decoded = BinaryFrameDecoder.decode(&packet.bytes)?;
3371 let RuntimeFrame::StationEvent(frame) = decoded else {
3372 return Err(StationEventTransportError::UnexpectedFrame);
3373 };
3374 if frame.source_station != packet.source_station
3375 || frame.target_station != packet.target_station
3376 {
3377 return Err(StationEventTransportError::EndpointMismatch {
3378 packet_source: packet.source_station,
3379 packet_target: packet.target_station,
3380 event_source: frame.source_station,
3381 event_target: frame.target_station,
3382 });
3383 }
3384
3385 router.route(frame.into_event())?;
3386 report.events_routed = report.events_routed.saturating_add(1);
3387 }
3388
3389 self.stats.packets_received = self
3390 .stats
3391 .packets_received
3392 .saturating_add(report.packets_received);
3393 self.stats.bytes_received = self
3394 .stats
3395 .bytes_received
3396 .saturating_add(report.bytes_received);
3397 self.stats.events_routed = self
3398 .stats
3399 .events_routed
3400 .saturating_add(report.events_routed);
3401 Ok(report)
3402 }
3403}
3404
3405#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3407pub struct CommandDispatchTransportStats {
3408 pub commands_sent: usize,
3410 pub bytes_sent: usize,
3412 pub packets_received: usize,
3414 pub bytes_received: usize,
3416 pub commands_enqueued: usize,
3418 pub commands_rejected_queue: usize,
3420}
3421
3422#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3424pub struct CommandDispatchPumpReport {
3425 pub target_station: StationId,
3427 pub packets_received: usize,
3429 pub bytes_received: usize,
3431 pub commands_enqueued: usize,
3433}
3434
3435#[derive(Clone, Debug, PartialEq, Eq)]
3437pub enum CommandDispatchTransportError<E> {
3438 Transport(E),
3440 Encode(BinaryEncodeError),
3442 Decode(BinaryDecodeError),
3444 UnexpectedFrame,
3446 EndpointMismatch {
3448 packet_source: StationId,
3450 packet_target: StationId,
3452 dispatch_target: StationId,
3454 },
3455 MissingQueue(StationId),
3457 Queue(CommandQueueError),
3459}
3460
3461impl<E: core::fmt::Display> core::fmt::Display for CommandDispatchTransportError<E> {
3462 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3463 match self {
3464 Self::Transport(error) => write!(f, "{error}"),
3465 Self::Encode(error) => write!(f, "{error}"),
3466 Self::Decode(error) => write!(f, "{error}"),
3467 Self::UnexpectedFrame => {
3468 f.write_str("station transport packet was not a command dispatch frame")
3469 }
3470 Self::EndpointMismatch {
3471 packet_source,
3472 packet_target,
3473 dispatch_target,
3474 } => write!(
3475 f,
3476 "command dispatch endpoint mismatch: packet {}->{}, dispatch target {}",
3477 packet_source.get(),
3478 packet_target.get(),
3479 dispatch_target.get()
3480 ),
3481 Self::MissingQueue(station_id) => {
3482 write!(
3483 f,
3484 "command dispatch target station {} has no queue",
3485 station_id.get()
3486 )
3487 }
3488 Self::Queue(error) => write!(f, "{error}"),
3489 }
3490 }
3491}
3492
3493impl<E> std::error::Error for CommandDispatchTransportError<E>
3494where
3495 E: std::error::Error + 'static,
3496{
3497 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
3498 match self {
3499 Self::Transport(error) => Some(error),
3500 Self::Encode(error) => Some(error),
3501 Self::Decode(error) => Some(error),
3502 Self::UnexpectedFrame | Self::EndpointMismatch { .. } | Self::MissingQueue(_) => None,
3503 Self::Queue(error) => Some(error),
3504 }
3505 }
3506}
3507
3508impl<E> From<BinaryEncodeError> for CommandDispatchTransportError<E> {
3509 fn from(value: BinaryEncodeError) -> Self {
3510 Self::Encode(value)
3511 }
3512}
3513
3514impl<E> From<BinaryDecodeError> for CommandDispatchTransportError<E> {
3515 fn from(value: BinaryDecodeError) -> Self {
3516 Self::Decode(value)
3517 }
3518}
3519
3520impl<E> From<CommandQueueError> for CommandDispatchTransportError<E> {
3521 fn from(value: CommandQueueError) -> Self {
3522 Self::Queue(value)
3523 }
3524}
3525
3526#[derive(Clone, Debug, Default)]
3528pub struct CommandDispatchTransportBridge {
3529 stats: CommandDispatchTransportStats,
3530}
3531
3532impl CommandDispatchTransportBridge {
3533 pub const fn stats(&self) -> CommandDispatchTransportStats {
3535 self.stats
3536 }
3537
3538 pub fn send_envelope<T>(
3540 &mut self,
3541 transport: &mut T,
3542 source_station: StationId,
3543 target_station: StationId,
3544 command: &CommandEnvelope,
3545 ) -> Result<(), CommandDispatchTransportError<T::Error>>
3546 where
3547 T: StationTransportSink,
3548 {
3549 let frame = CommandDispatchFrame::from_envelope(target_station, command);
3550 self.send_frame(transport, source_station, &frame)
3551 }
3552
3553 pub fn send_frame<T>(
3555 &mut self,
3556 transport: &mut T,
3557 source_station: StationId,
3558 frame: &CommandDispatchFrame,
3559 ) -> Result<(), CommandDispatchTransportError<T::Error>>
3560 where
3561 T: StationTransportSink,
3562 {
3563 let mut bytes = Vec::with_capacity(64);
3564 BinaryFrameEncoder.encode_command_dispatch(frame, &mut bytes)?;
3565 let byte_len = bytes.len();
3566 transport
3567 .send_station(StationOutboundPacket {
3568 source_station,
3569 target_station: frame.station_id,
3570 bytes,
3571 })
3572 .map_err(CommandDispatchTransportError::Transport)?;
3573 self.stats.commands_sent = self.stats.commands_sent.saturating_add(1);
3574 self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(byte_len);
3575 Ok(())
3576 }
3577
3578 pub fn pump_target<T>(
3581 &mut self,
3582 transport: &mut T,
3583 station_queues: &mut BTreeMap<StationId, CommandQueues>,
3584 target_station: StationId,
3585 max_packets: usize,
3586 ingress: CommandIngress,
3587 ) -> Result<CommandDispatchPumpReport, CommandDispatchTransportError<T::Error>>
3588 where
3589 T: StationTransportReceiver,
3590 {
3591 let mut report = CommandDispatchPumpReport {
3592 target_station,
3593 ..CommandDispatchPumpReport::default()
3594 };
3595 for _ in 0..max_packets {
3596 let Some(packet) = transport
3597 .try_recv_station(target_station)
3598 .map_err(CommandDispatchTransportError::Transport)?
3599 else {
3600 break;
3601 };
3602 report.packets_received = report.packets_received.saturating_add(1);
3603 report.bytes_received = report.bytes_received.saturating_add(packet.bytes.len());
3604
3605 let decoded = BinaryFrameDecoder.decode(&packet.bytes)?;
3606 let RuntimeFrame::CommandDispatch(frame) = decoded else {
3607 return Err(CommandDispatchTransportError::UnexpectedFrame);
3608 };
3609 if frame.station_id != packet.target_station {
3610 return Err(CommandDispatchTransportError::EndpointMismatch {
3611 packet_source: packet.source_station,
3612 packet_target: packet.target_station,
3613 dispatch_target: frame.station_id,
3614 });
3615 }
3616
3617 let queue = station_queues.get_mut(&frame.station_id).ok_or(
3618 CommandDispatchTransportError::MissingQueue(frame.station_id),
3619 )?;
3620 match queue.push(frame.into_envelope(), ingress) {
3621 Ok(_) => {
3622 report.commands_enqueued = report.commands_enqueued.saturating_add(1);
3623 }
3624 Err(error) => {
3625 self.stats.commands_rejected_queue =
3626 self.stats.commands_rejected_queue.saturating_add(1);
3627 return Err(CommandDispatchTransportError::Queue(error));
3628 }
3629 }
3630 }
3631
3632 self.stats.packets_received = self
3633 .stats
3634 .packets_received
3635 .saturating_add(report.packets_received);
3636 self.stats.bytes_received = self
3637 .stats
3638 .bytes_received
3639 .saturating_add(report.bytes_received);
3640 self.stats.commands_enqueued = self
3641 .stats
3642 .commands_enqueued
3643 .saturating_add(report.commands_enqueued);
3644 Ok(report)
3645 }
3646}
3647
3648#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3650pub struct StationScheduleConfig {
3651 pub max_station_advances_per_step: usize,
3653}
3654
3655impl Default for StationScheduleConfig {
3656 fn default() -> Self {
3657 Self {
3658 max_station_advances_per_step: usize::MAX,
3659 }
3660 }
3661}
3662
3663#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3665pub struct StationScheduleCandidate {
3666 pub station_id: StationId,
3668 pub load_score: u64,
3670 pub tick_lag: u64,
3672}
3673
3674#[derive(Clone, Debug, Default, PartialEq, Eq)]
3676pub struct StationSchedulePlan {
3677 pub candidates_considered: usize,
3679 pub stations_selected: usize,
3681 pub total_advances: usize,
3683 pub selected: Vec<StationScheduleCandidate>,
3685}
3686
3687#[derive(Clone, Debug, Default)]
3689pub struct StationScheduler {
3690 pub advanced_ticks: u64,
3692}
3693
3694impl StationScheduler {
3695 pub fn advance_all(&mut self, stations: &mut StationSet) {
3697 for station in stations.iter_mut() {
3698 station.advance_tick();
3699 self.advanced_ticks = self.advanced_ticks.saturating_add(1);
3700 }
3701 }
3702
3703 pub fn plan_loaded(
3705 &self,
3706 stations: &StationSet,
3707 samples: &[StationLoadSample],
3708 config: StationScheduleConfig,
3709 ) -> StationSchedulePlan {
3710 let candidates_considered = stations.len();
3711 let limit = config
3712 .max_station_advances_per_step
3713 .min(candidates_considered);
3714 let max_tick = stations
3715 .iter()
3716 .map(|station| station.tick().get())
3717 .max()
3718 .unwrap_or(0);
3719 let samples_by_station = samples
3720 .iter()
3721 .map(|sample| (sample.station_id, sample))
3722 .collect::<BTreeMap<_, _>>();
3723 let mut selected = stations
3724 .iter()
3725 .map(|station| {
3726 let station_id = station.config().station_id;
3727 let load_score = samples_by_station
3728 .get(&station_id)
3729 .map_or(0, |sample| station_schedule_score(sample));
3730 StationScheduleCandidate {
3731 station_id,
3732 load_score,
3733 tick_lag: max_tick.saturating_sub(station.tick().get()),
3734 }
3735 })
3736 .collect::<Vec<_>>();
3737
3738 selected.sort_by(|left, right| {
3739 right
3740 .load_score
3741 .cmp(&left.load_score)
3742 .then_with(|| right.tick_lag.cmp(&left.tick_lag))
3743 .then_with(|| left.station_id.cmp(&right.station_id))
3744 });
3745 selected.truncate(limit);
3746
3747 StationSchedulePlan {
3748 candidates_considered,
3749 stations_selected: selected.len(),
3750 total_advances: selected.len(),
3751 selected,
3752 }
3753 }
3754
3755 pub fn advance_loaded(
3757 &mut self,
3758 stations: &mut StationSet,
3759 samples: &[StationLoadSample],
3760 config: StationScheduleConfig,
3761 ) -> StationSchedulePlan {
3762 let plan = self.plan_loaded(stations, samples, config);
3763 for candidate in &plan.selected {
3764 if let Some(station) = stations.get_mut(candidate.station_id) {
3765 station.advance_tick();
3766 self.advanced_ticks = self.advanced_ticks.saturating_add(1);
3767 }
3768 }
3769 plan
3770 }
3771
3772 pub fn drain_ready_events(
3774 &mut self,
3775 stations: &StationSet,
3776 router: &mut EventRouter,
3777 ) -> Result<Vec<StationEvent>, EventRouterError> {
3778 let mut events = Vec::new();
3779 for station in stations.iter() {
3780 events.extend(router.drain_ready(station.config().station_id, station.tick())?);
3781 }
3782 Ok(events)
3783 }
3784}
3785
3786fn station_schedule_score(sample: &StationLoadSample) -> u64 {
3787 station_load_score(sample).saturating_add(sample.max_cell_pressure())
3788}
3789
3790#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3792pub enum StationBarrierPhase {
3793 WaitingTick,
3795 Frozen,
3797 Resumed,
3799}
3800
3801#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3803pub struct BarrierProgress {
3804 pub state: BarrierState,
3806 pub station_count: usize,
3808 pub frozen_count: usize,
3810 pub target_tick: Tick,
3812}
3813
3814#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3816pub struct BarrierMetrics {
3817 pub station_count: usize,
3819 pub snapshots_exported: usize,
3821 pub waiting_polls: u64,
3823 pub frozen_polls: u64,
3825}
3826
3827#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3829pub enum BarrierRuntimeError {
3830 AlreadyActive(BarrierId),
3832 NoActiveBarrier,
3834 EmptyScope(BarrierScope),
3836 NotFrozen(BarrierState),
3838 MissingStation(StationId),
3840}
3841
3842impl core::fmt::Display for BarrierRuntimeError {
3843 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3844 match self {
3845 Self::AlreadyActive(id) => write!(f, "barrier {} is already active", id.get()),
3846 Self::NoActiveBarrier => f.write_str("no active barrier"),
3847 Self::EmptyScope(scope) => write!(f, "barrier scope {scope:?} contains no stations"),
3848 Self::NotFrozen(state) => {
3849 write!(f, "barrier operation requires Frozen state, got {state:?}")
3850 }
3851 Self::MissingStation(id) => write!(f, "barrier station {} is missing", id.get()),
3852 }
3853 }
3854}
3855
3856impl std::error::Error for BarrierRuntimeError {}
3857
3858#[derive(Clone, Debug, Default)]
3860pub struct BarrierController {
3861 active: Option<RuntimeBarrier>,
3862 phases: BTreeMap<StationId, StationBarrierPhase>,
3863 metrics: BarrierMetrics,
3864}
3865
3866impl BarrierController {
3867 pub const fn active(&self) -> Option<RuntimeBarrier> {
3869 self.active
3870 }
3871
3872 pub fn request(
3874 &mut self,
3875 stations: &StationSet,
3876 id: BarrierId,
3877 scope: BarrierScope,
3878 target_tick: Tick,
3879 command_mode: CommandQueueMode,
3880 ) -> Result<BarrierProgress, BarrierRuntimeError> {
3881 if let Some(active) = self.active {
3882 return Err(BarrierRuntimeError::AlreadyActive(active.id));
3883 }
3884
3885 let station_ids = stations.station_ids_in_scope(scope);
3886 if station_ids.is_empty() {
3887 return Err(BarrierRuntimeError::EmptyScope(scope));
3888 }
3889
3890 let requested_at = station_ids
3891 .iter()
3892 .filter_map(|station_id| stations.get(*station_id).map(Station::tick))
3893 .map(Tick::get)
3894 .max()
3895 .map_or(Tick::new(0), Tick::new);
3896
3897 let mut barrier =
3898 RuntimeBarrier::requested(id, scope, requested_at, target_tick, command_mode);
3899 barrier.wait_for_tick_boundary();
3900
3901 self.metrics = BarrierMetrics {
3902 station_count: station_ids.len(),
3903 ..BarrierMetrics::default()
3904 };
3905 self.phases.clear();
3906 for station_id in station_ids {
3907 self.phases
3908 .insert(station_id, StationBarrierPhase::WaitingTick);
3909 }
3910 self.active = Some(barrier);
3911
3912 Ok(self.progress())
3913 }
3914
3915 pub fn poll(&mut self, stations: &StationSet) -> Result<BarrierProgress, BarrierRuntimeError> {
3917 let Some(mut barrier) = self.active else {
3918 return Err(BarrierRuntimeError::NoActiveBarrier);
3919 };
3920
3921 if matches!(barrier.state, BarrierState::Frozen) {
3922 self.metrics.frozen_polls = self.metrics.frozen_polls.saturating_add(1);
3923 return Ok(self.progress());
3924 }
3925
3926 let mut all_ready = true;
3927 for (station_id, phase) in &mut self.phases {
3928 let station = stations
3929 .get(*station_id)
3930 .ok_or(BarrierRuntimeError::MissingStation(*station_id))?;
3931 if station.tick() >= barrier.target_tick {
3932 *phase = StationBarrierPhase::Frozen;
3933 } else {
3934 all_ready = false;
3935 }
3936 }
3937
3938 if all_ready {
3939 barrier.freeze();
3940 self.active = Some(barrier);
3941 self.metrics.frozen_polls = self.metrics.frozen_polls.saturating_add(1);
3942 } else {
3943 self.metrics.waiting_polls = self.metrics.waiting_polls.saturating_add(1);
3944 }
3945
3946 Ok(self.progress())
3947 }
3948
3949 pub fn export_snapshots(
3951 &mut self,
3952 stations: &StationSet,
3953 version: SnapshotVersion,
3954 ) -> Result<Vec<StationSnapshot>, BarrierRuntimeError> {
3955 let barrier = self.active.ok_or(BarrierRuntimeError::NoActiveBarrier)?;
3956 if barrier.state != BarrierState::Frozen {
3957 return Err(BarrierRuntimeError::NotFrozen(barrier.state));
3958 }
3959
3960 let mut snapshots = Vec::with_capacity(self.phases.len());
3961 for station_id in self.phases.keys().copied() {
3962 let station = stations
3963 .get(station_id)
3964 .ok_or(BarrierRuntimeError::MissingStation(station_id))?;
3965 snapshots.push(station.snapshot(version));
3966 }
3967 self.metrics.snapshots_exported = self
3968 .metrics
3969 .snapshots_exported
3970 .saturating_add(snapshots.len());
3971 Ok(snapshots)
3972 }
3973
3974 pub fn resume(&mut self) -> Result<BarrierMetrics, BarrierRuntimeError> {
3976 let Some(mut barrier) = self.active else {
3977 return Err(BarrierRuntimeError::NoActiveBarrier);
3978 };
3979 if barrier.state != BarrierState::Frozen {
3980 return Err(BarrierRuntimeError::NotFrozen(barrier.state));
3981 }
3982
3983 barrier.resume();
3984 for phase in self.phases.values_mut() {
3985 *phase = StationBarrierPhase::Resumed;
3986 }
3987 barrier.finish();
3988 let metrics = self.metrics;
3989 self.active = None;
3990 self.phases.clear();
3991 self.metrics = BarrierMetrics::default();
3992 Ok(metrics)
3993 }
3994
3995 pub fn progress(&self) -> BarrierProgress {
3997 let state = self
3998 .active
3999 .map_or(BarrierState::Running, |barrier| barrier.state);
4000 let target_tick = self
4001 .active
4002 .map_or(Tick::new(0), |barrier| barrier.target_tick);
4003 let frozen_count = self
4004 .phases
4005 .values()
4006 .filter(|phase| matches!(phase, StationBarrierPhase::Frozen))
4007 .count();
4008
4009 BarrierProgress {
4010 state,
4011 station_count: self.phases.len(),
4012 frozen_count,
4013 target_tick,
4014 }
4015 }
4016}
4017
4018#[derive(Clone, Debug, PartialEq, Eq)]
4020pub struct BarrierUpgradeReport {
4021 pub version: SnapshotVersion,
4023 pub snapshots_migrated: usize,
4025 pub stations_restored: usize,
4027 pub entities_restored: usize,
4029}
4030
4031#[derive(Clone, Debug, PartialEq, Eq)]
4033pub enum BarrierUpgradeError {
4034 Barrier(BarrierRuntimeError),
4036 MissingStation(StationId),
4038 Restore {
4040 station_id: StationId,
4042 error: StationError,
4044 },
4045}
4046
4047impl core::fmt::Display for BarrierUpgradeError {
4048 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
4049 match self {
4050 Self::Barrier(error) => write!(f, "{error}"),
4051 Self::MissingStation(station_id) => {
4052 write!(f, "upgrade station {} is missing", station_id.get())
4053 }
4054 Self::Restore { station_id, error } => {
4055 write!(
4056 f,
4057 "upgrade restore for station {} failed: {error}",
4058 station_id.get()
4059 )
4060 }
4061 }
4062 }
4063}
4064
4065impl std::error::Error for BarrierUpgradeError {
4066 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
4067 match self {
4068 Self::Barrier(error) => Some(error),
4069 Self::Restore { error, .. } => Some(error),
4070 Self::MissingStation(_) => None,
4071 }
4072 }
4073}
4074
4075impl From<BarrierRuntimeError> for BarrierUpgradeError {
4076 fn from(value: BarrierRuntimeError) -> Self {
4077 Self::Barrier(value)
4078 }
4079}
4080
4081#[derive(Clone, Copy, Debug, Default)]
4083pub struct BarrierUpgradeExecutor;
4084
4085impl BarrierUpgradeExecutor {
4086 pub fn migrate_frozen<H>(
4089 controller: &mut BarrierController,
4090 stations: &mut StationSet,
4091 version: SnapshotVersion,
4092 hook: &mut H,
4093 ) -> Result<BarrierUpgradeReport, BarrierUpgradeError>
4094 where
4095 H: RuntimeUpgradeHook,
4096 {
4097 let report_version = version;
4098 let snapshots = controller.export_snapshots(stations, version)?;
4099 let mut restored = Vec::with_capacity(snapshots.len());
4100 let mut entities_restored = 0usize;
4101
4102 for snapshot in snapshots {
4103 let station_id = snapshot.meta.station_id;
4104 let config = stations
4105 .get(station_id)
4106 .ok_or(BarrierUpgradeError::MissingStation(station_id))?
4107 .config();
4108 hook.pre_upgrade(&snapshot.meta);
4109 let migrated = hook.migrate_state(snapshot);
4110 let migrated_meta = migrated.meta.clone();
4111 let restored_station = Station::restore(config, migrated)
4112 .map_err(|error| BarrierUpgradeError::Restore { station_id, error })?;
4113 entities_restored = entities_restored.saturating_add(restored_station.len());
4114 hook.post_upgrade(&migrated_meta);
4115 restored.push((station_id, restored_station));
4116 }
4117
4118 let stations_restored = restored.len();
4119 for (station_id, restored_station) in restored {
4120 let station = stations
4121 .get_mut(station_id)
4122 .ok_or(BarrierUpgradeError::MissingStation(station_id))?;
4123 *station = restored_station;
4124 }
4125
4126 Ok(BarrierUpgradeReport {
4127 version: report_version,
4128 snapshots_migrated: stations_restored,
4129 stations_restored,
4130 entities_restored,
4131 })
4132 }
4133}
4134
4135#[cfg(test)]
4136mod tests {
4137 use super::*;
4138 use sectorsync_core::prelude::{
4139 Bounds, CellCoord3, CellLoadSample, CommandEnvelope, CommandPriority, CommandQueueLimits,
4140 CompiledSyncPolicy, ComponentDescriptor, ComponentId, ComponentMigrationMode,
4141 ComponentSyncMode, EventId, EventKind, EventPriority, GatewayConfig, GridSpec,
4142 HotspotThresholds, InstanceId, NodeId, PolicyId, Position3, RangeOnlyVisibility,
4143 SnapshotMeta, StationConfig, StationLoadSample, U32LeCodec,
4144 };
4145 use sectorsync_transport::{
4146 ClientTransportLimits, FakeTransport, InMemoryStationTransport, InMemoryTransportHub,
4147 OutboundPacket, StationOutboundPacket, StationTransportSink, TransportReceiver,
4148 TransportSink,
4149 };
4150 use sectorsync_wire::{
4151 BarrierFrame, BinaryFrameDecoder, BinaryFrameEncoder, CommandAckFrame,
4152 CommandDispatchFrame, CommandFrame, ComponentDelta, EntityDelta, FrameDecoder,
4153 FrameEncoder, ReplicationFrame,
4154 };
4155
4156 fn station(station_id: u32, instance_id: u64) -> Station {
4157 Station::new(StationConfig {
4158 station_id: StationId::new(station_id),
4159 node_id: NodeId::new(0),
4160 instance_id: InstanceId::new(instance_id),
4161 tick_rate_hz: 20,
4162 })
4163 }
4164
4165 fn encode_command_frame(sequence: u64) -> Vec<u8> {
4166 let frame = CommandFrame {
4167 client_id: ClientId::new(7),
4168 command_id: CommandId::new(sequence),
4169 entity_id: EntityId::new(100),
4170 sequence,
4171 kind: 1,
4172 priority: CommandPriority::High,
4173 payload: b"move:north".to_vec(),
4174 };
4175 let mut bytes = Vec::new();
4176 BinaryFrameEncoder
4177 .encode_command(&frame, &mut bytes)
4178 .expect("command should encode");
4179 bytes
4180 }
4181
4182 fn command_queues() -> CommandQueues {
4183 CommandQueues::new(CommandQueueLimits {
4184 high: 4,
4185 normal: 4,
4186 low: 4,
4187 })
4188 }
4189
4190 fn gateway(max_commands_per_tick: usize) -> GatewaySessionTable {
4191 GatewaySessionTable::new(GatewayConfig {
4192 max_sessions: 8,
4193 reconnect_grace_ticks: 10,
4194 max_commands_per_tick,
4195 })
4196 }
4197
4198 #[test]
4199 fn barrier_freezes_snapshots_and_resumes_instance_scope() {
4200 let mut stations = StationSet::default();
4201 stations.push(station(1, 10));
4202 stations.push(station(2, 10));
4203
4204 for station in stations.iter_mut() {
4205 station.advance_tick();
4206 station.advance_tick();
4207 }
4208
4209 let mut controller = BarrierController::default();
4210 let requested = controller
4211 .request(
4212 &stations,
4213 BarrierId::new(7),
4214 BarrierScope::Instance(InstanceId::new(10)),
4215 Tick::new(2),
4216 CommandQueueMode::Buffer,
4217 )
4218 .expect("request should work");
4219 assert_eq!(requested.state, BarrierState::WaitingTickBoundary);
4220
4221 let frozen = controller.poll(&stations).expect("poll should work");
4222 assert_eq!(frozen.state, BarrierState::Frozen);
4223 assert_eq!(frozen.frozen_count, 2);
4224
4225 let snapshots = controller
4226 .export_snapshots(&stations, SnapshotVersion::default())
4227 .expect("snapshot should work while frozen");
4228 assert_eq!(snapshots.len(), 2);
4229
4230 let metrics = controller.resume().expect("resume should work");
4231 assert_eq!(metrics.station_count, 2);
4232 assert_eq!(metrics.snapshots_exported, 2);
4233 assert_eq!(controller.progress().state, BarrierState::Running);
4234 }
4235
4236 #[derive(Default)]
4237 struct MoveSnapshotUpgrade {
4238 pre: usize,
4239 migrations: usize,
4240 post: usize,
4241 }
4242
4243 impl RuntimeUpgradeHook for MoveSnapshotUpgrade {
4244 fn pre_upgrade(&mut self, meta: &SnapshotMeta) {
4245 self.pre = self.pre.saturating_add(1);
4246 assert_eq!(meta.version.runtime_version, 2);
4247 }
4248
4249 fn migrate_state(&mut self, mut snapshot: StationSnapshot) -> StationSnapshot {
4250 self.migrations = self.migrations.saturating_add(1);
4251 for entity in &mut snapshot.entities {
4252 entity.position.x += 10.0;
4253 }
4254 snapshot
4255 }
4256
4257 fn post_upgrade(&mut self, meta: &SnapshotMeta) {
4258 self.post = self.post.saturating_add(1);
4259 assert_eq!(meta.version.runtime_version, 2);
4260 }
4261 }
4262
4263 #[test]
4264 fn barrier_upgrade_executor_migrates_and_restores_frozen_snapshots() {
4265 let mut first = station(1, 10);
4266 first
4267 .spawn_owned(
4268 EntityId::new(100),
4269 Position3::new(1.0, 2.0, 3.0),
4270 Bounds::Point,
4271 PolicyId::new(0),
4272 )
4273 .expect("spawn should work");
4274 let mut stations = StationSet::default();
4275 stations.push(first);
4276 stations.push(station(2, 10));
4277
4278 for station in stations.iter_mut() {
4279 station.advance_tick();
4280 station.advance_tick();
4281 }
4282
4283 let mut controller = BarrierController::default();
4284 controller
4285 .request(
4286 &stations,
4287 BarrierId::new(8),
4288 BarrierScope::Instance(InstanceId::new(10)),
4289 Tick::new(2),
4290 CommandQueueMode::Buffer,
4291 )
4292 .expect("request should work");
4293 assert_eq!(
4294 controller.poll(&stations).expect("poll should work").state,
4295 BarrierState::Frozen
4296 );
4297
4298 let mut hook = MoveSnapshotUpgrade::default();
4299 let version = SnapshotVersion {
4300 runtime_version: 2,
4301 ..SnapshotVersion::default()
4302 };
4303 let report = BarrierUpgradeExecutor::migrate_frozen(
4304 &mut controller,
4305 &mut stations,
4306 version,
4307 &mut hook,
4308 )
4309 .expect("upgrade should migrate frozen snapshots");
4310
4311 assert_eq!(report.version, version);
4312 assert_eq!(report.snapshots_migrated, 2);
4313 assert_eq!(report.stations_restored, 2);
4314 assert_eq!(report.entities_restored, 1);
4315 assert_eq!(hook.pre, 2);
4316 assert_eq!(hook.migrations, 2);
4317 assert_eq!(hook.post, 2);
4318 let moved = stations
4319 .get(StationId::new(1))
4320 .expect("station should exist")
4321 .get_by_id(EntityId::new(100))
4322 .expect("entity should restore");
4323 assert_eq!(moved.position, Position3::new(11.0, 2.0, 3.0));
4324 assert_eq!(controller.progress().state, BarrierState::Frozen);
4325
4326 let metrics = controller.resume().expect("resume should work");
4327 assert_eq!(metrics.snapshots_exported, 2);
4328 assert_eq!(controller.progress().state, BarrierState::Running);
4329 }
4330
4331 #[test]
4332 fn barrier_transport_bridge_broadcasts_client_notifications() {
4333 let server_id = ClientId::new(0);
4334 let clients = [ClientId::new(7), ClientId::new(8)];
4335 let hub = InMemoryTransportHub::new(ClientTransportLimits {
4336 max_queued_packets_per_client: 4,
4337 max_packet_bytes: 512,
4338 });
4339 let mut server_transport = hub
4340 .endpoint(server_id, "127.0.0.1:23400".parse().expect("server addr"))
4341 .expect("server endpoint should register");
4342 let mut client_transports = clients
4343 .into_iter()
4344 .enumerate()
4345 .map(|(index, client_id)| {
4346 hub.endpoint(
4347 client_id,
4348 format!("127.0.0.1:{}", 23407 + index)
4349 .parse()
4350 .expect("client addr"),
4351 )
4352 .expect("client endpoint should register")
4353 })
4354 .collect::<Vec<_>>();
4355 let mut barrier = RuntimeBarrier::requested(
4356 BarrierId::new(5),
4357 BarrierScope::Instance(InstanceId::new(10)),
4358 Tick::new(10),
4359 Tick::new(12),
4360 CommandQueueMode::Buffer,
4361 );
4362 barrier.wait_for_tick_boundary();
4363 barrier.freeze();
4364
4365 let mut bridge = BarrierTransportBridge::default();
4366 let report = bridge
4367 .broadcast_barrier(&mut server_transport, clients, barrier)
4368 .expect("barrier should broadcast");
4369
4370 assert_eq!(report.barrier_id, barrier.id);
4371 assert_eq!(report.state, BarrierState::Frozen);
4372 assert_eq!(report.server_tick, Tick::new(12));
4373 assert_eq!(report.clients_requested, 2);
4374 assert_eq!(report.clients_sent, 2);
4375 assert!(report.bytes_sent > 0);
4376 assert_eq!(bridge.stats().notifications_sent, 2);
4377 assert_eq!(bridge.stats().clients_notified, 2);
4378 assert_eq!(bridge.stats().bytes_sent, report.bytes_sent);
4379
4380 for (index, client_id) in clients.into_iter().enumerate() {
4381 let mut client_bridge = ClientTransportBridge::new(
4382 ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
4383 );
4384 let pump = client_bridge
4385 .pump(&mut client_transports[index], 2)
4386 .expect("client should receive barrier");
4387 assert_eq!(pump.barrier_frames_received(), 1);
4388 assert_eq!(
4389 pump.barriers[0],
4390 BarrierFrame {
4391 client_id,
4392 barrier_id: barrier.id,
4393 server_tick: barrier.target_tick,
4394 state: BarrierState::Frozen,
4395 }
4396 );
4397 }
4398 }
4399
4400 #[test]
4401 fn replication_transport_bridge_sends_planned_frame() {
4402 let mut station = station(1, 10);
4403 let mut index = CellIndex::new(GridSpec::new(64.0).expect("grid is valid"));
4404 let mut policies = PolicyTable::default();
4405 policies.set(CompiledSyncPolicy::new(PolicyId::new(0), 1, 20, 256.0));
4406 let handle = station
4407 .spawn_owned(
4408 EntityId::new(100),
4409 Position3::new(0.0, 0.0, 0.0),
4410 Bounds::Point,
4411 PolicyId::new(0),
4412 )
4413 .expect("spawn should work");
4414 index.upsert(handle, Position3::new(0.0, 0.0, 0.0), Bounds::Point);
4415 let descriptor = ComponentDescriptor::sparse_blob(
4416 ComponentId::new(1),
4417 "health",
4418 ComponentSyncMode::Delta,
4419 ComponentMigrationMode::Copy,
4420 4,
4421 );
4422 let mut components = ComponentStore::default();
4423 components
4424 .set_typed(&descriptor, handle, 1, &U32LeCodec, &100)
4425 .expect("component should write");
4426 let selection = ComponentSelection {
4427 component_ids: vec![ComponentId::new(1)],
4428 };
4429 let viewer = ViewerQuery {
4430 client_id: ClientId::new(7),
4431 position: Position3::new(0.0, 0.0, 0.0),
4432 radius: 256.0,
4433 max_entities: 32,
4434 };
4435 let mut bridge = ReplicationTransportBridge::default();
4436 let mut transport = FakeTransport::default();
4437
4438 let report = bridge
4439 .send_viewer(
4440 &mut transport,
4441 &station,
4442 &index,
4443 &policies,
4444 &components,
4445 &selection,
4446 &viewer,
4447 &RangeOnlyVisibility,
4448 )
4449 .expect("replication should send");
4450
4451 assert!(report.sent);
4452 assert_eq!(report.client_id, viewer.client_id);
4453 assert_eq!(report.selected_entities, 1);
4454 assert_eq!(report.encoded_entities, 1);
4455 assert_eq!(report.encoded_components, 1);
4456 assert_eq!(transport.packets_sent(), 1);
4457 assert_eq!(transport.bytes_sent(), report.bytes_sent);
4458 assert_eq!(bridge.stats().frames_sent, 1);
4459 assert_eq!(bridge.stats().entities_selected, 1);
4460 assert_eq!(bridge.stats().components_encoded, 1);
4461 }
4462
4463 #[test]
4464 fn replication_transport_bridge_skips_empty_frames_by_default() {
4465 let mut station = station(1, 10);
4466 let mut index = CellIndex::new(GridSpec::new(64.0).expect("grid is valid"));
4467 let mut policies = PolicyTable::default();
4468 policies.set(CompiledSyncPolicy::new(PolicyId::new(0), 1, 20, 256.0));
4469 let handle = station
4470 .spawn_owned(
4471 EntityId::new(100),
4472 Position3::new(0.0, 0.0, 0.0),
4473 Bounds::Point,
4474 PolicyId::new(0),
4475 )
4476 .expect("spawn should work");
4477 index.upsert(handle, Position3::new(0.0, 0.0, 0.0), Bounds::Point);
4478 let components = ComponentStore::default();
4479 let selection = ComponentSelection {
4480 component_ids: vec![ComponentId::new(1)],
4481 };
4482 let viewer = ViewerQuery {
4483 client_id: ClientId::new(7),
4484 position: Position3::new(0.0, 0.0, 0.0),
4485 radius: 256.0,
4486 max_entities: 32,
4487 };
4488 let mut bridge = ReplicationTransportBridge::default();
4489 let mut transport = FakeTransport::default();
4490
4491 let report = bridge
4492 .send_viewer(
4493 &mut transport,
4494 &station,
4495 &index,
4496 &policies,
4497 &components,
4498 &selection,
4499 &viewer,
4500 &RangeOnlyVisibility,
4501 )
4502 .expect("empty replication should skip");
4503
4504 assert!(!report.sent);
4505 assert_eq!(report.selected_entities, 1);
4506 assert_eq!(report.encoded_entities, 0);
4507 assert_eq!(transport.packets_sent(), 0);
4508 assert_eq!(bridge.stats().frames_skipped_empty, 1);
4509 assert_eq!(bridge.stats().entities_selected, 1);
4510 }
4511
4512 #[test]
4513 #[allow(clippy::too_many_lines)]
4514 fn replication_transport_bridge_prioritized_reports_budget_skips() {
4515 let client_id = ClientId::new(7);
4516 let server_id = ClientId::new(0);
4517 let mut station = station(1, 10);
4518 let mut index = CellIndex::new(GridSpec::new(64.0).expect("grid is valid"));
4519 let mut policies = PolicyTable::default();
4520 let mut low = CompiledSyncPolicy::new(PolicyId::new(1), 1, 20, 256.0);
4521 low.priority_weight = 1;
4522 let mut high = CompiledSyncPolicy::new(PolicyId::new(2), 1, 20, 256.0);
4523 high.priority_weight = 10;
4524 policies.set(low);
4525 policies.set(high);
4526
4527 let low_handle = station
4528 .spawn_owned(
4529 EntityId::new(100),
4530 Position3::new(0.0, 0.0, 0.0),
4531 Bounds::Point,
4532 PolicyId::new(1),
4533 )
4534 .expect("spawn low should work");
4535 let high_handle = station
4536 .spawn_owned(
4537 EntityId::new(200),
4538 Position3::new(128.0, 0.0, 0.0),
4539 Bounds::Point,
4540 PolicyId::new(2),
4541 )
4542 .expect("spawn high should work");
4543 index.upsert(low_handle, Position3::new(0.0, 0.0, 0.0), Bounds::Point);
4544 index.upsert(high_handle, Position3::new(128.0, 0.0, 0.0), Bounds::Point);
4545
4546 let descriptor = ComponentDescriptor::sparse_blob(
4547 ComponentId::new(1),
4548 "health",
4549 ComponentSyncMode::Delta,
4550 ComponentMigrationMode::Copy,
4551 4,
4552 );
4553 let mut components = ComponentStore::default();
4554 components
4555 .set_typed(&descriptor, low_handle, 1, &U32LeCodec, &100)
4556 .expect("low component should write");
4557 components
4558 .set_typed(&descriptor, high_handle, 1, &U32LeCodec, &200)
4559 .expect("high component should write");
4560 let selection = ComponentSelection {
4561 component_ids: vec![ComponentId::new(1)],
4562 };
4563 let viewer = ViewerQuery {
4564 client_id,
4565 position: Position3::new(0.0, 0.0, 0.0),
4566 radius: 256.0,
4567 max_entities: 1,
4568 };
4569 let hub = InMemoryTransportHub::new(ClientTransportLimits {
4570 max_queued_packets_per_client: 4,
4571 max_packet_bytes: 512,
4572 });
4573 let mut client_transport = hub
4574 .endpoint(client_id, "127.0.0.1:23107".parse().expect("client addr"))
4575 .expect("client endpoint should register");
4576 let mut server_transport = hub
4577 .endpoint(server_id, "127.0.0.1:23100".parse().expect("server addr"))
4578 .expect("server endpoint should register");
4579 let mut bridge = ReplicationTransportBridge::new(
4580 ReplicationTransportConfig {
4581 budget: ReplicationBudget {
4582 max_entities: 1,
4583 max_bytes: 32,
4584 estimated_entity_bytes: 32,
4585 },
4586 send_empty_frames: false,
4587 },
4588 ReplicationFrameBuilder::default(),
4589 );
4590
4591 let report = bridge
4592 .send_viewer_prioritized(
4593 &mut server_transport,
4594 &station,
4595 &index,
4596 &policies,
4597 &components,
4598 &selection,
4599 &viewer,
4600 &RangeOnlyVisibility,
4601 )
4602 .expect("prioritized replication should send");
4603
4604 assert!(report.sent);
4605 assert_eq!(report.selected_entities, 1);
4606 assert_eq!(report.skipped_by_budget, 1);
4607 assert_eq!(bridge.stats().entities_skipped_by_budget, 1);
4608
4609 let packet = client_transport
4610 .try_recv()
4611 .expect("receive should work")
4612 .expect("packet should exist");
4613 let RuntimeFrame::Replication(frame) = BinaryFrameDecoder
4614 .decode(&packet.bytes)
4615 .expect("frame decodes")
4616 else {
4617 panic!("expected replication frame");
4618 };
4619 assert_eq!(frame.entities.len(), 1);
4620 assert_eq!(frame.entities[0].entity_id, EntityId::new(200));
4621 }
4622
4623 #[test]
4624 fn replication_receive_bridge_decodes_target_frames() {
4625 let client_id = ClientId::new(7);
4626 let server_id = ClientId::new(0);
4627 let hub = InMemoryTransportHub::new(ClientTransportLimits {
4628 max_queued_packets_per_client: 4,
4629 max_packet_bytes: 512,
4630 });
4631 let mut client_transport = hub
4632 .endpoint(client_id, "127.0.0.1:23007".parse().expect("client addr"))
4633 .expect("client endpoint should register");
4634 let mut server_transport = hub
4635 .endpoint(server_id, "127.0.0.1:23000".parse().expect("server addr"))
4636 .expect("server endpoint should register");
4637 let frame = ReplicationFrame {
4638 client_id,
4639 server_tick: Tick::new(12),
4640 entity_count: 1,
4641 estimated_payload_bytes: 4,
4642 entities: vec![EntityDelta {
4643 entity_id: EntityId::new(100),
4644 owner_epoch: OwnerEpoch::new(1),
4645 components: vec![ComponentDelta {
4646 component_id: ComponentId::new(1),
4647 version: 1,
4648 flags: 0,
4649 bytes: 100_u32.to_le_bytes().to_vec(),
4650 }],
4651 }],
4652 };
4653 let mut bytes = Vec::new();
4654 BinaryFrameEncoder
4655 .encode_replication(&frame, &mut bytes)
4656 .expect("replication should encode");
4657 server_transport
4658 .send(OutboundPacket { client_id, bytes })
4659 .expect("replication packet should send");
4660
4661 let mut receive = ReplicationReceiveBridge::new(
4662 ReplicationReceiveConfig::new(client_id).with_expected_source(server_id),
4663 );
4664 let pump = receive
4665 .pump(&mut client_transport, 4)
4666 .expect("replication packet should receive");
4667
4668 assert_eq!(pump.frames_received(), 1);
4669 assert_eq!(pump.entities_received(), 1);
4670 assert_eq!(pump.components_received(), 1);
4671 assert_eq!(pump.frames[0].client_id, client_id);
4672 assert_eq!(receive.stats().packets_received, 1);
4673 assert_eq!(receive.stats().frames_received, 1);
4674 assert_eq!(receive.stats().entities_received, 1);
4675 assert_eq!(receive.stats().components_received, 1);
4676 assert!(receive.stats().bytes_received > 0);
4677 }
4678
4679 #[test]
4680 fn replication_receive_bridge_rejects_wrong_target() {
4681 let client_id = ClientId::new(7);
4682 let server_id = ClientId::new(0);
4683 let wrong_client_id = ClientId::new(99);
4684 let hub = InMemoryTransportHub::new(ClientTransportLimits {
4685 max_queued_packets_per_client: 4,
4686 max_packet_bytes: 512,
4687 });
4688 let mut client_transport = hub
4689 .endpoint(client_id, "127.0.0.1:23107".parse().expect("client addr"))
4690 .expect("client endpoint should register");
4691 let mut server_transport = hub
4692 .endpoint(server_id, "127.0.0.1:23100".parse().expect("server addr"))
4693 .expect("server endpoint should register");
4694 let frame = ReplicationFrame {
4695 client_id: wrong_client_id,
4696 server_tick: Tick::new(12),
4697 entity_count: 1,
4698 estimated_payload_bytes: 4,
4699 entities: vec![EntityDelta {
4700 entity_id: EntityId::new(100),
4701 owner_epoch: OwnerEpoch::new(1),
4702 components: vec![ComponentDelta {
4703 component_id: ComponentId::new(1),
4704 version: 1,
4705 flags: 0,
4706 bytes: 100_u32.to_le_bytes().to_vec(),
4707 }],
4708 }],
4709 };
4710 let mut bytes = Vec::new();
4711 BinaryFrameEncoder
4712 .encode_replication(&frame, &mut bytes)
4713 .expect("replication should encode");
4714 server_transport
4715 .send(OutboundPacket { client_id, bytes })
4716 .expect("replication packet should send");
4717
4718 let mut receive = ReplicationReceiveBridge::new(
4719 ReplicationReceiveConfig::new(client_id).with_expected_source(server_id),
4720 );
4721 let error = receive
4722 .pump(&mut client_transport, 4)
4723 .expect_err("wrong target should be rejected");
4724
4725 assert!(matches!(
4726 error,
4727 ReplicationReceiveError::TargetMismatch {
4728 expected,
4729 actual,
4730 } if expected == client_id && actual == wrong_client_id
4731 ));
4732 assert_eq!(receive.stats().packets_received, 1);
4733 assert_eq!(receive.stats().frames_received, 0);
4734 assert_eq!(receive.stats().frames_rejected_target, 1);
4735 }
4736
4737 #[test]
4738 #[allow(clippy::too_many_lines)]
4739 fn client_transport_bridge_sends_command_and_receives_client_frames() {
4740 let client_id = ClientId::new(7);
4741 let server_id = ClientId::new(0);
4742 let hub = InMemoryTransportHub::new(ClientTransportLimits {
4743 max_queued_packets_per_client: 8,
4744 max_packet_bytes: 512,
4745 });
4746 let mut client_transport = hub
4747 .endpoint(client_id, "127.0.0.1:23207".parse().expect("client addr"))
4748 .expect("client endpoint should register");
4749 let mut server_transport = hub
4750 .endpoint(server_id, "127.0.0.1:23200".parse().expect("server addr"))
4751 .expect("server endpoint should register");
4752 let mut bridge = ClientTransportBridge::new(
4753 ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
4754 );
4755 let command = CommandFrame {
4756 client_id,
4757 command_id: CommandId::new(42),
4758 entity_id: EntityId::new(100),
4759 sequence: 9,
4760 kind: 1,
4761 priority: CommandPriority::High,
4762 payload: b"move:north".to_vec(),
4763 };
4764
4765 let send = bridge
4766 .send_command_frame(&mut client_transport, &command)
4767 .expect("command should send");
4768 assert_eq!(send.command_id, command.command_id);
4769 assert!(send.bytes_sent > 0);
4770 assert_eq!(bridge.stats().commands_sent, 1);
4771 assert_eq!(bridge.stats().command_bytes_sent, send.bytes_sent);
4772 let inbound = server_transport
4773 .try_recv()
4774 .expect("server receive should work")
4775 .expect("command packet should arrive");
4776 assert_eq!(inbound.client_id, Some(client_id));
4777 let RuntimeFrame::Command(decoded) = BinaryFrameDecoder
4778 .decode(&inbound.bytes)
4779 .expect("command should decode")
4780 else {
4781 panic!("expected command frame");
4782 };
4783 assert_eq!(decoded, command);
4784
4785 let ack = CommandAckFrame {
4786 client_id,
4787 command_id: command.command_id,
4788 server_tick: Tick::new(12),
4789 accepted: true,
4790 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
4791 };
4792 let mut ack_bytes = Vec::new();
4793 BinaryFrameEncoder
4794 .encode_command_ack(&ack, &mut ack_bytes)
4795 .expect("ACK should encode");
4796 server_transport
4797 .send(OutboundPacket {
4798 client_id,
4799 bytes: ack_bytes,
4800 })
4801 .expect("ACK should send");
4802
4803 let replication = ReplicationFrame {
4804 client_id,
4805 server_tick: Tick::new(12),
4806 entity_count: 1,
4807 estimated_payload_bytes: 4,
4808 entities: vec![EntityDelta {
4809 entity_id: EntityId::new(100),
4810 owner_epoch: OwnerEpoch::new(1),
4811 components: vec![ComponentDelta {
4812 component_id: ComponentId::new(1),
4813 version: 1,
4814 flags: 0,
4815 bytes: 100_u32.to_le_bytes().to_vec(),
4816 }],
4817 }],
4818 };
4819 let mut replication_bytes = Vec::new();
4820 BinaryFrameEncoder
4821 .encode_replication(&replication, &mut replication_bytes)
4822 .expect("replication should encode");
4823 server_transport
4824 .send(OutboundPacket {
4825 client_id,
4826 bytes: replication_bytes,
4827 })
4828 .expect("replication should send");
4829
4830 let barrier = BarrierFrame {
4831 client_id,
4832 barrier_id: BarrierId::new(5),
4833 server_tick: Tick::new(12),
4834 state: BarrierState::Frozen,
4835 };
4836 let mut barrier_bytes = Vec::new();
4837 BinaryFrameEncoder
4838 .encode_barrier(&barrier, &mut barrier_bytes)
4839 .expect("barrier should encode");
4840 server_transport
4841 .send(OutboundPacket {
4842 client_id,
4843 bytes: barrier_bytes,
4844 })
4845 .expect("barrier should send");
4846
4847 let pump = bridge
4848 .pump(&mut client_transport, 8)
4849 .expect("client frames should receive");
4850
4851 assert_eq!(pump.packets_received, 3);
4852 assert_eq!(pump.command_acks_received(), 1);
4853 assert_eq!(pump.replication_frames_received(), 1);
4854 assert_eq!(pump.barrier_frames_received(), 1);
4855 assert_eq!(pump.entities_received(), 1);
4856 assert_eq!(pump.components_received(), 1);
4857 assert_eq!(pump.command_acks[0], ack);
4858 assert_eq!(pump.replication_frames[0], replication);
4859 assert_eq!(pump.barriers[0], barrier);
4860 assert_eq!(bridge.stats().packets_received, 3);
4861 assert_eq!(bridge.stats().command_acks_received, 1);
4862 assert_eq!(bridge.stats().replication_frames_received, 1);
4863 assert_eq!(bridge.stats().barrier_frames_received, 1);
4864 assert_eq!(bridge.stats().entities_received, 1);
4865 assert_eq!(bridge.stats().components_received, 1);
4866 }
4867
4868 #[test]
4869 fn client_transport_bridge_rejects_wrong_ack_target() {
4870 let client_id = ClientId::new(7);
4871 let server_id = ClientId::new(0);
4872 let wrong_client_id = ClientId::new(99);
4873 let hub = InMemoryTransportHub::new(ClientTransportLimits {
4874 max_queued_packets_per_client: 4,
4875 max_packet_bytes: 512,
4876 });
4877 let mut client_transport = hub
4878 .endpoint(client_id, "127.0.0.1:23307".parse().expect("client addr"))
4879 .expect("client endpoint should register");
4880 let mut server_transport = hub
4881 .endpoint(server_id, "127.0.0.1:23300".parse().expect("server addr"))
4882 .expect("server endpoint should register");
4883 let mut bridge = ClientTransportBridge::new(
4884 ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
4885 );
4886 let ack = CommandAckFrame {
4887 client_id: wrong_client_id,
4888 command_id: CommandId::new(42),
4889 server_tick: Tick::new(12),
4890 accepted: true,
4891 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
4892 };
4893 let mut ack_bytes = Vec::new();
4894 BinaryFrameEncoder
4895 .encode_command_ack(&ack, &mut ack_bytes)
4896 .expect("ACK should encode");
4897 server_transport
4898 .send(OutboundPacket {
4899 client_id,
4900 bytes: ack_bytes,
4901 })
4902 .expect("ACK should send");
4903
4904 let error = bridge
4905 .pump(&mut client_transport, 4)
4906 .expect_err("wrong target should be rejected");
4907
4908 assert!(matches!(
4909 error,
4910 ClientTransportBridgeError::TargetMismatch {
4911 kind: ClientInboundFrameKind::CommandAck,
4912 expected,
4913 actual,
4914 } if expected == client_id && actual == wrong_client_id
4915 ));
4916 assert_eq!(bridge.stats().packets_received, 1);
4917 assert_eq!(bridge.stats().command_acks_received, 0);
4918 assert_eq!(bridge.stats().frames_rejected_target, 1);
4919 }
4920
4921 #[test]
4922 fn gateway_client_transport_bridge_queues_command_and_sends_ack() {
4923 let client_id = ClientId::new(7);
4924 let server_id = ClientId::new(0);
4925 let station_id = StationId::new(1);
4926 let hub = InMemoryTransportHub::new(ClientTransportLimits {
4927 max_queued_packets_per_client: 8,
4928 max_packet_bytes: 512,
4929 });
4930 let mut client_transport = hub
4931 .endpoint(client_id, "127.0.0.1:23507".parse().expect("client addr"))
4932 .expect("client endpoint should register");
4933 let mut server_transport = hub
4934 .endpoint(server_id, "127.0.0.1:23500".parse().expect("server addr"))
4935 .expect("server endpoint should register");
4936 let mut client_bridge = ClientTransportBridge::new(
4937 ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
4938 );
4939 let command = CommandFrame {
4940 client_id,
4941 command_id: CommandId::new(42),
4942 entity_id: EntityId::new(100),
4943 sequence: 9,
4944 kind: 1,
4945 priority: CommandPriority::High,
4946 payload: b"move:north".to_vec(),
4947 };
4948 client_bridge
4949 .send_command_frame(&mut client_transport, &command)
4950 .expect("client command should send");
4951
4952 let mut gateway = gateway(4);
4953 gateway
4954 .connect(client_id, station_id, Tick::new(10))
4955 .expect("client should connect");
4956 let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
4957 let mut pipeline = GatewayCommandPipeline::default();
4958 let mut gateway_bridge = GatewayClientTransportBridge::default();
4959
4960 let pump = gateway_bridge
4961 .pump_ingress(
4962 &mut server_transport,
4963 &mut pipeline,
4964 &mut gateway,
4965 &mut station_queues,
4966 Tick::new(10),
4967 CommandIngress::RUNNING,
4968 4,
4969 )
4970 .expect("gateway client transport should pump");
4971
4972 assert_eq!(pump.packets_received, 1);
4973 assert_eq!(pump.commands_processed(), 1);
4974 assert_eq!(pump.commands_accepted(), 1);
4975 assert_eq!(pump.acks_sent, 1);
4976 assert_eq!(gateway_bridge.stats().packets_received, 1);
4977 assert_eq!(gateway_bridge.stats().command_frames_received, 1);
4978 assert_eq!(gateway_bridge.stats().commands_accepted, 1);
4979 assert_eq!(gateway_bridge.stats().acks_sent, 1);
4980 let queued = station_queues
4981 .get_mut(&station_id)
4982 .expect("station queue should exist")
4983 .pop_next()
4984 .expect("command should queue");
4985 assert_eq!(queued.id, command.command_id);
4986
4987 let ack_pump = client_bridge
4988 .pump(&mut client_transport, 4)
4989 .expect("client should receive ACK");
4990 assert_eq!(ack_pump.command_acks_received(), 1);
4991 assert!(ack_pump.command_acks[0].accepted);
4992 assert_eq!(ack_pump.command_acks[0].command_id, command.command_id);
4993 }
4994
4995 #[test]
4996 fn gateway_client_transport_bridge_rejects_source_mismatch_before_admission() {
4997 let packet_client_id = ClientId::new(7);
4998 let frame_client_id = ClientId::new(8);
4999 let server_id = ClientId::new(0);
5000 let station_id = StationId::new(1);
5001 let hub = InMemoryTransportHub::new(ClientTransportLimits {
5002 max_queued_packets_per_client: 4,
5003 max_packet_bytes: 512,
5004 });
5005 let mut packet_client_transport = hub
5006 .endpoint(
5007 packet_client_id,
5008 "127.0.0.1:23607".parse().expect("client addr"),
5009 )
5010 .expect("client endpoint should register");
5011 let mut server_transport = hub
5012 .endpoint(server_id, "127.0.0.1:23600".parse().expect("server addr"))
5013 .expect("server endpoint should register");
5014 let command = CommandFrame {
5015 client_id: frame_client_id,
5016 command_id: CommandId::new(42),
5017 entity_id: EntityId::new(100),
5018 sequence: 9,
5019 kind: 1,
5020 priority: CommandPriority::High,
5021 payload: b"move:north".to_vec(),
5022 };
5023 let mut bytes = Vec::new();
5024 BinaryFrameEncoder
5025 .encode_command(&command, &mut bytes)
5026 .expect("command should encode");
5027 packet_client_transport
5028 .send(OutboundPacket {
5029 client_id: server_id,
5030 bytes,
5031 })
5032 .expect("packet should send");
5033
5034 let mut gateway = gateway(4);
5035 gateway
5036 .connect(frame_client_id, station_id, Tick::new(10))
5037 .expect("frame client should connect");
5038 let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
5039 let mut pipeline = GatewayCommandPipeline::default();
5040 let mut gateway_bridge = GatewayClientTransportBridge::default();
5041
5042 let error = gateway_bridge
5043 .pump_ingress(
5044 &mut server_transport,
5045 &mut pipeline,
5046 &mut gateway,
5047 &mut station_queues,
5048 Tick::new(10),
5049 CommandIngress::RUNNING,
5050 4,
5051 )
5052 .expect_err("source mismatch should reject before admission");
5053
5054 assert!(matches!(
5055 error,
5056 GatewayClientTransportError::SourceMismatch {
5057 packet_client_id: actual_packet,
5058 frame_client_id: actual_frame,
5059 } if actual_packet == packet_client_id && actual_frame == frame_client_id
5060 ));
5061 assert_eq!(gateway_bridge.stats().source_mismatches, 1);
5062 assert_eq!(gateway_bridge.stats().commands_accepted, 0);
5063 assert_eq!(pipeline.stats().commands_admitted, 0);
5064 assert_eq!(
5065 station_queues
5066 .get(&station_id)
5067 .expect("station queue should exist")
5068 .total_len(),
5069 0
5070 );
5071 }
5072
5073 #[test]
5074 fn gateway_command_pipeline_queues_command_and_encodes_ack() {
5075 let client_id = ClientId::new(7);
5076 let station_id = StationId::new(1);
5077 let mut gateway = gateway(4);
5078 gateway
5079 .connect(client_id, station_id, Tick::new(10))
5080 .expect("client should connect");
5081 let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
5082 let mut pipeline = GatewayCommandPipeline::default();
5083
5084 let report = pipeline.process(
5085 &mut gateway,
5086 &mut station_queues,
5087 &encode_command_frame(1),
5088 Tick::new(10),
5089 CommandIngress::RUNNING,
5090 );
5091
5092 assert!(report.accepted);
5093 assert_eq!(report.reason_code, GATEWAY_COMMAND_ACK_ACCEPTED);
5094 assert_eq!(report.station_id, Some(station_id));
5095 assert!(report.error.is_none());
5096 let ack_bytes = report.ack_bytes.expect("ACK should encode");
5097 let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
5098 .decode(&ack_bytes)
5099 .expect("ACK should decode")
5100 else {
5101 panic!("expected command ACK");
5102 };
5103 assert!(ack.accepted);
5104 assert_eq!(ack.command_id, CommandId::new(1));
5105 let queued = station_queues
5106 .get_mut(&station_id)
5107 .expect("queue should exist")
5108 .pop_next()
5109 .expect("command should queue");
5110 assert_eq!(queued.id, CommandId::new(1));
5111 assert_eq!(pipeline.stats().commands_admitted, 1);
5112 assert_eq!(pipeline.stats().commands_enqueued, 1);
5113 assert_eq!(pipeline.stats().acks_encoded, 1);
5114 }
5115
5116 #[test]
5117 fn gateway_command_pipeline_negative_acks_rate_limit() {
5118 let client_id = ClientId::new(7);
5119 let station_id = StationId::new(1);
5120 let mut gateway = gateway(1);
5121 gateway
5122 .connect(client_id, station_id, Tick::new(10))
5123 .expect("client should connect");
5124 let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
5125 let mut pipeline = GatewayCommandPipeline::default();
5126
5127 assert!(
5128 pipeline
5129 .process(
5130 &mut gateway,
5131 &mut station_queues,
5132 &encode_command_frame(1),
5133 Tick::new(10),
5134 CommandIngress::RUNNING,
5135 )
5136 .accepted
5137 );
5138 let rejected = pipeline.process(
5139 &mut gateway,
5140 &mut station_queues,
5141 &encode_command_frame(2),
5142 Tick::new(10),
5143 CommandIngress::RUNNING,
5144 );
5145
5146 assert!(!rejected.accepted);
5147 assert_eq!(rejected.reason_code, GATEWAY_COMMAND_ACK_RATE_LIMITED);
5148 assert!(matches!(
5149 rejected.error,
5150 Some(GatewayCommandPipelineError::Gateway(
5151 GatewayError::RateLimited { .. }
5152 ))
5153 ));
5154 let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
5155 .decode(&rejected.ack_bytes.expect("rejection ACK should encode"))
5156 .expect("ACK should decode")
5157 else {
5158 panic!("expected command ACK");
5159 };
5160 assert!(!ack.accepted);
5161 assert_eq!(ack.reason_code, GATEWAY_COMMAND_ACK_RATE_LIMITED);
5162 assert_eq!(pipeline.stats().commands_rejected_gateway, 1);
5163 }
5164
5165 #[test]
5166 fn gateway_command_pipeline_rejects_missing_station_queue() {
5167 let client_id = ClientId::new(7);
5168 let station_id = StationId::new(1);
5169 let mut gateway = gateway(4);
5170 gateway
5171 .connect(client_id, station_id, Tick::new(10))
5172 .expect("client should connect");
5173 let mut station_queues = BTreeMap::new();
5174 let mut pipeline = GatewayCommandPipeline::default();
5175
5176 let report = pipeline.process(
5177 &mut gateway,
5178 &mut station_queues,
5179 &encode_command_frame(1),
5180 Tick::new(10),
5181 CommandIngress::RUNNING,
5182 );
5183
5184 assert!(!report.accepted);
5185 assert_eq!(report.station_id, Some(station_id));
5186 assert_eq!(report.reason_code, GATEWAY_COMMAND_ACK_MISSING_QUEUE);
5187 assert!(matches!(
5188 report.error,
5189 Some(GatewayCommandPipelineError::MissingQueue(id)) if id == station_id
5190 ));
5191 assert_eq!(pipeline.stats().commands_admitted, 1);
5192 assert_eq!(pipeline.stats().commands_rejected_queue, 1);
5193 }
5194
5195 #[test]
5196 fn gateway_command_pipeline_dispatches_to_deployment_route() {
5197 let client_id = ClientId::new(7);
5198 let station_id = StationId::new(1);
5199 let node_id = NodeId::new(9);
5200 let mut gateway = gateway(4);
5201 gateway
5202 .connect(client_id, station_id, Tick::new(10))
5203 .expect("client should connect");
5204 let mut deployment = DeploymentRouteTable::new(DeploymentConfig {
5205 max_nodes: 4,
5206 max_stations_per_node: 4,
5207 stale_after_ticks: 10,
5208 });
5209 deployment
5210 .register_node(node_id, 4, Tick::new(10))
5211 .expect("node should register");
5212 deployment
5213 .assign_station(station_id, node_id, Tick::new(10))
5214 .expect("station should assign");
5215 let mut pipeline = GatewayCommandPipeline::default();
5216
5217 let report = pipeline.dispatch(
5218 &mut gateway,
5219 &deployment,
5220 &encode_command_frame(1),
5221 Tick::new(12),
5222 );
5223
5224 assert!(report.accepted);
5225 assert_eq!(report.station_id, Some(station_id));
5226 assert_eq!(report.node_id, Some(node_id));
5227 let delivery = report.delivery.expect("delivery should resolve");
5228 assert_eq!(delivery.client_id, client_id);
5229 assert_eq!(delivery.station_id, station_id);
5230 assert_eq!(delivery.node_id, node_id);
5231 assert_eq!(delivery.station_route_epoch, 1);
5232 assert_eq!(
5233 report
5234 .command
5235 .expect("command should be returned")
5236 .received_at,
5237 Tick::new(12)
5238 );
5239 let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
5240 .decode(&report.ack_bytes.expect("ACK should encode"))
5241 .expect("ACK should decode")
5242 else {
5243 panic!("expected command ACK");
5244 };
5245 assert!(ack.accepted);
5246 assert_eq!(pipeline.stats().commands_routed_deployment, 1);
5247 }
5248
5249 #[test]
5250 fn gateway_command_pipeline_negative_acks_missing_deployment_route() {
5251 let client_id = ClientId::new(7);
5252 let station_id = StationId::new(1);
5253 let mut gateway = gateway(4);
5254 gateway
5255 .connect(client_id, station_id, Tick::new(10))
5256 .expect("client should connect");
5257 let deployment = DeploymentRouteTable::default();
5258 let mut pipeline = GatewayCommandPipeline::default();
5259
5260 let report = pipeline.dispatch(
5261 &mut gateway,
5262 &deployment,
5263 &encode_command_frame(1),
5264 Tick::new(12),
5265 );
5266
5267 assert!(!report.accepted);
5268 assert_eq!(report.station_id, Some(station_id));
5269 assert_eq!(report.reason_code, GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED);
5270 assert!(matches!(
5271 report.error,
5272 Some(GatewayCommandPipelineError::Deployment(
5273 DeploymentError::MissingStation(id)
5274 )) if id == station_id
5275 ));
5276 let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
5277 .decode(&report.ack_bytes.expect("rejection ACK should encode"))
5278 .expect("ACK should decode")
5279 else {
5280 panic!("expected command ACK");
5281 };
5282 assert!(!ack.accepted);
5283 assert_eq!(ack.reason_code, GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED);
5284 assert_eq!(pipeline.stats().commands_rejected_deployment, 1);
5285 }
5286
5287 #[test]
5288 fn gateway_command_pipeline_rejects_non_command_frame() {
5289 let ack = CommandAckFrame {
5290 client_id: ClientId::new(7),
5291 command_id: CommandId::new(1),
5292 server_tick: Tick::new(10),
5293 accepted: true,
5294 reason_code: 0,
5295 };
5296 let mut bytes = Vec::new();
5297 BinaryFrameEncoder
5298 .encode_command_ack(&ack, &mut bytes)
5299 .expect("ACK should encode");
5300 let mut gateway = gateway(4);
5301 let mut station_queues = BTreeMap::new();
5302 let mut pipeline = GatewayCommandPipeline::default();
5303
5304 let report = pipeline.process(
5305 &mut gateway,
5306 &mut station_queues,
5307 &bytes,
5308 Tick::new(10),
5309 CommandIngress::RUNNING,
5310 );
5311
5312 assert!(!report.accepted);
5313 assert!(report.ack_bytes.is_none());
5314 assert_eq!(
5315 report.error,
5316 Some(GatewayCommandPipelineError::NonCommandFrame)
5317 );
5318 assert_eq!(pipeline.stats().frames_rejected_non_command, 1);
5319 }
5320
5321 #[test]
5322 fn migration_executor_moves_owner_and_leaves_source_ghost() {
5323 let mut stations = StationSet::default();
5324 let mut source = station(1, 10);
5325 source
5326 .spawn_owned(
5327 EntityId::new(99),
5328 Position3::new(1.0, 2.0, 3.0),
5329 Bounds::Point,
5330 PolicyId::new(0),
5331 )
5332 .expect("spawn should work");
5333 stations.push(source);
5334 stations.push(station(2, 10));
5335
5336 let report = EntityMigrationExecutor::migrate_entity(
5337 &mut stations,
5338 EntityId::new(99),
5339 StationId::new(1),
5340 StationId::new(2),
5341 4,
5342 )
5343 .expect("migration should work");
5344
5345 assert_eq!(report.transfer.target_station, StationId::new(2));
5346 assert!(
5347 !stations
5348 .get(StationId::new(1))
5349 .expect("source")
5350 .get_by_id(EntityId::new(99))
5351 .expect("source ghost")
5352 .is_owned()
5353 );
5354 assert!(
5355 stations
5356 .get(StationId::new(2))
5357 .expect("target")
5358 .get_by_id(EntityId::new(99))
5359 .expect("target owner")
5360 .is_owned()
5361 );
5362 }
5363
5364 #[test]
5365 fn event_router_delays_until_target_tick_and_scheduler_drains() {
5366 let mut stations = StationSet::default();
5367 stations.push(station(1, 10));
5368 stations.push(station(2, 10));
5369
5370 let mut router = EventRouter::default();
5371 router.register_stations(&stations);
5372 router
5373 .route(StationEvent {
5374 id: EventId::new(1),
5375 source: StationId::new(1),
5376 target: StationId::new(2),
5377 source_tick: Tick::new(0),
5378 target_tick: Tick::new(2),
5379 priority: EventPriority::Critical,
5380 kind: EventKind::Custom(7),
5381 })
5382 .expect("route should work");
5383
5384 let mut scheduler = StationScheduler::default();
5385 scheduler.advance_all(&mut stations);
5386 let drained = scheduler
5387 .drain_ready_events(&stations, &mut router)
5388 .expect("drain should work");
5389 assert!(drained.is_empty());
5390
5391 scheduler.advance_all(&mut stations);
5392 let drained = scheduler
5393 .drain_ready_events(&stations, &mut router)
5394 .expect("drain should work");
5395 assert_eq!(drained.len(), 1);
5396 assert_eq!(router.stats().routed_events, 1);
5397 assert_eq!(router.stats().drained_events, 1);
5398 }
5399
5400 #[test]
5401 fn station_scheduler_prioritizes_loaded_stations_with_budget() {
5402 let mut stations = StationSet::default();
5403 stations.push(station(1, 10));
5404 stations.push(station(2, 10));
5405 stations.push(station(3, 10));
5406
5407 let samples = vec![
5408 StationLoadSample {
5409 station_id: StationId::new(1),
5410 owned_entities: 1,
5411 ..StationLoadSample::default()
5412 },
5413 StationLoadSample {
5414 station_id: StationId::new(2),
5415 owned_entities: 100,
5416 subscribers: 40,
5417 queued_events: 20,
5418 tick_cost_units: 500,
5419 cells: vec![CellLoadSample {
5420 cell: CellCoord3::new(0, 0, 0),
5421 owned_entities: 90,
5422 subscribers: 40,
5423 event_pressure: 10,
5424 ..CellLoadSample::default()
5425 }],
5426 ..StationLoadSample::default()
5427 },
5428 StationLoadSample {
5429 station_id: StationId::new(3),
5430 owned_entities: 25,
5431 subscribers: 10,
5432 queued_events: 5,
5433 tick_cost_units: 50,
5434 ..StationLoadSample::default()
5435 },
5436 ];
5437
5438 let mut scheduler = StationScheduler::default();
5439 let plan = scheduler.advance_loaded(
5440 &mut stations,
5441 &samples,
5442 StationScheduleConfig {
5443 max_station_advances_per_step: 2,
5444 },
5445 );
5446
5447 assert_eq!(plan.candidates_considered, 3);
5448 assert_eq!(plan.stations_selected, 2);
5449 assert_eq!(plan.total_advances, 2);
5450 assert_eq!(
5451 plan.selected
5452 .iter()
5453 .map(|candidate| candidate.station_id)
5454 .collect::<Vec<_>>(),
5455 vec![StationId::new(2), StationId::new(3)]
5456 );
5457 assert_eq!(scheduler.advanced_ticks, 2);
5458 assert_eq!(
5459 stations.get(StationId::new(1)).expect("station").tick(),
5460 Tick::new(0)
5461 );
5462 assert_eq!(
5463 stations.get(StationId::new(2)).expect("station").tick(),
5464 Tick::new(1)
5465 );
5466 assert_eq!(
5467 stations.get(StationId::new(3)).expect("station").tick(),
5468 Tick::new(1)
5469 );
5470 }
5471
5472 #[test]
5473 fn station_load_sampler_derives_cells_router_and_subscribers() {
5474 let station_id = StationId::new(1);
5475 let owner_position = Position3::new(1.0, 0.0, 0.0);
5476 let ghost_position = Position3::new(12.0, 0.0, 0.0);
5477 let policy_id = PolicyId::new(1);
5478 let mut station = station(1, 10);
5479 let owner = station
5480 .spawn_owned(EntityId::new(10), owner_position, Bounds::Point, policy_id)
5481 .expect("owner should spawn");
5482 let ghost = station.upsert_ghost(
5483 EntityId::new(20),
5484 ghost_position,
5485 Bounds::Point,
5486 policy_id,
5487 StationId::new(9),
5488 OwnerEpoch::new(3),
5489 Tick::new(30),
5490 );
5491
5492 let grid = GridSpec::new(10.0).expect("grid should build");
5493 let mut index = CellIndex::new(grid);
5494 index.upsert(owner, owner_position, Bounds::Point);
5495 index.upsert(ghost, ghost_position, Bounds::Point);
5496 let mut indexes = StationIndexSet::default();
5497 indexes.insert(station_id, index);
5498
5499 let mut stations = StationSet::default();
5500 stations.push(station);
5501 let mut router = EventRouter::default();
5502 router.register_station(station_id);
5503 for (event_id, kind) in [(1_u64, 1_u32), (2, 2)] {
5504 router
5505 .route(StationEvent {
5506 id: EventId::new(event_id),
5507 source: StationId::new(9),
5508 target: station_id,
5509 source_tick: Tick::new(0),
5510 target_tick: Tick::new(4),
5511 priority: EventPriority::Important,
5512 kind: EventKind::Custom(kind),
5513 })
5514 .expect("event should queue");
5515 }
5516
5517 assert_eq!(indexes.iter().count(), 1);
5518 let load_sampler = StationLoadSampler::default();
5519 let samples = load_sampler.sample_all(
5520 &stations,
5521 &indexes,
5522 &router,
5523 &[(station_id, 2), (station_id, 3)],
5524 );
5525
5526 assert_eq!(samples.len(), 1);
5527 let sample = &samples[0];
5528 assert_eq!(sample.station_id, station_id);
5529 assert_eq!(sample.owned_entities, 1);
5530 assert_eq!(sample.ghost_entities, 1);
5531 assert_eq!(sample.subscribers, 5);
5532 assert_eq!(sample.queued_events, 2);
5533 assert_eq!(sample.estimated_bytes, 240);
5534 assert_eq!(sample.tick_cost_units, 7);
5535 assert_eq!(
5536 sample.cells,
5537 vec![
5538 CellLoadSample {
5539 cell: grid.cell_at(owner_position),
5540 owned_entities: 1,
5541 ghost_entities: 0,
5542 subscribers: 0,
5543 estimated_updates: 1,
5544 estimated_bytes: 48,
5545 tick_cost_units: 3,
5546 event_pressure: 0,
5547 },
5548 CellLoadSample {
5549 cell: grid.cell_at(ghost_position),
5550 owned_entities: 0,
5551 ghost_entities: 1,
5552 subscribers: 0,
5553 estimated_updates: 1,
5554 estimated_bytes: 48,
5555 tick_cost_units: 2,
5556 event_pressure: 0,
5557 },
5558 ]
5559 );
5560 }
5561
5562 #[test]
5563 fn station_event_transport_bridge_routes_events_through_bounded_packets() {
5564 let mut stations = StationSet::default();
5565 stations.push(station(1, 10));
5566 stations.push(station(2, 10));
5567
5568 let mut router = EventRouter::default();
5569 router.register_stations(&stations);
5570 let mut transport = InMemoryStationTransport::default();
5571 transport.register_station(StationId::new(2));
5572 let mut bridge = StationEventTransportBridge::default();
5573 let event = StationEvent {
5574 id: EventId::new(7),
5575 source: StationId::new(1),
5576 target: StationId::new(2),
5577 source_tick: Tick::new(0),
5578 target_tick: Tick::new(1),
5579 priority: EventPriority::Important,
5580 kind: EventKind::Custom(99),
5581 };
5582
5583 bridge
5584 .send_event(&mut transport, &event)
5585 .expect("event should encode and send");
5586 assert_eq!(transport.queued_len(StationId::new(2)), Some(1));
5587
5588 let report = bridge
5589 .pump_target(&mut transport, &mut router, StationId::new(2), 4)
5590 .expect("event should pump into router");
5591 assert_eq!(report.packets_received, 1);
5592 assert_eq!(report.events_routed, 1);
5593 assert_eq!(router.queued_len(StationId::new(2)), Some(1));
5594
5595 let mut scheduler = StationScheduler::default();
5596 scheduler.advance_all(&mut stations);
5597 let drained = scheduler
5598 .drain_ready_events(&stations, &mut router)
5599 .expect("drain should work");
5600 assert_eq!(drained, vec![event]);
5601 assert_eq!(bridge.stats().events_sent, 1);
5602 assert_eq!(bridge.stats().events_routed, 1);
5603 assert_eq!(transport.stats().packets_sent, 1);
5604 assert_eq!(transport.stats().packets_received, 1);
5605 }
5606
5607 #[test]
5608 fn command_dispatch_transport_bridge_enqueues_stamped_command() {
5609 let gateway_station = StationId::new(0);
5610 let target_station = StationId::new(2);
5611 let command = CommandEnvelope {
5612 id: CommandId::new(42),
5613 client_id: ClientId::new(7),
5614 entity_id: EntityId::new(100),
5615 sequence: 42,
5616 received_at: Tick::new(12),
5617 kind: 1,
5618 priority: CommandPriority::High,
5619 payload: b"move:north".to_vec(),
5620 };
5621 let mut transport = InMemoryStationTransport::default();
5622 transport.register_station(target_station);
5623 let mut queues = BTreeMap::from([(target_station, command_queues())]);
5624 let mut bridge = CommandDispatchTransportBridge::default();
5625
5626 bridge
5627 .send_envelope(&mut transport, gateway_station, target_station, &command)
5628 .expect("command dispatch should send");
5629 assert_eq!(transport.queued_len(target_station), Some(1));
5630 let report = bridge
5631 .pump_target(
5632 &mut transport,
5633 &mut queues,
5634 target_station,
5635 4,
5636 CommandIngress::RUNNING,
5637 )
5638 .expect("command dispatch should pump");
5639
5640 assert_eq!(report.packets_received, 1);
5641 assert_eq!(report.commands_enqueued, 1);
5642 let queued_command = queues
5643 .get_mut(&target_station)
5644 .expect("queue should exist")
5645 .pop_next()
5646 .expect("command should queue");
5647 assert_eq!(queued_command, command);
5648 assert_eq!(bridge.stats().commands_sent, 1);
5649 assert_eq!(bridge.stats().commands_enqueued, 1);
5650 assert_eq!(transport.stats().packets_sent, 1);
5651 assert_eq!(transport.stats().packets_received, 1);
5652 }
5653
5654 #[test]
5655 fn command_dispatch_transport_bridge_rejects_endpoint_mismatch() {
5656 let packet_target = StationId::new(2);
5657 let frame_target = StationId::new(3);
5658 let mut transport = InMemoryStationTransport::default();
5659 transport.register_station(packet_target);
5660 let frame = CommandDispatchFrame {
5661 station_id: frame_target,
5662 client_id: ClientId::new(7),
5663 command_id: CommandId::new(42),
5664 entity_id: EntityId::new(100),
5665 sequence: 42,
5666 received_at: Tick::new(12),
5667 kind: 1,
5668 priority: CommandPriority::High,
5669 payload: Vec::new(),
5670 };
5671 let mut bytes = Vec::new();
5672 BinaryFrameEncoder
5673 .encode_command_dispatch(&frame, &mut bytes)
5674 .expect("frame should encode");
5675 transport
5676 .send_station(StationOutboundPacket {
5677 source_station: StationId::new(0),
5678 target_station: packet_target,
5679 bytes,
5680 })
5681 .expect("bad packet should enter transport");
5682 let mut queues = BTreeMap::from([(packet_target, command_queues())]);
5683 let mut bridge = CommandDispatchTransportBridge::default();
5684
5685 let error = bridge
5686 .pump_target(
5687 &mut transport,
5688 &mut queues,
5689 packet_target,
5690 4,
5691 CommandIngress::RUNNING,
5692 )
5693 .expect_err("endpoint mismatch should reject");
5694
5695 assert!(matches!(
5696 error,
5697 CommandDispatchTransportError::EndpointMismatch {
5698 packet_source,
5699 packet_target: observed_packet_target,
5700 dispatch_target,
5701 } if packet_source == StationId::new(0)
5702 && observed_packet_target == packet_target
5703 && dispatch_target == frame_target
5704 ));
5705 assert!(
5706 queues
5707 .get_mut(&packet_target)
5708 .expect("queue should exist")
5709 .pop_next()
5710 .is_none()
5711 );
5712 }
5713
5714 #[test]
5715 fn cell_migration_moves_owned_entities_and_updates_indexes() {
5716 let grid = GridSpec::new(16.0).expect("valid grid");
5717 let cell = CellCoord3::new(0, 0, 0);
5718 let mut stations = StationSet::default();
5719 let mut source = station(1, 10);
5720 let first = source
5721 .spawn_owned(
5722 EntityId::new(1),
5723 Position3::new(1.0, 1.0, 1.0),
5724 Bounds::Point,
5725 PolicyId::new(0),
5726 )
5727 .expect("first spawn should work");
5728 let second = source
5729 .spawn_owned(
5730 EntityId::new(2),
5731 Position3::new(2.0, 1.0, 1.0),
5732 Bounds::Point,
5733 PolicyId::new(0),
5734 )
5735 .expect("second spawn should work");
5736 stations.push(source);
5737 stations.push(station(2, 10));
5738
5739 let mut source_index = CellIndex::new(grid);
5740 source_index.upsert(first, Position3::new(1.0, 1.0, 1.0), Bounds::Point);
5741 source_index.upsert(second, Position3::new(2.0, 1.0, 1.0), Bounds::Point);
5742 let mut target_index = CellIndex::new(grid);
5743
5744 let mut ownership = CellOwnershipTable::default();
5745 ownership.assign(cell, StationId::new(1));
5746 let update = ownership.apply_split(
5747 &SplitProposal {
5748 source_station: StationId::new(1),
5749 cells_to_move: vec![cell],
5750 moved_pressure_score: 10,
5751 },
5752 StationId::new(2),
5753 );
5754 assert_eq!(ownership.owner_of(cell), Some(StationId::new(2)));
5755 assert_eq!(update.moved_cells, vec![cell]);
5756
5757 let report = CellMigrationExecutor::migrate_cells(
5758 &mut stations,
5759 &mut source_index,
5760 &mut target_index,
5761 StationId::new(1),
5762 StationId::new(2),
5763 &update.moved_cells,
5764 4,
5765 )
5766 .expect("cell migration should work");
5767
5768 assert_eq!(report.entity_migrations.len(), 2);
5769 assert_eq!(target_index.entity_count(), 2);
5770 assert!(
5771 !stations
5772 .get(StationId::new(1))
5773 .expect("source")
5774 .get_by_id(EntityId::new(1))
5775 .expect("source ghost")
5776 .is_owned()
5777 );
5778 assert!(
5779 stations
5780 .get(StationId::new(2))
5781 .expect("target")
5782 .get_by_id(EntityId::new(1))
5783 .expect("target owner")
5784 .is_owned()
5785 );
5786 }
5787
5788 #[test]
5789 fn split_scheduler_plans_and_executes_hot_cell_move() {
5790 let grid = GridSpec::new(16.0).expect("valid grid");
5791 let hot_cell = CellCoord3::new(0, 0, 0);
5792 let mut stations = StationSet::default();
5793 let mut source = station(1, 10);
5794 let handle = source
5795 .spawn_owned(
5796 EntityId::new(1),
5797 Position3::new(1.0, 1.0, 1.0),
5798 Bounds::Point,
5799 PolicyId::new(0),
5800 )
5801 .expect("spawn should work");
5802 stations.push(source);
5803 stations.push(station(2, 10));
5804
5805 let mut source_index = CellIndex::new(grid);
5806 source_index.upsert(handle, Position3::new(1.0, 1.0, 1.0), Bounds::Point);
5807 let mut indexes = StationIndexSet::default();
5808 indexes.insert(StationId::new(1), source_index);
5809 indexes.insert(StationId::new(2), CellIndex::new(grid));
5810
5811 let samples = vec![
5812 StationLoadSample {
5813 station_id: StationId::new(1),
5814 owned_entities: 100,
5815 subscribers: 100,
5816 tick_cost_units: 1000,
5817 cells: vec![CellLoadSample {
5818 cell: hot_cell,
5819 owned_entities: 100,
5820 subscribers: 100,
5821 event_pressure: 10,
5822 ..CellLoadSample::default()
5823 }],
5824 ..StationLoadSample::default()
5825 },
5826 StationLoadSample {
5827 station_id: StationId::new(2),
5828 owned_entities: 1,
5829 cells: vec![CellLoadSample {
5830 cell: CellCoord3::new(10, 0, 0),
5831 owned_entities: 1,
5832 ..CellLoadSample::default()
5833 }],
5834 ..StationLoadSample::default()
5835 },
5836 ];
5837 let scheduler = SplitScheduler::new(SplitSchedulerConfig {
5838 thresholds: HotspotThresholds {
5839 max_station_entities: 10,
5840 max_station_subscribers: 10,
5841 max_cell_pressure: 10,
5842 ..HotspotThresholds::default()
5843 },
5844 max_actions_per_pass: 1,
5845 max_cells_per_action: 1,
5846 ghost_ttl_ticks: 4,
5847 ..SplitSchedulerConfig::default()
5848 });
5849 let schedule = scheduler.plan(&samples);
5850 assert_eq!(schedule.actions.len(), 1);
5851 assert_eq!(schedule.actions[0].target_station, StationId::new(2));
5852
5853 let mut ownership = CellOwnershipTable::default();
5854 ownership.assign(hot_cell, StationId::new(1));
5855 let report = scheduler
5856 .execute(&schedule, &mut stations, &mut indexes, &mut ownership)
5857 .expect("execute should work");
5858
5859 assert_eq!(ownership.owner_of(hot_cell), Some(StationId::new(2)));
5860 assert_eq!(report.cell_migrations.len(), 1);
5861 assert_eq!(report.cell_migrations[0].entity_migrations.len(), 1);
5862 assert_eq!(
5863 indexes
5864 .get(StationId::new(2))
5865 .expect("target index")
5866 .entity_count(),
5867 1
5868 );
5869 }
5870
5871 #[test]
5872 fn split_scheduler_respects_source_cooldown() {
5873 let hot_cell = CellCoord3::new(0, 0, 0);
5874 let samples = split_test_samples(hot_cell);
5875 let scheduler = SplitScheduler::new(SplitSchedulerConfig {
5876 thresholds: split_test_thresholds(),
5877 max_actions_per_pass: 1,
5878 max_cells_per_action: 1,
5879 split_cooldown_ticks: 10,
5880 ..SplitSchedulerConfig::default()
5881 });
5882 let mut state = SplitSchedulerState::default();
5883
5884 let initial = scheduler.plan_with_state(&samples, Some(&state), Tick::new(5));
5885 assert_eq!(initial.actions.len(), 1);
5886 state.record_schedule(&initial, Tick::new(5));
5887
5888 let cooled_down = scheduler.plan_with_state(&samples, Some(&state), Tick::new(8));
5889 assert!(cooled_down.actions.is_empty());
5890 assert_eq!(cooled_down.skipped_cooldown, 1);
5891
5892 let after_cooldown = scheduler.plan_with_state(&samples, Some(&state), Tick::new(16));
5893 assert_eq!(after_cooldown.actions.len(), 1);
5894 }
5895
5896 #[test]
5897 fn split_scheduler_reports_capacity_and_improvement_skips() {
5898 let hot_cell = CellCoord3::new(0, 0, 0);
5899 let samples = split_test_samples(hot_cell);
5900
5901 let capacity_guard = SplitScheduler::new(SplitSchedulerConfig {
5902 thresholds: split_test_thresholds(),
5903 max_actions_per_pass: 1,
5904 max_cells_per_action: 1,
5905 max_target_score_after_move: 1,
5906 ..SplitSchedulerConfig::default()
5907 });
5908 let capacity_schedule = capacity_guard.plan(&samples);
5909 assert!(capacity_schedule.actions.is_empty());
5910 assert_eq!(capacity_schedule.skipped_target_capacity, 1);
5911
5912 let improvement_guard = SplitScheduler::new(SplitSchedulerConfig {
5913 thresholds: split_test_thresholds(),
5914 max_actions_per_pass: 1,
5915 max_cells_per_action: 1,
5916 min_score_improvement: u64::MAX,
5917 ..SplitSchedulerConfig::default()
5918 });
5919 let improvement_schedule = improvement_guard.plan(&samples);
5920 assert!(improvement_schedule.actions.is_empty());
5921 assert_eq!(improvement_schedule.skipped_insufficient_improvement, 1);
5922 }
5923
5924 fn split_test_thresholds() -> HotspotThresholds {
5925 HotspotThresholds {
5926 max_station_entities: 10,
5927 max_station_subscribers: 10,
5928 max_cell_pressure: 10,
5929 ..HotspotThresholds::default()
5930 }
5931 }
5932
5933 fn split_test_samples(hot_cell: CellCoord3) -> Vec<StationLoadSample> {
5934 vec![
5935 StationLoadSample {
5936 station_id: StationId::new(1),
5937 owned_entities: 100,
5938 subscribers: 100,
5939 tick_cost_units: 1000,
5940 cells: vec![CellLoadSample {
5941 cell: hot_cell,
5942 owned_entities: 100,
5943 subscribers: 100,
5944 event_pressure: 10,
5945 ..CellLoadSample::default()
5946 }],
5947 ..StationLoadSample::default()
5948 },
5949 StationLoadSample {
5950 station_id: StationId::new(2),
5951 owned_entities: 1,
5952 cells: vec![CellLoadSample {
5953 cell: CellCoord3::new(10, 0, 0),
5954 owned_entities: 1,
5955 ..CellLoadSample::default()
5956 }],
5957 ..StationLoadSample::default()
5958 },
5959 ]
5960 }
5961}