1#![forbid(unsafe_code)]
4
5pub mod deployment;
6#[cfg(feature = "parallel")]
7mod parallel;
8
9use std::collections::{BTreeMap, HashMap, HashSet};
10
11use sectorsync_core::prelude::{
12 BarrierId, BarrierScope, BarrierState, CellCoord3, CellIndex, CellLoadSample, CellOccupancy,
13 ClientId, CommandEnvelope, CommandId, CommandIngress, CommandQueueError, CommandQueueMode,
14 CommandQueues, ComponentStore, EntityHandle, EntityId, EventQueueError, EventQueueLimits,
15 EventQueues, GatewayError, GatewaySessionTable, HandoffTransfer, HotspotDecision,
16 HotspotPlanner, HotspotSeverity, HotspotSplitScratch, HotspotThresholds, NodeId, OwnerEpoch,
17 PushOutcome, ReplicationBudget, ReplicationPlan, RuntimeBarrier, RuntimeUpgradeHook,
18 SnapshotVersion, SplitProposal, Station, StationError, StationEvent, StationId,
19 StationLoadSample, StationSnapshot, Tick,
20};
21use sectorsync_transport::{
22 InboundPacket, OutboundPacket, StationOutboundPacket, StationTransportReceiver,
23 StationTransportSink, TransportReceiver, TransportSink,
24};
25use sectorsync_wire::{
26 BarrierFrame, BinaryDecodeError, BinaryEncodeError, BinaryFrameDecoder, BinaryFrameEncoder,
27 CommandAckFrame, CommandDispatchFrame, CommandFrame, ComponentSelection, FrameDecoder,
28 FrameEncoder, ReplicationFrame, ReplicationFrameBuildStats, ReplicationFrameBuilder,
29 ReplicationFrameRef, ReplicationFrameRefDecodeError, 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 ParallelReplicationScratch, ParallelReplicationView, ReplicationThreadPool,
40 ReplicationThreadPoolBuildError, ReplicationThreadPoolConfig, StationReplicationBatch,
41 StationReplicationBatchSource,
42};
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub struct ReplicationTransportConfig {
47 pub budget: ReplicationBudget,
49 pub send_empty_frames: bool,
51}
52
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub struct ReplicationTransportStats {
56 pub viewers_planned: usize,
58 pub frames_skipped_empty: usize,
60 pub frames_sent: usize,
62 pub bytes_sent: usize,
64 pub packet_capacity_hint_bytes: usize,
66 pub entities_selected: usize,
68 pub entities_skipped_by_budget: usize,
70 pub entities_skipped_by_cadence: usize,
72 pub entities_encoded: usize,
74 pub components_encoded: usize,
76 pub entities_skipped_by_frame_bytes: usize,
78}
79
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
82pub struct ReplicationTransportReport {
83 pub client_id: ClientId,
85 pub selected_entities: usize,
87 pub encoded_entities: usize,
89 pub encoded_components: usize,
91 pub estimated_plan_bytes: usize,
93 pub skipped_by_budget: usize,
95 pub skipped_by_cadence: usize,
97 pub skipped_by_frame_bytes: usize,
99 pub bytes_sent: usize,
101 pub sent: bool,
103}
104
105#[derive(Clone, Debug, PartialEq, Eq)]
107pub enum ReplicationTransportError<E> {
108 Encode(BinaryEncodeError),
110 Transport(E),
112}
113
114impl<E: core::fmt::Display> core::fmt::Display for ReplicationTransportError<E> {
115 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116 match self {
117 Self::Encode(error) => write!(f, "{error}"),
118 Self::Transport(error) => write!(f, "{error}"),
119 }
120 }
121}
122
123impl<E> std::error::Error for ReplicationTransportError<E>
124where
125 E: std::error::Error + 'static,
126{
127 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
128 match self {
129 Self::Encode(error) => Some(error),
130 Self::Transport(error) => Some(error),
131 }
132 }
133}
134
135impl<E> From<BinaryEncodeError> for ReplicationTransportError<E> {
136 fn from(value: BinaryEncodeError) -> Self {
137 Self::Encode(value)
138 }
139}
140
141#[derive(Clone, Debug)]
143pub struct ReplicationTransportBridge {
144 config: ReplicationTransportConfig,
145 builder: ReplicationFrameBuilder,
146 stats: ReplicationTransportStats,
147}
148
149impl ReplicationTransportBridge {
150 pub const fn new(config: ReplicationTransportConfig, builder: ReplicationFrameBuilder) -> Self {
152 Self {
153 config,
154 builder,
155 stats: ReplicationTransportStats {
156 viewers_planned: 0,
157 frames_skipped_empty: 0,
158 frames_sent: 0,
159 bytes_sent: 0,
160 packet_capacity_hint_bytes: 0,
161 entities_selected: 0,
162 entities_skipped_by_budget: 0,
163 entities_skipped_by_cadence: 0,
164 entities_encoded: 0,
165 components_encoded: 0,
166 entities_skipped_by_frame_bytes: 0,
167 },
168 }
169 }
170
171 pub const fn config(&self) -> ReplicationTransportConfig {
173 self.config
174 }
175
176 pub const fn builder(&self) -> ReplicationFrameBuilder {
178 self.builder
179 }
180
181 pub const fn stats(&self) -> ReplicationTransportStats {
183 self.stats
184 }
185
186 #[allow(clippy::too_many_arguments)]
188 pub fn send_plan<T>(
189 &mut self,
190 transport: &mut T,
191 client_id: ClientId,
192 server_tick: Tick,
193 station: &Station,
194 components: &ComponentStore,
195 selection: &ComponentSelection,
196 plan: &ReplicationPlan,
197 ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
198 where
199 T: TransportSink,
200 {
201 self.stats.viewers_planned = self.stats.viewers_planned.saturating_add(1);
202 self.stats.entities_selected = self
203 .stats
204 .entities_selected
205 .saturating_add(plan.stats.selected);
206 self.stats.entities_skipped_by_budget = self
207 .stats
208 .entities_skipped_by_budget
209 .saturating_add(plan.stats.skipped_by_budget);
210 self.stats.entities_skipped_by_cadence = self
211 .stats
212 .entities_skipped_by_cadence
213 .saturating_add(plan.stats.skipped_by_cadence);
214
215 let capacity_hint = self
216 .builder
217 .sampled_binary_capacity_hint(station, plan, components, selection)
218 .min(self.config.budget.max_bytes);
219 let mut bytes = Vec::with_capacity(capacity_hint);
220 self.stats.packet_capacity_hint_bytes = self
221 .stats
222 .packet_capacity_hint_bytes
223 .saturating_add(capacity_hint);
224 let build_stats = self.builder.encode_binary_bounded_into(
225 client_id,
226 server_tick,
227 station,
228 plan,
229 components,
230 selection,
231 self.config.budget.max_bytes,
232 &mut bytes,
233 )?;
234 self.stats.entities_encoded = self
235 .stats
236 .entities_encoded
237 .saturating_add(build_stats.encoded_entities);
238 self.stats.components_encoded = self
239 .stats
240 .components_encoded
241 .saturating_add(build_stats.encoded_components);
242 self.stats.entities_skipped_by_frame_bytes = self
243 .stats
244 .entities_skipped_by_frame_bytes
245 .saturating_add(build_stats.skipped_entities_by_frame_bytes);
246
247 if build_stats.encoded_entities == 0 && !self.config.send_empty_frames {
248 self.stats.frames_skipped_empty = self.stats.frames_skipped_empty.saturating_add(1);
249 return Ok(replication_report(client_id, plan, build_stats, 0, false));
250 }
251
252 let byte_len = bytes.len();
253 transport
254 .send(OutboundPacket { client_id, bytes })
255 .map_err(ReplicationTransportError::Transport)?;
256 self.stats.frames_sent = self.stats.frames_sent.saturating_add(1);
257 self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(byte_len);
258
259 Ok(replication_report(
260 client_id,
261 plan,
262 build_stats,
263 byte_len,
264 true,
265 ))
266 }
267}
268
269impl Default for ReplicationTransportBridge {
270 fn default() -> Self {
271 Self::new(
272 ReplicationTransportConfig::default(),
273 ReplicationFrameBuilder::default(),
274 )
275 }
276}
277
278fn replication_report(
279 client_id: ClientId,
280 plan: &ReplicationPlan,
281 build_stats: ReplicationFrameBuildStats,
282 bytes_sent: usize,
283 sent: bool,
284) -> ReplicationTransportReport {
285 ReplicationTransportReport {
286 client_id,
287 selected_entities: plan.stats.selected,
288 encoded_entities: build_stats.encoded_entities,
289 encoded_components: build_stats.encoded_components,
290 estimated_plan_bytes: plan.stats.estimated_bytes,
291 skipped_by_budget: plan.stats.skipped_by_budget,
292 skipped_by_cadence: plan.stats.skipped_by_cadence,
293 skipped_by_frame_bytes: build_stats.skipped_entities_by_frame_bytes,
294 bytes_sent,
295 sent,
296 }
297}
298
299#[derive(Clone, Copy, Debug, PartialEq, Eq)]
301pub struct ReplicationReceiveConfig {
302 pub client_id: ClientId,
304 pub expected_source: Option<ClientId>,
306}
307
308impl ReplicationReceiveConfig {
309 pub const fn new(client_id: ClientId) -> Self {
311 Self {
312 client_id,
313 expected_source: None,
314 }
315 }
316
317 #[must_use]
319 pub const fn with_expected_source(mut self, source: ClientId) -> Self {
320 self.expected_source = Some(source);
321 self
322 }
323}
324
325#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
327pub struct ReplicationReceiveStats {
328 pub packets_received: usize,
330 pub bytes_received: usize,
332 pub frames_received: usize,
334 pub frames_rejected_decode: usize,
336 pub frames_rejected_unexpected: usize,
338 pub frames_rejected_source: usize,
340 pub frames_rejected_target: usize,
342 pub entities_received: usize,
344 pub components_received: usize,
346}
347
348#[derive(Clone, Debug, Default, PartialEq, Eq)]
350pub struct ReplicationReceivePump {
351 pub packets_received: usize,
353 pub bytes_received: usize,
355 pub frames: Vec<ReplicationFrame>,
357}
358
359impl ReplicationReceivePump {
360 pub fn frames_received(&self) -> usize {
362 self.frames.len()
363 }
364
365 pub fn entities_received(&self) -> usize {
367 self.frames.iter().map(|frame| frame.entities.len()).sum()
368 }
369
370 pub fn components_received(&self) -> usize {
372 self.frames
373 .iter()
374 .flat_map(|frame| &frame.entities)
375 .map(|entity| entity.components.len())
376 .sum()
377 }
378}
379
380#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
382pub struct ReplicationReceiveVisitReport {
383 pub packets_received: usize,
385 pub bytes_received: usize,
387 pub frames_received: usize,
389 pub entities_received: usize,
391 pub components_received: usize,
393}
394
395#[derive(Clone, Debug, PartialEq, Eq)]
397pub enum ReplicationReceiveError<E> {
398 Transport(E),
400 Decode(BinaryDecodeError),
402 UnexpectedFrame,
404 SourceMismatch {
406 expected: ClientId,
408 actual: Option<ClientId>,
410 },
411 TargetMismatch {
413 expected: ClientId,
415 actual: ClientId,
417 },
418}
419
420impl<E: core::fmt::Display> core::fmt::Display for ReplicationReceiveError<E> {
421 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
422 match self {
423 Self::Transport(error) => write!(f, "{error}"),
424 Self::Decode(error) => write!(f, "{error}"),
425 Self::UnexpectedFrame => f.write_str("client packet was not a replication frame"),
426 Self::SourceMismatch { expected, actual } => write!(
427 f,
428 "replication source mismatch: expected {}, actual {:?}",
429 expected.get(),
430 actual.map(ClientId::get)
431 ),
432 Self::TargetMismatch { expected, actual } => write!(
433 f,
434 "replication target mismatch: expected {}, actual {}",
435 expected.get(),
436 actual.get()
437 ),
438 }
439 }
440}
441
442impl<E> std::error::Error for ReplicationReceiveError<E>
443where
444 E: std::error::Error + 'static,
445{
446 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
447 match self {
448 Self::Transport(error) => Some(error),
449 Self::Decode(error) => Some(error),
450 Self::UnexpectedFrame | Self::SourceMismatch { .. } | Self::TargetMismatch { .. } => {
451 None
452 }
453 }
454 }
455}
456
457impl<E> From<BinaryDecodeError> for ReplicationReceiveError<E> {
458 fn from(value: BinaryDecodeError) -> Self {
459 Self::Decode(value)
460 }
461}
462
463#[derive(Clone, Debug, PartialEq, Eq)]
465pub enum ReplicationReceiveVisitError<T, V> {
466 Receive(ReplicationReceiveError<T>),
468 Visitor(V),
470}
471
472impl<T: core::fmt::Display, V: core::fmt::Display> core::fmt::Display
473 for ReplicationReceiveVisitError<T, V>
474{
475 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
476 match self {
477 Self::Receive(error) => error.fmt(f),
478 Self::Visitor(error) => write!(f, "replication frame visitor failed: {error}"),
479 }
480 }
481}
482
483impl<T, V> std::error::Error for ReplicationReceiveVisitError<T, V>
484where
485 T: std::error::Error + 'static,
486 V: std::error::Error + 'static,
487{
488 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
489 match self {
490 Self::Receive(error) => Some(error),
491 Self::Visitor(error) => Some(error),
492 }
493 }
494}
495
496#[derive(Clone, Debug)]
498pub struct ReplicationReceiveBridge {
499 config: ReplicationReceiveConfig,
500 stats: ReplicationReceiveStats,
501}
502
503impl ReplicationReceiveBridge {
504 pub const fn new(config: ReplicationReceiveConfig) -> Self {
506 Self {
507 config,
508 stats: ReplicationReceiveStats {
509 packets_received: 0,
510 bytes_received: 0,
511 frames_received: 0,
512 frames_rejected_decode: 0,
513 frames_rejected_unexpected: 0,
514 frames_rejected_source: 0,
515 frames_rejected_target: 0,
516 entities_received: 0,
517 components_received: 0,
518 },
519 }
520 }
521
522 pub const fn config(&self) -> ReplicationReceiveConfig {
524 self.config
525 }
526
527 pub const fn stats(&self) -> ReplicationReceiveStats {
529 self.stats
530 }
531
532 pub fn pump_owned<T>(
534 &mut self,
535 transport: &mut T,
536 max_packets: usize,
537 ) -> Result<ReplicationReceivePump, ReplicationReceiveError<T::Error>>
538 where
539 T: TransportReceiver,
540 {
541 let mut pump = ReplicationReceivePump::default();
542 for _ in 0..max_packets {
543 let Some(packet) = transport
544 .try_recv()
545 .map_err(ReplicationReceiveError::Transport)?
546 else {
547 break;
548 };
549 self.stats.packets_received = self.stats.packets_received.saturating_add(1);
550 self.stats.bytes_received =
551 self.stats.bytes_received.saturating_add(packet.bytes.len());
552 pump.packets_received = pump.packets_received.saturating_add(1);
553 pump.bytes_received = pump.bytes_received.saturating_add(packet.bytes.len());
554
555 if let Some(expected) = self.config.expected_source
556 && packet.client_id != Some(expected)
557 {
558 self.stats.frames_rejected_source =
559 self.stats.frames_rejected_source.saturating_add(1);
560 return Err(ReplicationReceiveError::SourceMismatch {
561 expected,
562 actual: packet.client_id,
563 });
564 }
565
566 let decoded = match BinaryFrameDecoder.decode(&packet.bytes) {
567 Ok(decoded) => decoded,
568 Err(error) => {
569 self.stats.frames_rejected_decode =
570 self.stats.frames_rejected_decode.saturating_add(1);
571 return Err(ReplicationReceiveError::Decode(error));
572 }
573 };
574 let RuntimeFrame::Replication(frame) = decoded else {
575 self.stats.frames_rejected_unexpected =
576 self.stats.frames_rejected_unexpected.saturating_add(1);
577 return Err(ReplicationReceiveError::UnexpectedFrame);
578 };
579 if frame.client_id != self.config.client_id {
580 self.stats.frames_rejected_target =
581 self.stats.frames_rejected_target.saturating_add(1);
582 return Err(ReplicationReceiveError::TargetMismatch {
583 expected: self.config.client_id,
584 actual: frame.client_id,
585 });
586 }
587
588 self.stats.frames_received = self.stats.frames_received.saturating_add(1);
589 self.stats.entities_received = self
590 .stats
591 .entities_received
592 .saturating_add(frame.entities.len());
593 let components = frame
594 .entities
595 .iter()
596 .map(|entity| entity.components.len())
597 .sum::<usize>();
598 self.stats.components_received =
599 self.stats.components_received.saturating_add(components);
600 pump.frames.push(frame);
601 }
602 Ok(pump)
603 }
604
605 pub fn pump<T, F, V>(
614 &mut self,
615 transport: &mut T,
616 max_packets: usize,
617 mut visitor: F,
618 ) -> Result<ReplicationReceiveVisitReport, ReplicationReceiveVisitError<T::Error, V>>
619 where
620 T: TransportReceiver,
621 F: for<'frame> FnMut(ReplicationFrameRef<'frame>) -> Result<(), V>,
622 {
623 let mut report = ReplicationReceiveVisitReport::default();
624 for _ in 0..max_packets {
625 let Some(packet) = transport.try_recv().map_err(|error| {
626 ReplicationReceiveVisitError::Receive(ReplicationReceiveError::Transport(error))
627 })?
628 else {
629 break;
630 };
631 self.stats.packets_received = self.stats.packets_received.saturating_add(1);
632 self.stats.bytes_received =
633 self.stats.bytes_received.saturating_add(packet.bytes.len());
634 report.packets_received = report.packets_received.saturating_add(1);
635 report.bytes_received = report.bytes_received.saturating_add(packet.bytes.len());
636
637 if let Some(expected) = self.config.expected_source
638 && packet.client_id != Some(expected)
639 {
640 self.stats.frames_rejected_source =
641 self.stats.frames_rejected_source.saturating_add(1);
642 return Err(ReplicationReceiveVisitError::Receive(
643 ReplicationReceiveError::SourceMismatch {
644 expected,
645 actual: packet.client_id,
646 },
647 ));
648 }
649
650 let frame = match BinaryFrameDecoder.decode_replication(&packet.bytes) {
651 Ok(frame) => frame,
652 Err(ReplicationFrameRefDecodeError::Binary(error)) => {
653 self.stats.frames_rejected_decode =
654 self.stats.frames_rejected_decode.saturating_add(1);
655 return Err(ReplicationReceiveVisitError::Receive(
656 ReplicationReceiveError::Decode(error),
657 ));
658 }
659 Err(ReplicationFrameRefDecodeError::UnexpectedFrameKind(_)) => {
660 self.stats.frames_rejected_unexpected =
661 self.stats.frames_rejected_unexpected.saturating_add(1);
662 return Err(ReplicationReceiveVisitError::Receive(
663 ReplicationReceiveError::UnexpectedFrame,
664 ));
665 }
666 };
667 if frame.client_id != self.config.client_id {
668 self.stats.frames_rejected_target =
669 self.stats.frames_rejected_target.saturating_add(1);
670 return Err(ReplicationReceiveVisitError::Receive(
671 ReplicationReceiveError::TargetMismatch {
672 expected: self.config.client_id,
673 actual: frame.client_id,
674 },
675 ));
676 }
677
678 let entities = frame.encoded_entity_count();
679 let components = frame
680 .entities()
681 .map(sectorsync_wire::EntityDeltaRef::encoded_component_count)
682 .sum::<usize>();
683 self.stats.frames_received = self.stats.frames_received.saturating_add(1);
684 self.stats.entities_received = self.stats.entities_received.saturating_add(entities);
685 self.stats.components_received =
686 self.stats.components_received.saturating_add(components);
687 report.frames_received = report.frames_received.saturating_add(1);
688 report.entities_received = report.entities_received.saturating_add(entities);
689 report.components_received = report.components_received.saturating_add(components);
690 visitor(frame).map_err(ReplicationReceiveVisitError::Visitor)?;
691 }
692 Ok(report)
693 }
694}
695
696#[derive(Clone, Copy, Debug, PartialEq, Eq)]
698pub struct ClientTransportConfig {
699 pub client_id: ClientId,
701 pub server_id: ClientId,
703 pub expected_source: Option<ClientId>,
705}
706
707impl ClientTransportConfig {
708 pub const fn new(client_id: ClientId, server_id: ClientId) -> Self {
710 Self {
711 client_id,
712 server_id,
713 expected_source: None,
714 }
715 }
716
717 #[must_use]
719 pub const fn with_expected_source(mut self, source: ClientId) -> Self {
720 self.expected_source = Some(source);
721 self
722 }
723}
724
725#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
727pub struct ClientTransportStats {
728 pub commands_sent: usize,
730 pub command_bytes_sent: usize,
732 pub packets_received: usize,
734 pub bytes_received: usize,
736 pub command_acks_received: usize,
738 pub replication_frames_received: usize,
740 pub barrier_frames_received: usize,
742 pub frames_rejected_decode: usize,
744 pub frames_rejected_unexpected: usize,
746 pub frames_rejected_source: usize,
748 pub frames_rejected_target: usize,
750 pub entities_received: usize,
752 pub components_received: usize,
754}
755
756#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
758pub struct ClientCommandSendReport {
759 pub command_id: CommandId,
761 pub bytes_sent: usize,
763}
764
765#[derive(Clone, Copy, Debug, PartialEq, Eq)]
767pub enum ClientInboundFrameKind {
768 CommandAck,
770 Replication,
772 Barrier,
774}
775
776#[derive(Clone, Debug, Default, PartialEq, Eq)]
778pub struct ClientTransportPump {
779 pub packets_received: usize,
781 pub bytes_received: usize,
783 pub command_acks: Vec<CommandAckFrame>,
785 pub replication_frames: Vec<ReplicationFrame>,
787 pub barriers: Vec<BarrierFrame>,
789}
790
791impl ClientTransportPump {
792 pub fn command_acks_received(&self) -> usize {
794 self.command_acks.len()
795 }
796
797 pub fn replication_frames_received(&self) -> usize {
799 self.replication_frames.len()
800 }
801
802 pub fn barrier_frames_received(&self) -> usize {
804 self.barriers.len()
805 }
806
807 pub fn entities_received(&self) -> usize {
809 self.replication_frames
810 .iter()
811 .map(|frame| frame.entities.len())
812 .sum()
813 }
814
815 pub fn components_received(&self) -> usize {
817 self.replication_frames
818 .iter()
819 .flat_map(|frame| &frame.entities)
820 .map(|entity| entity.components.len())
821 .sum()
822 }
823}
824
825#[derive(Clone, Debug)]
827pub enum ClientInboundFrameRef<'a> {
828 CommandAck(CommandAckFrame),
830 Replication(ReplicationFrameRef<'a>),
832 Barrier(BarrierFrame),
834}
835
836#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
838pub struct ClientTransportVisitReport {
839 pub packets_received: usize,
841 pub bytes_received: usize,
843 pub command_acks_received: usize,
845 pub replication_frames_received: usize,
847 pub barrier_frames_received: usize,
849 pub entities_received: usize,
851 pub components_received: usize,
853}
854
855#[derive(Clone, Debug, PartialEq, Eq)]
857pub enum ClientTransportVisitError<T, V> {
858 Receive(ClientTransportBridgeError<T>),
860 Visitor(V),
862}
863
864impl<T: core::fmt::Display, V: core::fmt::Display> core::fmt::Display
865 for ClientTransportVisitError<T, V>
866{
867 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
868 match self {
869 Self::Receive(error) => error.fmt(formatter),
870 Self::Visitor(error) => write!(formatter, "client frame visitor failed: {error}"),
871 }
872 }
873}
874
875impl<T, V> std::error::Error for ClientTransportVisitError<T, V>
876where
877 T: std::error::Error + 'static,
878 V: std::error::Error + 'static,
879{
880 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
881 match self {
882 Self::Receive(error) => Some(error),
883 Self::Visitor(error) => Some(error),
884 }
885 }
886}
887
888#[derive(Clone, Debug, PartialEq, Eq)]
890pub enum ClientTransportBridgeError<E> {
891 CommandClientMismatch {
893 expected: ClientId,
895 actual: ClientId,
897 },
898 Encode(BinaryEncodeError),
900 Transport(E),
902 Decode(BinaryDecodeError),
904 UnexpectedFrame,
906 SourceMismatch {
908 expected: ClientId,
910 actual: Option<ClientId>,
912 },
913 TargetMismatch {
915 kind: ClientInboundFrameKind,
917 expected: ClientId,
919 actual: ClientId,
921 },
922}
923
924impl<E: core::fmt::Display> core::fmt::Display for ClientTransportBridgeError<E> {
925 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
926 match self {
927 Self::CommandClientMismatch { expected, actual } => write!(
928 f,
929 "command client mismatch: expected {}, actual {}",
930 expected.get(),
931 actual.get()
932 ),
933 Self::Encode(error) => write!(f, "{error}"),
934 Self::Transport(error) => write!(f, "{error}"),
935 Self::Decode(error) => write!(f, "{error}"),
936 Self::UnexpectedFrame => f.write_str("packet was not a client-bound frame"),
937 Self::SourceMismatch { expected, actual } => write!(
938 f,
939 "client packet source mismatch: expected {}, actual {:?}",
940 expected.get(),
941 actual.map(ClientId::get)
942 ),
943 Self::TargetMismatch {
944 kind,
945 expected,
946 actual,
947 } => write!(
948 f,
949 "client {:?} frame target mismatch: expected {}, actual {}",
950 kind,
951 expected.get(),
952 actual.get()
953 ),
954 }
955 }
956}
957
958impl<E> std::error::Error for ClientTransportBridgeError<E>
959where
960 E: std::error::Error + 'static,
961{
962 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
963 match self {
964 Self::Encode(error) => Some(error),
965 Self::Transport(error) => Some(error),
966 Self::Decode(error) => Some(error),
967 Self::CommandClientMismatch { .. }
968 | Self::UnexpectedFrame
969 | Self::SourceMismatch { .. }
970 | Self::TargetMismatch { .. } => None,
971 }
972 }
973}
974
975impl<E> From<BinaryEncodeError> for ClientTransportBridgeError<E> {
976 fn from(value: BinaryEncodeError) -> Self {
977 Self::Encode(value)
978 }
979}
980
981impl<E> From<BinaryDecodeError> for ClientTransportBridgeError<E> {
982 fn from(value: BinaryDecodeError) -> Self {
983 Self::Decode(value)
984 }
985}
986
987#[derive(Clone, Debug)]
989pub struct ClientTransportBridge {
990 config: ClientTransportConfig,
991 stats: ClientTransportStats,
992}
993
994impl ClientTransportBridge {
995 pub const fn new(config: ClientTransportConfig) -> Self {
997 Self {
998 config,
999 stats: ClientTransportStats {
1000 commands_sent: 0,
1001 command_bytes_sent: 0,
1002 packets_received: 0,
1003 bytes_received: 0,
1004 command_acks_received: 0,
1005 replication_frames_received: 0,
1006 barrier_frames_received: 0,
1007 frames_rejected_decode: 0,
1008 frames_rejected_unexpected: 0,
1009 frames_rejected_source: 0,
1010 frames_rejected_target: 0,
1011 entities_received: 0,
1012 components_received: 0,
1013 },
1014 }
1015 }
1016
1017 pub const fn config(&self) -> ClientTransportConfig {
1019 self.config
1020 }
1021
1022 pub const fn stats(&self) -> ClientTransportStats {
1024 self.stats
1025 }
1026
1027 pub fn send_command_frame<T>(
1029 &mut self,
1030 transport: &mut T,
1031 frame: &CommandFrame,
1032 ) -> Result<ClientCommandSendReport, ClientTransportBridgeError<T::Error>>
1033 where
1034 T: TransportSink,
1035 {
1036 if frame.client_id != self.config.client_id {
1037 return Err(ClientTransportBridgeError::CommandClientMismatch {
1038 expected: self.config.client_id,
1039 actual: frame.client_id,
1040 });
1041 }
1042
1043 let mut bytes = Vec::new();
1044 BinaryFrameEncoder.encode_command(frame, &mut bytes)?;
1045 let bytes_sent = bytes.len();
1046 transport
1047 .send(OutboundPacket {
1048 client_id: self.config.server_id,
1049 bytes,
1050 })
1051 .map_err(ClientTransportBridgeError::Transport)?;
1052 self.stats.commands_sent = self.stats.commands_sent.saturating_add(1);
1053 self.stats.command_bytes_sent = self.stats.command_bytes_sent.saturating_add(bytes_sent);
1054
1055 Ok(ClientCommandSendReport {
1056 command_id: frame.command_id,
1057 bytes_sent,
1058 })
1059 }
1060
1061 pub fn pump_owned<T>(
1063 &mut self,
1064 transport: &mut T,
1065 max_packets: usize,
1066 ) -> Result<ClientTransportPump, ClientTransportBridgeError<T::Error>>
1067 where
1068 T: TransportReceiver,
1069 {
1070 let mut pump = ClientTransportPump::default();
1071 for _ in 0..max_packets {
1072 let Some(packet) = transport
1073 .try_recv()
1074 .map_err(ClientTransportBridgeError::Transport)?
1075 else {
1076 break;
1077 };
1078 self.stats.packets_received = self.stats.packets_received.saturating_add(1);
1079 self.stats.bytes_received =
1080 self.stats.bytes_received.saturating_add(packet.bytes.len());
1081 pump.packets_received = pump.packets_received.saturating_add(1);
1082 pump.bytes_received = pump.bytes_received.saturating_add(packet.bytes.len());
1083
1084 if let Some(expected) = self.config.expected_source
1085 && packet.client_id != Some(expected)
1086 {
1087 self.stats.frames_rejected_source =
1088 self.stats.frames_rejected_source.saturating_add(1);
1089 return Err(ClientTransportBridgeError::SourceMismatch {
1090 expected,
1091 actual: packet.client_id,
1092 });
1093 }
1094
1095 let decoded = match BinaryFrameDecoder.decode(&packet.bytes) {
1096 Ok(decoded) => decoded,
1097 Err(error) => {
1098 self.stats.frames_rejected_decode =
1099 self.stats.frames_rejected_decode.saturating_add(1);
1100 return Err(ClientTransportBridgeError::Decode(error));
1101 }
1102 };
1103 match decoded {
1104 RuntimeFrame::CommandAck(frame) => {
1105 self.validate_client_target(
1106 ClientInboundFrameKind::CommandAck,
1107 frame.client_id,
1108 )?;
1109 self.stats.command_acks_received =
1110 self.stats.command_acks_received.saturating_add(1);
1111 pump.command_acks.push(frame);
1112 }
1113 RuntimeFrame::Replication(frame) => {
1114 self.validate_client_target(
1115 ClientInboundFrameKind::Replication,
1116 frame.client_id,
1117 )?;
1118 self.stats.replication_frames_received =
1119 self.stats.replication_frames_received.saturating_add(1);
1120 self.stats.entities_received = self
1121 .stats
1122 .entities_received
1123 .saturating_add(frame.entities.len());
1124 let components = frame
1125 .entities
1126 .iter()
1127 .map(|entity| entity.components.len())
1128 .sum::<usize>();
1129 self.stats.components_received =
1130 self.stats.components_received.saturating_add(components);
1131 pump.replication_frames.push(frame);
1132 }
1133 RuntimeFrame::Barrier(frame) => {
1134 self.validate_client_target(ClientInboundFrameKind::Barrier, frame.client_id)?;
1135 self.stats.barrier_frames_received =
1136 self.stats.barrier_frames_received.saturating_add(1);
1137 pump.barriers.push(frame);
1138 }
1139 RuntimeFrame::Command(_)
1140 | RuntimeFrame::CommandDispatch(_)
1141 | RuntimeFrame::StationEvent(_) => {
1142 self.stats.frames_rejected_unexpected =
1143 self.stats.frames_rejected_unexpected.saturating_add(1);
1144 return Err(ClientTransportBridgeError::UnexpectedFrame);
1145 }
1146 }
1147 }
1148 Ok(pump)
1149 }
1150
1151 #[allow(clippy::too_many_lines)]
1159 pub fn pump<T, F, V>(
1160 &mut self,
1161 transport: &mut T,
1162 max_packets: usize,
1163 mut visitor: F,
1164 ) -> Result<ClientTransportVisitReport, ClientTransportVisitError<T::Error, V>>
1165 where
1166 T: TransportReceiver,
1167 F: for<'frame> FnMut(ClientInboundFrameRef<'frame>) -> Result<(), V>,
1168 {
1169 let mut report = ClientTransportVisitReport::default();
1170 for _ in 0..max_packets {
1171 let Some(packet) = transport.try_recv().map_err(|error| {
1172 ClientTransportVisitError::Receive(ClientTransportBridgeError::Transport(error))
1173 })?
1174 else {
1175 break;
1176 };
1177 self.stats.packets_received = self.stats.packets_received.saturating_add(1);
1178 self.stats.bytes_received =
1179 self.stats.bytes_received.saturating_add(packet.bytes.len());
1180 report.packets_received = report.packets_received.saturating_add(1);
1181 report.bytes_received = report.bytes_received.saturating_add(packet.bytes.len());
1182
1183 if let Some(expected) = self.config.expected_source
1184 && packet.client_id != Some(expected)
1185 {
1186 self.stats.frames_rejected_source =
1187 self.stats.frames_rejected_source.saturating_add(1);
1188 return Err(ClientTransportVisitError::Receive(
1189 ClientTransportBridgeError::SourceMismatch {
1190 expected,
1191 actual: packet.client_id,
1192 },
1193 ));
1194 }
1195
1196 match BinaryFrameDecoder.decode_replication(&packet.bytes) {
1197 Ok(frame) => {
1198 self.validate_client_target::<T::Error>(
1199 ClientInboundFrameKind::Replication,
1200 frame.client_id,
1201 )
1202 .map_err(ClientTransportVisitError::Receive)?;
1203 let entities = frame.encoded_entity_count();
1204 let components = frame
1205 .entities()
1206 .map(sectorsync_wire::EntityDeltaRef::encoded_component_count)
1207 .sum::<usize>();
1208 self.stats.replication_frames_received =
1209 self.stats.replication_frames_received.saturating_add(1);
1210 self.stats.entities_received =
1211 self.stats.entities_received.saturating_add(entities);
1212 self.stats.components_received =
1213 self.stats.components_received.saturating_add(components);
1214 report.replication_frames_received =
1215 report.replication_frames_received.saturating_add(1);
1216 report.entities_received = report.entities_received.saturating_add(entities);
1217 report.components_received =
1218 report.components_received.saturating_add(components);
1219 visitor(ClientInboundFrameRef::Replication(frame))
1220 .map_err(ClientTransportVisitError::Visitor)?;
1221 }
1222 Err(ReplicationFrameRefDecodeError::Binary(error)) => {
1223 self.stats.frames_rejected_decode =
1224 self.stats.frames_rejected_decode.saturating_add(1);
1225 return Err(ClientTransportVisitError::Receive(
1226 ClientTransportBridgeError::Decode(error),
1227 ));
1228 }
1229 Err(ReplicationFrameRefDecodeError::UnexpectedFrameKind(_)) => {
1230 let decoded = BinaryFrameDecoder.decode(&packet.bytes).map_err(|error| {
1231 self.stats.frames_rejected_decode =
1232 self.stats.frames_rejected_decode.saturating_add(1);
1233 ClientTransportVisitError::Receive(ClientTransportBridgeError::Decode(
1234 error,
1235 ))
1236 })?;
1237 match decoded {
1238 RuntimeFrame::CommandAck(frame) => {
1239 self.validate_client_target::<T::Error>(
1240 ClientInboundFrameKind::CommandAck,
1241 frame.client_id,
1242 )
1243 .map_err(ClientTransportVisitError::Receive)?;
1244 self.stats.command_acks_received =
1245 self.stats.command_acks_received.saturating_add(1);
1246 report.command_acks_received =
1247 report.command_acks_received.saturating_add(1);
1248 visitor(ClientInboundFrameRef::CommandAck(frame))
1249 .map_err(ClientTransportVisitError::Visitor)?;
1250 }
1251 RuntimeFrame::Barrier(frame) => {
1252 self.validate_client_target::<T::Error>(
1253 ClientInboundFrameKind::Barrier,
1254 frame.client_id,
1255 )
1256 .map_err(ClientTransportVisitError::Receive)?;
1257 self.stats.barrier_frames_received =
1258 self.stats.barrier_frames_received.saturating_add(1);
1259 report.barrier_frames_received =
1260 report.barrier_frames_received.saturating_add(1);
1261 visitor(ClientInboundFrameRef::Barrier(frame))
1262 .map_err(ClientTransportVisitError::Visitor)?;
1263 }
1264 RuntimeFrame::Replication(_)
1265 | RuntimeFrame::Command(_)
1266 | RuntimeFrame::CommandDispatch(_)
1267 | RuntimeFrame::StationEvent(_) => {
1268 self.stats.frames_rejected_unexpected =
1269 self.stats.frames_rejected_unexpected.saturating_add(1);
1270 return Err(ClientTransportVisitError::Receive(
1271 ClientTransportBridgeError::UnexpectedFrame,
1272 ));
1273 }
1274 }
1275 }
1276 }
1277 }
1278 Ok(report)
1279 }
1280
1281 fn validate_client_target<E>(
1282 &mut self,
1283 kind: ClientInboundFrameKind,
1284 actual: ClientId,
1285 ) -> Result<(), ClientTransportBridgeError<E>> {
1286 if actual == self.config.client_id {
1287 return Ok(());
1288 }
1289 self.stats.frames_rejected_target = self.stats.frames_rejected_target.saturating_add(1);
1290 Err(ClientTransportBridgeError::TargetMismatch {
1291 kind,
1292 expected: self.config.client_id,
1293 actual,
1294 })
1295 }
1296}
1297
1298#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1300pub struct BarrierTransportStats {
1301 pub notifications_sent: usize,
1303 pub clients_notified: usize,
1305 pub bytes_sent: usize,
1307}
1308
1309#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1311pub struct BarrierTransportReport {
1312 pub barrier_id: BarrierId,
1314 pub state: BarrierState,
1316 pub server_tick: Tick,
1318 pub clients_requested: usize,
1320 pub clients_sent: usize,
1322 pub bytes_sent: usize,
1324}
1325
1326#[derive(Clone, Debug, PartialEq, Eq)]
1328pub enum BarrierTransportError<E> {
1329 Encode(BinaryEncodeError),
1331 Transport(E),
1333}
1334
1335impl<E: core::fmt::Display> core::fmt::Display for BarrierTransportError<E> {
1336 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1337 match self {
1338 Self::Encode(error) => write!(f, "{error}"),
1339 Self::Transport(error) => write!(f, "{error}"),
1340 }
1341 }
1342}
1343
1344impl<E> std::error::Error for BarrierTransportError<E>
1345where
1346 E: std::error::Error + 'static,
1347{
1348 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1349 match self {
1350 Self::Encode(error) => Some(error),
1351 Self::Transport(error) => Some(error),
1352 }
1353 }
1354}
1355
1356impl<E> From<BinaryEncodeError> for BarrierTransportError<E> {
1357 fn from(value: BinaryEncodeError) -> Self {
1358 Self::Encode(value)
1359 }
1360}
1361
1362#[derive(Clone, Debug, Default)]
1364pub struct BarrierTransportBridge {
1365 stats: BarrierTransportStats,
1366}
1367
1368impl BarrierTransportBridge {
1369 pub const fn new() -> Self {
1371 Self {
1372 stats: BarrierTransportStats {
1373 notifications_sent: 0,
1374 clients_notified: 0,
1375 bytes_sent: 0,
1376 },
1377 }
1378 }
1379
1380 pub const fn stats(&self) -> BarrierTransportStats {
1382 self.stats
1383 }
1384
1385 pub fn send_state<T>(
1387 &mut self,
1388 transport: &mut T,
1389 client_id: ClientId,
1390 barrier_id: BarrierId,
1391 server_tick: Tick,
1392 state: BarrierState,
1393 ) -> Result<usize, BarrierTransportError<T::Error>>
1394 where
1395 T: TransportSink,
1396 {
1397 let frame = BarrierFrame {
1398 client_id,
1399 barrier_id,
1400 server_tick,
1401 state,
1402 };
1403 let mut bytes = Vec::new();
1404 BinaryFrameEncoder.encode_barrier(&frame, &mut bytes)?;
1405 let bytes_sent = bytes.len();
1406 transport
1407 .send(OutboundPacket { client_id, bytes })
1408 .map_err(BarrierTransportError::Transport)?;
1409 self.stats.notifications_sent = self.stats.notifications_sent.saturating_add(1);
1410 self.stats.clients_notified = self.stats.clients_notified.saturating_add(1);
1411 self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(bytes_sent);
1412 Ok(bytes_sent)
1413 }
1414
1415 pub fn send_barrier<T>(
1417 &mut self,
1418 transport: &mut T,
1419 client_id: ClientId,
1420 barrier: RuntimeBarrier,
1421 ) -> Result<usize, BarrierTransportError<T::Error>>
1422 where
1423 T: TransportSink,
1424 {
1425 self.send_state(
1426 transport,
1427 client_id,
1428 barrier.id,
1429 barrier.target_tick,
1430 barrier.state,
1431 )
1432 }
1433
1434 pub fn broadcast_state<T, I>(
1436 &mut self,
1437 transport: &mut T,
1438 clients: I,
1439 barrier_id: BarrierId,
1440 server_tick: Tick,
1441 state: BarrierState,
1442 ) -> Result<BarrierTransportReport, BarrierTransportError<T::Error>>
1443 where
1444 T: TransportSink,
1445 I: IntoIterator<Item = ClientId>,
1446 {
1447 let mut report = BarrierTransportReport {
1448 barrier_id,
1449 state,
1450 server_tick,
1451 clients_requested: 0,
1452 clients_sent: 0,
1453 bytes_sent: 0,
1454 };
1455 for client_id in clients {
1456 report.clients_requested = report.clients_requested.saturating_add(1);
1457 let bytes_sent =
1458 self.send_state(transport, client_id, barrier_id, server_tick, state)?;
1459 report.clients_sent = report.clients_sent.saturating_add(1);
1460 report.bytes_sent = report.bytes_sent.saturating_add(bytes_sent);
1461 }
1462 Ok(report)
1463 }
1464
1465 pub fn broadcast_barrier<T, I>(
1467 &mut self,
1468 transport: &mut T,
1469 clients: I,
1470 barrier: RuntimeBarrier,
1471 ) -> Result<BarrierTransportReport, BarrierTransportError<T::Error>>
1472 where
1473 T: TransportSink,
1474 I: IntoIterator<Item = ClientId>,
1475 {
1476 self.broadcast_state(
1477 transport,
1478 clients,
1479 barrier.id,
1480 barrier.target_tick,
1481 barrier.state,
1482 )
1483 }
1484}
1485
1486pub const GATEWAY_COMMAND_ACK_ACCEPTED: u16 = 0;
1488pub const GATEWAY_COMMAND_ACK_GATEWAY_REJECTED: u16 = 1;
1490pub const GATEWAY_COMMAND_ACK_RATE_LIMITED: u16 = 2;
1492pub const GATEWAY_COMMAND_ACK_REPLAY_OR_STALE: u16 = 3;
1494pub const GATEWAY_COMMAND_ACK_QUEUE_FULL: u16 = 4;
1496pub const GATEWAY_COMMAND_ACK_BARRIER_REJECTED: u16 = 5;
1498pub const GATEWAY_COMMAND_ACK_MISSING_QUEUE: u16 = 6;
1500pub const GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED: u16 = 7;
1502
1503#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1505pub struct GatewayCommandPipelineConfig {
1506 pub ack_rejections: bool,
1508}
1509
1510impl Default for GatewayCommandPipelineConfig {
1511 fn default() -> Self {
1512 Self {
1513 ack_rejections: true,
1514 }
1515 }
1516}
1517
1518#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1520pub struct GatewayCommandPipelineStats {
1521 pub command_frames_decoded: usize,
1523 pub frames_rejected_decode: usize,
1525 pub frames_rejected_non_command: usize,
1527 pub commands_admitted: usize,
1529 pub commands_enqueued: usize,
1531 pub commands_rejected_gateway: usize,
1533 pub commands_rejected_queue: usize,
1535 pub commands_routed_deployment: usize,
1537 pub commands_rejected_deployment: usize,
1539 pub acks_encoded: usize,
1541}
1542
1543#[derive(Clone, Debug, PartialEq, Eq)]
1545pub enum GatewayCommandPipelineError {
1546 Decode(BinaryDecodeError),
1548 NonCommandFrame,
1550 Gateway(GatewayError),
1552 MissingQueue(StationId),
1554 Queue(CommandQueueError),
1556 Deployment(DeploymentError),
1558 Encode(BinaryEncodeError),
1560}
1561
1562impl core::fmt::Display for GatewayCommandPipelineError {
1563 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1564 match self {
1565 Self::Decode(error) => write!(f, "{error}"),
1566 Self::NonCommandFrame => f.write_str("gateway command pipeline expected command frame"),
1567 Self::Gateway(error) => write!(f, "{error}"),
1568 Self::MissingQueue(station_id) => write!(
1569 f,
1570 "gateway command route target station {} has no queue",
1571 station_id.get()
1572 ),
1573 Self::Queue(error) => write!(f, "{error}"),
1574 Self::Deployment(error) => write!(f, "{error}"),
1575 Self::Encode(error) => write!(f, "{error}"),
1576 }
1577 }
1578}
1579
1580impl std::error::Error for GatewayCommandPipelineError {
1581 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1582 match self {
1583 Self::Decode(error) => Some(error),
1584 Self::Gateway(error) => Some(error),
1585 Self::Queue(error) => Some(error),
1586 Self::Deployment(error) => Some(error),
1587 Self::Encode(error) => Some(error),
1588 Self::NonCommandFrame | Self::MissingQueue(_) => None,
1589 }
1590 }
1591}
1592
1593#[derive(Clone, Debug, Default, PartialEq, Eq)]
1595pub struct GatewayCommandPipelineReport {
1596 pub client_id: Option<ClientId>,
1598 pub command_id: Option<CommandId>,
1600 pub station_id: Option<StationId>,
1602 pub node_id: Option<NodeId>,
1604 pub delivery: Option<GatewayDeliveryRoute>,
1606 pub command: Option<CommandEnvelope>,
1608 pub accepted: bool,
1610 pub reason_code: u16,
1612 pub ack_bytes: Option<Vec<u8>>,
1614 pub error: Option<GatewayCommandPipelineError>,
1616}
1617
1618#[derive(Clone, Debug)]
1620pub struct GatewayCommandPipeline {
1621 config: GatewayCommandPipelineConfig,
1622 decoder: BinaryFrameDecoder,
1623 encoder: BinaryFrameEncoder,
1624 stats: GatewayCommandPipelineStats,
1625}
1626
1627impl GatewayCommandPipeline {
1628 pub fn new(config: GatewayCommandPipelineConfig) -> Self {
1630 Self {
1631 config,
1632 decoder: BinaryFrameDecoder,
1633 encoder: BinaryFrameEncoder,
1634 stats: GatewayCommandPipelineStats::default(),
1635 }
1636 }
1637
1638 pub const fn config(&self) -> GatewayCommandPipelineConfig {
1640 self.config
1641 }
1642
1643 pub const fn stats(&self) -> GatewayCommandPipelineStats {
1645 self.stats
1646 }
1647
1648 pub fn process(
1651 &mut self,
1652 gateway: &mut GatewaySessionTable,
1653 station_queues: &mut BTreeMap<StationId, CommandQueues>,
1654 input: &[u8],
1655 now: Tick,
1656 ingress: CommandIngress,
1657 ) -> GatewayCommandPipelineReport {
1658 let command_frame = match self.decode_command_frame(input) {
1659 Ok(command_frame) => command_frame,
1660 Err(error) => {
1661 return GatewayCommandPipelineReport {
1662 error: Some(error),
1663 ..GatewayCommandPipelineReport::default()
1664 };
1665 }
1666 };
1667
1668 self.process_command_frame(gateway, station_queues, command_frame, now, ingress)
1669 }
1670
1671 pub fn dispatch(
1674 &mut self,
1675 gateway: &mut GatewaySessionTable,
1676 deployment: &DeploymentRouteTable,
1677 input: &[u8],
1678 now: Tick,
1679 ) -> GatewayCommandPipelineReport {
1680 let command_frame = match self.decode_command_frame(input) {
1681 Ok(command_frame) => command_frame,
1682 Err(error) => {
1683 return GatewayCommandPipelineReport {
1684 error: Some(error),
1685 ..GatewayCommandPipelineReport::default()
1686 };
1687 }
1688 };
1689
1690 self.dispatch_command_frame(gateway, deployment, command_frame, now)
1691 }
1692
1693 fn decode_command_frame(
1694 &mut self,
1695 input: &[u8],
1696 ) -> Result<CommandFrame, GatewayCommandPipelineError> {
1697 let frame = match self.decoder.decode(input) {
1698 Ok(frame) => frame,
1699 Err(error) => {
1700 self.stats.frames_rejected_decode =
1701 self.stats.frames_rejected_decode.saturating_add(1);
1702 return Err(GatewayCommandPipelineError::Decode(error));
1703 }
1704 };
1705
1706 let RuntimeFrame::Command(command_frame) = frame else {
1707 self.stats.frames_rejected_non_command =
1708 self.stats.frames_rejected_non_command.saturating_add(1);
1709 return Err(GatewayCommandPipelineError::NonCommandFrame);
1710 };
1711 self.stats.command_frames_decoded = self.stats.command_frames_decoded.saturating_add(1);
1712
1713 Ok(command_frame)
1714 }
1715
1716 fn process_command_frame(
1717 &mut self,
1718 gateway: &mut GatewaySessionTable,
1719 station_queues: &mut BTreeMap<StationId, CommandQueues>,
1720 command_frame: CommandFrame,
1721 now: Tick,
1722 ingress: CommandIngress,
1723 ) -> GatewayCommandPipelineReport {
1724 let client_id = command_frame.client_id;
1725 let command_id = command_frame.command_id;
1726 let command = command_frame.into_envelope(now);
1727 let admission = match gateway.admit_command(&command) {
1728 Ok(admission) => {
1729 self.stats.commands_admitted = self.stats.commands_admitted.saturating_add(1);
1730 admission
1731 }
1732 Err(error) => {
1733 self.stats.commands_rejected_gateway =
1734 self.stats.commands_rejected_gateway.saturating_add(1);
1735 return self.rejected_report(
1736 client_id,
1737 command_id,
1738 None,
1739 now,
1740 gateway_reject_reason_code(error),
1741 GatewayCommandPipelineError::Gateway(error),
1742 );
1743 }
1744 };
1745
1746 let station_id = admission.route.station_id;
1747 let Some(queue) = station_queues.get_mut(&station_id) else {
1748 self.stats.commands_rejected_queue =
1749 self.stats.commands_rejected_queue.saturating_add(1);
1750 return self.rejected_report(
1751 client_id,
1752 command_id,
1753 Some(station_id),
1754 now,
1755 GATEWAY_COMMAND_ACK_MISSING_QUEUE,
1756 GatewayCommandPipelineError::MissingQueue(station_id),
1757 );
1758 };
1759
1760 if let Err(error) = queue.push(command, ingress) {
1761 self.stats.commands_rejected_queue =
1762 self.stats.commands_rejected_queue.saturating_add(1);
1763 return self.rejected_report(
1764 client_id,
1765 command_id,
1766 Some(station_id),
1767 now,
1768 queue_reject_reason_code(error),
1769 GatewayCommandPipelineError::Queue(error),
1770 );
1771 }
1772
1773 self.stats.commands_enqueued = self.stats.commands_enqueued.saturating_add(1);
1774 self.accepted_report(client_id, command_id, station_id, now)
1775 }
1776
1777 fn dispatch_command_frame(
1778 &mut self,
1779 gateway: &mut GatewaySessionTable,
1780 deployment: &DeploymentRouteTable,
1781 command_frame: CommandFrame,
1782 now: Tick,
1783 ) -> GatewayCommandPipelineReport {
1784 let client_id = command_frame.client_id;
1785 let command_id = command_frame.command_id;
1786 let command = command_frame.into_envelope(now);
1787 let admission = match gateway.admit_command(&command) {
1788 Ok(admission) => {
1789 self.stats.commands_admitted = self.stats.commands_admitted.saturating_add(1);
1790 admission
1791 }
1792 Err(error) => {
1793 self.stats.commands_rejected_gateway =
1794 self.stats.commands_rejected_gateway.saturating_add(1);
1795 return self.rejected_report(
1796 client_id,
1797 command_id,
1798 None,
1799 now,
1800 gateway_reject_reason_code(error),
1801 GatewayCommandPipelineError::Gateway(error),
1802 );
1803 }
1804 };
1805
1806 let delivery = match deployment.resolve_gateway_route(admission.route) {
1807 Ok(delivery) => {
1808 self.stats.commands_routed_deployment =
1809 self.stats.commands_routed_deployment.saturating_add(1);
1810 delivery
1811 }
1812 Err(error) => {
1813 self.stats.commands_rejected_deployment =
1814 self.stats.commands_rejected_deployment.saturating_add(1);
1815 return self.rejected_report(
1816 client_id,
1817 command_id,
1818 Some(admission.route.station_id),
1819 now,
1820 GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED,
1821 GatewayCommandPipelineError::Deployment(error),
1822 );
1823 }
1824 };
1825
1826 self.dispatch_report(command, delivery, now)
1827 }
1828
1829 fn accepted_report(
1830 &mut self,
1831 client_id: ClientId,
1832 command_id: CommandId,
1833 station_id: StationId,
1834 now: Tick,
1835 ) -> GatewayCommandPipelineReport {
1836 let ack = CommandAckFrame {
1837 client_id,
1838 command_id,
1839 server_tick: now,
1840 accepted: true,
1841 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1842 };
1843 match self.encode_ack(&ack) {
1844 Ok(ack_bytes) => GatewayCommandPipelineReport {
1845 client_id: Some(client_id),
1846 command_id: Some(command_id),
1847 station_id: Some(station_id),
1848 node_id: None,
1849 delivery: None,
1850 command: None,
1851 accepted: true,
1852 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1853 ack_bytes: Some(ack_bytes),
1854 error: None,
1855 },
1856 Err(error) => GatewayCommandPipelineReport {
1857 client_id: Some(client_id),
1858 command_id: Some(command_id),
1859 station_id: Some(station_id),
1860 node_id: None,
1861 delivery: None,
1862 command: None,
1863 accepted: false,
1864 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1865 ack_bytes: None,
1866 error: Some(GatewayCommandPipelineError::Encode(error)),
1867 },
1868 }
1869 }
1870
1871 fn dispatch_report(
1872 &mut self,
1873 command: CommandEnvelope,
1874 delivery: GatewayDeliveryRoute,
1875 now: Tick,
1876 ) -> GatewayCommandPipelineReport {
1877 let ack = CommandAckFrame {
1878 client_id: command.client_id,
1879 command_id: command.id,
1880 server_tick: now,
1881 accepted: true,
1882 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1883 };
1884 match self.encode_ack(&ack) {
1885 Ok(ack_bytes) => GatewayCommandPipelineReport {
1886 client_id: Some(command.client_id),
1887 command_id: Some(command.id),
1888 station_id: Some(delivery.station_id),
1889 node_id: Some(delivery.node_id),
1890 delivery: Some(delivery),
1891 command: Some(command),
1892 accepted: true,
1893 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1894 ack_bytes: Some(ack_bytes),
1895 error: None,
1896 },
1897 Err(error) => GatewayCommandPipelineReport {
1898 client_id: Some(command.client_id),
1899 command_id: Some(command.id),
1900 station_id: Some(delivery.station_id),
1901 node_id: Some(delivery.node_id),
1902 delivery: Some(delivery),
1903 command: None,
1904 accepted: false,
1905 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1906 ack_bytes: None,
1907 error: Some(GatewayCommandPipelineError::Encode(error)),
1908 },
1909 }
1910 }
1911
1912 fn rejected_report(
1913 &mut self,
1914 client_id: ClientId,
1915 command_id: CommandId,
1916 station_id: Option<StationId>,
1917 now: Tick,
1918 reason_code: u16,
1919 error: GatewayCommandPipelineError,
1920 ) -> GatewayCommandPipelineReport {
1921 let ack_bytes = if self.config.ack_rejections {
1922 let ack = CommandAckFrame {
1923 client_id,
1924 command_id,
1925 server_tick: now,
1926 accepted: false,
1927 reason_code,
1928 };
1929 match self.encode_ack(&ack) {
1930 Ok(bytes) => Some(bytes),
1931 Err(encode_error) => {
1932 return GatewayCommandPipelineReport {
1933 client_id: Some(client_id),
1934 command_id: Some(command_id),
1935 station_id,
1936 node_id: None,
1937 delivery: None,
1938 command: None,
1939 accepted: false,
1940 reason_code,
1941 ack_bytes: None,
1942 error: Some(GatewayCommandPipelineError::Encode(encode_error)),
1943 };
1944 }
1945 }
1946 } else {
1947 None
1948 };
1949
1950 GatewayCommandPipelineReport {
1951 client_id: Some(client_id),
1952 command_id: Some(command_id),
1953 station_id,
1954 node_id: None,
1955 delivery: None,
1956 command: None,
1957 accepted: false,
1958 reason_code,
1959 ack_bytes,
1960 error: Some(error),
1961 }
1962 }
1963
1964 fn encode_ack(&mut self, ack: &CommandAckFrame) -> Result<Vec<u8>, BinaryEncodeError> {
1965 let mut out = Vec::new();
1966 self.encoder.encode_command_ack(ack, &mut out)?;
1967 self.stats.acks_encoded = self.stats.acks_encoded.saturating_add(1);
1968 Ok(out)
1969 }
1970}
1971
1972impl Default for GatewayCommandPipeline {
1973 fn default() -> Self {
1974 Self::new(GatewayCommandPipelineConfig::default())
1975 }
1976}
1977
1978const fn gateway_reject_reason_code(error: GatewayError) -> u16 {
1979 match error {
1980 GatewayError::ReplayOrStale { .. } => GATEWAY_COMMAND_ACK_REPLAY_OR_STALE,
1981 GatewayError::RateLimited { .. } => GATEWAY_COMMAND_ACK_RATE_LIMITED,
1982 GatewayError::MissingSession(_)
1983 | GatewayError::SessionDisconnected { .. }
1984 | GatewayError::BadGeneration { .. }
1985 | GatewayError::CapacityFull { .. } => GATEWAY_COMMAND_ACK_GATEWAY_REJECTED,
1986 }
1987}
1988
1989const fn queue_reject_reason_code(error: CommandQueueError) -> u16 {
1990 match error {
1991 CommandQueueError::QueueFull(_) => GATEWAY_COMMAND_ACK_QUEUE_FULL,
1992 CommandQueueError::RejectedByBarrier(_) => GATEWAY_COMMAND_ACK_BARRIER_REJECTED,
1993 }
1994}
1995
1996#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1998pub struct GatewayClientTransportStats {
1999 pub packets_received: usize,
2001 pub bytes_received: usize,
2003 pub command_frames_received: usize,
2005 pub source_mismatches: usize,
2007 pub commands_accepted: usize,
2009 pub commands_rejected: usize,
2011 pub acks_sent: usize,
2013 pub ack_bytes_sent: usize,
2015}
2016
2017#[derive(Clone, Debug, Default, PartialEq, Eq)]
2019pub struct GatewayClientTransportPump {
2020 pub packets_received: usize,
2022 pub bytes_received: usize,
2024 pub reports: Vec<GatewayCommandPipelineReport>,
2026 pub acks_sent: usize,
2028 pub ack_bytes_sent: usize,
2030}
2031
2032impl GatewayClientTransportPump {
2033 pub fn commands_processed(&self) -> usize {
2035 self.reports.len()
2036 }
2037
2038 pub fn commands_accepted(&self) -> usize {
2040 self.reports.iter().filter(|report| report.accepted).count()
2041 }
2042
2043 pub fn commands_rejected(&self) -> usize {
2045 self.reports
2046 .iter()
2047 .filter(|report| !report.accepted)
2048 .count()
2049 }
2050}
2051
2052#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2054pub struct GatewayClientTransportSummary {
2055 pub packets_received: usize,
2057 pub bytes_received: usize,
2059 pub commands_accepted: usize,
2061 pub commands_rejected: usize,
2063 pub acks_sent: usize,
2065 pub ack_bytes_sent: usize,
2067}
2068
2069#[derive(Clone, Debug, PartialEq, Eq)]
2071pub enum GatewayClientTransportError<E> {
2072 Transport(E),
2074 Decode(BinaryDecodeError),
2076 NonCommandFrame,
2078 SourceMismatch {
2080 packet_client_id: ClientId,
2082 frame_client_id: ClientId,
2084 },
2085}
2086
2087impl<E: core::fmt::Display> core::fmt::Display for GatewayClientTransportError<E> {
2088 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2089 match self {
2090 Self::Transport(error) => write!(f, "{error}"),
2091 Self::Decode(error) => write!(f, "{error}"),
2092 Self::NonCommandFrame => f.write_str("gateway client transport expected command frame"),
2093 Self::SourceMismatch {
2094 packet_client_id,
2095 frame_client_id,
2096 } => write!(
2097 f,
2098 "gateway client source mismatch: packet {}, frame {}",
2099 packet_client_id.get(),
2100 frame_client_id.get()
2101 ),
2102 }
2103 }
2104}
2105
2106impl<E> std::error::Error for GatewayClientTransportError<E>
2107where
2108 E: std::error::Error + 'static,
2109{
2110 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2111 match self {
2112 Self::Transport(error) => Some(error),
2113 Self::Decode(error) => Some(error),
2114 Self::NonCommandFrame | Self::SourceMismatch { .. } => None,
2115 }
2116 }
2117}
2118
2119#[derive(Clone, Debug, Default)]
2121pub struct GatewayClientTransportBridge {
2122 stats: GatewayClientTransportStats,
2123}
2124
2125impl GatewayClientTransportBridge {
2126 pub const fn new() -> Self {
2128 Self {
2129 stats: GatewayClientTransportStats {
2130 packets_received: 0,
2131 bytes_received: 0,
2132 command_frames_received: 0,
2133 source_mismatches: 0,
2134 commands_accepted: 0,
2135 commands_rejected: 0,
2136 acks_sent: 0,
2137 ack_bytes_sent: 0,
2138 },
2139 }
2140 }
2141
2142 pub const fn stats(&self) -> GatewayClientTransportStats {
2144 self.stats
2145 }
2146
2147 #[allow(clippy::too_many_arguments)]
2150 pub fn pump_ingress<T, E>(
2151 &mut self,
2152 transport: &mut T,
2153 pipeline: &mut GatewayCommandPipeline,
2154 gateway: &mut GatewaySessionTable,
2155 station_queues: &mut BTreeMap<StationId, CommandQueues>,
2156 now: Tick,
2157 ingress: CommandIngress,
2158 max_packets: usize,
2159 ) -> Result<GatewayClientTransportPump, GatewayClientTransportError<E>>
2160 where
2161 T: TransportReceiver<Error = E> + TransportSink<Error = E>,
2162 {
2163 let mut pump = GatewayClientTransportPump::default();
2164 for _ in 0..max_packets {
2165 let Some(packet) = transport
2166 .try_recv()
2167 .map_err(GatewayClientTransportError::Transport)?
2168 else {
2169 break;
2170 };
2171 let (ack_client_id, packet_bytes, report) = self.process_ingress_packet::<E>(
2172 pipeline,
2173 gateway,
2174 station_queues,
2175 &packet,
2176 now,
2177 ingress,
2178 )?;
2179 pump.packets_received = pump.packets_received.saturating_add(1);
2180 pump.bytes_received = pump.bytes_received.saturating_add(packet_bytes);
2181
2182 if let Some(bytes) = &report.ack_bytes {
2183 let ack_len = bytes.len();
2184 transport
2185 .send(OutboundPacket {
2186 client_id: ack_client_id,
2187 bytes: bytes.clone(),
2188 })
2189 .map_err(GatewayClientTransportError::Transport)?;
2190 self.stats.acks_sent = self.stats.acks_sent.saturating_add(1);
2191 self.stats.ack_bytes_sent = self.stats.ack_bytes_sent.saturating_add(ack_len);
2192 pump.acks_sent = pump.acks_sent.saturating_add(1);
2193 pump.ack_bytes_sent = pump.ack_bytes_sent.saturating_add(ack_len);
2194 }
2195 pump.reports.push(report);
2196 }
2197 Ok(pump)
2198 }
2199
2200 #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
2206 pub fn pump_ingress_compact<T, E>(
2207 &mut self,
2208 transport: &mut T,
2209 pipeline: &mut GatewayCommandPipeline,
2210 gateway: &mut GatewaySessionTable,
2211 station_queues: &mut BTreeMap<StationId, CommandQueues>,
2212 now: Tick,
2213 ingress: CommandIngress,
2214 max_packets: usize,
2215 ) -> Result<GatewayClientTransportSummary, GatewayClientTransportError<E>>
2216 where
2217 T: TransportReceiver<Error = E> + TransportSink<Error = E>,
2218 {
2219 let mut summary = GatewayClientTransportSummary::default();
2220 for _ in 0..max_packets {
2221 let Some(packet) = transport
2222 .try_recv()
2223 .map_err(GatewayClientTransportError::Transport)?
2224 else {
2225 break;
2226 };
2227 let (ack_client_id, packet_bytes, mut report) = self.process_ingress_packet::<E>(
2228 pipeline,
2229 gateway,
2230 station_queues,
2231 &packet,
2232 now,
2233 ingress,
2234 )?;
2235 summary.packets_received = summary.packets_received.saturating_add(1);
2236 summary.bytes_received = summary.bytes_received.saturating_add(packet_bytes);
2237 if report.accepted {
2238 summary.commands_accepted = summary.commands_accepted.saturating_add(1);
2239 } else {
2240 summary.commands_rejected = summary.commands_rejected.saturating_add(1);
2241 }
2242
2243 if let Some(bytes) = report.ack_bytes.take() {
2244 let ack_len = bytes.len();
2245 transport
2246 .send(OutboundPacket {
2247 client_id: ack_client_id,
2248 bytes,
2249 })
2250 .map_err(GatewayClientTransportError::Transport)?;
2251 self.stats.acks_sent = self.stats.acks_sent.saturating_add(1);
2252 self.stats.ack_bytes_sent = self.stats.ack_bytes_sent.saturating_add(ack_len);
2253 summary.acks_sent = summary.acks_sent.saturating_add(1);
2254 summary.ack_bytes_sent = summary.ack_bytes_sent.saturating_add(ack_len);
2255 }
2256 }
2257 Ok(summary)
2258 }
2259
2260 #[allow(clippy::too_many_arguments)]
2261 fn process_ingress_packet<E>(
2262 &mut self,
2263 pipeline: &mut GatewayCommandPipeline,
2264 gateway: &mut GatewaySessionTable,
2265 station_queues: &mut BTreeMap<StationId, CommandQueues>,
2266 packet: &InboundPacket,
2267 now: Tick,
2268 ingress: CommandIngress,
2269 ) -> Result<(ClientId, usize, GatewayCommandPipelineReport), GatewayClientTransportError<E>>
2270 {
2271 let packet_bytes = packet.bytes.len();
2272 self.stats.packets_received = self.stats.packets_received.saturating_add(1);
2273 self.stats.bytes_received = self.stats.bytes_received.saturating_add(packet_bytes);
2274 let command_frame = match pipeline.decode_command_frame(&packet.bytes) {
2275 Ok(command_frame) => command_frame,
2276 Err(GatewayCommandPipelineError::Decode(error)) => {
2277 return Err(GatewayClientTransportError::Decode(error));
2278 }
2279 Err(GatewayCommandPipelineError::NonCommandFrame) => {
2280 return Err(GatewayClientTransportError::NonCommandFrame);
2281 }
2282 Err(error) => {
2283 unreachable!(
2284 "decode_command_frame only returns decode/non-command errors: {error}"
2285 );
2286 }
2287 };
2288 self.stats.command_frames_received = self.stats.command_frames_received.saturating_add(1);
2289 if let Some(packet_client_id) = packet.client_id
2290 && packet_client_id != command_frame.client_id
2291 {
2292 self.stats.source_mismatches = self.stats.source_mismatches.saturating_add(1);
2293 return Err(GatewayClientTransportError::SourceMismatch {
2294 packet_client_id,
2295 frame_client_id: command_frame.client_id,
2296 });
2297 }
2298 let ack_client_id = command_frame.client_id;
2299 let report =
2300 pipeline.process_command_frame(gateway, station_queues, command_frame, now, ingress);
2301 if report.accepted {
2302 self.stats.commands_accepted = self.stats.commands_accepted.saturating_add(1);
2303 } else {
2304 self.stats.commands_rejected = self.stats.commands_rejected.saturating_add(1);
2305 }
2306 Ok((ack_client_id, packet_bytes, report))
2307 }
2308}
2309
2310const STATION_LOOKUP_INDEX_THRESHOLD: usize = 64;
2312
2313#[derive(Clone, Debug, Default)]
2315pub struct StationSet {
2316 stations: Vec<Station>,
2317 positions: HashMap<StationId, usize>,
2318}
2319
2320impl StationSet {
2321 pub fn with_capacity(capacity: usize) -> Self {
2323 Self {
2324 stations: Vec::with_capacity(capacity),
2325 positions: if capacity >= STATION_LOOKUP_INDEX_THRESHOLD {
2326 HashMap::with_capacity(capacity)
2327 } else {
2328 HashMap::new()
2329 },
2330 }
2331 }
2332
2333 pub fn reserve(&mut self, additional: usize) {
2335 self.stations.reserve(additional);
2336 if !self.positions.is_empty() {
2337 self.positions.reserve(additional);
2338 } else if self.stations.len().saturating_add(additional) >= STATION_LOOKUP_INDEX_THRESHOLD {
2339 self.positions.reserve(self.stations.len() + additional);
2340 }
2341 }
2342
2343 pub fn push(&mut self, station: Station) {
2345 let station_id = station.config().station_id;
2346 self.activate_lookup_for(self.stations.len().saturating_add(1));
2347 if !self.positions.is_empty() {
2348 self.positions
2349 .entry(station_id)
2350 .or_insert(self.stations.len());
2351 }
2352 self.stations.push(station);
2353 }
2354
2355 pub fn remove(&mut self, station_id: StationId) -> Option<Station> {
2360 let position = self.position(station_id)?;
2361 let station = self.stations.remove(position);
2362 self.rebuild_positions();
2363 Some(station)
2364 }
2365
2366 pub fn get(&self, station_id: StationId) -> Option<&Station> {
2368 self.position(station_id)
2369 .and_then(|index| self.stations.get(index))
2370 }
2371
2372 pub fn get_mut(&mut self, station_id: StationId) -> Option<&mut Station> {
2374 let index = self.position(station_id)?;
2375 self.stations.get_mut(index)
2376 }
2377
2378 pub fn get_pair_mut(
2380 &mut self,
2381 left_id: StationId,
2382 right_id: StationId,
2383 ) -> Option<(&mut Station, &mut Station)> {
2384 if left_id == right_id {
2385 return None;
2386 }
2387
2388 let left_index = self.position(left_id)?;
2389 let right_index = self.position(right_id)?;
2390
2391 if left_index < right_index {
2392 let (left, right) = self.stations.split_at_mut(right_index);
2393 Some((&mut left[left_index], &mut right[0]))
2394 } else {
2395 let (left, right) = self.stations.split_at_mut(left_index);
2396 Some((&mut right[0], &mut left[right_index]))
2397 }
2398 }
2399
2400 pub fn iter(&self) -> impl Iterator<Item = &Station> {
2402 self.stations.iter()
2403 }
2404
2405 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Station> {
2407 self.stations.iter_mut()
2408 }
2409
2410 pub fn station_ids_in_scope(&self, scope: BarrierScope) -> Vec<StationId> {
2412 self.stations
2413 .iter()
2414 .filter(|station| match scope {
2415 BarrierScope::Instance(instance_id) => station.config().instance_id == instance_id,
2416 BarrierScope::Station(station_id) => station.config().station_id == station_id,
2417 })
2418 .map(|station| station.config().station_id)
2419 .collect()
2420 }
2421
2422 pub fn len(&self) -> usize {
2424 self.stations.len()
2425 }
2426
2427 pub fn station_capacity(&self) -> usize {
2429 self.stations.capacity()
2430 }
2431
2432 pub fn lookup_capacity(&self) -> usize {
2434 self.positions.capacity()
2435 }
2436
2437 pub fn lookup_index_active(&self) -> bool {
2439 !self.positions.is_empty()
2440 }
2441
2442 pub fn is_empty(&self) -> bool {
2444 self.stations.is_empty()
2445 }
2446
2447 fn position(&self, station_id: StationId) -> Option<usize> {
2448 if self.positions.is_empty() {
2449 self.stations
2450 .iter()
2451 .position(|station| station.config().station_id == station_id)
2452 } else {
2453 self.positions.get(&station_id).copied()
2454 }
2455 }
2456
2457 fn activate_lookup_for(&mut self, new_len: usize) {
2458 if new_len < STATION_LOOKUP_INDEX_THRESHOLD || !self.positions.is_empty() {
2459 return;
2460 }
2461 self.positions.reserve(new_len);
2462 for (index, station) in self.stations.iter().enumerate() {
2463 self.positions
2464 .entry(station.config().station_id)
2465 .or_insert(index);
2466 }
2467 }
2468
2469 fn rebuild_positions(&mut self) {
2470 if self.positions.is_empty() {
2471 return;
2472 }
2473 self.positions.clear();
2474 for (index, station) in self.stations.iter().enumerate() {
2475 self.positions
2476 .entry(station.config().station_id)
2477 .or_insert(index);
2478 }
2479 }
2480}
2481
2482#[derive(Clone, Debug, Default)]
2484pub struct StationIndexSet {
2485 indexes: Vec<(StationId, CellIndex)>,
2486 positions: HashMap<StationId, usize>,
2487}
2488
2489impl StationIndexSet {
2490 pub fn with_capacity(capacity: usize) -> Self {
2492 Self {
2493 indexes: Vec::with_capacity(capacity),
2494 positions: if capacity >= STATION_LOOKUP_INDEX_THRESHOLD {
2495 HashMap::with_capacity(capacity)
2496 } else {
2497 HashMap::new()
2498 },
2499 }
2500 }
2501
2502 pub fn reserve(&mut self, additional: usize) {
2504 self.indexes.reserve(additional);
2505 if !self.positions.is_empty() {
2506 self.positions.reserve(additional);
2507 } else if self.indexes.len().saturating_add(additional) >= STATION_LOOKUP_INDEX_THRESHOLD {
2508 self.positions.reserve(self.indexes.len() + additional);
2509 }
2510 }
2511
2512 pub fn insert(&mut self, station_id: StationId, index: CellIndex) {
2514 if let Some(position) = self.position(station_id) {
2515 self.indexes[position].1 = index;
2516 } else {
2517 self.activate_lookup_for(self.indexes.len().saturating_add(1));
2518 if !self.positions.is_empty() {
2519 self.positions.insert(station_id, self.indexes.len());
2520 }
2521 self.indexes.push((station_id, index));
2522 }
2523 }
2524
2525 pub fn remove(&mut self, station_id: StationId) -> Option<CellIndex> {
2530 let position = self.position(station_id)?;
2531 let (_, index) = self.indexes.remove(position);
2532 self.rebuild_positions();
2533 Some(index)
2534 }
2535
2536 pub fn get(&self, station_id: StationId) -> Option<&CellIndex> {
2538 self.position(station_id)
2539 .and_then(|position| self.indexes.get(position))
2540 .map(|(_, index)| index)
2541 }
2542
2543 pub fn get_mut(&mut self, station_id: StationId) -> Option<&mut CellIndex> {
2545 let position = self.position(station_id)?;
2546 self.indexes.get_mut(position).map(|(_, index)| index)
2547 }
2548
2549 pub fn get_pair_mut(
2551 &mut self,
2552 left_id: StationId,
2553 right_id: StationId,
2554 ) -> Option<(&mut CellIndex, &mut CellIndex)> {
2555 if left_id == right_id {
2556 return None;
2557 }
2558
2559 let left_index = self.position(left_id)?;
2560 let right_index = self.position(right_id)?;
2561
2562 if left_index < right_index {
2563 let (left, right) = self.indexes.split_at_mut(right_index);
2564 Some((&mut left[left_index].1, &mut right[0].1))
2565 } else {
2566 let (left, right) = self.indexes.split_at_mut(left_index);
2567 Some((&mut right[0].1, &mut left[right_index].1))
2568 }
2569 }
2570
2571 pub fn len(&self) -> usize {
2573 self.indexes.len()
2574 }
2575
2576 pub fn index_capacity(&self) -> usize {
2578 self.indexes.capacity()
2579 }
2580
2581 pub fn lookup_capacity(&self) -> usize {
2583 self.positions.capacity()
2584 }
2585
2586 pub fn lookup_index_active(&self) -> bool {
2588 !self.positions.is_empty()
2589 }
2590
2591 pub fn iter(&self) -> impl Iterator<Item = (StationId, &CellIndex)> {
2593 self.indexes
2594 .iter()
2595 .map(|(station_id, index)| (*station_id, index))
2596 }
2597
2598 pub fn is_empty(&self) -> bool {
2600 self.indexes.is_empty()
2601 }
2602
2603 fn position(&self, station_id: StationId) -> Option<usize> {
2604 if self.positions.is_empty() {
2605 self.indexes.iter().position(|(id, _)| *id == station_id)
2606 } else {
2607 self.positions.get(&station_id).copied()
2608 }
2609 }
2610
2611 fn activate_lookup_for(&mut self, new_len: usize) {
2612 if new_len < STATION_LOOKUP_INDEX_THRESHOLD || !self.positions.is_empty() {
2613 return;
2614 }
2615 self.positions.reserve(new_len);
2616 for (index, (station_id, _)) in self.indexes.iter().enumerate() {
2617 self.positions.entry(*station_id).or_insert(index);
2618 }
2619 }
2620
2621 fn rebuild_positions(&mut self) {
2622 if self.positions.is_empty() {
2623 return;
2624 }
2625 self.positions.clear();
2626 for (index, (station_id, _)) in self.indexes.iter().enumerate() {
2627 self.positions.entry(*station_id).or_insert(index);
2628 }
2629 }
2630}
2631
2632#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2634pub struct StationLoadSamplerConfig {
2635 pub estimated_bytes_per_entity: usize,
2637 pub estimated_bytes_per_subscriber: usize,
2639 pub estimated_bytes_per_event: usize,
2641 pub tick_cost_per_owned_entity: u64,
2643 pub tick_cost_per_ghost_entity: u64,
2645 pub tick_cost_per_occupied_cell: u64,
2647 pub tick_cost_per_queued_event: u64,
2649}
2650
2651impl Default for StationLoadSamplerConfig {
2652 fn default() -> Self {
2653 Self {
2654 estimated_bytes_per_entity: 48,
2655 estimated_bytes_per_subscriber: 16,
2656 estimated_bytes_per_event: 32,
2657 tick_cost_per_owned_entity: 2,
2658 tick_cost_per_ghost_entity: 1,
2659 tick_cost_per_occupied_cell: 1,
2660 tick_cost_per_queued_event: 1,
2661 }
2662 }
2663}
2664
2665#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2667pub struct StationLoadSampler {
2668 config: StationLoadSamplerConfig,
2669}
2670
2671#[derive(Clone, Debug, Default)]
2673pub struct StationLoadSamplerScratch {
2674 subscribers_by_station: HashMap<StationId, usize>,
2675 occupancy: Vec<CellOccupancy>,
2676 samples: Vec<StationLoadSample>,
2677}
2678
2679impl StationLoadSamplerScratch {
2680 pub fn new() -> Self {
2682 Self::default()
2683 }
2684
2685 pub fn retained_subscriber_capacity(&self) -> usize {
2687 self.subscribers_by_station.capacity()
2688 }
2689
2690 pub fn retained_occupancy_capacity(&self) -> usize {
2692 self.occupancy.capacity()
2693 }
2694
2695 pub fn retained_sample_slots(&self) -> usize {
2697 self.samples.len()
2698 }
2699
2700 pub fn retained_cell_capacity(&self) -> usize {
2702 self.samples
2703 .iter()
2704 .map(|sample| sample.cells.capacity())
2705 .sum()
2706 }
2707}
2708
2709impl StationLoadSampler {
2710 pub const fn new(config: StationLoadSamplerConfig) -> Self {
2712 Self { config }
2713 }
2714
2715 pub const fn config(&self) -> StationLoadSamplerConfig {
2717 self.config
2718 }
2719
2720 pub fn sample_all_into<'a>(
2724 &self,
2725 stations: &StationSet,
2726 indexes: &StationIndexSet,
2727 router: &EventRouter,
2728 subscriber_counts: &[(StationId, usize)],
2729 scratch: &'a mut StationLoadSamplerScratch,
2730 ) -> &'a [StationLoadSample] {
2731 scratch.subscribers_by_station.clear();
2732 for (station_id, count) in subscriber_counts {
2733 let entry = scratch
2734 .subscribers_by_station
2735 .entry(*station_id)
2736 .or_insert(0);
2737 *entry = entry.saturating_add(*count);
2738 }
2739
2740 let station_count = stations.len();
2741 if scratch.samples.len() < station_count {
2742 scratch
2743 .samples
2744 .resize_with(station_count, StationLoadSample::default);
2745 }
2746 for (station, sample) in stations
2747 .iter()
2748 .zip(scratch.samples[..station_count].iter_mut())
2749 {
2750 let station_id = station.config().station_id;
2751 self.sample_station_into(
2752 station,
2753 indexes.get(station_id),
2754 router.queued_len(station_id).unwrap_or(0),
2755 scratch
2756 .subscribers_by_station
2757 .get(&station_id)
2758 .copied()
2759 .unwrap_or(0),
2760 &mut scratch.occupancy,
2761 sample,
2762 );
2763 }
2764 &scratch.samples[..station_count]
2765 }
2766
2767 fn sample_station_into(
2768 &self,
2769 station: &Station,
2770 index: Option<&CellIndex>,
2771 queued_events: usize,
2772 subscribers: usize,
2773 occupancy: &mut Vec<CellOccupancy>,
2774 sample: &mut StationLoadSample,
2775 ) {
2776 let (owned_entities, ghost_entities) = count_station_roles(station);
2777 sample.cells.clear();
2778 if let Some(index) = index {
2779 index.cell_occupancy_into(occupancy);
2780 for occupancy in occupancy.iter() {
2781 let mut cell_owned_entities = 0usize;
2782 let mut cell_ghost_entities = 0usize;
2783 for handle in index.handles_in_cell_slice(occupancy.cell) {
2784 if let Some(record) = station.get(*handle) {
2785 if record.is_owned() {
2786 cell_owned_entities = cell_owned_entities.saturating_add(1);
2787 } else {
2788 cell_ghost_entities = cell_ghost_entities.saturating_add(1);
2789 }
2790 }
2791 }
2792 let entities = cell_owned_entities.saturating_add(cell_ghost_entities);
2793 sample.cells.push(CellLoadSample {
2794 cell: occupancy.cell,
2795 owned_entities: cell_owned_entities,
2796 ghost_entities: cell_ghost_entities,
2797 subscribers: 0,
2798 estimated_updates: entities,
2799 estimated_bytes: entities
2800 .saturating_mul(self.config.estimated_bytes_per_entity),
2801 tick_cost_units: self.estimate_tick_cost(
2802 cell_owned_entities,
2803 cell_ghost_entities,
2804 1,
2805 0,
2806 ),
2807 event_pressure: 0,
2808 });
2809 }
2810 } else {
2811 occupancy.clear();
2812 }
2813 sample.station_id = station.config().station_id;
2814 sample.owned_entities = owned_entities;
2815 sample.ghost_entities = ghost_entities;
2816 sample.subscribers = subscribers;
2817 sample.queued_events = queued_events;
2818 sample.estimated_bytes =
2819 self.estimate_station_bytes(owned_entities, ghost_entities, subscribers, queued_events);
2820 sample.tick_cost_units = self.estimate_tick_cost(
2821 owned_entities,
2822 ghost_entities,
2823 sample.cells.len(),
2824 queued_events,
2825 );
2826 }
2827
2828 fn estimate_station_bytes(
2829 &self,
2830 owned_entities: usize,
2831 ghost_entities: usize,
2832 subscribers: usize,
2833 queued_events: usize,
2834 ) -> usize {
2835 owned_entities
2836 .saturating_add(ghost_entities)
2837 .saturating_mul(self.config.estimated_bytes_per_entity)
2838 .saturating_add(subscribers.saturating_mul(self.config.estimated_bytes_per_subscriber))
2839 .saturating_add(queued_events.saturating_mul(self.config.estimated_bytes_per_event))
2840 }
2841
2842 fn estimate_tick_cost(
2843 &self,
2844 owned_entities: usize,
2845 ghost_entities: usize,
2846 occupied_cells: usize,
2847 queued_events: usize,
2848 ) -> u64 {
2849 (owned_entities as u64)
2850 .saturating_mul(self.config.tick_cost_per_owned_entity)
2851 .saturating_add(
2852 (ghost_entities as u64).saturating_mul(self.config.tick_cost_per_ghost_entity),
2853 )
2854 .saturating_add(
2855 (occupied_cells as u64).saturating_mul(self.config.tick_cost_per_occupied_cell),
2856 )
2857 .saturating_add(
2858 (queued_events as u64).saturating_mul(self.config.tick_cost_per_queued_event),
2859 )
2860 }
2861}
2862
2863impl Default for StationLoadSampler {
2864 fn default() -> Self {
2865 Self::new(StationLoadSamplerConfig::default())
2866 }
2867}
2868
2869fn count_station_roles(station: &Station) -> (usize, usize) {
2870 let mut owned_entities = 0usize;
2871 let mut ghost_entities = 0usize;
2872 for record in station.iter() {
2873 if record.is_owned() {
2874 owned_entities = owned_entities.saturating_add(1);
2875 } else {
2876 ghost_entities = ghost_entities.saturating_add(1);
2877 }
2878 }
2879 (owned_entities, ghost_entities)
2880}
2881
2882#[derive(Clone, Debug, PartialEq)]
2884pub struct EntityMigrationReport {
2885 pub transfer: HandoffTransfer,
2887 pub source_ghost: EntityHandle,
2889 pub target_owner: EntityHandle,
2891}
2892
2893#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2895pub enum EntityMigrationError {
2896 SameSourceAndTarget(StationId),
2898 MissingSource(StationId),
2900 MissingTarget(StationId),
2902 Station(StationError),
2904}
2905
2906impl core::fmt::Display for EntityMigrationError {
2907 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2908 match self {
2909 Self::SameSourceAndTarget(id) => {
2910 write!(f, "source and target station are both {}", id.get())
2911 }
2912 Self::MissingSource(id) => write!(f, "source station {} is missing", id.get()),
2913 Self::MissingTarget(id) => write!(f, "target station {} is missing", id.get()),
2914 Self::Station(error) => write!(f, "{error}"),
2915 }
2916 }
2917}
2918
2919impl std::error::Error for EntityMigrationError {}
2920
2921impl From<StationError> for EntityMigrationError {
2922 fn from(value: StationError) -> Self {
2923 Self::Station(value)
2924 }
2925}
2926
2927#[derive(Clone, Copy, Debug, Default)]
2929pub struct EntityMigrationExecutor;
2930
2931impl EntityMigrationExecutor {
2932 pub fn migrate_entity(
2934 stations: &mut StationSet,
2935 entity_id: EntityId,
2936 source_station: StationId,
2937 target_station: StationId,
2938 ghost_ttl_ticks: u64,
2939 ) -> Result<EntityMigrationReport, EntityMigrationError> {
2940 if source_station == target_station {
2941 return Err(EntityMigrationError::SameSourceAndTarget(source_station));
2942 }
2943
2944 if stations.get(source_station).is_none() {
2945 return Err(EntityMigrationError::MissingSource(source_station));
2946 }
2947 if stations.get(target_station).is_none() {
2948 return Err(EntityMigrationError::MissingTarget(target_station));
2949 }
2950
2951 let (source, target) = stations
2952 .get_pair_mut(source_station, target_station)
2953 .expect("stations were checked above");
2954 let target_epoch = next_target_epoch(target);
2955 let source_ghost_expires_at =
2956 Tick::new(source.tick().get().saturating_add(ghost_ttl_ticks));
2957 let transfer = source.prepare_outgoing_handoff(
2958 entity_id,
2959 target_station,
2960 target_epoch,
2961 source_ghost_expires_at,
2962 )?;
2963 target.prewarm_handoff_ghost(&transfer)?;
2964 let target_owner = target.commit_incoming_handoff(transfer.clone())?;
2965 let source_ghost = source.commit_outgoing_handoff(&transfer)?;
2966
2967 Ok(EntityMigrationReport {
2968 transfer,
2969 source_ghost,
2970 target_owner,
2971 })
2972 }
2973}
2974
2975fn next_target_epoch(station: &mut Station) -> OwnerEpoch {
2976 station.next_owner_epoch()
2977}
2978
2979#[derive(Clone, Debug, Default, PartialEq, Eq)]
2981pub struct CellOwnershipTable {
2982 owners: BTreeMap<CellCoord3, StationId>,
2983}
2984
2985impl CellOwnershipTable {
2986 pub fn assign(&mut self, cell: CellCoord3, station_id: StationId) -> Option<StationId> {
2988 self.owners.insert(cell, station_id)
2989 }
2990
2991 pub fn owner_of(&self, cell: CellCoord3) -> Option<StationId> {
2993 self.owners.get(&cell).copied()
2994 }
2995
2996 pub fn apply_split(
2998 &mut self,
2999 proposal: &SplitProposal,
3000 target_station: StationId,
3001 ) -> CellOwnershipUpdate {
3002 let mut update = CellOwnershipUpdate::default();
3003 self.apply_split_into(proposal, target_station, &mut update);
3004 update
3005 }
3006
3007 pub fn apply_split_into(
3009 &mut self,
3010 proposal: &SplitProposal,
3011 target_station: StationId,
3012 update: &mut CellOwnershipUpdate,
3013 ) {
3014 update.source_station = proposal.source_station;
3015 update.target_station = target_station;
3016 update.moved_cells.clear();
3017 for cell in &proposal.cells_to_move {
3018 let previous = self.assign(*cell, target_station);
3019 if previous != Some(target_station) {
3020 update.moved_cells.push(*cell);
3021 }
3022 }
3023 }
3024
3025 pub fn len(&self) -> usize {
3027 self.owners.len()
3028 }
3029
3030 pub fn is_empty(&self) -> bool {
3032 self.owners.is_empty()
3033 }
3034}
3035
3036#[derive(Clone, Debug, Default, PartialEq, Eq)]
3038pub struct CellOwnershipUpdate {
3039 pub source_station: StationId,
3041 pub target_station: StationId,
3043 pub moved_cells: Vec<CellCoord3>,
3045}
3046
3047#[derive(Clone, Debug, Default, PartialEq)]
3049pub struct CellMigrationReport {
3050 pub source_station: StationId,
3052 pub target_station: StationId,
3054 pub scanned_cells: Vec<CellCoord3>,
3056 pub entity_migrations: Vec<EntityMigrationReport>,
3058 pub skipped_missing_handles: usize,
3060 pub skipped_non_owned: usize,
3062 pub skipped_duplicate_entities: usize,
3064}
3065
3066#[derive(Clone, Debug, Default)]
3068pub struct CellMigrationScratch {
3069 seen_handles: HashSet<EntityHandle>,
3070 seen_entities: HashSet<EntityId>,
3071 entity_ids: Vec<EntityId>,
3072}
3073
3074impl CellMigrationScratch {
3075 pub fn new() -> Self {
3077 Self::default()
3078 }
3079
3080 pub fn reserve(&mut self, handles: usize, entities: usize) {
3082 if self.seen_handles.capacity() < handles {
3083 self.seen_handles
3084 .reserve(handles.saturating_sub(self.seen_handles.len()));
3085 }
3086 if self.seen_entities.capacity() < entities {
3087 self.seen_entities
3088 .reserve(entities.saturating_sub(self.seen_entities.len()));
3089 }
3090 if self.entity_ids.capacity() < entities {
3091 self.entity_ids
3092 .reserve(entities.saturating_sub(self.entity_ids.len()));
3093 }
3094 }
3095
3096 pub fn handle_capacity(&self) -> usize {
3098 self.seen_handles.capacity()
3099 }
3100
3101 pub fn entity_capacity(&self) -> usize {
3103 self.seen_entities.capacity()
3104 }
3105
3106 pub fn candidate_capacity(&self) -> usize {
3108 self.entity_ids.capacity()
3109 }
3110
3111 fn clear(&mut self) {
3112 self.seen_handles.clear();
3113 self.seen_entities.clear();
3114 self.entity_ids.clear();
3115 }
3116}
3117
3118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3120pub enum CellMigrationError {
3121 Entity(EntityMigrationError),
3123 MissingTargetRecord(EntityId),
3125 MissingSourceRecord(EntityId),
3127}
3128
3129impl core::fmt::Display for CellMigrationError {
3130 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3131 match self {
3132 Self::Entity(error) => write!(f, "{error}"),
3133 Self::MissingTargetRecord(id) => {
3134 write!(f, "target owner record for entity {} is missing", id.get())
3135 }
3136 Self::MissingSourceRecord(id) => {
3137 write!(f, "source ghost record for entity {} is missing", id.get())
3138 }
3139 }
3140 }
3141}
3142
3143impl std::error::Error for CellMigrationError {}
3144
3145impl From<EntityMigrationError> for CellMigrationError {
3146 fn from(value: EntityMigrationError) -> Self {
3147 Self::Entity(value)
3148 }
3149}
3150
3151#[derive(Clone, Copy, Debug, Default)]
3153pub struct CellMigrationExecutor;
3154
3155impl CellMigrationExecutor {
3156 pub fn migrate_cells(
3158 stations: &mut StationSet,
3159 source_index: &mut CellIndex,
3160 target_index: &mut CellIndex,
3161 source_station: StationId,
3162 target_station: StationId,
3163 cells: &[CellCoord3],
3164 ghost_ttl_ticks: u64,
3165 ) -> Result<CellMigrationReport, CellMigrationError> {
3166 let mut report = CellMigrationReport::default();
3167 let mut scratch = CellMigrationScratch::new();
3168 Self::migrate_cells_into(
3169 stations,
3170 source_index,
3171 target_index,
3172 source_station,
3173 target_station,
3174 cells,
3175 ghost_ttl_ticks,
3176 &mut scratch,
3177 &mut report,
3178 )?;
3179 Ok(report)
3180 }
3181
3182 #[allow(clippy::too_many_arguments)]
3187 pub fn migrate_cells_into(
3188 stations: &mut StationSet,
3189 source_index: &mut CellIndex,
3190 target_index: &mut CellIndex,
3191 source_station: StationId,
3192 target_station: StationId,
3193 cells: &[CellCoord3],
3194 ghost_ttl_ticks: u64,
3195 scratch: &mut CellMigrationScratch,
3196 report: &mut CellMigrationReport,
3197 ) -> Result<(), CellMigrationError> {
3198 report.source_station = source_station;
3199 report.target_station = target_station;
3200 report.scanned_cells.clear();
3201 report.scanned_cells.extend_from_slice(cells);
3202 report.entity_migrations.clear();
3203 report.skipped_missing_handles = 0;
3204 report.skipped_non_owned = 0;
3205 report.skipped_duplicate_entities = 0;
3206 scratch.clear();
3207
3208 {
3209 let source = stations
3210 .get(source_station)
3211 .ok_or(EntityMigrationError::MissingSource(source_station))?;
3212 for cell in cells {
3213 for &handle in source_index.handles_in_cell_slice(*cell) {
3214 if !scratch.seen_handles.insert(handle) {
3215 report.skipped_duplicate_entities += 1;
3216 continue;
3217 }
3218 let Some(record) = source.get(handle) else {
3219 report.skipped_missing_handles += 1;
3220 continue;
3221 };
3222 if record.is_owned() {
3223 scratch.entity_ids.push(record.id);
3224 } else {
3225 report.skipped_non_owned += 1;
3226 }
3227 }
3228 }
3229 }
3230
3231 for &entity_id in &scratch.entity_ids {
3232 if !scratch.seen_entities.insert(entity_id) {
3233 report.skipped_duplicate_entities += 1;
3234 continue;
3235 }
3236 let migration = EntityMigrationExecutor::migrate_entity(
3237 stations,
3238 entity_id,
3239 source_station,
3240 target_station,
3241 ghost_ttl_ticks,
3242 )?;
3243
3244 {
3245 let target = stations
3246 .get(target_station)
3247 .ok_or(EntityMigrationError::MissingTarget(target_station))?;
3248 let target_record = target
3249 .get(migration.target_owner)
3250 .ok_or(CellMigrationError::MissingTargetRecord(entity_id))?;
3251 target_index.upsert(
3252 migration.target_owner,
3253 target_record.position,
3254 target_record.bounds,
3255 );
3256 }
3257
3258 {
3259 let source = stations
3260 .get(source_station)
3261 .ok_or(EntityMigrationError::MissingSource(source_station))?;
3262 let source_record = source
3263 .get(migration.source_ghost)
3264 .ok_or(CellMigrationError::MissingSourceRecord(entity_id))?;
3265 source_index.upsert(
3266 migration.source_ghost,
3267 source_record.position,
3268 source_record.bounds,
3269 );
3270 }
3271
3272 report.entity_migrations.push(migration);
3273 }
3274
3275 Ok(())
3276 }
3277}
3278
3279#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3281pub struct SplitSchedulerConfig {
3282 pub thresholds: HotspotThresholds,
3284 pub max_actions_per_pass: usize,
3286 pub max_cells_per_action: usize,
3288 pub ghost_ttl_ticks: u64,
3290 pub min_score_improvement: u64,
3292 pub max_target_score_after_move: u64,
3294 pub split_cooldown_ticks: u64,
3296 pub allow_warm_targets: bool,
3298}
3299
3300impl Default for SplitSchedulerConfig {
3301 fn default() -> Self {
3302 Self {
3303 thresholds: HotspotThresholds::default(),
3304 max_actions_per_pass: 4,
3305 max_cells_per_action: 4,
3306 ghost_ttl_ticks: 4,
3307 min_score_improvement: 1,
3308 max_target_score_after_move: u64::MAX,
3309 split_cooldown_ticks: 0,
3310 allow_warm_targets: true,
3311 }
3312 }
3313}
3314
3315#[derive(Clone, Debug, Default, PartialEq, Eq)]
3317pub struct SplitAction {
3318 pub source_station: StationId,
3320 pub target_station: StationId,
3322 pub proposal: SplitProposal,
3324 pub source_score: u64,
3326 pub target_score: u64,
3328 pub estimated_target_score_after_move: u64,
3330}
3331
3332#[derive(Clone, Debug, Default, PartialEq, Eq)]
3334pub struct SplitSchedule {
3335 pub decisions: Vec<HotspotDecision>,
3337 pub actions: Vec<SplitAction>,
3339 pub skipped_no_target: usize,
3341 pub skipped_no_cells: usize,
3343 pub skipped_cooldown: usize,
3345 pub skipped_target_severity: usize,
3347 pub skipped_target_capacity: usize,
3349 pub skipped_insufficient_improvement: usize,
3351}
3352
3353#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3355pub struct SplitScheduleView<'a> {
3356 pub decisions: &'a [HotspotDecision],
3358 pub actions: &'a [SplitAction],
3360 pub skipped_no_target: usize,
3362 pub skipped_no_cells: usize,
3364 pub skipped_cooldown: usize,
3366 pub skipped_target_severity: usize,
3368 pub skipped_target_capacity: usize,
3370 pub skipped_insufficient_improvement: usize,
3372}
3373
3374impl SplitSchedule {
3375 pub fn view(&self) -> SplitScheduleView<'_> {
3377 SplitScheduleView {
3378 decisions: &self.decisions,
3379 actions: &self.actions,
3380 skipped_no_target: self.skipped_no_target,
3381 skipped_no_cells: self.skipped_no_cells,
3382 skipped_cooldown: self.skipped_cooldown,
3383 skipped_target_severity: self.skipped_target_severity,
3384 skipped_target_capacity: self.skipped_target_capacity,
3385 skipped_insufficient_improvement: self.skipped_insufficient_improvement,
3386 }
3387 }
3388}
3389
3390impl From<SplitScheduleView<'_>> for SplitSchedule {
3391 fn from(view: SplitScheduleView<'_>) -> Self {
3392 Self {
3393 decisions: view.decisions.to_vec(),
3394 actions: view.actions.to_vec(),
3395 skipped_no_target: view.skipped_no_target,
3396 skipped_no_cells: view.skipped_no_cells,
3397 skipped_cooldown: view.skipped_cooldown,
3398 skipped_target_severity: view.skipped_target_severity,
3399 skipped_target_capacity: view.skipped_target_capacity,
3400 skipped_insufficient_improvement: view.skipped_insufficient_improvement,
3401 }
3402 }
3403}
3404
3405#[derive(Clone, Debug, Default)]
3407pub struct SplitSchedulerScratch {
3408 decisions: Vec<HotspotDecision>,
3409 active_decisions: usize,
3410 actions: Vec<SplitAction>,
3411 active_actions: usize,
3412 hotspot: HotspotSplitScratch,
3413 proposal: SplitProposal,
3414 skipped_no_target: usize,
3415 skipped_no_cells: usize,
3416 skipped_cooldown: usize,
3417 skipped_target_severity: usize,
3418 skipped_target_capacity: usize,
3419 skipped_insufficient_improvement: usize,
3420}
3421
3422impl SplitSchedulerScratch {
3423 pub fn new() -> Self {
3425 Self::default()
3426 }
3427
3428 pub fn retained_decision_slots(&self) -> usize {
3430 self.decisions.len()
3431 }
3432
3433 pub fn retained_action_slots(&self) -> usize {
3435 self.actions.len()
3436 }
3437
3438 pub fn retained_reason_capacity(&self) -> usize {
3440 self.decisions
3441 .iter()
3442 .map(|decision| decision.reasons.capacity())
3443 .sum()
3444 }
3445
3446 pub fn retained_action_cell_capacity(&self) -> usize {
3448 self.actions
3449 .iter()
3450 .map(|action| action.proposal.cells_to_move.capacity())
3451 .sum()
3452 }
3453
3454 pub fn retained_candidate_capacity(&self) -> usize {
3456 self.hotspot.candidate_capacity()
3457 }
3458
3459 fn prepare(&mut self, decisions: usize) {
3460 if self.decisions.len() < decisions {
3461 self.decisions
3462 .resize_with(decisions, HotspotDecision::default);
3463 }
3464 self.active_decisions = decisions;
3465 self.active_actions = 0;
3466 self.skipped_no_target = 0;
3467 self.skipped_no_cells = 0;
3468 self.skipped_cooldown = 0;
3469 self.skipped_target_severity = 0;
3470 self.skipped_target_capacity = 0;
3471 self.skipped_insufficient_improvement = 0;
3472 }
3473
3474 pub fn view(&self) -> SplitScheduleView<'_> {
3476 SplitScheduleView {
3477 decisions: &self.decisions[..self.active_decisions],
3478 actions: &self.actions[..self.active_actions],
3479 skipped_no_target: self.skipped_no_target,
3480 skipped_no_cells: self.skipped_no_cells,
3481 skipped_cooldown: self.skipped_cooldown,
3482 skipped_target_severity: self.skipped_target_severity,
3483 skipped_target_capacity: self.skipped_target_capacity,
3484 skipped_insufficient_improvement: self.skipped_insufficient_improvement,
3485 }
3486 }
3487}
3488
3489#[derive(Clone, Debug, Default, PartialEq, Eq)]
3491pub struct SplitSchedulerState {
3492 last_split_at: BTreeMap<StationId, Tick>,
3493}
3494
3495impl SplitSchedulerState {
3496 pub fn last_split_at(&self, station_id: StationId) -> Option<Tick> {
3498 self.last_split_at.get(&station_id).copied()
3499 }
3500
3501 pub fn record_action(&mut self, action: &SplitAction, tick: Tick) {
3503 self.last_split_at.insert(action.source_station, tick);
3504 }
3505
3506 pub fn record_schedule(&mut self, schedule: &SplitSchedule, tick: Tick) {
3508 for action in &schedule.actions {
3509 self.record_action(action, tick);
3510 }
3511 }
3512
3513 pub fn record_schedule_view(&mut self, schedule: SplitScheduleView<'_>, tick: Tick) {
3515 for action in schedule.actions {
3516 self.record_action(action, tick);
3517 }
3518 }
3519
3520 pub fn is_in_cooldown(
3522 &self,
3523 station_id: StationId,
3524 current_tick: Tick,
3525 cooldown_ticks: u64,
3526 ) -> bool {
3527 if cooldown_ticks == 0 {
3528 return false;
3529 }
3530 let Some(last_split) = self.last_split_at(station_id) else {
3531 return false;
3532 };
3533 current_tick.get().saturating_sub(last_split.get()) < cooldown_ticks
3534 }
3535}
3536
3537#[derive(Clone, Debug, Default, PartialEq)]
3539pub struct SplitScheduleExecutionReport {
3540 pub ownership_updates: Vec<CellOwnershipUpdate>,
3542 pub cell_migrations: Vec<CellMigrationReport>,
3544}
3545
3546#[derive(Clone, Copy, Debug, PartialEq)]
3548pub struct SplitScheduleExecutionView<'a> {
3549 pub ownership_updates: &'a [CellOwnershipUpdate],
3551 pub cell_migrations: &'a [CellMigrationReport],
3553}
3554
3555#[derive(Clone, Debug, Default)]
3557pub struct SplitScheduleExecutionScratch {
3558 ownership_updates: Vec<CellOwnershipUpdate>,
3559 cell_migrations: Vec<CellMigrationReport>,
3560 active_actions: usize,
3561 migration: CellMigrationScratch,
3562}
3563
3564impl SplitScheduleExecutionScratch {
3565 pub fn new() -> Self {
3567 Self::default()
3568 }
3569
3570 pub fn reserve(&mut self, actions: usize, cells_per_action: usize, entities_per_action: usize) {
3572 while self.ownership_updates.len() < actions {
3573 self.ownership_updates.push(CellOwnershipUpdate::default());
3574 self.cell_migrations.push(CellMigrationReport::default());
3575 }
3576 for update in &mut self.ownership_updates[..actions] {
3577 if update.moved_cells.capacity() < cells_per_action {
3578 update
3579 .moved_cells
3580 .reserve(cells_per_action.saturating_sub(update.moved_cells.len()));
3581 }
3582 }
3583 for report in &mut self.cell_migrations[..actions] {
3584 if report.scanned_cells.capacity() < cells_per_action {
3585 report
3586 .scanned_cells
3587 .reserve(cells_per_action.saturating_sub(report.scanned_cells.len()));
3588 }
3589 if report.entity_migrations.capacity() < entities_per_action {
3590 report
3591 .entity_migrations
3592 .reserve(entities_per_action.saturating_sub(report.entity_migrations.len()));
3593 }
3594 }
3595 self.migration
3596 .reserve(entities_per_action, entities_per_action);
3597 }
3598
3599 pub fn retained_ownership_slots(&self) -> usize {
3601 self.ownership_updates.len()
3602 }
3603
3604 pub fn retained_migration_slots(&self) -> usize {
3606 self.cell_migrations.len()
3607 }
3608
3609 pub fn retained_update_cell_capacity(&self) -> usize {
3611 self.ownership_updates
3612 .iter()
3613 .map(|update| update.moved_cells.capacity())
3614 .sum()
3615 }
3616
3617 pub fn retained_entity_migration_capacity(&self) -> usize {
3619 self.cell_migrations
3620 .iter()
3621 .map(|report| report.entity_migrations.capacity())
3622 .sum()
3623 }
3624
3625 pub fn retained_candidate_capacity(&self) -> usize {
3627 self.migration.candidate_capacity()
3628 }
3629
3630 fn prepare(&mut self, actions: usize) {
3631 while self.ownership_updates.len() < actions {
3632 self.ownership_updates.push(CellOwnershipUpdate::default());
3633 self.cell_migrations.push(CellMigrationReport::default());
3634 }
3635 self.active_actions = 0;
3636 }
3637
3638 fn view(&self) -> SplitScheduleExecutionView<'_> {
3639 SplitScheduleExecutionView {
3640 ownership_updates: &self.ownership_updates[..self.active_actions],
3641 cell_migrations: &self.cell_migrations[..self.active_actions],
3642 }
3643 }
3644}
3645
3646#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3648pub enum SplitScheduleExecutionError {
3649 MissingSourceIndex(StationId),
3651 MissingTargetIndex(StationId),
3653 CellMigration(CellMigrationError),
3655}
3656
3657impl core::fmt::Display for SplitScheduleExecutionError {
3658 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3659 match self {
3660 Self::MissingSourceIndex(id) => write!(f, "source index {} is missing", id.get()),
3661 Self::MissingTargetIndex(id) => write!(f, "target index {} is missing", id.get()),
3662 Self::CellMigration(error) => write!(f, "{error}"),
3663 }
3664 }
3665}
3666
3667impl std::error::Error for SplitScheduleExecutionError {}
3668
3669impl From<CellMigrationError> for SplitScheduleExecutionError {
3670 fn from(value: CellMigrationError) -> Self {
3671 Self::CellMigration(value)
3672 }
3673}
3674
3675#[derive(Clone, Copy, Debug)]
3677pub struct SplitScheduler {
3678 pub config: SplitSchedulerConfig,
3680}
3681
3682impl SplitScheduler {
3683 pub const fn new(config: SplitSchedulerConfig) -> Self {
3685 Self { config }
3686 }
3687
3688 pub fn plan_into<'a>(
3690 &self,
3691 samples: &[StationLoadSample],
3692 state: Option<&SplitSchedulerState>,
3693 current_tick: Tick,
3694 scratch: &'a mut SplitSchedulerScratch,
3695 ) -> SplitScheduleView<'a> {
3696 scratch.prepare(samples.len());
3697 for (decision, sample) in scratch.decisions[..samples.len()].iter_mut().zip(samples) {
3698 HotspotPlanner::evaluate_into(sample, self.config.thresholds, decision);
3699 }
3700
3701 for (source_index, source) in samples.iter().enumerate() {
3702 if scratch.active_actions >= self.config.max_actions_per_pass {
3703 break;
3704 }
3705 let source_decision = &scratch.decisions[source_index];
3706 if source_decision.severity != HotspotSeverity::Hot {
3707 continue;
3708 }
3709 if state.is_some_and(|state| {
3710 state.is_in_cooldown(
3711 source.station_id,
3712 current_tick,
3713 self.config.split_cooldown_ticks,
3714 )
3715 }) {
3716 scratch.skipped_cooldown = scratch.skipped_cooldown.saturating_add(1);
3717 continue;
3718 }
3719
3720 HotspotPlanner::propose_cell_split_into(
3721 source,
3722 self.config.max_cells_per_action,
3723 &mut scratch.hotspot,
3724 &mut scratch.proposal,
3725 );
3726 if scratch.proposal.cells_to_move.is_empty() {
3727 scratch.skipped_no_cells = scratch.skipped_no_cells.saturating_add(1);
3728 continue;
3729 }
3730 let target_selection = select_split_target(
3731 source,
3732 &scratch.proposal,
3733 samples,
3734 &scratch.decisions[..scratch.active_decisions],
3735 self.config,
3736 );
3737 let Some(target) = target_selection.target else {
3738 if target_selection.considered_targets == 0 {
3739 scratch.skipped_no_target = scratch.skipped_no_target.saturating_add(1);
3740 } else {
3741 scratch.skipped_target_severity = scratch
3742 .skipped_target_severity
3743 .saturating_add(usize::from(target_selection.rejected_by_severity > 0));
3744 scratch.skipped_target_capacity = scratch
3745 .skipped_target_capacity
3746 .saturating_add(usize::from(target_selection.rejected_by_capacity > 0));
3747 scratch.skipped_insufficient_improvement = scratch
3748 .skipped_insufficient_improvement
3749 .saturating_add(usize::from(target_selection.rejected_by_improvement > 0));
3750 }
3751 continue;
3752 };
3753 let target_score = station_load_score(target);
3754 let estimated_target_score_after_move =
3755 target_score.saturating_add(scratch.proposal.moved_pressure_score);
3756 let action_index = scratch.active_actions;
3757 if action_index == scratch.actions.len() {
3758 scratch.actions.push(SplitAction::default());
3759 }
3760 let action = &mut scratch.actions[action_index];
3761 action.source_station = source.station_id;
3762 action.target_station = target.station_id;
3763 action.proposal.source_station = scratch.proposal.source_station;
3764 action.proposal.cells_to_move.clear();
3765 action
3766 .proposal
3767 .cells_to_move
3768 .extend_from_slice(&scratch.proposal.cells_to_move);
3769 action.proposal.moved_pressure_score = scratch.proposal.moved_pressure_score;
3770 action.source_score = station_load_score(source);
3771 action.target_score = target_score;
3772 action.estimated_target_score_after_move = estimated_target_score_after_move;
3773 scratch.active_actions = scratch.active_actions.saturating_add(1);
3774 }
3775
3776 scratch.view()
3777 }
3778
3779 pub fn execute_into<'a>(
3781 &self,
3782 schedule: SplitScheduleView<'_>,
3783 stations: &mut StationSet,
3784 indexes: &mut StationIndexSet,
3785 ownership: &mut CellOwnershipTable,
3786 scratch: &'a mut SplitScheduleExecutionScratch,
3787 ) -> Result<SplitScheduleExecutionView<'a>, SplitScheduleExecutionError> {
3788 self.execute_actions_into(schedule.actions, stations, indexes, ownership, scratch)
3789 }
3790
3791 fn execute_actions_into<'a>(
3792 &self,
3793 actions: &[SplitAction],
3794 stations: &mut StationSet,
3795 indexes: &mut StationIndexSet,
3796 ownership: &mut CellOwnershipTable,
3797 scratch: &'a mut SplitScheduleExecutionScratch,
3798 ) -> Result<SplitScheduleExecutionView<'a>, SplitScheduleExecutionError> {
3799 scratch.prepare(actions.len());
3800
3801 for action in actions {
3802 if indexes.get(action.source_station).is_none() {
3803 return Err(SplitScheduleExecutionError::MissingSourceIndex(
3804 action.source_station,
3805 ));
3806 }
3807 if indexes.get(action.target_station).is_none() {
3808 return Err(SplitScheduleExecutionError::MissingTargetIndex(
3809 action.target_station,
3810 ));
3811 }
3812
3813 let action_index = scratch.active_actions;
3814 let update = &mut scratch.ownership_updates[action_index];
3815 ownership.apply_split_into(&action.proposal, action.target_station, update);
3816 let (source_index, target_index) = indexes
3817 .get_pair_mut(action.source_station, action.target_station)
3818 .expect("indexes were checked above");
3819 CellMigrationExecutor::migrate_cells_into(
3820 stations,
3821 source_index,
3822 target_index,
3823 action.source_station,
3824 action.target_station,
3825 &update.moved_cells,
3826 self.config.ghost_ttl_ticks,
3827 &mut scratch.migration,
3828 &mut scratch.cell_migrations[action_index],
3829 )?;
3830 scratch.active_actions = scratch.active_actions.saturating_add(1);
3831 }
3832
3833 Ok(scratch.view())
3834 }
3835}
3836
3837impl Default for SplitScheduler {
3838 fn default() -> Self {
3839 Self::new(SplitSchedulerConfig::default())
3840 }
3841}
3842
3843#[derive(Clone, Copy, Debug, Default)]
3844struct SplitTargetSelection<'a> {
3845 target: Option<&'a StationLoadSample>,
3846 target_key: Option<(u8, u64, u32)>,
3847 considered_targets: usize,
3848 rejected_by_severity: usize,
3849 rejected_by_capacity: usize,
3850 rejected_by_improvement: usize,
3851}
3852
3853fn select_split_target<'a>(
3854 source: &StationLoadSample,
3855 proposal: &SplitProposal,
3856 samples: &'a [StationLoadSample],
3857 decisions: &[HotspotDecision],
3858 config: SplitSchedulerConfig,
3859) -> SplitTargetSelection<'a> {
3860 let mut selection = SplitTargetSelection::default();
3861 let source_score = station_load_score(source);
3862
3863 for (target, decision) in samples.iter().zip(decisions) {
3864 if target.station_id == source.station_id {
3865 continue;
3866 }
3867 selection.considered_targets += 1;
3868
3869 debug_assert_eq!(decision.station_id, target.station_id);
3870 let severity = decision.severity;
3871 if severity == HotspotSeverity::Hot
3872 || (severity == HotspotSeverity::Warm && !config.allow_warm_targets)
3873 {
3874 selection.rejected_by_severity += 1;
3875 continue;
3876 }
3877
3878 let target_score = station_load_score(target);
3879 if source_score.saturating_sub(target_score) < config.min_score_improvement {
3880 selection.rejected_by_improvement += 1;
3881 continue;
3882 }
3883 if target_score.saturating_add(proposal.moved_pressure_score)
3884 > config.max_target_score_after_move
3885 {
3886 selection.rejected_by_capacity += 1;
3887 continue;
3888 }
3889
3890 let target_key = (
3891 severity_rank(severity),
3892 target_score,
3893 target.station_id.get(),
3894 );
3895 if selection
3896 .target_key
3897 .is_none_or(|current_key| target_key < current_key)
3898 {
3899 selection.target = Some(target);
3900 selection.target_key = Some(target_key);
3901 }
3902 }
3903
3904 selection
3905}
3906
3907fn severity_rank(severity: HotspotSeverity) -> u8 {
3908 match severity {
3909 HotspotSeverity::Normal => 0,
3910 HotspotSeverity::Warm => 1,
3911 HotspotSeverity::Hot => 2,
3912 }
3913}
3914
3915fn station_load_score(sample: &StationLoadSample) -> u64 {
3916 (sample.total_entities() as u64)
3917 .saturating_mul(8)
3918 .saturating_add((sample.subscribers as u64).saturating_mul(4))
3919 .saturating_add(sample.queued_events as u64)
3920 .saturating_add((sample.estimated_bytes / 256) as u64)
3921 .saturating_add(sample.tick_cost_units)
3922}
3923
3924#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3926pub struct EventRouterStats {
3927 pub routed_events: usize,
3929 pub drained_events: usize,
3931 pub dropped_best_effort_events: usize,
3933}
3934
3935#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3937pub enum EventRouterError {
3938 MissingTarget(StationId),
3940 Queue(EventQueueError),
3942}
3943
3944impl core::fmt::Display for EventRouterError {
3945 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3946 match self {
3947 Self::MissingTarget(id) => write!(f, "event target station {} is missing", id.get()),
3948 Self::Queue(error) => write!(f, "{error}"),
3949 }
3950 }
3951}
3952
3953impl std::error::Error for EventRouterError {}
3954
3955impl From<EventQueueError> for EventRouterError {
3956 fn from(value: EventQueueError) -> Self {
3957 Self::Queue(value)
3958 }
3959}
3960
3961#[derive(Clone, Debug)]
3963pub struct EventRouter {
3964 limits: EventQueueLimits,
3965 queues: BTreeMap<StationId, EventQueues>,
3966 stats: EventRouterStats,
3967}
3968
3969impl EventRouter {
3970 pub fn new(limits: EventQueueLimits) -> Self {
3972 Self {
3973 limits,
3974 queues: BTreeMap::new(),
3975 stats: EventRouterStats::default(),
3976 }
3977 }
3978
3979 pub fn register_station(&mut self, station_id: StationId) {
3981 self.queues
3982 .entry(station_id)
3983 .or_insert_with(|| EventQueues::new(self.limits));
3984 }
3985
3986 pub fn register_stations(&mut self, stations: &StationSet) {
3988 for station in stations.iter() {
3989 self.register_station(station.config().station_id);
3990 }
3991 }
3992
3993 pub fn unregister_station(&mut self, station_id: StationId) -> Option<usize> {
3995 self.queues.remove(&station_id).map(|queue| queue.len())
3996 }
3997
3998 pub fn route(&mut self, event: StationEvent) -> Result<PushOutcome, EventRouterError> {
4000 let queue = self
4001 .queues
4002 .get_mut(&event.target)
4003 .ok_or(EventRouterError::MissingTarget(event.target))?;
4004 let outcome = queue.push(event)?;
4005 self.stats.routed_events += 1;
4006 if outcome == PushOutcome::DroppedOldestBestEffort {
4007 self.stats.dropped_best_effort_events += 1;
4008 }
4009 Ok(outcome)
4010 }
4011
4012 pub fn drain_ready(
4014 &mut self,
4015 station_id: StationId,
4016 current_tick: Tick,
4017 ) -> Result<Vec<StationEvent>, EventRouterError> {
4018 let mut ready = Vec::new();
4019 self.drain_ready_into(station_id, current_tick, &mut ready)?;
4020 Ok(ready)
4021 }
4022
4023 pub fn drain_ready_into(
4025 &mut self,
4026 station_id: StationId,
4027 current_tick: Tick,
4028 ready: &mut Vec<StationEvent>,
4029 ) -> Result<(), EventRouterError> {
4030 ready.clear();
4031 self.append_ready(station_id, current_tick, ready)?;
4032 Ok(())
4033 }
4034
4035 fn append_ready(
4036 &mut self,
4037 station_id: StationId,
4038 current_tick: Tick,
4039 ready: &mut Vec<StationEvent>,
4040 ) -> Result<(), EventRouterError> {
4041 let queue = self
4042 .queues
4043 .get_mut(&station_id)
4044 .ok_or(EventRouterError::MissingTarget(station_id))?;
4045 let drained = queue.drain_ready_into(current_tick, ready);
4046 self.stats.drained_events = self.stats.drained_events.saturating_add(drained);
4047 Ok(())
4048 }
4049
4050 pub fn queued_len(&self, station_id: StationId) -> Option<usize> {
4052 self.queues.get(&station_id).map(EventQueues::len)
4053 }
4054
4055 pub const fn stats(&self) -> EventRouterStats {
4057 self.stats
4058 }
4059}
4060
4061impl Default for EventRouter {
4062 fn default() -> Self {
4063 Self::new(EventQueueLimits::default())
4064 }
4065}
4066
4067#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4069pub struct StationEventTransportStats {
4070 pub events_sent: usize,
4072 pub bytes_sent: usize,
4074 pub packets_received: usize,
4076 pub bytes_received: usize,
4078 pub events_routed: usize,
4080}
4081
4082#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4084pub struct StationEventPumpReport {
4085 pub target_station: StationId,
4087 pub packets_received: usize,
4089 pub bytes_received: usize,
4091 pub events_routed: usize,
4093}
4094
4095#[derive(Clone, Debug, PartialEq, Eq)]
4097pub enum StationEventTransportError<E> {
4098 Transport(E),
4100 Encode(BinaryEncodeError),
4102 Decode(BinaryDecodeError),
4104 UnexpectedFrame,
4106 EndpointMismatch {
4108 packet_source: StationId,
4110 packet_target: StationId,
4112 event_source: StationId,
4114 event_target: StationId,
4116 },
4117 Router(EventRouterError),
4119}
4120
4121impl<E: core::fmt::Display> core::fmt::Display for StationEventTransportError<E> {
4122 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
4123 match self {
4124 Self::Transport(error) => write!(f, "{error}"),
4125 Self::Encode(error) => write!(f, "{error}"),
4126 Self::Decode(error) => write!(f, "{error}"),
4127 Self::UnexpectedFrame => f.write_str("station transport packet was not an event frame"),
4128 Self::EndpointMismatch {
4129 packet_source,
4130 packet_target,
4131 event_source,
4132 event_target,
4133 } => write!(
4134 f,
4135 "station event endpoint mismatch: packet {}->{}, event {}->{}",
4136 packet_source.get(),
4137 packet_target.get(),
4138 event_source.get(),
4139 event_target.get()
4140 ),
4141 Self::Router(error) => write!(f, "{error}"),
4142 }
4143 }
4144}
4145
4146impl<E> std::error::Error for StationEventTransportError<E>
4147where
4148 E: std::error::Error + 'static,
4149{
4150 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
4151 match self {
4152 Self::Transport(error) => Some(error),
4153 Self::Encode(error) => Some(error),
4154 Self::Decode(error) => Some(error),
4155 Self::UnexpectedFrame | Self::EndpointMismatch { .. } => None,
4156 Self::Router(error) => Some(error),
4157 }
4158 }
4159}
4160
4161impl<E> From<BinaryEncodeError> for StationEventTransportError<E> {
4162 fn from(value: BinaryEncodeError) -> Self {
4163 Self::Encode(value)
4164 }
4165}
4166
4167impl<E> From<BinaryDecodeError> for StationEventTransportError<E> {
4168 fn from(value: BinaryDecodeError) -> Self {
4169 Self::Decode(value)
4170 }
4171}
4172
4173impl<E> From<EventRouterError> for StationEventTransportError<E> {
4174 fn from(value: EventRouterError) -> Self {
4175 Self::Router(value)
4176 }
4177}
4178
4179#[derive(Clone, Debug, Default)]
4181pub struct StationEventTransportBridge {
4182 stats: StationEventTransportStats,
4183}
4184
4185impl StationEventTransportBridge {
4186 pub const fn stats(&self) -> StationEventTransportStats {
4188 self.stats
4189 }
4190
4191 pub fn send_event<T>(
4193 &mut self,
4194 transport: &mut T,
4195 event: &StationEvent,
4196 ) -> Result<(), StationEventTransportError<T::Error>>
4197 where
4198 T: StationTransportSink,
4199 {
4200 let frame = StationEventFrame::from_event(event);
4201 let mut bytes = Vec::with_capacity(64);
4202 BinaryFrameEncoder.encode_station_event(&frame, &mut bytes)?;
4203 let byte_len = bytes.len();
4204 transport
4205 .send_station(StationOutboundPacket {
4206 source_station: event.source,
4207 target_station: event.target,
4208 bytes,
4209 })
4210 .map_err(StationEventTransportError::Transport)?;
4211 self.stats.events_sent = self.stats.events_sent.saturating_add(1);
4212 self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(byte_len);
4213 Ok(())
4214 }
4215
4216 pub fn pump_target<T>(
4219 &mut self,
4220 transport: &mut T,
4221 router: &mut EventRouter,
4222 target_station: StationId,
4223 max_packets: usize,
4224 ) -> Result<StationEventPumpReport, StationEventTransportError<T::Error>>
4225 where
4226 T: StationTransportReceiver,
4227 {
4228 let mut report = StationEventPumpReport {
4229 target_station,
4230 ..StationEventPumpReport::default()
4231 };
4232 for _ in 0..max_packets {
4233 let Some(packet) = transport
4234 .try_recv_station(target_station)
4235 .map_err(StationEventTransportError::Transport)?
4236 else {
4237 break;
4238 };
4239 report.packets_received = report.packets_received.saturating_add(1);
4240 report.bytes_received = report.bytes_received.saturating_add(packet.bytes.len());
4241
4242 let decoded = BinaryFrameDecoder.decode(&packet.bytes)?;
4243 let RuntimeFrame::StationEvent(frame) = decoded else {
4244 return Err(StationEventTransportError::UnexpectedFrame);
4245 };
4246 if frame.source_station != packet.source_station
4247 || frame.target_station != packet.target_station
4248 {
4249 return Err(StationEventTransportError::EndpointMismatch {
4250 packet_source: packet.source_station,
4251 packet_target: packet.target_station,
4252 event_source: frame.source_station,
4253 event_target: frame.target_station,
4254 });
4255 }
4256
4257 router.route(frame.into_event())?;
4258 report.events_routed = report.events_routed.saturating_add(1);
4259 }
4260
4261 self.stats.packets_received = self
4262 .stats
4263 .packets_received
4264 .saturating_add(report.packets_received);
4265 self.stats.bytes_received = self
4266 .stats
4267 .bytes_received
4268 .saturating_add(report.bytes_received);
4269 self.stats.events_routed = self
4270 .stats
4271 .events_routed
4272 .saturating_add(report.events_routed);
4273 Ok(report)
4274 }
4275}
4276
4277#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4279pub struct CommandDispatchTransportStats {
4280 pub commands_sent: usize,
4282 pub bytes_sent: usize,
4284 pub packets_received: usize,
4286 pub bytes_received: usize,
4288 pub commands_enqueued: usize,
4290 pub commands_rejected_queue: usize,
4292}
4293
4294#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4296pub struct CommandDispatchPumpReport {
4297 pub target_station: StationId,
4299 pub packets_received: usize,
4301 pub bytes_received: usize,
4303 pub commands_enqueued: usize,
4305}
4306
4307#[derive(Clone, Debug, PartialEq, Eq)]
4309pub enum CommandDispatchTransportError<E> {
4310 Transport(E),
4312 Encode(BinaryEncodeError),
4314 Decode(BinaryDecodeError),
4316 UnexpectedFrame,
4318 EndpointMismatch {
4320 packet_source: StationId,
4322 packet_target: StationId,
4324 dispatch_target: StationId,
4326 },
4327 MissingQueue(StationId),
4329 Queue(CommandQueueError),
4331}
4332
4333impl<E: core::fmt::Display> core::fmt::Display for CommandDispatchTransportError<E> {
4334 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
4335 match self {
4336 Self::Transport(error) => write!(f, "{error}"),
4337 Self::Encode(error) => write!(f, "{error}"),
4338 Self::Decode(error) => write!(f, "{error}"),
4339 Self::UnexpectedFrame => {
4340 f.write_str("station transport packet was not a command dispatch frame")
4341 }
4342 Self::EndpointMismatch {
4343 packet_source,
4344 packet_target,
4345 dispatch_target,
4346 } => write!(
4347 f,
4348 "command dispatch endpoint mismatch: packet {}->{}, dispatch target {}",
4349 packet_source.get(),
4350 packet_target.get(),
4351 dispatch_target.get()
4352 ),
4353 Self::MissingQueue(station_id) => {
4354 write!(
4355 f,
4356 "command dispatch target station {} has no queue",
4357 station_id.get()
4358 )
4359 }
4360 Self::Queue(error) => write!(f, "{error}"),
4361 }
4362 }
4363}
4364
4365impl<E> std::error::Error for CommandDispatchTransportError<E>
4366where
4367 E: std::error::Error + 'static,
4368{
4369 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
4370 match self {
4371 Self::Transport(error) => Some(error),
4372 Self::Encode(error) => Some(error),
4373 Self::Decode(error) => Some(error),
4374 Self::UnexpectedFrame | Self::EndpointMismatch { .. } | Self::MissingQueue(_) => None,
4375 Self::Queue(error) => Some(error),
4376 }
4377 }
4378}
4379
4380impl<E> From<BinaryEncodeError> for CommandDispatchTransportError<E> {
4381 fn from(value: BinaryEncodeError) -> Self {
4382 Self::Encode(value)
4383 }
4384}
4385
4386impl<E> From<BinaryDecodeError> for CommandDispatchTransportError<E> {
4387 fn from(value: BinaryDecodeError) -> Self {
4388 Self::Decode(value)
4389 }
4390}
4391
4392impl<E> From<CommandQueueError> for CommandDispatchTransportError<E> {
4393 fn from(value: CommandQueueError) -> Self {
4394 Self::Queue(value)
4395 }
4396}
4397
4398#[derive(Clone, Debug, Default)]
4400pub struct CommandDispatchTransportBridge {
4401 stats: CommandDispatchTransportStats,
4402}
4403
4404impl CommandDispatchTransportBridge {
4405 pub const fn stats(&self) -> CommandDispatchTransportStats {
4407 self.stats
4408 }
4409
4410 pub fn send_envelope<T>(
4412 &mut self,
4413 transport: &mut T,
4414 source_station: StationId,
4415 target_station: StationId,
4416 command: &CommandEnvelope,
4417 ) -> Result<(), CommandDispatchTransportError<T::Error>>
4418 where
4419 T: StationTransportSink,
4420 {
4421 let mut bytes = Vec::with_capacity(64_usize.saturating_add(command.payload.len()));
4422 BinaryFrameEncoder.encode_command_dispatch_envelope(target_station, command, &mut bytes)?;
4423 self.send_encoded(transport, source_station, target_station, bytes)
4424 }
4425
4426 pub fn send_frame<T>(
4428 &mut self,
4429 transport: &mut T,
4430 source_station: StationId,
4431 frame: &CommandDispatchFrame,
4432 ) -> Result<(), CommandDispatchTransportError<T::Error>>
4433 where
4434 T: StationTransportSink,
4435 {
4436 let mut bytes = Vec::with_capacity(64);
4437 BinaryFrameEncoder.encode_command_dispatch(frame, &mut bytes)?;
4438 self.send_encoded(transport, source_station, frame.station_id, bytes)
4439 }
4440
4441 fn send_encoded<T>(
4442 &mut self,
4443 transport: &mut T,
4444 source_station: StationId,
4445 target_station: StationId,
4446 bytes: Vec<u8>,
4447 ) -> Result<(), CommandDispatchTransportError<T::Error>>
4448 where
4449 T: StationTransportSink,
4450 {
4451 let byte_len = bytes.len();
4452 transport
4453 .send_station(StationOutboundPacket {
4454 source_station,
4455 target_station,
4456 bytes,
4457 })
4458 .map_err(CommandDispatchTransportError::Transport)?;
4459 self.stats.commands_sent = self.stats.commands_sent.saturating_add(1);
4460 self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(byte_len);
4461 Ok(())
4462 }
4463
4464 pub fn pump_target<T>(
4467 &mut self,
4468 transport: &mut T,
4469 station_queues: &mut BTreeMap<StationId, CommandQueues>,
4470 target_station: StationId,
4471 max_packets: usize,
4472 ingress: CommandIngress,
4473 ) -> Result<CommandDispatchPumpReport, CommandDispatchTransportError<T::Error>>
4474 where
4475 T: StationTransportReceiver,
4476 {
4477 let mut report = CommandDispatchPumpReport {
4478 target_station,
4479 ..CommandDispatchPumpReport::default()
4480 };
4481 for _ in 0..max_packets {
4482 let Some(packet) = transport
4483 .try_recv_station(target_station)
4484 .map_err(CommandDispatchTransportError::Transport)?
4485 else {
4486 break;
4487 };
4488 report.packets_received = report.packets_received.saturating_add(1);
4489 report.bytes_received = report.bytes_received.saturating_add(packet.bytes.len());
4490
4491 let decoded = BinaryFrameDecoder.decode(&packet.bytes)?;
4492 let RuntimeFrame::CommandDispatch(frame) = decoded else {
4493 return Err(CommandDispatchTransportError::UnexpectedFrame);
4494 };
4495 if frame.station_id != packet.target_station {
4496 return Err(CommandDispatchTransportError::EndpointMismatch {
4497 packet_source: packet.source_station,
4498 packet_target: packet.target_station,
4499 dispatch_target: frame.station_id,
4500 });
4501 }
4502
4503 let queue = station_queues.get_mut(&frame.station_id).ok_or(
4504 CommandDispatchTransportError::MissingQueue(frame.station_id),
4505 )?;
4506 match queue.push(frame.into_envelope(), ingress) {
4507 Ok(_) => {
4508 report.commands_enqueued = report.commands_enqueued.saturating_add(1);
4509 }
4510 Err(error) => {
4511 self.stats.commands_rejected_queue =
4512 self.stats.commands_rejected_queue.saturating_add(1);
4513 return Err(CommandDispatchTransportError::Queue(error));
4514 }
4515 }
4516 }
4517
4518 self.stats.packets_received = self
4519 .stats
4520 .packets_received
4521 .saturating_add(report.packets_received);
4522 self.stats.bytes_received = self
4523 .stats
4524 .bytes_received
4525 .saturating_add(report.bytes_received);
4526 self.stats.commands_enqueued = self
4527 .stats
4528 .commands_enqueued
4529 .saturating_add(report.commands_enqueued);
4530 Ok(report)
4531 }
4532}
4533
4534#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4536pub struct StationScheduleConfig {
4537 pub max_station_advances_per_step: usize,
4539}
4540
4541impl Default for StationScheduleConfig {
4542 fn default() -> Self {
4543 Self {
4544 max_station_advances_per_step: usize::MAX,
4545 }
4546 }
4547}
4548
4549#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4551pub struct StationScheduleCandidate {
4552 pub station_id: StationId,
4554 pub load_score: u64,
4556 pub tick_lag: u64,
4558}
4559
4560#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4563pub struct StationScheduleView<'a> {
4564 pub candidates_considered: usize,
4566 pub stations_selected: usize,
4568 pub total_advances: usize,
4570 pub selected: &'a [StationScheduleCandidate],
4572}
4573
4574#[derive(Clone, Debug, Default)]
4579pub struct StationScheduleScratch {
4580 scores: HashMap<StationId, u64>,
4581 candidates: Vec<StationScheduleCandidate>,
4582}
4583
4584impl StationScheduleScratch {
4585 pub fn new() -> Self {
4587 Self::default()
4588 }
4589
4590 pub fn score_capacity(&self) -> usize {
4592 self.scores.capacity()
4593 }
4594
4595 pub fn candidate_capacity(&self) -> usize {
4597 self.candidates.capacity()
4598 }
4599}
4600
4601#[derive(Clone, Debug, Default)]
4603pub struct StationScheduler {
4604 pub advanced_ticks: u64,
4606}
4607
4608impl StationScheduler {
4609 pub fn advance_all(&mut self, stations: &mut StationSet) {
4611 for station in stations.iter_mut() {
4612 station.advance_tick();
4613 self.advanced_ticks = self.advanced_ticks.saturating_add(1);
4614 }
4615 }
4616
4617 pub fn plan_loaded_into<'a>(
4619 &self,
4620 stations: &StationSet,
4621 samples: &[StationLoadSample],
4622 config: StationScheduleConfig,
4623 scratch: &'a mut StationScheduleScratch,
4624 ) -> StationScheduleView<'a> {
4625 let candidates_considered = stations.len();
4626 let limit = config
4627 .max_station_advances_per_step
4628 .min(candidates_considered);
4629 let max_tick = stations
4630 .iter()
4631 .map(|station| station.tick().get())
4632 .max()
4633 .unwrap_or(0);
4634 scratch.scores.clear();
4635 scratch.scores.reserve(samples.len());
4636 for sample in samples {
4637 scratch
4638 .scores
4639 .insert(sample.station_id, station_schedule_score(sample));
4640 }
4641 scratch.candidates.clear();
4642 scratch.candidates.reserve(candidates_considered);
4643 scratch.candidates.extend(stations.iter().map(|station| {
4644 let station_id = station.config().station_id;
4645 StationScheduleCandidate {
4646 station_id,
4647 load_score: scratch.scores.get(&station_id).copied().unwrap_or(0),
4648 tick_lag: max_tick.saturating_sub(station.tick().get()),
4649 }
4650 }));
4651 prioritize_station_candidates(&mut scratch.candidates, limit);
4652 let selected = &scratch.candidates[..limit];
4653
4654 StationScheduleView {
4655 candidates_considered,
4656 stations_selected: selected.len(),
4657 total_advances: selected.len(),
4658 selected,
4659 }
4660 }
4661
4662 pub fn advance_loaded_into<'a>(
4664 &mut self,
4665 stations: &mut StationSet,
4666 samples: &[StationLoadSample],
4667 config: StationScheduleConfig,
4668 scratch: &'a mut StationScheduleScratch,
4669 ) -> StationScheduleView<'a> {
4670 let plan = self.plan_loaded_into(stations, samples, config, scratch);
4671 for candidate in plan.selected {
4672 if let Some(station) = stations.get_mut(candidate.station_id) {
4673 station.advance_tick();
4674 self.advanced_ticks = self.advanced_ticks.saturating_add(1);
4675 }
4676 }
4677 plan
4678 }
4679
4680 pub fn drain_ready_events_into(
4682 &mut self,
4683 stations: &StationSet,
4684 router: &mut EventRouter,
4685 events: &mut Vec<StationEvent>,
4686 ) -> Result<(), EventRouterError> {
4687 events.clear();
4688 for station in stations.iter() {
4689 router.append_ready(station.config().station_id, station.tick(), events)?;
4690 }
4691 Ok(())
4692 }
4693}
4694
4695fn compare_station_schedule_candidates(
4696 left: &StationScheduleCandidate,
4697 right: &StationScheduleCandidate,
4698) -> core::cmp::Ordering {
4699 right
4700 .load_score
4701 .cmp(&left.load_score)
4702 .then_with(|| right.tick_lag.cmp(&left.tick_lag))
4703 .then_with(|| left.station_id.cmp(&right.station_id))
4704}
4705
4706fn prioritize_station_candidates(candidates: &mut [StationScheduleCandidate], limit: usize) {
4707 if limit == 0 {
4708 return;
4709 }
4710 if limit.saturating_mul(2) < candidates.len() {
4711 candidates.select_nth_unstable_by(limit, compare_station_schedule_candidates);
4712 candidates[..limit].sort_by(compare_station_schedule_candidates);
4713 } else {
4714 candidates.sort_by(compare_station_schedule_candidates);
4715 }
4716}
4717
4718fn station_schedule_score(sample: &StationLoadSample) -> u64 {
4719 station_load_score(sample).saturating_add(sample.max_cell_pressure())
4720}
4721
4722#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4724pub enum StationBarrierPhase {
4725 WaitingTick,
4727 Frozen,
4729 Resumed,
4731}
4732
4733#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4735pub struct BarrierProgress {
4736 pub state: BarrierState,
4738 pub station_count: usize,
4740 pub frozen_count: usize,
4742 pub target_tick: Tick,
4744}
4745
4746#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4748pub struct BarrierMetrics {
4749 pub station_count: usize,
4751 pub snapshots_exported: usize,
4753 pub waiting_polls: u64,
4755 pub frozen_polls: u64,
4757}
4758
4759#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4761pub enum BarrierRuntimeError {
4762 AlreadyActive(BarrierId),
4764 NoActiveBarrier,
4766 EmptyScope(BarrierScope),
4768 NotFrozen(BarrierState),
4770 MissingStation(StationId),
4772}
4773
4774#[derive(Clone, Debug, Default)]
4776pub struct BarrierSnapshotScratch {
4777 snapshots: Vec<StationSnapshot>,
4778 active_snapshots: usize,
4779}
4780
4781impl BarrierSnapshotScratch {
4782 pub fn new() -> Self {
4784 Self::default()
4785 }
4786
4787 pub fn reserve(&mut self, stations: usize, entities_per_station: usize) {
4789 if self.snapshots.len() < stations {
4790 self.snapshots
4791 .resize_with(stations, StationSnapshot::default);
4792 }
4793 for snapshot in &mut self.snapshots[..stations] {
4794 if snapshot.entities.capacity() < entities_per_station {
4795 snapshot
4796 .entities
4797 .reserve(entities_per_station.saturating_sub(snapshot.entities.len()));
4798 }
4799 }
4800 }
4801
4802 pub fn retained_snapshot_slots(&self) -> usize {
4804 self.snapshots.len()
4805 }
4806
4807 pub fn retained_entity_capacity(&self) -> usize {
4809 self.snapshots
4810 .iter()
4811 .map(|snapshot| snapshot.entities.capacity())
4812 .sum()
4813 }
4814
4815 fn prepare(&mut self, stations: usize) {
4816 if self.snapshots.len() < stations {
4817 self.snapshots
4818 .resize_with(stations, StationSnapshot::default);
4819 }
4820 self.active_snapshots = stations;
4821 }
4822
4823 fn active(&self) -> &[StationSnapshot] {
4824 &self.snapshots[..self.active_snapshots]
4825 }
4826}
4827
4828impl core::fmt::Display for BarrierRuntimeError {
4829 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
4830 match self {
4831 Self::AlreadyActive(id) => write!(f, "barrier {} is already active", id.get()),
4832 Self::NoActiveBarrier => f.write_str("no active barrier"),
4833 Self::EmptyScope(scope) => write!(f, "barrier scope {scope:?} contains no stations"),
4834 Self::NotFrozen(state) => {
4835 write!(f, "barrier operation requires Frozen state, got {state:?}")
4836 }
4837 Self::MissingStation(id) => write!(f, "barrier station {} is missing", id.get()),
4838 }
4839 }
4840}
4841
4842impl std::error::Error for BarrierRuntimeError {}
4843
4844#[derive(Clone, Debug, Default)]
4846pub struct BarrierController {
4847 active: Option<RuntimeBarrier>,
4848 phases: BTreeMap<StationId, StationBarrierPhase>,
4849 metrics: BarrierMetrics,
4850}
4851
4852impl BarrierController {
4853 pub const fn active(&self) -> Option<RuntimeBarrier> {
4855 self.active
4856 }
4857
4858 pub fn request(
4860 &mut self,
4861 stations: &StationSet,
4862 id: BarrierId,
4863 scope: BarrierScope,
4864 target_tick: Tick,
4865 command_mode: CommandQueueMode,
4866 ) -> Result<BarrierProgress, BarrierRuntimeError> {
4867 if let Some(active) = self.active {
4868 return Err(BarrierRuntimeError::AlreadyActive(active.id));
4869 }
4870
4871 let station_ids = stations.station_ids_in_scope(scope);
4872 if station_ids.is_empty() {
4873 return Err(BarrierRuntimeError::EmptyScope(scope));
4874 }
4875
4876 let requested_at = station_ids
4877 .iter()
4878 .filter_map(|station_id| stations.get(*station_id).map(Station::tick))
4879 .map(Tick::get)
4880 .max()
4881 .map_or(Tick::new(0), Tick::new);
4882
4883 let mut barrier =
4884 RuntimeBarrier::requested(id, scope, requested_at, target_tick, command_mode);
4885 barrier.wait_for_tick_boundary();
4886
4887 self.metrics = BarrierMetrics {
4888 station_count: station_ids.len(),
4889 ..BarrierMetrics::default()
4890 };
4891 self.phases.clear();
4892 for station_id in station_ids {
4893 self.phases
4894 .insert(station_id, StationBarrierPhase::WaitingTick);
4895 }
4896 self.active = Some(barrier);
4897
4898 Ok(self.progress())
4899 }
4900
4901 pub fn poll(&mut self, stations: &StationSet) -> Result<BarrierProgress, BarrierRuntimeError> {
4903 let Some(mut barrier) = self.active else {
4904 return Err(BarrierRuntimeError::NoActiveBarrier);
4905 };
4906
4907 if matches!(barrier.state, BarrierState::Frozen) {
4908 self.metrics.frozen_polls = self.metrics.frozen_polls.saturating_add(1);
4909 return Ok(self.progress());
4910 }
4911
4912 let mut all_ready = true;
4913 for (station_id, phase) in &mut self.phases {
4914 let station = stations
4915 .get(*station_id)
4916 .ok_or(BarrierRuntimeError::MissingStation(*station_id))?;
4917 if station.tick() >= barrier.target_tick {
4918 *phase = StationBarrierPhase::Frozen;
4919 } else {
4920 all_ready = false;
4921 }
4922 }
4923
4924 if all_ready {
4925 barrier.freeze();
4926 self.active = Some(barrier);
4927 self.metrics.frozen_polls = self.metrics.frozen_polls.saturating_add(1);
4928 } else {
4929 self.metrics.waiting_polls = self.metrics.waiting_polls.saturating_add(1);
4930 }
4931
4932 Ok(self.progress())
4933 }
4934
4935 pub fn export_snapshots(
4937 &mut self,
4938 stations: &StationSet,
4939 version: SnapshotVersion,
4940 ) -> Result<Vec<StationSnapshot>, BarrierRuntimeError> {
4941 let barrier = self.active.ok_or(BarrierRuntimeError::NoActiveBarrier)?;
4942 if barrier.state != BarrierState::Frozen {
4943 return Err(BarrierRuntimeError::NotFrozen(barrier.state));
4944 }
4945
4946 let mut snapshots = Vec::with_capacity(self.phases.len());
4947 for station_id in self.phases.keys().copied() {
4948 let station = stations
4949 .get(station_id)
4950 .ok_or(BarrierRuntimeError::MissingStation(station_id))?;
4951 snapshots.push(station.snapshot(version));
4952 }
4953 self.metrics.snapshots_exported = self
4954 .metrics
4955 .snapshots_exported
4956 .saturating_add(snapshots.len());
4957 Ok(snapshots)
4958 }
4959
4960 pub fn export_snapshots_into<'a>(
4962 &mut self,
4963 stations: &StationSet,
4964 version: SnapshotVersion,
4965 scratch: &'a mut BarrierSnapshotScratch,
4966 ) -> Result<&'a [StationSnapshot], BarrierRuntimeError> {
4967 let barrier = self.active.ok_or(BarrierRuntimeError::NoActiveBarrier)?;
4968 if barrier.state != BarrierState::Frozen {
4969 return Err(BarrierRuntimeError::NotFrozen(barrier.state));
4970 }
4971
4972 scratch.prepare(self.phases.len());
4973 for (snapshot, station_id) in scratch.snapshots[..scratch.active_snapshots]
4974 .iter_mut()
4975 .zip(self.phases.keys().copied())
4976 {
4977 let station = stations
4978 .get(station_id)
4979 .ok_or(BarrierRuntimeError::MissingStation(station_id))?;
4980 station.snapshot_into(version, snapshot);
4981 }
4982 self.metrics.snapshots_exported = self
4983 .metrics
4984 .snapshots_exported
4985 .saturating_add(scratch.active_snapshots);
4986 Ok(scratch.active())
4987 }
4988
4989 pub fn resume(&mut self) -> Result<BarrierMetrics, BarrierRuntimeError> {
4991 let Some(mut barrier) = self.active else {
4992 return Err(BarrierRuntimeError::NoActiveBarrier);
4993 };
4994 if barrier.state != BarrierState::Frozen {
4995 return Err(BarrierRuntimeError::NotFrozen(barrier.state));
4996 }
4997
4998 barrier.resume();
4999 for phase in self.phases.values_mut() {
5000 *phase = StationBarrierPhase::Resumed;
5001 }
5002 barrier.finish();
5003 let metrics = self.metrics;
5004 self.active = None;
5005 self.phases.clear();
5006 self.metrics = BarrierMetrics::default();
5007 Ok(metrics)
5008 }
5009
5010 pub fn progress(&self) -> BarrierProgress {
5012 let state = self
5013 .active
5014 .map_or(BarrierState::Running, |barrier| barrier.state);
5015 let target_tick = self
5016 .active
5017 .map_or(Tick::new(0), |barrier| barrier.target_tick);
5018 let frozen_count = self
5019 .phases
5020 .values()
5021 .filter(|phase| matches!(phase, StationBarrierPhase::Frozen))
5022 .count();
5023
5024 BarrierProgress {
5025 state,
5026 station_count: self.phases.len(),
5027 frozen_count,
5028 target_tick,
5029 }
5030 }
5031}
5032
5033#[derive(Clone, Debug, PartialEq, Eq)]
5035pub struct BarrierUpgradeReport {
5036 pub version: SnapshotVersion,
5038 pub snapshots_migrated: usize,
5040 pub stations_restored: usize,
5042 pub entities_restored: usize,
5044}
5045
5046#[derive(Clone, Debug, PartialEq, Eq)]
5048pub enum BarrierUpgradeError {
5049 Barrier(BarrierRuntimeError),
5051 MissingStation(StationId),
5053 Restore {
5055 station_id: StationId,
5057 error: StationError,
5059 },
5060}
5061
5062impl core::fmt::Display for BarrierUpgradeError {
5063 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
5064 match self {
5065 Self::Barrier(error) => write!(f, "{error}"),
5066 Self::MissingStation(station_id) => {
5067 write!(f, "upgrade station {} is missing", station_id.get())
5068 }
5069 Self::Restore { station_id, error } => {
5070 write!(
5071 f,
5072 "upgrade restore for station {} failed: {error}",
5073 station_id.get()
5074 )
5075 }
5076 }
5077 }
5078}
5079
5080impl std::error::Error for BarrierUpgradeError {
5081 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
5082 match self {
5083 Self::Barrier(error) => Some(error),
5084 Self::Restore { error, .. } => Some(error),
5085 Self::MissingStation(_) => None,
5086 }
5087 }
5088}
5089
5090impl From<BarrierRuntimeError> for BarrierUpgradeError {
5091 fn from(value: BarrierRuntimeError) -> Self {
5092 Self::Barrier(value)
5093 }
5094}
5095
5096#[derive(Clone, Copy, Debug, Default)]
5098pub struct BarrierUpgradeExecutor;
5099
5100impl BarrierUpgradeExecutor {
5101 pub fn migrate_frozen<H>(
5104 controller: &mut BarrierController,
5105 stations: &mut StationSet,
5106 version: SnapshotVersion,
5107 hook: &mut H,
5108 ) -> Result<BarrierUpgradeReport, BarrierUpgradeError>
5109 where
5110 H: RuntimeUpgradeHook,
5111 {
5112 let report_version = version;
5113 let snapshots = controller.export_snapshots(stations, version)?;
5114 let mut restored = Vec::with_capacity(snapshots.len());
5115 let mut entities_restored = 0usize;
5116
5117 for snapshot in snapshots {
5118 let station_id = snapshot.meta.station_id;
5119 let config = stations
5120 .get(station_id)
5121 .ok_or(BarrierUpgradeError::MissingStation(station_id))?
5122 .config();
5123 hook.pre_upgrade(&snapshot.meta);
5124 let migrated = hook.migrate_state(snapshot);
5125 let migrated_meta = migrated.meta.clone();
5126 let restored_station = Station::restore(config, migrated)
5127 .map_err(|error| BarrierUpgradeError::Restore { station_id, error })?;
5128 entities_restored = entities_restored.saturating_add(restored_station.len());
5129 hook.post_upgrade(&migrated_meta);
5130 restored.push((station_id, restored_station));
5131 }
5132
5133 let stations_restored = restored.len();
5134 for (station_id, restored_station) in restored {
5135 let station = stations
5136 .get_mut(station_id)
5137 .ok_or(BarrierUpgradeError::MissingStation(station_id))?;
5138 *station = restored_station;
5139 }
5140
5141 Ok(BarrierUpgradeReport {
5142 version: report_version,
5143 snapshots_migrated: stations_restored,
5144 stations_restored,
5145 entities_restored,
5146 })
5147 }
5148}
5149
5150#[cfg(test)]
5151mod tests {
5152 use super::*;
5153 use sectorsync_core::prelude::{
5154 Bounds, CellCoord3, CellLoadSample, CommandEnvelope, CommandPriority, CommandQueueLimits,
5155 ComponentId, EventId, EventKind, EventPriority, GatewayConfig, GridSpec, HotspotThresholds,
5156 InstanceId, NodeId, PolicyId, Position3, SnapshotMeta, StationConfig, StationLoadSample,
5157 };
5158 use sectorsync_transport::{
5159 ClientTransportLimits, InMemoryStationTransport, InMemoryTransportHub, OutboundPacket,
5160 StationOutboundPacket, StationTransportSink, TransportReceiver, TransportSink,
5161 };
5162 use sectorsync_wire::{
5163 BarrierFrame, BinaryFrameDecoder, BinaryFrameEncoder, CommandAckFrame,
5164 CommandDispatchFrame, CommandFrame, ComponentDelta, EntityDelta, FrameDecoder,
5165 FrameEncoder, ReplicationFrame,
5166 };
5167
5168 fn station(station_id: u32, instance_id: u64) -> Station {
5169 Station::new(StationConfig {
5170 station_id: StationId::new(station_id),
5171 node_id: NodeId::new(0),
5172 instance_id: InstanceId::new(instance_id),
5173 tick_rate_hz: 20,
5174 })
5175 }
5176
5177 fn encode_command_frame(sequence: u64) -> Vec<u8> {
5178 let frame = CommandFrame {
5179 client_id: ClientId::new(7),
5180 command_id: CommandId::new(sequence),
5181 entity_id: EntityId::new(100),
5182 sequence,
5183 kind: 1,
5184 priority: CommandPriority::High,
5185 payload: b"move:north".to_vec(),
5186 };
5187 let mut bytes = Vec::new();
5188 BinaryFrameEncoder
5189 .encode_command(&frame, &mut bytes)
5190 .expect("command should encode");
5191 bytes
5192 }
5193
5194 fn command_queues() -> CommandQueues {
5195 CommandQueues::new(CommandQueueLimits {
5196 high: 4,
5197 normal: 4,
5198 low: 4,
5199 })
5200 }
5201
5202 fn gateway(max_commands_per_tick: usize) -> GatewaySessionTable {
5203 GatewaySessionTable::new(GatewayConfig {
5204 max_sessions: 8,
5205 reconnect_grace_ticks: 10,
5206 max_commands_per_tick,
5207 })
5208 }
5209
5210 #[test]
5211 fn station_set_indexes_first_slot_and_reserves_both_storage_classes() {
5212 let mut stations = StationSet::with_capacity(3);
5213 let mut duplicate = station(1, 99);
5214 duplicate.advance_tick();
5215 stations.push(station(1, 10));
5216 stations.push(duplicate);
5217 stations.push(station(2, 10));
5218
5219 assert!(stations.station_capacity() >= 3);
5220 assert!(!stations.lookup_index_active());
5221 assert_eq!(
5222 stations
5223 .get(StationId::new(1))
5224 .expect("first exists")
5225 .tick(),
5226 Tick::new(0)
5227 );
5228 let (first, second) = stations
5229 .get_pair_mut(StationId::new(1), StationId::new(2))
5230 .expect("distinct indexed Stations should borrow");
5231 first.advance_tick();
5232 second.advance_tick();
5233 assert_eq!(
5234 stations
5235 .get(StationId::new(1))
5236 .expect("first exists")
5237 .tick(),
5238 Tick::new(1)
5239 );
5240 assert_eq!(
5241 stations
5242 .get(StationId::new(2))
5243 .expect("second exists")
5244 .tick(),
5245 Tick::new(1)
5246 );
5247
5248 let lookup_capacity = stations.lookup_capacity();
5249 stations.reserve(4);
5250 assert!(stations.station_capacity() >= stations.len().saturating_add(4));
5251 assert!(stations.lookup_capacity() >= lookup_capacity);
5252 }
5253
5254 #[test]
5255 fn station_index_set_replaces_in_place_and_indexes_mutable_pairs() {
5256 let grid = GridSpec::new(10.0).expect("grid should build");
5257 let first_id = StationId::new(1);
5258 let second_id = StationId::new(2);
5259 let first_handle = EntityHandle::new(1, 0);
5260 let second_handle = EntityHandle::new(2, 0);
5261 let mut indexes = StationIndexSet::with_capacity(2);
5262 indexes.insert(first_id, CellIndex::new(grid));
5263 indexes.insert(second_id, CellIndex::new(grid));
5264
5265 let mut replacement = CellIndex::new(grid);
5266 replacement.upsert(first_handle, Position3::new(1.0, 0.0, 0.0), Bounds::Point);
5267 indexes.insert(first_id, replacement);
5268 assert_eq!(indexes.len(), 2);
5269 assert_eq!(
5270 indexes.iter().map(|(id, _)| id).collect::<Vec<_>>(),
5271 vec![first_id, second_id]
5272 );
5273 assert_eq!(
5274 indexes
5275 .get(first_id)
5276 .expect("first index exists")
5277 .entity_count(),
5278 1
5279 );
5280
5281 let (first, second) = indexes
5282 .get_pair_mut(first_id, second_id)
5283 .expect("distinct indexed cells should borrow");
5284 first.remove(first_handle);
5285 second.upsert(second_handle, Position3::new(11.0, 0.0, 0.0), Bounds::Point);
5286 assert_eq!(
5287 indexes
5288 .get(first_id)
5289 .expect("first index exists")
5290 .entity_count(),
5291 0
5292 );
5293 assert_eq!(
5294 indexes
5295 .get(second_id)
5296 .expect("second index exists")
5297 .entity_count(),
5298 1
5299 );
5300 assert!(indexes.index_capacity() >= 2);
5301 assert!(!indexes.lookup_index_active());
5302 }
5303
5304 #[test]
5305 fn station_registries_activate_lookup_index_at_adaptive_threshold() {
5306 let grid = GridSpec::new(10.0).expect("grid should build");
5307 let mut stations = StationSet::with_capacity(STATION_LOOKUP_INDEX_THRESHOLD);
5308 let mut indexes = StationIndexSet::with_capacity(STATION_LOOKUP_INDEX_THRESHOLD);
5309 for raw_id in 1..=STATION_LOOKUP_INDEX_THRESHOLD {
5310 let station_id = StationId::new(u32::try_from(raw_id).expect("threshold fits u32"));
5311 stations.push(station(station_id.get(), 10));
5312 indexes.insert(station_id, CellIndex::new(grid));
5313 if raw_id < STATION_LOOKUP_INDEX_THRESHOLD {
5314 assert!(!stations.lookup_index_active());
5315 assert!(!indexes.lookup_index_active());
5316 }
5317 }
5318
5319 assert!(stations.lookup_index_active());
5320 assert!(indexes.lookup_index_active());
5321 assert!(stations.lookup_capacity() >= STATION_LOOKUP_INDEX_THRESHOLD);
5322 assert!(indexes.lookup_capacity() >= STATION_LOOKUP_INDEX_THRESHOLD);
5323 let last = StationId::new(
5324 u32::try_from(STATION_LOOKUP_INDEX_THRESHOLD).expect("threshold fits u32"),
5325 );
5326 assert_eq!(
5327 stations
5328 .get(last)
5329 .expect("last Station exists")
5330 .config()
5331 .station_id,
5332 last
5333 );
5334 assert!(indexes.get(last).is_some());
5335
5336 let removed_id = StationId::new(2);
5337 let removed_station = stations.remove(removed_id).expect("Station should remove");
5338 let removed_index = indexes.remove(removed_id).expect("index should remove");
5339 assert_eq!(removed_station.config().station_id, removed_id);
5340 assert_eq!(removed_index.entity_count(), 0);
5341 assert!(stations.get(removed_id).is_none());
5342 assert!(indexes.get(removed_id).is_none());
5343 assert_eq!(
5344 stations
5345 .get(last)
5346 .expect("shifted Station resolves")
5347 .config()
5348 .station_id,
5349 last
5350 );
5351 assert!(indexes.get(last).is_some());
5352 assert_eq!(
5353 stations
5354 .iter()
5355 .map(|station| station.config().station_id)
5356 .nth(1),
5357 Some(StationId::new(3))
5358 );
5359 assert_eq!(
5360 indexes.iter().nth(1).map(|(id, _)| id),
5361 Some(StationId::new(3))
5362 );
5363 assert!(stations.lookup_index_active());
5364 assert!(indexes.lookup_index_active());
5365 }
5366
5367 #[test]
5368 fn barrier_freezes_snapshots_and_resumes_instance_scope() {
5369 let mut stations = StationSet::default();
5370 stations.push(station(1, 10));
5371 stations.push(station(2, 10));
5372
5373 for station in stations.iter_mut() {
5374 station.advance_tick();
5375 station.advance_tick();
5376 }
5377
5378 let mut controller = BarrierController::default();
5379 let requested = controller
5380 .request(
5381 &stations,
5382 BarrierId::new(7),
5383 BarrierScope::Instance(InstanceId::new(10)),
5384 Tick::new(2),
5385 CommandQueueMode::Buffer,
5386 )
5387 .expect("request should work");
5388 assert_eq!(requested.state, BarrierState::WaitingTickBoundary);
5389
5390 let frozen = controller.poll(&stations).expect("poll should work");
5391 assert_eq!(frozen.state, BarrierState::Frozen);
5392 assert_eq!(frozen.frozen_count, 2);
5393
5394 let mut scratch = BarrierSnapshotScratch::new();
5395 scratch.reserve(2, 1);
5396 let snapshots = controller
5397 .export_snapshots_into(&stations, SnapshotVersion::default(), &mut scratch)
5398 .expect("reusable snapshot should work while frozen");
5399 assert_eq!(snapshots.len(), 2);
5400 let retained_slots = scratch.retained_snapshot_slots();
5401 let retained_entities = scratch.retained_entity_capacity();
5402 scratch.reserve(2, 1);
5403 let snapshots = controller
5404 .export_snapshots_into(&stations, SnapshotVersion::default(), &mut scratch)
5405 .expect("second reusable snapshot should work while frozen");
5406 assert_eq!(snapshots.len(), 2);
5407 assert_eq!(scratch.retained_snapshot_slots(), retained_slots);
5408 assert_eq!(scratch.retained_entity_capacity(), retained_entities);
5409
5410 let metrics = controller.resume().expect("resume should work");
5411 assert_eq!(metrics.station_count, 2);
5412 assert_eq!(metrics.snapshots_exported, 4);
5413 assert_eq!(controller.progress().state, BarrierState::Running);
5414 }
5415
5416 #[derive(Default)]
5417 struct MoveSnapshotUpgrade {
5418 pre: usize,
5419 migrations: usize,
5420 post: usize,
5421 }
5422
5423 impl RuntimeUpgradeHook for MoveSnapshotUpgrade {
5424 fn pre_upgrade(&mut self, meta: &SnapshotMeta) {
5425 self.pre = self.pre.saturating_add(1);
5426 assert_eq!(meta.version.runtime_version, 2);
5427 }
5428
5429 fn migrate_state(&mut self, mut snapshot: StationSnapshot) -> StationSnapshot {
5430 self.migrations = self.migrations.saturating_add(1);
5431 for entity in &mut snapshot.entities {
5432 entity.position.x += 10.0;
5433 }
5434 snapshot
5435 }
5436
5437 fn post_upgrade(&mut self, meta: &SnapshotMeta) {
5438 self.post = self.post.saturating_add(1);
5439 assert_eq!(meta.version.runtime_version, 2);
5440 }
5441 }
5442
5443 #[test]
5444 fn barrier_upgrade_executor_migrates_and_restores_frozen_snapshots() {
5445 let mut first = station(1, 10);
5446 first
5447 .spawn_owned(
5448 EntityId::new(100),
5449 Position3::new(1.0, 2.0, 3.0),
5450 Bounds::Point,
5451 PolicyId::new(0),
5452 )
5453 .expect("spawn should work");
5454 let mut stations = StationSet::default();
5455 stations.push(first);
5456 stations.push(station(2, 10));
5457
5458 for station in stations.iter_mut() {
5459 station.advance_tick();
5460 station.advance_tick();
5461 }
5462
5463 let mut controller = BarrierController::default();
5464 controller
5465 .request(
5466 &stations,
5467 BarrierId::new(8),
5468 BarrierScope::Instance(InstanceId::new(10)),
5469 Tick::new(2),
5470 CommandQueueMode::Buffer,
5471 )
5472 .expect("request should work");
5473 assert_eq!(
5474 controller.poll(&stations).expect("poll should work").state,
5475 BarrierState::Frozen
5476 );
5477
5478 let mut hook = MoveSnapshotUpgrade::default();
5479 let version = SnapshotVersion {
5480 runtime_version: 2,
5481 ..SnapshotVersion::default()
5482 };
5483 let report = BarrierUpgradeExecutor::migrate_frozen(
5484 &mut controller,
5485 &mut stations,
5486 version,
5487 &mut hook,
5488 )
5489 .expect("upgrade should migrate frozen snapshots");
5490
5491 assert_eq!(report.version, version);
5492 assert_eq!(report.snapshots_migrated, 2);
5493 assert_eq!(report.stations_restored, 2);
5494 assert_eq!(report.entities_restored, 1);
5495 assert_eq!(hook.pre, 2);
5496 assert_eq!(hook.migrations, 2);
5497 assert_eq!(hook.post, 2);
5498 let moved = stations
5499 .get(StationId::new(1))
5500 .expect("station should exist")
5501 .get_by_id(EntityId::new(100))
5502 .expect("entity should restore");
5503 assert_eq!(moved.position, Position3::new(11.0, 2.0, 3.0));
5504 assert_eq!(controller.progress().state, BarrierState::Frozen);
5505
5506 let metrics = controller.resume().expect("resume should work");
5507 assert_eq!(metrics.snapshots_exported, 2);
5508 assert_eq!(controller.progress().state, BarrierState::Running);
5509 }
5510
5511 #[test]
5512 fn barrier_transport_bridge_broadcasts_client_notifications() {
5513 let server_id = ClientId::new(0);
5514 let clients = [ClientId::new(7), ClientId::new(8)];
5515 let hub = InMemoryTransportHub::new(ClientTransportLimits {
5516 max_queued_packets_per_client: 4,
5517 max_packet_bytes: 512,
5518 });
5519 let mut server_transport = hub
5520 .endpoint(server_id, "127.0.0.1:23400".parse().expect("server addr"))
5521 .expect("server endpoint should register");
5522 let mut client_transports = clients
5523 .into_iter()
5524 .enumerate()
5525 .map(|(index, client_id)| {
5526 hub.endpoint(
5527 client_id,
5528 format!("127.0.0.1:{}", 23407 + index)
5529 .parse()
5530 .expect("client addr"),
5531 )
5532 .expect("client endpoint should register")
5533 })
5534 .collect::<Vec<_>>();
5535 let mut barrier = RuntimeBarrier::requested(
5536 BarrierId::new(5),
5537 BarrierScope::Instance(InstanceId::new(10)),
5538 Tick::new(10),
5539 Tick::new(12),
5540 CommandQueueMode::Buffer,
5541 );
5542 barrier.wait_for_tick_boundary();
5543 barrier.freeze();
5544
5545 let mut bridge = BarrierTransportBridge::default();
5546 let report = bridge
5547 .broadcast_barrier(&mut server_transport, clients, barrier)
5548 .expect("barrier should broadcast");
5549
5550 assert_eq!(report.barrier_id, barrier.id);
5551 assert_eq!(report.state, BarrierState::Frozen);
5552 assert_eq!(report.server_tick, Tick::new(12));
5553 assert_eq!(report.clients_requested, 2);
5554 assert_eq!(report.clients_sent, 2);
5555 assert!(report.bytes_sent > 0);
5556 assert_eq!(bridge.stats().notifications_sent, 2);
5557 assert_eq!(bridge.stats().clients_notified, 2);
5558 assert_eq!(bridge.stats().bytes_sent, report.bytes_sent);
5559
5560 for (index, client_id) in clients.into_iter().enumerate() {
5561 let mut client_bridge = ClientTransportBridge::new(
5562 ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
5563 );
5564 let pump = client_bridge
5565 .pump_owned(&mut client_transports[index], 2)
5566 .expect("client should receive barrier");
5567 assert_eq!(pump.barrier_frames_received(), 1);
5568 assert_eq!(
5569 pump.barriers[0],
5570 BarrierFrame {
5571 client_id,
5572 barrier_id: barrier.id,
5573 server_tick: barrier.target_tick,
5574 state: BarrierState::Frozen,
5575 }
5576 );
5577 }
5578 }
5579
5580 #[test]
5581 fn replication_receive_bridge_decodes_target_frames() {
5582 let client_id = ClientId::new(7);
5583 let server_id = ClientId::new(0);
5584 let hub = InMemoryTransportHub::new(ClientTransportLimits {
5585 max_queued_packets_per_client: 4,
5586 max_packet_bytes: 512,
5587 });
5588 let mut client_transport = hub
5589 .endpoint(client_id, "127.0.0.1:23007".parse().expect("client addr"))
5590 .expect("client endpoint should register");
5591 let mut server_transport = hub
5592 .endpoint(server_id, "127.0.0.1:23000".parse().expect("server addr"))
5593 .expect("server endpoint should register");
5594 let frame = ReplicationFrame {
5595 client_id,
5596 server_tick: Tick::new(12),
5597 entity_count: 1,
5598 estimated_payload_bytes: 4,
5599 entities: vec![EntityDelta {
5600 entity_id: EntityId::new(100),
5601 owner_epoch: OwnerEpoch::new(1),
5602 components: vec![ComponentDelta {
5603 component_id: ComponentId::new(1),
5604 version: 1,
5605 flags: 0,
5606 bytes: 100_u32.to_le_bytes().to_vec(),
5607 }],
5608 }],
5609 };
5610 let mut bytes = Vec::new();
5611 BinaryFrameEncoder
5612 .encode_replication(&frame, &mut bytes)
5613 .expect("replication should encode");
5614 server_transport
5615 .send(OutboundPacket {
5616 client_id,
5617 bytes: bytes.clone(),
5618 })
5619 .expect("replication packet should send");
5620
5621 let mut receive = ReplicationReceiveBridge::new(
5622 ReplicationReceiveConfig::new(client_id).with_expected_source(server_id),
5623 );
5624 let pump = receive
5625 .pump_owned(&mut client_transport, 4)
5626 .expect("replication packet should receive");
5627
5628 assert_eq!(pump.frames_received(), 1);
5629 assert_eq!(pump.entities_received(), 1);
5630 assert_eq!(pump.components_received(), 1);
5631 assert_eq!(pump.frames[0].client_id, client_id);
5632 assert_eq!(receive.stats().packets_received, 1);
5633 assert_eq!(receive.stats().frames_received, 1);
5634 assert_eq!(receive.stats().entities_received, 1);
5635 assert_eq!(receive.stats().components_received, 1);
5636 assert!(receive.stats().bytes_received > 0);
5637
5638 server_transport
5639 .send(OutboundPacket {
5640 client_id,
5641 bytes: bytes.clone(),
5642 })
5643 .expect("visitor packet should send");
5644 let mut visited_payload = 0_u32;
5645 let visit = receive
5646 .pump(&mut client_transport, 4, |borrowed| {
5647 assert_eq!(borrowed.client_id, client_id);
5648 for entity in borrowed.entities() {
5649 assert_eq!(entity.entity_id, EntityId::new(100));
5650 for component in entity.components() {
5651 visited_payload = u32::from_le_bytes(
5652 component
5653 .bytes
5654 .try_into()
5655 .expect("health payload should be four bytes"),
5656 );
5657 }
5658 }
5659 Ok::<_, core::convert::Infallible>(())
5660 })
5661 .expect("borrowed replication packet should visit");
5662 assert_eq!(visited_payload, 100);
5663 assert_eq!(visit.packets_received, 1);
5664 assert_eq!(visit.frames_received, 1);
5665 assert_eq!(visit.entities_received, 1);
5666 assert_eq!(visit.components_received, 1);
5667
5668 server_transport
5669 .send(OutboundPacket { client_id, bytes })
5670 .expect("visitor failure packet should send");
5671 let error = receive
5672 .pump(&mut client_transport, 4, |_| Err("apply failed"))
5673 .expect_err("visitor failure should surface separately");
5674 assert!(matches!(
5675 error,
5676 ReplicationReceiveVisitError::Visitor("apply failed")
5677 ));
5678 assert_eq!(receive.stats().packets_received, 3);
5679 assert_eq!(receive.stats().frames_received, 3);
5680 assert_eq!(receive.stats().entities_received, 3);
5681 assert_eq!(receive.stats().components_received, 3);
5682 }
5683
5684 #[test]
5685 fn replication_receive_bridge_rejects_wrong_target() {
5686 let client_id = ClientId::new(7);
5687 let server_id = ClientId::new(0);
5688 let wrong_client_id = ClientId::new(99);
5689 let hub = InMemoryTransportHub::new(ClientTransportLimits {
5690 max_queued_packets_per_client: 4,
5691 max_packet_bytes: 512,
5692 });
5693 let mut client_transport = hub
5694 .endpoint(client_id, "127.0.0.1:23107".parse().expect("client addr"))
5695 .expect("client endpoint should register");
5696 let mut server_transport = hub
5697 .endpoint(server_id, "127.0.0.1:23100".parse().expect("server addr"))
5698 .expect("server endpoint should register");
5699 let frame = ReplicationFrame {
5700 client_id: wrong_client_id,
5701 server_tick: Tick::new(12),
5702 entity_count: 1,
5703 estimated_payload_bytes: 4,
5704 entities: vec![EntityDelta {
5705 entity_id: EntityId::new(100),
5706 owner_epoch: OwnerEpoch::new(1),
5707 components: vec![ComponentDelta {
5708 component_id: ComponentId::new(1),
5709 version: 1,
5710 flags: 0,
5711 bytes: 100_u32.to_le_bytes().to_vec(),
5712 }],
5713 }],
5714 };
5715 let mut bytes = Vec::new();
5716 BinaryFrameEncoder
5717 .encode_replication(&frame, &mut bytes)
5718 .expect("replication should encode");
5719 server_transport
5720 .send(OutboundPacket { client_id, bytes })
5721 .expect("replication packet should send");
5722
5723 let mut receive = ReplicationReceiveBridge::new(
5724 ReplicationReceiveConfig::new(client_id).with_expected_source(server_id),
5725 );
5726 let error = receive
5727 .pump_owned(&mut client_transport, 4)
5728 .expect_err("wrong target should be rejected");
5729
5730 assert!(matches!(
5731 error,
5732 ReplicationReceiveError::TargetMismatch {
5733 expected,
5734 actual,
5735 } if expected == client_id && actual == wrong_client_id
5736 ));
5737 assert_eq!(receive.stats().packets_received, 1);
5738 assert_eq!(receive.stats().frames_received, 0);
5739 assert_eq!(receive.stats().frames_rejected_target, 1);
5740 }
5741
5742 #[test]
5743 #[allow(clippy::too_many_lines)]
5744 fn client_transport_bridge_sends_command_and_receives_client_frames() {
5745 let client_id = ClientId::new(7);
5746 let server_id = ClientId::new(0);
5747 let hub = InMemoryTransportHub::new(ClientTransportLimits {
5748 max_queued_packets_per_client: 8,
5749 max_packet_bytes: 512,
5750 });
5751 let mut client_transport = hub
5752 .endpoint(client_id, "127.0.0.1:23207".parse().expect("client addr"))
5753 .expect("client endpoint should register");
5754 let mut server_transport = hub
5755 .endpoint(server_id, "127.0.0.1:23200".parse().expect("server addr"))
5756 .expect("server endpoint should register");
5757 let mut bridge = ClientTransportBridge::new(
5758 ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
5759 );
5760 let command = CommandFrame {
5761 client_id,
5762 command_id: CommandId::new(42),
5763 entity_id: EntityId::new(100),
5764 sequence: 9,
5765 kind: 1,
5766 priority: CommandPriority::High,
5767 payload: b"move:north".to_vec(),
5768 };
5769
5770 let send = bridge
5771 .send_command_frame(&mut client_transport, &command)
5772 .expect("command should send");
5773 assert_eq!(send.command_id, command.command_id);
5774 assert!(send.bytes_sent > 0);
5775 assert_eq!(bridge.stats().commands_sent, 1);
5776 assert_eq!(bridge.stats().command_bytes_sent, send.bytes_sent);
5777 let inbound = server_transport
5778 .try_recv()
5779 .expect("server receive should work")
5780 .expect("command packet should arrive");
5781 assert_eq!(inbound.client_id, Some(client_id));
5782 let RuntimeFrame::Command(decoded) = BinaryFrameDecoder
5783 .decode(&inbound.bytes)
5784 .expect("command should decode")
5785 else {
5786 panic!("expected command frame");
5787 };
5788 assert_eq!(decoded, command);
5789
5790 let ack = CommandAckFrame {
5791 client_id,
5792 command_id: command.command_id,
5793 server_tick: Tick::new(12),
5794 accepted: true,
5795 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
5796 };
5797 let mut ack_bytes = Vec::new();
5798 BinaryFrameEncoder
5799 .encode_command_ack(&ack, &mut ack_bytes)
5800 .expect("ACK should encode");
5801 server_transport
5802 .send(OutboundPacket {
5803 client_id,
5804 bytes: ack_bytes.clone(),
5805 })
5806 .expect("ACK should send");
5807
5808 let replication = ReplicationFrame {
5809 client_id,
5810 server_tick: Tick::new(12),
5811 entity_count: 1,
5812 estimated_payload_bytes: 4,
5813 entities: vec![EntityDelta {
5814 entity_id: EntityId::new(100),
5815 owner_epoch: OwnerEpoch::new(1),
5816 components: vec![ComponentDelta {
5817 component_id: ComponentId::new(1),
5818 version: 1,
5819 flags: 0,
5820 bytes: 100_u32.to_le_bytes().to_vec(),
5821 }],
5822 }],
5823 };
5824 let mut replication_bytes = Vec::new();
5825 BinaryFrameEncoder
5826 .encode_replication(&replication, &mut replication_bytes)
5827 .expect("replication should encode");
5828 server_transport
5829 .send(OutboundPacket {
5830 client_id,
5831 bytes: replication_bytes.clone(),
5832 })
5833 .expect("replication should send");
5834
5835 let barrier = BarrierFrame {
5836 client_id,
5837 barrier_id: BarrierId::new(5),
5838 server_tick: Tick::new(12),
5839 state: BarrierState::Frozen,
5840 };
5841 let mut barrier_bytes = Vec::new();
5842 BinaryFrameEncoder
5843 .encode_barrier(&barrier, &mut barrier_bytes)
5844 .expect("barrier should encode");
5845 server_transport
5846 .send(OutboundPacket {
5847 client_id,
5848 bytes: barrier_bytes.clone(),
5849 })
5850 .expect("barrier should send");
5851
5852 let pump = bridge
5853 .pump_owned(&mut client_transport, 8)
5854 .expect("client frames should receive");
5855
5856 assert_eq!(pump.packets_received, 3);
5857 assert_eq!(pump.command_acks_received(), 1);
5858 assert_eq!(pump.replication_frames_received(), 1);
5859 assert_eq!(pump.barrier_frames_received(), 1);
5860 assert_eq!(pump.entities_received(), 1);
5861 assert_eq!(pump.components_received(), 1);
5862 assert_eq!(pump.command_acks[0], ack);
5863 assert_eq!(pump.replication_frames[0], replication);
5864 assert_eq!(pump.barriers[0], barrier);
5865 assert_eq!(bridge.stats().packets_received, 3);
5866 assert_eq!(bridge.stats().command_acks_received, 1);
5867 assert_eq!(bridge.stats().replication_frames_received, 1);
5868 assert_eq!(bridge.stats().barrier_frames_received, 1);
5869 assert_eq!(bridge.stats().entities_received, 1);
5870 assert_eq!(bridge.stats().components_received, 1);
5871
5872 for bytes in [ack_bytes.clone(), replication_bytes, barrier_bytes] {
5873 server_transport
5874 .send(OutboundPacket { client_id, bytes })
5875 .expect("visitor packet should send");
5876 }
5877 let mut visited_ack = 0_usize;
5878 let mut visited_replication = 0_usize;
5879 let mut visited_barrier = 0_usize;
5880 let mut payload_checksum = 0_u64;
5881 let visit = bridge
5882 .pump(&mut client_transport, 8, |frame| {
5883 match frame {
5884 ClientInboundFrameRef::CommandAck(frame) => {
5885 assert_eq!(frame, ack);
5886 visited_ack = visited_ack.saturating_add(1);
5887 }
5888 ClientInboundFrameRef::Replication(frame) => {
5889 assert_eq!(frame.client_id, client_id);
5890 assert_eq!(frame.encoded_entity_count(), 1);
5891 for entity in frame.entities() {
5892 for component in entity.components() {
5893 payload_checksum = payload_checksum.saturating_add(
5894 component.bytes.iter().map(|byte| u64::from(*byte)).sum(),
5895 );
5896 }
5897 }
5898 visited_replication = visited_replication.saturating_add(1);
5899 }
5900 ClientInboundFrameRef::Barrier(frame) => {
5901 assert_eq!(frame, barrier);
5902 visited_barrier = visited_barrier.saturating_add(1);
5903 }
5904 }
5905 Ok::<(), &'static str>(())
5906 })
5907 .expect("mixed visitor pump should work");
5908 assert_eq!(visit.packets_received, 3);
5909 assert_eq!(visit.command_acks_received, 1);
5910 assert_eq!(visit.replication_frames_received, 1);
5911 assert_eq!(visit.barrier_frames_received, 1);
5912 assert_eq!(visit.entities_received, 1);
5913 assert_eq!(visit.components_received, 1);
5914 assert_eq!(
5915 (visited_ack, visited_replication, visited_barrier),
5916 (1, 1, 1)
5917 );
5918 assert_eq!(payload_checksum, 100);
5919 assert_eq!(bridge.stats().packets_received, 6);
5920
5921 server_transport
5922 .send(OutboundPacket {
5923 client_id,
5924 bytes: ack_bytes,
5925 })
5926 .expect("failing visitor packet should send");
5927 let visitor_error = bridge
5928 .pump(&mut client_transport, 1, |_| Err("apply failed"))
5929 .expect_err("visitor failure should propagate");
5930 assert_eq!(
5931 visitor_error,
5932 ClientTransportVisitError::Visitor("apply failed")
5933 );
5934 assert_eq!(bridge.stats().packets_received, 7);
5935 assert_eq!(bridge.stats().command_acks_received, 3);
5936 }
5937
5938 #[test]
5939 fn client_transport_bridge_rejects_wrong_ack_target() {
5940 let client_id = ClientId::new(7);
5941 let server_id = ClientId::new(0);
5942 let wrong_client_id = ClientId::new(99);
5943 let hub = InMemoryTransportHub::new(ClientTransportLimits {
5944 max_queued_packets_per_client: 4,
5945 max_packet_bytes: 512,
5946 });
5947 let mut client_transport = hub
5948 .endpoint(client_id, "127.0.0.1:23307".parse().expect("client addr"))
5949 .expect("client endpoint should register");
5950 let mut server_transport = hub
5951 .endpoint(server_id, "127.0.0.1:23300".parse().expect("server addr"))
5952 .expect("server endpoint should register");
5953 let mut bridge = ClientTransportBridge::new(
5954 ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
5955 );
5956 let ack = CommandAckFrame {
5957 client_id: wrong_client_id,
5958 command_id: CommandId::new(42),
5959 server_tick: Tick::new(12),
5960 accepted: true,
5961 reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
5962 };
5963 let mut ack_bytes = Vec::new();
5964 BinaryFrameEncoder
5965 .encode_command_ack(&ack, &mut ack_bytes)
5966 .expect("ACK should encode");
5967 server_transport
5968 .send(OutboundPacket {
5969 client_id,
5970 bytes: ack_bytes,
5971 })
5972 .expect("ACK should send");
5973
5974 let error = bridge
5975 .pump_owned(&mut client_transport, 4)
5976 .expect_err("wrong target should be rejected");
5977
5978 assert!(matches!(
5979 error,
5980 ClientTransportBridgeError::TargetMismatch {
5981 kind: ClientInboundFrameKind::CommandAck,
5982 expected,
5983 actual,
5984 } if expected == client_id && actual == wrong_client_id
5985 ));
5986 assert_eq!(bridge.stats().packets_received, 1);
5987 assert_eq!(bridge.stats().command_acks_received, 0);
5988 assert_eq!(bridge.stats().frames_rejected_target, 1);
5989 }
5990
5991 #[test]
5992 #[allow(clippy::too_many_lines)]
5993 fn gateway_client_transport_bridge_queues_command_and_sends_ack() {
5994 let client_id = ClientId::new(7);
5995 let server_id = ClientId::new(0);
5996 let station_id = StationId::new(1);
5997 let hub = InMemoryTransportHub::new(ClientTransportLimits {
5998 max_queued_packets_per_client: 8,
5999 max_packet_bytes: 512,
6000 });
6001 let mut client_transport = hub
6002 .endpoint(client_id, "127.0.0.1:23507".parse().expect("client addr"))
6003 .expect("client endpoint should register");
6004 let mut server_transport = hub
6005 .endpoint(server_id, "127.0.0.1:23500".parse().expect("server addr"))
6006 .expect("server endpoint should register");
6007 let mut client_bridge = ClientTransportBridge::new(
6008 ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
6009 );
6010 let command = CommandFrame {
6011 client_id,
6012 command_id: CommandId::new(42),
6013 entity_id: EntityId::new(100),
6014 sequence: 9,
6015 kind: 1,
6016 priority: CommandPriority::High,
6017 payload: b"move:north".to_vec(),
6018 };
6019 client_bridge
6020 .send_command_frame(&mut client_transport, &command)
6021 .expect("client command should send");
6022
6023 let mut gateway = gateway(4);
6024 gateway
6025 .connect(client_id, station_id, Tick::new(10))
6026 .expect("client should connect");
6027 let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
6028 let mut pipeline = GatewayCommandPipeline::default();
6029 let mut gateway_bridge = GatewayClientTransportBridge::default();
6030
6031 let pump = gateway_bridge
6032 .pump_ingress(
6033 &mut server_transport,
6034 &mut pipeline,
6035 &mut gateway,
6036 &mut station_queues,
6037 Tick::new(10),
6038 CommandIngress::RUNNING,
6039 4,
6040 )
6041 .expect("gateway client transport should pump");
6042
6043 assert_eq!(pump.packets_received, 1);
6044 assert_eq!(pump.commands_processed(), 1);
6045 assert_eq!(pump.commands_accepted(), 1);
6046 assert_eq!(pump.acks_sent, 1);
6047 assert_eq!(gateway_bridge.stats().packets_received, 1);
6048 assert_eq!(gateway_bridge.stats().command_frames_received, 1);
6049 assert_eq!(gateway_bridge.stats().commands_accepted, 1);
6050 assert_eq!(gateway_bridge.stats().acks_sent, 1);
6051 let queued = station_queues
6052 .get_mut(&station_id)
6053 .expect("station queue should exist")
6054 .pop_next()
6055 .expect("command should queue");
6056 assert_eq!(queued.id, command.command_id);
6057
6058 let ack_pump = client_bridge
6059 .pump_owned(&mut client_transport, 4)
6060 .expect("client should receive ACK");
6061 assert_eq!(ack_pump.command_acks_received(), 1);
6062 assert!(ack_pump.command_acks[0].accepted);
6063 assert_eq!(ack_pump.command_acks[0].command_id, command.command_id);
6064
6065 let compact_command = CommandFrame {
6066 command_id: CommandId::new(43),
6067 sequence: 10,
6068 ..command
6069 };
6070 client_bridge
6071 .send_command_frame(&mut client_transport, &compact_command)
6072 .expect("second client command should send");
6073 let summary = gateway_bridge
6074 .pump_ingress_compact(
6075 &mut server_transport,
6076 &mut pipeline,
6077 &mut gateway,
6078 &mut station_queues,
6079 Tick::new(11),
6080 CommandIngress::RUNNING,
6081 4,
6082 )
6083 .expect("compact gateway transport should pump");
6084 assert_eq!(summary.packets_received, 1);
6085 assert_eq!(summary.commands_accepted, 1);
6086 assert_eq!(summary.commands_rejected, 0);
6087 assert_eq!(summary.acks_sent, 1);
6088 assert!(summary.ack_bytes_sent > 0);
6089 let compact_queued = station_queues
6090 .get_mut(&station_id)
6091 .expect("station queue should exist")
6092 .pop_next()
6093 .expect("compact command should queue");
6094 assert_eq!(compact_queued.id, compact_command.command_id);
6095 let compact_ack = client_bridge
6096 .pump_owned(&mut client_transport, 4)
6097 .expect("client should receive compact ACK");
6098 assert_eq!(compact_ack.command_acks_received(), 1);
6099 assert_eq!(
6100 compact_ack.command_acks[0].command_id,
6101 compact_command.command_id
6102 );
6103 assert_eq!(gateway_bridge.stats().packets_received, 2);
6104 assert_eq!(gateway_bridge.stats().commands_accepted, 2);
6105 assert_eq!(gateway_bridge.stats().acks_sent, 2);
6106 }
6107
6108 #[test]
6109 fn gateway_client_transport_bridge_rejects_source_mismatch_before_admission() {
6110 let packet_client_id = ClientId::new(7);
6111 let frame_client_id = ClientId::new(8);
6112 let server_id = ClientId::new(0);
6113 let station_id = StationId::new(1);
6114 let hub = InMemoryTransportHub::new(ClientTransportLimits {
6115 max_queued_packets_per_client: 4,
6116 max_packet_bytes: 512,
6117 });
6118 let mut packet_client_transport = hub
6119 .endpoint(
6120 packet_client_id,
6121 "127.0.0.1:23607".parse().expect("client addr"),
6122 )
6123 .expect("client endpoint should register");
6124 let mut server_transport = hub
6125 .endpoint(server_id, "127.0.0.1:23600".parse().expect("server addr"))
6126 .expect("server endpoint should register");
6127 let command = CommandFrame {
6128 client_id: frame_client_id,
6129 command_id: CommandId::new(42),
6130 entity_id: EntityId::new(100),
6131 sequence: 9,
6132 kind: 1,
6133 priority: CommandPriority::High,
6134 payload: b"move:north".to_vec(),
6135 };
6136 let mut bytes = Vec::new();
6137 BinaryFrameEncoder
6138 .encode_command(&command, &mut bytes)
6139 .expect("command should encode");
6140 packet_client_transport
6141 .send(OutboundPacket {
6142 client_id: server_id,
6143 bytes,
6144 })
6145 .expect("packet should send");
6146
6147 let mut gateway = gateway(4);
6148 gateway
6149 .connect(frame_client_id, station_id, Tick::new(10))
6150 .expect("frame client should connect");
6151 let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
6152 let mut pipeline = GatewayCommandPipeline::default();
6153 let mut gateway_bridge = GatewayClientTransportBridge::default();
6154
6155 let error = gateway_bridge
6156 .pump_ingress(
6157 &mut server_transport,
6158 &mut pipeline,
6159 &mut gateway,
6160 &mut station_queues,
6161 Tick::new(10),
6162 CommandIngress::RUNNING,
6163 4,
6164 )
6165 .expect_err("source mismatch should reject before admission");
6166
6167 assert!(matches!(
6168 error,
6169 GatewayClientTransportError::SourceMismatch {
6170 packet_client_id: actual_packet,
6171 frame_client_id: actual_frame,
6172 } if actual_packet == packet_client_id && actual_frame == frame_client_id
6173 ));
6174 assert_eq!(gateway_bridge.stats().source_mismatches, 1);
6175 assert_eq!(gateway_bridge.stats().commands_accepted, 0);
6176 assert_eq!(pipeline.stats().commands_admitted, 0);
6177 assert_eq!(
6178 station_queues
6179 .get(&station_id)
6180 .expect("station queue should exist")
6181 .total_len(),
6182 0
6183 );
6184 }
6185
6186 #[test]
6187 fn gateway_command_pipeline_queues_command_and_encodes_ack() {
6188 let client_id = ClientId::new(7);
6189 let station_id = StationId::new(1);
6190 let mut gateway = gateway(4);
6191 gateway
6192 .connect(client_id, station_id, Tick::new(10))
6193 .expect("client should connect");
6194 let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
6195 let mut pipeline = GatewayCommandPipeline::default();
6196
6197 let report = pipeline.process(
6198 &mut gateway,
6199 &mut station_queues,
6200 &encode_command_frame(1),
6201 Tick::new(10),
6202 CommandIngress::RUNNING,
6203 );
6204
6205 assert!(report.accepted);
6206 assert_eq!(report.reason_code, GATEWAY_COMMAND_ACK_ACCEPTED);
6207 assert_eq!(report.station_id, Some(station_id));
6208 assert!(report.error.is_none());
6209 let ack_bytes = report.ack_bytes.expect("ACK should encode");
6210 let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
6211 .decode(&ack_bytes)
6212 .expect("ACK should decode")
6213 else {
6214 panic!("expected command ACK");
6215 };
6216 assert!(ack.accepted);
6217 assert_eq!(ack.command_id, CommandId::new(1));
6218 let queued = station_queues
6219 .get_mut(&station_id)
6220 .expect("queue should exist")
6221 .pop_next()
6222 .expect("command should queue");
6223 assert_eq!(queued.id, CommandId::new(1));
6224 assert_eq!(pipeline.stats().commands_admitted, 1);
6225 assert_eq!(pipeline.stats().commands_enqueued, 1);
6226 assert_eq!(pipeline.stats().acks_encoded, 1);
6227 }
6228
6229 #[test]
6230 fn gateway_command_pipeline_negative_acks_rate_limit() {
6231 let client_id = ClientId::new(7);
6232 let station_id = StationId::new(1);
6233 let mut gateway = gateway(1);
6234 gateway
6235 .connect(client_id, station_id, Tick::new(10))
6236 .expect("client should connect");
6237 let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
6238 let mut pipeline = GatewayCommandPipeline::default();
6239
6240 assert!(
6241 pipeline
6242 .process(
6243 &mut gateway,
6244 &mut station_queues,
6245 &encode_command_frame(1),
6246 Tick::new(10),
6247 CommandIngress::RUNNING,
6248 )
6249 .accepted
6250 );
6251 let rejected = pipeline.process(
6252 &mut gateway,
6253 &mut station_queues,
6254 &encode_command_frame(2),
6255 Tick::new(10),
6256 CommandIngress::RUNNING,
6257 );
6258
6259 assert!(!rejected.accepted);
6260 assert_eq!(rejected.reason_code, GATEWAY_COMMAND_ACK_RATE_LIMITED);
6261 assert!(matches!(
6262 rejected.error,
6263 Some(GatewayCommandPipelineError::Gateway(
6264 GatewayError::RateLimited { .. }
6265 ))
6266 ));
6267 let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
6268 .decode(&rejected.ack_bytes.expect("rejection ACK should encode"))
6269 .expect("ACK should decode")
6270 else {
6271 panic!("expected command ACK");
6272 };
6273 assert!(!ack.accepted);
6274 assert_eq!(ack.reason_code, GATEWAY_COMMAND_ACK_RATE_LIMITED);
6275 assert_eq!(pipeline.stats().commands_rejected_gateway, 1);
6276 }
6277
6278 #[test]
6279 fn gateway_command_pipeline_rejects_missing_station_queue() {
6280 let client_id = ClientId::new(7);
6281 let station_id = StationId::new(1);
6282 let mut gateway = gateway(4);
6283 gateway
6284 .connect(client_id, station_id, Tick::new(10))
6285 .expect("client should connect");
6286 let mut station_queues = BTreeMap::new();
6287 let mut pipeline = GatewayCommandPipeline::default();
6288
6289 let report = pipeline.process(
6290 &mut gateway,
6291 &mut station_queues,
6292 &encode_command_frame(1),
6293 Tick::new(10),
6294 CommandIngress::RUNNING,
6295 );
6296
6297 assert!(!report.accepted);
6298 assert_eq!(report.station_id, Some(station_id));
6299 assert_eq!(report.reason_code, GATEWAY_COMMAND_ACK_MISSING_QUEUE);
6300 assert!(matches!(
6301 report.error,
6302 Some(GatewayCommandPipelineError::MissingQueue(id)) if id == station_id
6303 ));
6304 assert_eq!(pipeline.stats().commands_admitted, 1);
6305 assert_eq!(pipeline.stats().commands_rejected_queue, 1);
6306 }
6307
6308 #[test]
6309 fn gateway_command_pipeline_dispatches_to_deployment_route() {
6310 let client_id = ClientId::new(7);
6311 let station_id = StationId::new(1);
6312 let node_id = NodeId::new(9);
6313 let mut gateway = gateway(4);
6314 gateway
6315 .connect(client_id, station_id, Tick::new(10))
6316 .expect("client should connect");
6317 let mut deployment = DeploymentRouteTable::new(DeploymentConfig {
6318 max_nodes: 4,
6319 max_stations_per_node: 4,
6320 stale_after_ticks: 10,
6321 });
6322 deployment
6323 .register_node(node_id, 4, Tick::new(10))
6324 .expect("node should register");
6325 deployment
6326 .assign_station(station_id, node_id, Tick::new(10))
6327 .expect("station should assign");
6328 let mut pipeline = GatewayCommandPipeline::default();
6329
6330 let report = pipeline.dispatch(
6331 &mut gateway,
6332 &deployment,
6333 &encode_command_frame(1),
6334 Tick::new(12),
6335 );
6336
6337 assert!(report.accepted);
6338 assert_eq!(report.station_id, Some(station_id));
6339 assert_eq!(report.node_id, Some(node_id));
6340 let delivery = report.delivery.expect("delivery should resolve");
6341 assert_eq!(delivery.client_id, client_id);
6342 assert_eq!(delivery.station_id, station_id);
6343 assert_eq!(delivery.node_id, node_id);
6344 assert_eq!(delivery.station_route_epoch, 1);
6345 assert_eq!(
6346 report
6347 .command
6348 .expect("command should be returned")
6349 .received_at,
6350 Tick::new(12)
6351 );
6352 let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
6353 .decode(&report.ack_bytes.expect("ACK should encode"))
6354 .expect("ACK should decode")
6355 else {
6356 panic!("expected command ACK");
6357 };
6358 assert!(ack.accepted);
6359 assert_eq!(pipeline.stats().commands_routed_deployment, 1);
6360 }
6361
6362 #[test]
6363 fn gateway_command_pipeline_negative_acks_missing_deployment_route() {
6364 let client_id = ClientId::new(7);
6365 let station_id = StationId::new(1);
6366 let mut gateway = gateway(4);
6367 gateway
6368 .connect(client_id, station_id, Tick::new(10))
6369 .expect("client should connect");
6370 let deployment = DeploymentRouteTable::default();
6371 let mut pipeline = GatewayCommandPipeline::default();
6372
6373 let report = pipeline.dispatch(
6374 &mut gateway,
6375 &deployment,
6376 &encode_command_frame(1),
6377 Tick::new(12),
6378 );
6379
6380 assert!(!report.accepted);
6381 assert_eq!(report.station_id, Some(station_id));
6382 assert_eq!(report.reason_code, GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED);
6383 assert!(matches!(
6384 report.error,
6385 Some(GatewayCommandPipelineError::Deployment(
6386 DeploymentError::MissingStation(id)
6387 )) if id == station_id
6388 ));
6389 let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
6390 .decode(&report.ack_bytes.expect("rejection ACK should encode"))
6391 .expect("ACK should decode")
6392 else {
6393 panic!("expected command ACK");
6394 };
6395 assert!(!ack.accepted);
6396 assert_eq!(ack.reason_code, GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED);
6397 assert_eq!(pipeline.stats().commands_rejected_deployment, 1);
6398 }
6399
6400 #[test]
6401 fn gateway_command_pipeline_rejects_non_command_frame() {
6402 let ack = CommandAckFrame {
6403 client_id: ClientId::new(7),
6404 command_id: CommandId::new(1),
6405 server_tick: Tick::new(10),
6406 accepted: true,
6407 reason_code: 0,
6408 };
6409 let mut bytes = Vec::new();
6410 BinaryFrameEncoder
6411 .encode_command_ack(&ack, &mut bytes)
6412 .expect("ACK should encode");
6413 let mut gateway = gateway(4);
6414 let mut station_queues = BTreeMap::new();
6415 let mut pipeline = GatewayCommandPipeline::default();
6416
6417 let report = pipeline.process(
6418 &mut gateway,
6419 &mut station_queues,
6420 &bytes,
6421 Tick::new(10),
6422 CommandIngress::RUNNING,
6423 );
6424
6425 assert!(!report.accepted);
6426 assert!(report.ack_bytes.is_none());
6427 assert_eq!(
6428 report.error,
6429 Some(GatewayCommandPipelineError::NonCommandFrame)
6430 );
6431 assert_eq!(pipeline.stats().frames_rejected_non_command, 1);
6432 }
6433
6434 #[test]
6435 fn migration_executor_moves_owner_and_leaves_source_ghost() {
6436 let mut stations = StationSet::default();
6437 let mut source = station(1, 10);
6438 source
6439 .spawn_owned(
6440 EntityId::new(99),
6441 Position3::new(1.0, 2.0, 3.0),
6442 Bounds::Point,
6443 PolicyId::new(0),
6444 )
6445 .expect("spawn should work");
6446 stations.push(source);
6447 stations.push(station(2, 10));
6448
6449 let report = EntityMigrationExecutor::migrate_entity(
6450 &mut stations,
6451 EntityId::new(99),
6452 StationId::new(1),
6453 StationId::new(2),
6454 4,
6455 )
6456 .expect("migration should work");
6457
6458 assert_eq!(report.transfer.target_station, StationId::new(2));
6459 assert!(
6460 !stations
6461 .get(StationId::new(1))
6462 .expect("source")
6463 .get_by_id(EntityId::new(99))
6464 .expect("source ghost")
6465 .is_owned()
6466 );
6467 assert!(
6468 stations
6469 .get(StationId::new(2))
6470 .expect("target")
6471 .get_by_id(EntityId::new(99))
6472 .expect("target owner")
6473 .is_owned()
6474 );
6475 }
6476
6477 #[test]
6478 fn event_router_delays_until_target_tick_and_scheduler_drains() {
6479 let mut stations = StationSet::default();
6480 stations.push(station(1, 10));
6481 stations.push(station(2, 10));
6482
6483 let mut router = EventRouter::default();
6484 router.register_stations(&stations);
6485 router
6486 .route(StationEvent {
6487 id: EventId::new(1),
6488 source: StationId::new(1),
6489 target: StationId::new(2),
6490 source_tick: Tick::new(0),
6491 target_tick: Tick::new(2),
6492 priority: EventPriority::Critical,
6493 kind: EventKind::Custom(7),
6494 })
6495 .expect("route should work");
6496
6497 let mut scheduler = StationScheduler::default();
6498 let mut drained = Vec::new();
6499 scheduler.advance_all(&mut stations);
6500 scheduler
6501 .drain_ready_events_into(&stations, &mut router, &mut drained)
6502 .expect("drain should work");
6503 assert!(drained.is_empty());
6504
6505 scheduler.advance_all(&mut stations);
6506 scheduler
6507 .drain_ready_events_into(&stations, &mut router, &mut drained)
6508 .expect("drain should work");
6509 assert_eq!(drained.len(), 1);
6510 let retained_capacity = drained.capacity();
6511
6512 scheduler
6513 .drain_ready_events_into(&stations, &mut router, &mut drained)
6514 .expect("empty drain should work");
6515 assert!(drained.is_empty());
6516 assert_eq!(drained.capacity(), retained_capacity);
6517 assert_eq!(router.stats().routed_events, 1);
6518 assert_eq!(router.stats().drained_events, 1);
6519 }
6520
6521 #[test]
6522 fn event_router_unregisters_station_and_discards_queued_events() {
6523 let station_id = StationId::new(2);
6524 let mut router = EventRouter::default();
6525 router.register_station(station_id);
6526 router
6527 .route(StationEvent {
6528 id: EventId::new(1),
6529 source: StationId::new(1),
6530 target: station_id,
6531 source_tick: Tick::new(0),
6532 target_tick: Tick::new(10),
6533 priority: EventPriority::Important,
6534 kind: EventKind::Custom(1),
6535 })
6536 .expect("event should queue");
6537
6538 assert_eq!(router.unregister_station(station_id), Some(1));
6539 assert_eq!(router.unregister_station(station_id), None);
6540 assert_eq!(router.queued_len(station_id), None);
6541 assert_eq!(
6542 router.drain_ready(station_id, Tick::new(10)),
6543 Err(EventRouterError::MissingTarget(station_id))
6544 );
6545 }
6546
6547 #[test]
6548 fn station_scheduler_prioritizes_loaded_stations_with_budget() {
6549 let mut stations = StationSet::default();
6550 stations.push(station(1, 10));
6551 stations.push(station(2, 10));
6552 stations.push(station(3, 10));
6553
6554 let samples = vec![
6555 StationLoadSample {
6556 station_id: StationId::new(1),
6557 owned_entities: 1,
6558 ..StationLoadSample::default()
6559 },
6560 StationLoadSample {
6561 station_id: StationId::new(2),
6562 owned_entities: 100,
6563 subscribers: 40,
6564 queued_events: 20,
6565 tick_cost_units: 500,
6566 cells: vec![CellLoadSample {
6567 cell: CellCoord3::new(0, 0, 0),
6568 owned_entities: 90,
6569 subscribers: 40,
6570 event_pressure: 10,
6571 ..CellLoadSample::default()
6572 }],
6573 ..StationLoadSample::default()
6574 },
6575 StationLoadSample {
6576 station_id: StationId::new(3),
6577 owned_entities: 25,
6578 subscribers: 10,
6579 queued_events: 5,
6580 tick_cost_units: 50,
6581 ..StationLoadSample::default()
6582 },
6583 ];
6584
6585 let mut scheduler = StationScheduler::default();
6586 let mut schedule_scratch = StationScheduleScratch::new();
6587 let plan = scheduler.advance_loaded_into(
6588 &mut stations,
6589 &samples,
6590 StationScheduleConfig {
6591 max_station_advances_per_step: 2,
6592 },
6593 &mut schedule_scratch,
6594 );
6595
6596 assert_eq!(plan.candidates_considered, 3);
6597 assert_eq!(plan.stations_selected, 2);
6598 assert_eq!(plan.total_advances, 2);
6599 assert_eq!(
6600 plan.selected
6601 .iter()
6602 .map(|candidate| candidate.station_id)
6603 .collect::<Vec<_>>(),
6604 vec![StationId::new(2), StationId::new(3)]
6605 );
6606 assert_eq!(scheduler.advanced_ticks, 2);
6607 assert_eq!(
6608 stations.get(StationId::new(1)).expect("station").tick(),
6609 Tick::new(0)
6610 );
6611 assert_eq!(
6612 stations.get(StationId::new(2)).expect("station").tick(),
6613 Tick::new(1)
6614 );
6615 assert_eq!(
6616 stations.get(StationId::new(3)).expect("station").tick(),
6617 Tick::new(1)
6618 );
6619 }
6620
6621 #[test]
6622 fn station_scheduler_top_k_matches_full_sort_for_budget_edges() {
6623 let candidates = (0_u32..257)
6624 .map(|index| StationScheduleCandidate {
6625 station_id: StationId::new(index),
6626 load_score: u64::from(index.wrapping_mul(37) % 23),
6627 tick_lag: u64::from(index.wrapping_mul(19) % 11),
6628 })
6629 .collect::<Vec<_>>();
6630
6631 for requested in [0, 1, 7, 64, 128, 129, 256, 257, 300] {
6632 let limit = requested.min(candidates.len());
6633 let mut expected = candidates.clone();
6634 expected.sort_by(compare_station_schedule_candidates);
6635 expected.truncate(limit);
6636 let mut actual = candidates.clone();
6637 prioritize_station_candidates(&mut actual, limit);
6638
6639 assert_eq!(&actual[..limit], expected.as_slice());
6640 }
6641 }
6642
6643 #[test]
6644 fn station_schedule_scratch_reuses_capacity_and_last_sample_wins() {
6645 let mut stations = StationSet::default();
6646 for station_id in 1..=8 {
6647 stations.push(station(station_id, 10));
6648 }
6649 let samples = [
6650 StationLoadSample {
6651 station_id: StationId::new(3),
6652 owned_entities: 1,
6653 ..StationLoadSample::default()
6654 },
6655 StationLoadSample {
6656 station_id: StationId::new(3),
6657 owned_entities: 500,
6658 ..StationLoadSample::default()
6659 },
6660 ];
6661 let scheduler = StationScheduler::default();
6662 let mut scratch = StationScheduleScratch::new();
6663
6664 {
6665 let plan = scheduler.plan_loaded_into(
6666 &stations,
6667 &samples,
6668 StationScheduleConfig {
6669 max_station_advances_per_step: 2,
6670 },
6671 &mut scratch,
6672 );
6673 assert_eq!(plan.candidates_considered, 8);
6674 assert_eq!(plan.selected[0].station_id, StationId::new(3));
6675 assert_eq!(
6676 plan.selected[0].load_score,
6677 station_schedule_score(&samples[1])
6678 );
6679 }
6680 let score_capacity = scratch.score_capacity();
6681 let candidate_capacity = scratch.candidate_capacity();
6682
6683 let plan = scheduler.plan_loaded_into(
6684 &stations,
6685 &samples[..1],
6686 StationScheduleConfig {
6687 max_station_advances_per_step: 1,
6688 },
6689 &mut scratch,
6690 );
6691 assert_eq!(plan.selected.len(), 1);
6692 assert_eq!(scratch.score_capacity(), score_capacity);
6693 assert_eq!(scratch.candidate_capacity(), candidate_capacity);
6694 }
6695
6696 #[test]
6697 fn station_load_sampler_derives_cells_router_and_subscribers() {
6698 let station_id = StationId::new(1);
6699 let owner_position = Position3::new(1.0, 0.0, 0.0);
6700 let ghost_position = Position3::new(12.0, 0.0, 0.0);
6701 let policy_id = PolicyId::new(1);
6702 let mut station = station(1, 10);
6703 let owner = station
6704 .spawn_owned(EntityId::new(10), owner_position, Bounds::Point, policy_id)
6705 .expect("owner should spawn");
6706 let ghost = station.upsert_ghost(
6707 EntityId::new(20),
6708 ghost_position,
6709 Bounds::Point,
6710 policy_id,
6711 StationId::new(9),
6712 OwnerEpoch::new(3),
6713 Tick::new(30),
6714 );
6715
6716 let grid = GridSpec::new(10.0).expect("grid should build");
6717 let mut index = CellIndex::new(grid);
6718 index.upsert(owner, owner_position, Bounds::Point);
6719 index.upsert(ghost, ghost_position, Bounds::Point);
6720 let mut indexes = StationIndexSet::default();
6721 indexes.insert(station_id, index);
6722
6723 let mut stations = StationSet::default();
6724 stations.push(station);
6725 let mut router = EventRouter::default();
6726 router.register_station(station_id);
6727 for (event_id, kind) in [(1_u64, 1_u32), (2, 2)] {
6728 router
6729 .route(StationEvent {
6730 id: EventId::new(event_id),
6731 source: StationId::new(9),
6732 target: station_id,
6733 source_tick: Tick::new(0),
6734 target_tick: Tick::new(4),
6735 priority: EventPriority::Important,
6736 kind: EventKind::Custom(kind),
6737 })
6738 .expect("event should queue");
6739 }
6740
6741 assert_eq!(indexes.iter().count(), 1);
6742 let load_sampler = StationLoadSampler::default();
6743 let mut load_scratch = StationLoadSamplerScratch::new();
6744 let samples = load_sampler.sample_all_into(
6745 &stations,
6746 &indexes,
6747 &router,
6748 &[(station_id, 2), (station_id, 3)],
6749 &mut load_scratch,
6750 );
6751
6752 assert_eq!(samples.len(), 1);
6753 let sample = &samples[0];
6754 assert_eq!(sample.station_id, station_id);
6755 assert_eq!(sample.owned_entities, 1);
6756 assert_eq!(sample.ghost_entities, 1);
6757 assert_eq!(sample.subscribers, 5);
6758 assert_eq!(sample.queued_events, 2);
6759 assert_eq!(sample.estimated_bytes, 240);
6760 assert_eq!(sample.tick_cost_units, 7);
6761 assert_eq!(
6762 sample.cells,
6763 vec![
6764 CellLoadSample {
6765 cell: grid.cell_at(owner_position),
6766 owned_entities: 1,
6767 ghost_entities: 0,
6768 subscribers: 0,
6769 estimated_updates: 1,
6770 estimated_bytes: 48,
6771 tick_cost_units: 3,
6772 event_pressure: 0,
6773 },
6774 CellLoadSample {
6775 cell: grid.cell_at(ghost_position),
6776 owned_entities: 0,
6777 ghost_entities: 1,
6778 subscribers: 0,
6779 estimated_updates: 1,
6780 estimated_bytes: 48,
6781 tick_cost_units: 2,
6782 event_pressure: 0,
6783 },
6784 ]
6785 );
6786
6787 assert_load_sampler_scratch_reuse(
6788 &load_sampler,
6789 &stations,
6790 &indexes,
6791 &router,
6792 station_id,
6793 samples,
6794 );
6795 }
6796
6797 fn assert_load_sampler_scratch_reuse(
6798 load_sampler: &StationLoadSampler,
6799 stations: &StationSet,
6800 indexes: &StationIndexSet,
6801 router: &EventRouter,
6802 station_id: StationId,
6803 samples: &[StationLoadSample],
6804 ) {
6805 let mut scratch = StationLoadSamplerScratch::new();
6806 let (sample_ptr, cell_ptr) = {
6807 let reused = load_sampler.sample_all_into(
6808 stations,
6809 indexes,
6810 router,
6811 &[(station_id, 2), (station_id, 3)],
6812 &mut scratch,
6813 );
6814 assert_eq!(reused, samples);
6815 (reused.as_ptr(), reused[0].cells.as_ptr())
6816 };
6817 let subscriber_capacity = scratch.retained_subscriber_capacity();
6818 let occupancy_capacity = scratch.retained_occupancy_capacity();
6819 let cell_capacity = scratch.retained_cell_capacity();
6820
6821 let reused = load_sampler.sample_all_into(
6822 stations,
6823 indexes,
6824 router,
6825 &[(station_id, 2), (station_id, 3)],
6826 &mut scratch,
6827 );
6828 assert_eq!(reused, samples);
6829 assert_eq!(reused.as_ptr(), sample_ptr);
6830 assert_eq!(reused[0].cells.as_ptr(), cell_ptr);
6831 assert_eq!(scratch.retained_sample_slots(), 1);
6832 assert_eq!(scratch.retained_subscriber_capacity(), subscriber_capacity);
6833 assert_eq!(scratch.retained_occupancy_capacity(), occupancy_capacity);
6834 assert_eq!(scratch.retained_cell_capacity(), cell_capacity);
6835 }
6836
6837 #[test]
6838 fn station_event_transport_bridge_routes_events_through_bounded_packets() {
6839 let mut stations = StationSet::default();
6840 stations.push(station(1, 10));
6841 stations.push(station(2, 10));
6842
6843 let mut router = EventRouter::default();
6844 router.register_stations(&stations);
6845 let mut transport = InMemoryStationTransport::default();
6846 transport.register_station(StationId::new(2));
6847 let mut bridge = StationEventTransportBridge::default();
6848 let event = StationEvent {
6849 id: EventId::new(7),
6850 source: StationId::new(1),
6851 target: StationId::new(2),
6852 source_tick: Tick::new(0),
6853 target_tick: Tick::new(1),
6854 priority: EventPriority::Important,
6855 kind: EventKind::Custom(99),
6856 };
6857
6858 bridge
6859 .send_event(&mut transport, &event)
6860 .expect("event should encode and send");
6861 assert_eq!(transport.queued_len(StationId::new(2)), Some(1));
6862
6863 let report = bridge
6864 .pump_target(&mut transport, &mut router, StationId::new(2), 4)
6865 .expect("event should pump into router");
6866 assert_eq!(report.packets_received, 1);
6867 assert_eq!(report.events_routed, 1);
6868 assert_eq!(router.queued_len(StationId::new(2)), Some(1));
6869
6870 let mut scheduler = StationScheduler::default();
6871 scheduler.advance_all(&mut stations);
6872 let mut drained = Vec::new();
6873 scheduler
6874 .drain_ready_events_into(&stations, &mut router, &mut drained)
6875 .expect("drain should work");
6876 assert_eq!(drained, vec![event]);
6877 assert_eq!(bridge.stats().events_sent, 1);
6878 assert_eq!(bridge.stats().events_routed, 1);
6879 assert_eq!(transport.stats().packets_sent, 1);
6880 assert_eq!(transport.stats().packets_received, 1);
6881 }
6882
6883 #[test]
6884 fn command_dispatch_transport_bridge_enqueues_stamped_command() {
6885 let gateway_station = StationId::new(0);
6886 let target_station = StationId::new(2);
6887 let command = CommandEnvelope {
6888 id: CommandId::new(42),
6889 client_id: ClientId::new(7),
6890 entity_id: EntityId::new(100),
6891 sequence: 42,
6892 received_at: Tick::new(12),
6893 kind: 1,
6894 priority: CommandPriority::High,
6895 payload: b"move:north".to_vec(),
6896 };
6897 let mut transport = InMemoryStationTransport::default();
6898 transport.register_station(target_station);
6899 let mut queues = BTreeMap::from([(target_station, command_queues())]);
6900 let mut bridge = CommandDispatchTransportBridge::default();
6901
6902 bridge
6903 .send_envelope(&mut transport, gateway_station, target_station, &command)
6904 .expect("command dispatch should send");
6905 assert_eq!(transport.queued_len(target_station), Some(1));
6906 let report = bridge
6907 .pump_target(
6908 &mut transport,
6909 &mut queues,
6910 target_station,
6911 4,
6912 CommandIngress::RUNNING,
6913 )
6914 .expect("command dispatch should pump");
6915
6916 assert_eq!(report.packets_received, 1);
6917 assert_eq!(report.commands_enqueued, 1);
6918 let queued_command = queues
6919 .get_mut(&target_station)
6920 .expect("queue should exist")
6921 .pop_next()
6922 .expect("command should queue");
6923 assert_eq!(queued_command, command);
6924 assert_eq!(bridge.stats().commands_sent, 1);
6925 assert_eq!(bridge.stats().commands_enqueued, 1);
6926 assert_eq!(transport.stats().packets_sent, 1);
6927 assert_eq!(transport.stats().packets_received, 1);
6928 }
6929
6930 #[test]
6931 fn command_dispatch_transport_bridge_rejects_endpoint_mismatch() {
6932 let packet_target = StationId::new(2);
6933 let frame_target = StationId::new(3);
6934 let mut transport = InMemoryStationTransport::default();
6935 transport.register_station(packet_target);
6936 let frame = CommandDispatchFrame {
6937 station_id: frame_target,
6938 client_id: ClientId::new(7),
6939 command_id: CommandId::new(42),
6940 entity_id: EntityId::new(100),
6941 sequence: 42,
6942 received_at: Tick::new(12),
6943 kind: 1,
6944 priority: CommandPriority::High,
6945 payload: Vec::new(),
6946 };
6947 let mut bytes = Vec::new();
6948 BinaryFrameEncoder
6949 .encode_command_dispatch(&frame, &mut bytes)
6950 .expect("frame should encode");
6951 transport
6952 .send_station(StationOutboundPacket {
6953 source_station: StationId::new(0),
6954 target_station: packet_target,
6955 bytes,
6956 })
6957 .expect("bad packet should enter transport");
6958 let mut queues = BTreeMap::from([(packet_target, command_queues())]);
6959 let mut bridge = CommandDispatchTransportBridge::default();
6960
6961 let error = bridge
6962 .pump_target(
6963 &mut transport,
6964 &mut queues,
6965 packet_target,
6966 4,
6967 CommandIngress::RUNNING,
6968 )
6969 .expect_err("endpoint mismatch should reject");
6970
6971 assert!(matches!(
6972 error,
6973 CommandDispatchTransportError::EndpointMismatch {
6974 packet_source,
6975 packet_target: observed_packet_target,
6976 dispatch_target,
6977 } if packet_source == StationId::new(0)
6978 && observed_packet_target == packet_target
6979 && dispatch_target == frame_target
6980 ));
6981 assert!(
6982 queues
6983 .get_mut(&packet_target)
6984 .expect("queue should exist")
6985 .pop_next()
6986 .is_none()
6987 );
6988 }
6989
6990 #[test]
6991 fn cell_migration_moves_owned_entities_and_updates_indexes() {
6992 let grid = GridSpec::new(16.0).expect("valid grid");
6993 let cell = CellCoord3::new(0, 0, 0);
6994 let mut stations = StationSet::default();
6995 let mut source = station(1, 10);
6996 let first = source
6997 .spawn_owned(
6998 EntityId::new(1),
6999 Position3::new(1.0, 1.0, 1.0),
7000 Bounds::Point,
7001 PolicyId::new(0),
7002 )
7003 .expect("first spawn should work");
7004 let second = source
7005 .spawn_owned(
7006 EntityId::new(2),
7007 Position3::new(2.0, 1.0, 1.0),
7008 Bounds::Point,
7009 PolicyId::new(0),
7010 )
7011 .expect("second spawn should work");
7012 stations.push(source);
7013 stations.push(station(2, 10));
7014
7015 let mut source_index = CellIndex::new(grid);
7016 source_index.upsert(first, Position3::new(1.0, 1.0, 1.0), Bounds::Point);
7017 source_index.upsert(second, Position3::new(2.0, 1.0, 1.0), Bounds::Point);
7018 let mut target_index = CellIndex::new(grid);
7019
7020 let mut ownership = CellOwnershipTable::default();
7021 ownership.assign(cell, StationId::new(1));
7022 let update = ownership.apply_split(
7023 &SplitProposal {
7024 source_station: StationId::new(1),
7025 cells_to_move: vec![cell],
7026 moved_pressure_score: 10,
7027 },
7028 StationId::new(2),
7029 );
7030 assert_eq!(ownership.owner_of(cell), Some(StationId::new(2)));
7031 assert_eq!(update.moved_cells, vec![cell]);
7032
7033 let mut scratch = CellMigrationScratch::new();
7034 scratch.reserve(2, 2);
7035 let mut report = CellMigrationReport::default();
7036 report.scanned_cells.reserve(1);
7037 report.entity_migrations.reserve(2);
7038 CellMigrationExecutor::migrate_cells_into(
7039 &mut stations,
7040 &mut source_index,
7041 &mut target_index,
7042 StationId::new(1),
7043 StationId::new(2),
7044 &update.moved_cells,
7045 4,
7046 &mut scratch,
7047 &mut report,
7048 )
7049 .expect("cell migration should work");
7050
7051 assert_eq!(report.entity_migrations.len(), 2);
7052 assert_eq!(target_index.entity_count(), 2);
7053 assert!(
7054 !stations
7055 .get(StationId::new(1))
7056 .expect("source")
7057 .get_by_id(EntityId::new(1))
7058 .expect("source ghost")
7059 .is_owned()
7060 );
7061 assert!(
7062 stations
7063 .get(StationId::new(2))
7064 .expect("target")
7065 .get_by_id(EntityId::new(1))
7066 .expect("target owner")
7067 .is_owned()
7068 );
7069
7070 let retained_handle_capacity = scratch.handle_capacity();
7071 let retained_entity_capacity = scratch.entity_capacity();
7072 let retained_candidate_capacity = scratch.candidate_capacity();
7073 let retained_scanned_capacity = report.scanned_cells.capacity();
7074 let retained_migration_capacity = report.entity_migrations.capacity();
7075 CellMigrationExecutor::migrate_cells_into(
7076 &mut stations,
7077 &mut source_index,
7078 &mut target_index,
7079 StationId::new(1),
7080 StationId::new(2),
7081 &[],
7082 4,
7083 &mut scratch,
7084 &mut report,
7085 )
7086 .expect("empty reusable migration should work");
7087 assert!(report.scanned_cells.is_empty());
7088 assert!(report.entity_migrations.is_empty());
7089 assert_eq!(report.scanned_cells.capacity(), retained_scanned_capacity);
7090 assert_eq!(
7091 report.entity_migrations.capacity(),
7092 retained_migration_capacity
7093 );
7094 assert_eq!(scratch.handle_capacity(), retained_handle_capacity);
7095 assert_eq!(scratch.entity_capacity(), retained_entity_capacity);
7096 assert_eq!(scratch.candidate_capacity(), retained_candidate_capacity);
7097 }
7098
7099 #[test]
7100 #[allow(clippy::too_many_lines)]
7101 fn split_scheduler_plans_and_executes_hot_cell_move() {
7102 let grid = GridSpec::new(16.0).expect("valid grid");
7103 let hot_cell = CellCoord3::new(0, 0, 0);
7104 let mut stations = StationSet::default();
7105 let mut source = station(1, 10);
7106 let handle = source
7107 .spawn_owned(
7108 EntityId::new(1),
7109 Position3::new(1.0, 1.0, 1.0),
7110 Bounds::Point,
7111 PolicyId::new(0),
7112 )
7113 .expect("spawn should work");
7114 stations.push(source);
7115 stations.push(station(2, 10));
7116
7117 let mut source_index = CellIndex::new(grid);
7118 source_index.upsert(handle, Position3::new(1.0, 1.0, 1.0), Bounds::Point);
7119 let mut indexes = StationIndexSet::default();
7120 indexes.insert(StationId::new(1), source_index);
7121 indexes.insert(StationId::new(2), CellIndex::new(grid));
7122
7123 let samples = vec![
7124 StationLoadSample {
7125 station_id: StationId::new(1),
7126 owned_entities: 100,
7127 subscribers: 100,
7128 tick_cost_units: 1000,
7129 cells: vec![CellLoadSample {
7130 cell: hot_cell,
7131 owned_entities: 100,
7132 subscribers: 100,
7133 event_pressure: 10,
7134 ..CellLoadSample::default()
7135 }],
7136 ..StationLoadSample::default()
7137 },
7138 StationLoadSample {
7139 station_id: StationId::new(2),
7140 owned_entities: 1,
7141 cells: vec![CellLoadSample {
7142 cell: CellCoord3::new(10, 0, 0),
7143 owned_entities: 1,
7144 ..CellLoadSample::default()
7145 }],
7146 ..StationLoadSample::default()
7147 },
7148 ];
7149 let scheduler = SplitScheduler::new(SplitSchedulerConfig {
7150 thresholds: HotspotThresholds {
7151 max_station_entities: 10,
7152 max_station_subscribers: 10,
7153 max_cell_pressure: 10,
7154 ..HotspotThresholds::default()
7155 },
7156 max_actions_per_pass: 1,
7157 max_cells_per_action: 1,
7158 ghost_ttl_ticks: 4,
7159 ..SplitSchedulerConfig::default()
7160 });
7161 let mut planning = SplitSchedulerScratch::new();
7162 let schedule =
7163 SplitSchedule::from(scheduler.plan_into(&samples, None, Tick::new(0), &mut planning));
7164 assert_eq!(schedule.actions.len(), 1);
7165 assert_eq!(schedule.actions[0].target_station, StationId::new(2));
7166
7167 let mut ownership = CellOwnershipTable::default();
7168 ownership.assign(hot_cell, StationId::new(1));
7169 let mut execution_scratch = SplitScheduleExecutionScratch::new();
7170 execution_scratch.reserve(1, 1, 1);
7171 {
7172 let report = scheduler
7173 .execute_into(
7174 schedule.view(),
7175 &mut stations,
7176 &mut indexes,
7177 &mut ownership,
7178 &mut execution_scratch,
7179 )
7180 .expect("reusable execute should work");
7181 assert_eq!(report.cell_migrations.len(), 1);
7182 assert_eq!(report.cell_migrations[0].entity_migrations.len(), 1);
7183 }
7184
7185 assert_eq!(ownership.owner_of(hot_cell), Some(StationId::new(2)));
7186 assert_eq!(
7187 indexes
7188 .get(StationId::new(2))
7189 .expect("target index")
7190 .entity_count(),
7191 1
7192 );
7193
7194 let retained_ownership_slots = execution_scratch.retained_ownership_slots();
7195 let retained_migration_slots = execution_scratch.retained_migration_slots();
7196 let retained_update_cells = execution_scratch.retained_update_cell_capacity();
7197 let retained_entity_migrations = execution_scratch.retained_entity_migration_capacity();
7198 let retained_candidates = execution_scratch.retained_candidate_capacity();
7199 execution_scratch.reserve(1, 1, 1);
7200 assert_eq!(
7201 execution_scratch.retained_update_cell_capacity(),
7202 retained_update_cells
7203 );
7204 assert_eq!(
7205 execution_scratch.retained_entity_migration_capacity(),
7206 retained_entity_migrations
7207 );
7208 assert_eq!(
7209 execution_scratch.retained_candidate_capacity(),
7210 retained_candidates
7211 );
7212 let empty = SplitSchedule::default();
7213 let empty_report = scheduler
7214 .execute_into(
7215 empty.view(),
7216 &mut stations,
7217 &mut indexes,
7218 &mut ownership,
7219 &mut execution_scratch,
7220 )
7221 .expect("empty reusable execute should work");
7222 assert!(empty_report.ownership_updates.is_empty());
7223 assert!(empty_report.cell_migrations.is_empty());
7224 assert_eq!(
7225 execution_scratch.retained_ownership_slots(),
7226 retained_ownership_slots
7227 );
7228 assert_eq!(
7229 execution_scratch.retained_migration_slots(),
7230 retained_migration_slots
7231 );
7232 assert_eq!(
7233 execution_scratch.retained_update_cell_capacity(),
7234 retained_update_cells
7235 );
7236 assert_eq!(
7237 execution_scratch.retained_entity_migration_capacity(),
7238 retained_entity_migrations
7239 );
7240 assert_eq!(
7241 execution_scratch.retained_candidate_capacity(),
7242 retained_candidates
7243 );
7244 }
7245
7246 #[test]
7247 fn split_scheduler_respects_source_cooldown() {
7248 let hot_cell = CellCoord3::new(0, 0, 0);
7249 let samples = split_test_samples(hot_cell);
7250 let scheduler = SplitScheduler::new(SplitSchedulerConfig {
7251 thresholds: split_test_thresholds(),
7252 max_actions_per_pass: 1,
7253 max_cells_per_action: 1,
7254 split_cooldown_ticks: 10,
7255 ..SplitSchedulerConfig::default()
7256 });
7257 let mut state = SplitSchedulerState::default();
7258
7259 let mut scratch = SplitSchedulerScratch::new();
7260 let initial = SplitSchedule::from(scheduler.plan_into(
7261 &samples,
7262 Some(&state),
7263 Tick::new(5),
7264 &mut scratch,
7265 ));
7266 assert_eq!(initial.actions.len(), 1);
7267 state.record_schedule(&initial, Tick::new(5));
7268
7269 let cooled_down = scheduler.plan_into(&samples, Some(&state), Tick::new(8), &mut scratch);
7270 assert!(cooled_down.actions.is_empty());
7271 assert_eq!(cooled_down.skipped_cooldown, 1);
7272
7273 let after_cooldown =
7274 scheduler.plan_into(&samples, Some(&state), Tick::new(16), &mut scratch);
7275 assert_eq!(after_cooldown.actions.len(), 1);
7276 }
7277
7278 #[test]
7279 fn split_scheduler_reports_capacity_and_improvement_skips() {
7280 let hot_cell = CellCoord3::new(0, 0, 0);
7281 let samples = split_test_samples(hot_cell);
7282
7283 let capacity_guard = SplitScheduler::new(SplitSchedulerConfig {
7284 thresholds: split_test_thresholds(),
7285 max_actions_per_pass: 1,
7286 max_cells_per_action: 1,
7287 max_target_score_after_move: 1,
7288 ..SplitSchedulerConfig::default()
7289 });
7290 let mut scratch = SplitSchedulerScratch::new();
7291 let capacity_schedule =
7292 capacity_guard.plan_into(&samples, None, Tick::new(0), &mut scratch);
7293 assert!(capacity_schedule.actions.is_empty());
7294 assert_eq!(capacity_schedule.skipped_target_capacity, 1);
7295
7296 let improvement_guard = SplitScheduler::new(SplitSchedulerConfig {
7297 thresholds: split_test_thresholds(),
7298 max_actions_per_pass: 1,
7299 max_cells_per_action: 1,
7300 min_score_improvement: u64::MAX,
7301 ..SplitSchedulerConfig::default()
7302 });
7303 let improvement_schedule =
7304 improvement_guard.plan_into(&samples, None, Tick::new(0), &mut scratch);
7305 assert!(improvement_schedule.actions.is_empty());
7306 assert_eq!(improvement_schedule.skipped_insufficient_improvement, 1);
7307 }
7308
7309 #[test]
7310 fn split_scheduler_view_matches_owned_and_retains_nested_capacity() {
7311 let hot_cell = CellCoord3::new(0, 0, 0);
7312 let samples = split_test_samples(hot_cell);
7313 let scheduler = SplitScheduler::new(SplitSchedulerConfig {
7314 thresholds: split_test_thresholds(),
7315 max_actions_per_pass: 1,
7316 max_cells_per_action: 1,
7317 ..SplitSchedulerConfig::default()
7318 });
7319 let mut scratch = SplitSchedulerScratch::new();
7320 let expected =
7321 SplitSchedule::from(scheduler.plan_into(&samples, None, Tick::new(0), &mut scratch));
7322
7323 {
7324 let view = scheduler.plan_into(&samples, None, Tick::new(0), &mut scratch);
7325 assert_eq!(SplitSchedule::from(view), expected);
7326 }
7327 let decision_slots = scratch.retained_decision_slots();
7328 let action_slots = scratch.retained_action_slots();
7329 let reason_capacity = scratch.retained_reason_capacity();
7330 let action_cell_capacity = scratch.retained_action_cell_capacity();
7331 let candidate_capacity = scratch.retained_candidate_capacity();
7332 assert_eq!(decision_slots, samples.len());
7333 assert_eq!(action_slots, 1);
7334 assert!(reason_capacity > 0);
7335 assert!(action_cell_capacity > 0);
7336 assert!(candidate_capacity > 0);
7337
7338 let reduced = scheduler.plan_into(&samples[1..], None, Tick::new(0), &mut scratch);
7339 assert_eq!(reduced.decisions.len(), 1);
7340 assert!(reduced.actions.is_empty());
7341 assert_eq!(scratch.retained_decision_slots(), decision_slots);
7342 assert_eq!(scratch.retained_action_slots(), action_slots);
7343 assert_eq!(scratch.retained_reason_capacity(), reason_capacity);
7344 assert_eq!(
7345 scratch.retained_action_cell_capacity(),
7346 action_cell_capacity
7347 );
7348 assert_eq!(scratch.retained_candidate_capacity(), candidate_capacity);
7349 }
7350
7351 fn split_test_thresholds() -> HotspotThresholds {
7352 HotspotThresholds {
7353 max_station_entities: 10,
7354 max_station_subscribers: 10,
7355 max_cell_pressure: 10,
7356 ..HotspotThresholds::default()
7357 }
7358 }
7359
7360 fn split_test_samples(hot_cell: CellCoord3) -> Vec<StationLoadSample> {
7361 vec![
7362 StationLoadSample {
7363 station_id: StationId::new(1),
7364 owned_entities: 100,
7365 subscribers: 100,
7366 tick_cost_units: 1000,
7367 cells: vec![CellLoadSample {
7368 cell: hot_cell,
7369 owned_entities: 100,
7370 subscribers: 100,
7371 event_pressure: 10,
7372 ..CellLoadSample::default()
7373 }],
7374 ..StationLoadSample::default()
7375 },
7376 StationLoadSample {
7377 station_id: StationId::new(2),
7378 owned_entities: 1,
7379 cells: vec![CellLoadSample {
7380 cell: CellCoord3::new(10, 0, 0),
7381 owned_entities: 1,
7382 ..CellLoadSample::default()
7383 }],
7384 ..StationLoadSample::default()
7385 },
7386 ]
7387 }
7388}