1use crate::utils::FxHashMap;
53use base64::prelude::*;
54use oko_multicast_socket::{MulticastOptions, MulticastSocket, all_ipv4_interfaces};
55use serde::{Deserialize, Serialize};
56use std::net::SocketAddrV4;
57use web_time::{Duration, Instant};
58
59use crate::Config;
60use crate::actor::{Actor, ActorContext};
61use crate::message::Message;
62use async_trait::async_trait;
63use log::{debug, error, info, warn};
64use std::sync::Arc;
65use tokio::sync::RwLock;
66
67const MAX_DATAGRAM_SIZE: usize = 1400;
75
76const CHUNK_PAYLOAD_SIZE: usize = 900;
84
85const CHUNK_TIMEOUT: Duration = Duration::from_secs(5);
87
88const MAX_REASSEMBLY_SLOTS: usize = 64;
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
107struct ChunkEnvelope {
108 beam_chunk: ChunkFields,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112struct ChunkFields {
113 id: String,
115 seq: usize,
117 total: usize,
119 data: String,
121}
122
123#[derive(Debug)]
136struct ReassemblyBuffer {
137 slots: FxHashMap<String, PartialMessage>,
139}
140
141#[derive(Debug)]
143struct PartialMessage {
144 total: usize,
146 received: Vec<Option<Vec<u8>>>,
148 deadline: Instant,
150}
151
152impl ReassemblyBuffer {
153 fn new() -> Self {
155 Self {
156 slots: FxHashMap::default(),
157 }
158 }
159
160 fn insert(&mut self, chunk: &ChunkFields) -> Result<Option<Vec<u8>>, &'static str> {
170 self.evict_expired();
171
172 if let Some(partial) = self.slots.get(&chunk.id) {
174 if chunk.seq < partial.received.len() && partial.received[chunk.seq].is_some() {
175 debug!("duplicate chunk {} seq {} — ignoring", chunk.id, chunk.seq);
176 return Ok(None);
177 }
178 }
179
180 if chunk.seq >= chunk.total {
182 return Err("chunk seq >= total");
183 }
184
185 if !self.slots.contains_key(&chunk.id) && self.slots.len() >= MAX_REASSEMBLY_SLOTS {
187 self.evict_oldest();
188 }
189
190 let partial = self
192 .slots
193 .entry(chunk.id.clone())
194 .or_insert_with(|| PartialMessage {
195 total: chunk.total,
196 received: vec![None; chunk.total],
197 deadline: Instant::now() + CHUNK_TIMEOUT,
198 });
199
200 if partial.total != chunk.total {
202 warn!(
203 "chunk total mismatch for {}: expected {}, got {}",
204 chunk.id, partial.total, chunk.total
205 );
206 return Err("chunk total mismatch");
207 }
208
209 if chunk.seq >= partial.received.len() {
211 partial.received.resize(chunk.total, None);
213 }
214
215 let fragment = BASE64_STANDARD
217 .decode(&chunk.data)
218 .map_err(|_| "invalid base64 in chunk data")?;
219 partial.received[chunk.seq] = Some(fragment);
220
221 if partial.received.iter().all(|f| f.is_some()) {
223 let mut assembled = Vec::with_capacity(
225 partial
226 .received
227 .iter()
228 .map(|f| f.as_ref().map(|d| d.len()).unwrap_or(0))
229 .sum(),
230 );
231 for data in partial.received.iter().flatten() {
232 assembled.extend_from_slice(data);
233 }
234 self.slots.remove(&chunk.id);
235 Ok(Some(assembled))
236 } else {
237 Ok(None)
238 }
239 }
240
241 fn evict_expired(&mut self) {
243 let now = Instant::now();
244 self.slots.retain(|id, partial| {
245 if partial.deadline <= now {
246 warn!("evicting expired incomplete chunk: {}", id);
247 false
248 } else {
249 true
250 }
251 });
252 }
253
254 fn evict_oldest(&mut self) {
256 if let Some((oldest_id, _)) = self
257 .slots
258 .iter()
259 .min_by_key(|(_, partial)| partial.deadline)
260 .map(|(id, p)| (id.clone(), p.deadline))
261 {
262 warn!("evicting oldest chunk to make room: {}", oldest_id);
263 self.slots.remove(&oldest_id);
264 }
265 }
266
267 #[cfg(test)]
269 fn len(&self) -> usize {
270 self.slots.len()
271 }
272}
273
274fn chunk_message(data: &str, msg_id: &str) -> Vec<String> {
294 if data.len() <= MAX_DATAGRAM_SIZE {
296 return vec![data.to_string()];
297 }
298
299 let data_bytes = data.as_bytes();
300 let total = data_bytes.len().div_ceil(CHUNK_PAYLOAD_SIZE);
301
302 let mut chunks = Vec::with_capacity(total);
303 for (seq, fragment) in data_bytes.chunks(CHUNK_PAYLOAD_SIZE).enumerate() {
304 let envelope = ChunkEnvelope {
305 beam_chunk: ChunkFields {
306 id: msg_id.to_string(),
307 seq,
308 total,
309 data: BASE64_STANDARD.encode(fragment),
310 },
311 };
312 let serialized = serde_json::to_string(&envelope)
313 .expect("chunk envelope serialization should never fail");
314 debug_assert!(
315 serialized.len() <= MAX_DATAGRAM_SIZE,
316 "chunk envelope {} bytes exceeds MAX_DATAGRAM_SIZE ({}): total={}, seq={}",
317 serialized.len(),
318 MAX_DATAGRAM_SIZE,
319 total,
320 seq
321 );
322 chunks.push(serialized);
323 }
324
325 chunks
326}
327
328pub struct Multicast {
336 socket: Arc<RwLock<MulticastSocket>>,
337 config: Config,
338}
339
340impl Multicast {
341 pub fn new(config: Config) -> Self {
348 let bind_address = SocketAddrV4::new([233, 255, 255, 255].into(), 7654);
349 let options = MulticastOptions {
350 buffer_size: 64 * 1024,
351 ..MulticastOptions::default()
352 };
353 let interfaces = all_ipv4_interfaces().expect("could not list multicast interfaces");
354 let socket = MulticastSocket::with_options(bind_address, interfaces, options)
355 .expect("could not create and bind multicast socket");
356 let socket = Arc::new(RwLock::new(socket));
357 Multicast { socket, config }
358 }
359
360 fn handle_incoming_message(
370 data: &str,
371 ctx: &ActorContext,
372 allow_public_space: bool,
373 reassembly: &mut ReassemblyBuffer,
374 ) {
375 debug!("in {} bytes", data.len());
376
377 match Message::try_from(data, ctx.addr.clone(), allow_public_space) {
379 Ok(msgs) => {
380 for msg in msgs.into_iter() {
381 Self::forward_message(msg, ctx);
382 }
383 return;
384 }
385 Err(_) => {
386 }
388 }
389
390 match serde_json::from_str::<ChunkEnvelope>(data) {
392 Ok(envelope) => {
393 let chunk = &envelope.beam_chunk;
394 debug!("chunk id={} seq={}/{}", chunk.id, chunk.seq, chunk.total);
395
396 match reassembly.insert(chunk) {
397 Ok(Some(reassembled)) => {
398 match String::from_utf8(reassembled) {
400 Ok(json_str) => {
401 debug!(
402 "reassembled message {} ({} bytes)",
403 chunk.id,
404 json_str.len()
405 );
406 match Message::try_from(
407 &json_str,
408 ctx.addr.clone(),
409 allow_public_space,
410 ) {
411 Ok(msgs) => {
412 for msg in msgs.into_iter() {
413 Self::forward_message(msg, ctx);
414 }
415 }
416 Err(e) => {
417 error!(
418 "reassembled message parse failed: {} (id={})",
419 e, chunk.id
420 );
421 }
422 }
423 }
424 Err(e) => {
425 error!(
426 "reassembled payload not valid UTF-8: {} (id={})",
427 e, chunk.id
428 );
429 }
430 }
431 }
432 Ok(None) => {
433 }
435 Err(e) => {
436 warn!("chunk insert failed: {} (id={})", e, chunk.id);
437 }
438 }
439 }
440 Err(_) => {
441 debug!("discarding unrecognizable multicast datagram");
443 }
444 }
445 }
446
447 fn forward_message(msg: Message, ctx: &ActorContext) {
449 match msg {
450 Message::Put(put) => {
451 let put = put.clone();
452 if let Err(e) = ctx.router.read().send(Message::Put(put)) {
453 error!("failed to send message to node: {:?}", e);
454 }
455 }
456 Message::Get(get) => {
457 let get = get.clone();
458 if let Err(e) = ctx.router.read().send(Message::Get(get)) {
459 error!("failed to send message to node: {:?}", e);
460 }
461 }
462 _ => {}
463 }
464 }
465
466 async fn broadcast_message(&self, serialized: String, msg_id: String) {
468 let chunks = chunk_message(&serialized, &msg_id);
469 let socket = self.socket.read().await;
470 for chunk in chunks {
471 if let Err(e) = socket.broadcast(chunk.as_bytes()) {
472 error!("multicast send error: {}", e);
473 }
474 }
475 }
476}
477
478#[async_trait]
479impl Actor for Multicast {
480 async fn handle(&mut self, msg: Arc<Message>, ctx: &ActorContext) {
481 debug!("out {}", msg.get_id());
482 if msg.is_from(&ctx.addr) {
483 return;
484 }
485 match &*msg {
486 Message::Put(put) => {
487 let msg_id = put.id.clone();
488 let serialized = put.to_string();
489 self.broadcast_message(serialized, msg_id).await;
490 }
491 Message::Get(get) => {
492 let msg_id = get.id.clone();
493 let serialized = get.to_string();
494 self.broadcast_message(serialized, msg_id).await;
495 }
496 _ => {
497 debug!("not sending");
498 }
499 }
500 }
501
502 fn subscribe_to_everything(&self) -> bool {
504 true
505 }
506
507 async fn pre_start(&mut self, ctx: &ActorContext) {
508 info!("Syncing over multicast\n");
509
510 let ctx_clone = ctx.clone();
511
512 let bind_address = SocketAddrV4::new([233, 255, 255, 255].into(), 7654);
513 let options = MulticastOptions {
514 buffer_size: 64 * 1024,
515 ..MulticastOptions::default()
516 };
517 let interfaces = all_ipv4_interfaces().expect("could not list multicast interfaces");
518 let socket = MulticastSocket::with_options(bind_address, interfaces, options)
519 .expect("could not create and bind multicast socket");
520
521 let allow_public_space = self.config.allow_public_space;
522 ctx.blocking_child_task(move || {
523 let mut reassembly = ReassemblyBuffer::new();
524 loop {
525 if let Ok(message) = socket.receive() {
526 if let Ok(data) = std::str::from_utf8(&message.data) {
528 Self::handle_incoming_message(
529 data,
530 &ctx_clone,
531 allow_public_space,
532 &mut reassembly,
533 );
534 }
535 }
536 if *ctx_clone.is_stopped.read() {
537 break;
538 }
539 }
540 });
541 }
542
543 async fn stopping(&mut self, _ctx: &ActorContext) {
544 info!("Multicast stopping");
548 }
549}
550
551#[cfg(test)]
554mod tests {
555 use super::*;
556
557 #[test]
560 fn test_reassembly_single_chunk() {
561 let mut buf = ReassemblyBuffer::new();
562 let data = b"hello world";
563 let encoded = BASE64_STANDARD.encode(data);
564
565 let chunk = ChunkFields {
566 id: "msg1".to_string(),
567 seq: 0,
568 total: 1,
569 data: encoded,
570 };
571
572 let result = buf.insert(&chunk).expect("insert should succeed");
573 assert_eq!(result, Some(b"hello world".to_vec()));
574 assert_eq!(buf.len(), 0); }
576
577 #[test]
578 fn test_reassembly_multiple_chunks_in_order() {
579 let mut buf = ReassemblyBuffer::new();
580 let fragments: Vec<Vec<u8>> = vec![b"AAA".to_vec(), b"BBB".to_vec(), b"CCC".to_vec()];
581
582 for (seq, frag) in fragments.iter().enumerate() {
583 let chunk = ChunkFields {
584 id: "msg2".to_string(),
585 seq,
586 total: 3,
587 data: BASE64_STANDARD.encode(frag),
588 };
589 let result = buf.insert(&chunk).expect("insert should succeed");
590 if seq < 2 {
591 assert!(result.is_none(), "should not be complete at seq {}", seq);
592 } else {
593 assert_eq!(
594 result,
595 Some(b"AAABBBCCC".to_vec()),
596 "should reassemble on final chunk"
597 );
598 }
599 }
600 assert_eq!(buf.len(), 0);
601 }
602
603 #[test]
604 fn test_reassembly_multiple_chunks_out_of_order() {
605 let mut buf = ReassemblyBuffer::new();
606 let fragments: Vec<Vec<u8>> = vec![b"AAA".to_vec(), b"BBB".to_vec(), b"CCC".to_vec()];
607
608 let order = [2, 0, 1];
610 for &seq in &order {
611 let chunk = ChunkFields {
612 id: "msg3".to_string(),
613 seq,
614 total: 3,
615 data: BASE64_STANDARD.encode(&fragments[seq]),
616 };
617 let result = buf.insert(&chunk).expect("insert should succeed");
618 if seq != 1 {
619 assert!(result.is_none(), "should not be complete yet");
620 } else {
621 assert_eq!(
622 result,
623 Some(b"AAABBBCCC".to_vec()),
624 "should reassemble when last chunk arrives"
625 );
626 }
627 }
628 }
629
630 #[test]
631 fn test_reassembly_duplicate_chunk_ignored() {
632 let mut buf = ReassemblyBuffer::new();
633
634 let chunk = ChunkFields {
635 id: "msg4".to_string(),
636 seq: 0,
637 total: 2,
638 data: BASE64_STANDARD.encode(b"AAA"),
639 };
640 buf.insert(&chunk).expect("first insert");
641
642 let result = buf.insert(&chunk).expect("duplicate insert");
644 assert!(result.is_none(), "duplicate should return None");
645 assert_eq!(buf.len(), 1, "slot should still exist");
646 }
647
648 #[test]
649 fn test_reassembly_total_mismatch_rejected() {
650 let mut buf = ReassemblyBuffer::new();
651
652 let chunk1 = ChunkFields {
653 id: "msg5".to_string(),
654 seq: 0,
655 total: 3,
656 data: BASE64_STANDARD.encode(b"AAA"),
657 };
658 buf.insert(&chunk1).expect("first insert");
659
660 let chunk2 = ChunkFields {
661 id: "msg5".to_string(),
662 seq: 1,
663 total: 2, data: BASE64_STANDARD.encode(b"BBB"),
665 };
666 let result = buf.insert(&chunk2);
667 assert!(result.is_err(), "total mismatch should be rejected");
668 }
669
670 #[test]
671 fn test_reassembly_seq_out_of_bounds_rejected() {
672 let mut buf = ReassemblyBuffer::new();
673
674 let chunk = ChunkFields {
675 id: "msg6".to_string(),
676 seq: 5,
677 total: 3,
678 data: BASE64_STANDARD.encode(b"AAA"),
679 };
680 let result = buf.insert(&chunk);
681 assert!(result.is_err(), "seq >= total should be rejected");
682 }
683
684 #[test]
685 fn test_reassembly_invalid_base64_rejected() {
686 let mut buf = ReassemblyBuffer::new();
687
688 let chunk = ChunkFields {
689 id: "msg7".to_string(),
690 seq: 0,
691 total: 1,
692 data: "not valid base64!!!".to_string(),
693 };
694 let result = buf.insert(&chunk);
695 assert!(result.is_err(), "invalid base64 should be rejected");
696 }
697
698 #[test]
699 fn test_reassembly_concurrent_messages() {
700 let mut buf = ReassemblyBuffer::new();
701
702 let chunks = [
704 ChunkFields {
705 id: "a".to_string(),
706 seq: 0,
707 total: 2,
708 data: BASE64_STANDARD.encode(b"A0"),
709 },
710 ChunkFields {
711 id: "b".to_string(),
712 seq: 0,
713 total: 2,
714 data: BASE64_STANDARD.encode(b"B0"),
715 },
716 ChunkFields {
717 id: "a".to_string(),
718 seq: 1,
719 total: 2,
720 data: BASE64_STANDARD.encode(b"A1"),
721 },
722 ChunkFields {
723 id: "b".to_string(),
724 seq: 1,
725 total: 2,
726 data: BASE64_STANDARD.encode(b"B1"),
727 },
728 ];
729
730 let results: Vec<_> = chunks.iter().map(|c| buf.insert(c).unwrap()).collect();
731
732 assert!(results[0].is_none()); assert!(results[1].is_none()); assert_eq!(results[2], Some(b"A0A1".to_vec())); assert_eq!(results[3], Some(b"B0B1".to_vec())); }
737
738 #[test]
739 fn test_reassembly_max_slots_eviction() {
740 let mut buf = ReassemblyBuffer::new();
741
742 for i in 0..MAX_REASSEMBLY_SLOTS {
744 let chunk = ChunkFields {
745 id: format!("fill{}", i),
746 seq: 0,
747 total: 2, data: BASE64_STANDARD.encode(b"x"),
749 };
750 buf.insert(&chunk).expect("fill insert");
751 }
752 assert_eq!(buf.len(), MAX_REASSEMBLY_SLOTS);
753
754 let chunk = ChunkFields {
756 id: "new".to_string(),
757 seq: 0,
758 total: 2,
759 data: BASE64_STANDARD.encode(b"y"),
760 };
761 buf.insert(&chunk).expect("overflow insert");
762 assert_eq!(
763 buf.len(),
764 MAX_REASSEMBLY_SLOTS,
765 "should evict oldest to maintain cap"
766 );
767 assert!(buf.slots.contains_key("new"));
769 assert!(!buf.slots.contains_key("fill0"));
771 }
772
773 #[test]
774 fn test_reassembly_empty_data() {
775 let mut buf = ReassemblyBuffer::new();
776 let chunk = ChunkFields {
777 id: "empty".to_string(),
778 seq: 0,
779 total: 1,
780 data: BASE64_STANDARD.encode(b""),
781 };
782 let result = buf.insert(&chunk).expect("empty insert");
783 assert_eq!(result, Some(Vec::new()));
784 }
785
786 #[test]
789 fn test_chunk_small_message_passthrough() {
790 let msg =
791 r##"{"put":{"~test":{"_":{"#":"~test",">":{"name":1}},"name":"hello"}},"#":"abc123"}"##;
792 let chunks = chunk_message(msg, "abc123");
793 assert_eq!(chunks.len(), 1, "small message should not be chunked");
794 assert_eq!(chunks[0], msg, "passthrough should be identical");
795 }
796
797 #[test]
798 fn test_chunk_exactly_at_threshold() {
799 let msg = "x".repeat(MAX_DATAGRAM_SIZE);
801 let chunks = chunk_message(&msg, "threshold");
802 assert_eq!(
803 chunks.len(),
804 1,
805 "message at threshold should not be chunked"
806 );
807 }
808
809 #[test]
810 fn test_chunk_one_byte_over_threshold() {
811 let msg = "x".repeat(MAX_DATAGRAM_SIZE + 1);
812 let chunks = chunk_message(&msg, "over");
813 assert!(chunks.len() > 1, "message over threshold should be chunked");
814
815 for chunk in &chunks {
817 assert!(
818 chunk.len() <= MAX_DATAGRAM_SIZE,
819 "chunk of {} bytes exceeds MAX_DATAGRAM_SIZE",
820 chunk.len()
821 );
822 }
823 }
824
825 #[test]
826 fn test_chunk_round_trip_reassembly() {
827 let mut buf = ReassemblyBuffer::new();
828 let payload = "Z".repeat(MAX_DATAGRAM_SIZE * 3 + 42);
830 let msg_id = "roundtrip";
831
832 let chunks = chunk_message(&payload, msg_id);
833 assert!(chunks.len() > 1, "should produce multiple chunks");
834
835 for (i, chunk) in chunks.iter().enumerate() {
838 let envelope: ChunkEnvelope =
839 serde_json::from_str(chunk).expect("chunk should be valid envelope");
840 assert_eq!(envelope.beam_chunk.id, msg_id);
841 assert_eq!(envelope.beam_chunk.seq, i);
842 assert_eq!(envelope.beam_chunk.total, chunks.len());
843
844 let result = buf
845 .insert(&envelope.beam_chunk)
846 .expect("insert should succeed");
847 if i < chunks.len() - 1 {
848 assert!(result.is_none(), "should not be complete at chunk {}", i);
849 } else {
850 let reassembled = result.expect("should be complete on last chunk");
851 let reassembled_str =
852 String::from_utf8(reassembled).expect("reassembled should be UTF-8");
853 assert_eq!(
854 reassembled_str, payload,
855 "reassembled payload should match original"
856 );
857 }
858 }
859 }
860
861 #[test]
862 fn test_chunk_each_envelope_under_max() {
863 let large_value = "A".repeat(5000);
865 let msg = format!(
866 r##"{{"put":{{"~test":{{"_":{{"#":"~test",">":{{"data":1}}}},"data":"{}"}}}},"#":"bigmsg"}}"##,
867 large_value
868 );
869 let chunks = chunk_message(&msg, "bigmsg");
870 assert!(chunks.len() > 1, "large message should be chunked");
871
872 for (i, chunk) in chunks.iter().enumerate() {
873 assert!(
874 chunk.len() <= MAX_DATAGRAM_SIZE,
875 "chunk {} is {} bytes, exceeds MAX_DATAGRAM_SIZE ({})",
876 i,
877 chunk.len(),
878 MAX_DATAGRAM_SIZE
879 );
880 }
881 }
882
883 #[test]
884 fn test_chunk_empty_message() {
885 let chunks = chunk_message("", "empty");
886 assert_eq!(
887 chunks.len(),
888 1,
889 "empty message should be single passthrough"
890 );
891 assert_eq!(chunks[0], "");
892 }
893
894 #[test]
895 fn test_chunk_preserves_message_id() {
896 let msg = "x".repeat(MAX_DATAGRAM_SIZE + 100);
897 let chunks = chunk_message(&msg, "myID123");
898
899 for chunk in &chunks {
900 let envelope: ChunkEnvelope =
901 serde_json::from_str(chunk).expect("chunk should be valid envelope");
902 assert_eq!(envelope.beam_chunk.id, "myID123");
903 }
904 }
905
906 #[test]
907 fn test_chunk_total_is_consistent() {
908 let msg = "y".repeat(CHUNK_PAYLOAD_SIZE * 5 + 1);
909 let chunks = chunk_message(&msg, "consistency");
910
911 let total = chunks
912 .first()
913 .map(|c| {
914 let env: ChunkEnvelope = serde_json::from_str(c).expect("first chunk is envelope");
915 env.beam_chunk.total
916 })
917 .expect("should have at least one chunk");
918
919 assert_eq!(chunks.len(), total, "chunk count should match total field");
920
921 for (i, chunk) in chunks.iter().enumerate() {
922 let env: ChunkEnvelope = serde_json::from_str(chunk).expect("chunk is valid envelope");
923 assert_eq!(env.beam_chunk.total, total);
924 assert_eq!(env.beam_chunk.seq, i);
925 }
926 }
927}