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