1use std::net::{IpAddr, Ipv4Addr, SocketAddr};
15use std::os::unix::io::AsRawFd;
16use std::path::PathBuf;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
19
20use std::hash::{Hash, Hasher};
21
22use tracing::{debug, error, info, warn};
23
24use melin_journal::BufferedWriter;
25use melin_journal::JournalError;
26use melin_journal::JournalWrite;
27use melin_journal::SectorWriter;
28use melin_transport_core::journaled_app::JournaledApp;
29use melin_transport_core::pipeline::{
30 InputSlot, JournalStage, JournalStageRun, OutputSlot as GenericOutputSlot,
31 Pipeline as GenericPipeline, build_pipeline_with_replication,
32};
33type Pipeline<A, W> = GenericPipeline<A, W>;
38
39use crate::reader::RequestDecoderArc;
40use crate::response::ResponseEncoderArc;
41use melin_app::Application;
42use melin_app::app_factory::AppFactory;
43use melin_app::auth::AuthorizedKeys;
44use melin_app::auth::Permission;
45use melin_app::decoder::RequestDecoder;
46use melin_app::encoder::ResponseEncoder;
47use melin_pipeline::ring::Consumer;
48
49type OutputSlot<A> =
53 GenericOutputSlot<<A as Application>::Report, <A as Application>::QueryResponse>;
54
55pub type EventPublisherFn<A> = fn(
67 consumer: Consumer<OutputSlot<A>>,
68 bind_addr: SocketAddr,
69 authorized_keys: Arc<AuthorizedKeys>,
70 shutdown: &AtomicBool,
71 busy_spin: bool,
72);
73
74use melin_wire_protocol::blocking::BlockingFrameWriter;
75use melin_wire_protocol::control::ConnectionId;
76use melin_wire_protocol::transport::BlockingTransportListener;
77
78const DEFAULT_REPLICATION_PIPELINE_DEPTH: usize = 8;
80
81#[derive(clap::Parser)]
83#[command(name = "melin-server", about = "Low-latency matching engine server")]
84pub struct ServerConfig {
85 #[arg(long, default_value = "127.0.0.1:9876")]
87 pub bind: SocketAddr,
88 #[arg(long, default_value = "melin.journal")]
90 pub journal: PathBuf,
91 #[arg(long)]
93 pub snapshot: Option<PathBuf>,
94 #[arg(long, default_value = "1,2,3,4,5,6,7,8,9", value_parser = parse_cores)]
101 pub cores: PipelineCores,
102 #[arg(long, default_value_t = 0)]
104 pub group_commit_us: u64,
105 #[arg(long, default_value_t = 10)]
108 pub heartbeat_interval_secs: u64,
109 #[arg(long, default_value_t = 30)]
112 pub connection_timeout_secs: u64,
113 #[arg(long, default_value_t = 1024)]
117 pub max_connections: u64,
118 #[arg(long, default_value_t = 100_000)]
121 pub accounts: u32,
122 #[arg(long, default_value_t = 100)]
124 pub instruments: u32,
125 #[arg(long, default_value = "authorized_keys")]
130 pub authorized_keys: PathBuf,
131 #[arg(long, default_value_t = 256)]
135 pub max_journal_mib: u64,
136 #[arg(
145 long,
146 default_value_t = melin_journal::JournalWriterMode::default(),
147 value_parser = melin_journal::JournalWriterMode::parse,
148 )]
149 pub journal_writer: melin_journal::JournalWriterMode,
150 #[arg(long, default_value_t = 10_000)]
158 pub max_orders_per_account: u32,
159 #[arg(long, default_value_t = 1_000)]
171 pub max_orders_per_second: u32,
172 #[arg(long, default_value_t = 5_000)]
179 pub max_orders_burst: u32,
180
181 #[arg(long)]
184 pub replication_bind: Option<std::net::SocketAddr>,
185
186 #[arg(long, default_value_t = false)]
191 pub standalone: bool,
192
193 #[arg(long)]
197 pub replica_of: Option<std::net::SocketAddr>,
198
199 #[arg(long)]
204 pub replication_key: Option<std::path::PathBuf>,
205
206 #[arg(long, default_value_t = 128)]
210 pub replication_batch_size: usize,
211
212 #[arg(long, default_value_t = 4096)]
216 pub max_journal_batch: usize,
217
218 #[arg(long, default_value_t = 5)]
222 pub replication_heartbeat_secs: u64,
223
224 #[arg(long, default_value_t = DEFAULT_REPLICATION_PIPELINE_DEPTH)]
230 pub replication_pipeline_depth: usize,
231
232 #[arg(long, default_value_t = 256)]
236 pub replication_ring_size: usize,
237
238 #[arg(long, value_enum, default_value_t = crate::durability_policy::DurabilityMode::Hybrid)]
257 pub durability_mode: crate::durability_policy::DurabilityMode,
258
259 #[arg(long, default_value_t = false)]
264 pub yield_idle: bool,
265
266 #[arg(long, default_value = "", allow_hyphen_values = true)]
270 pub dpdk_eal_args: String,
271
272 #[arg(long, default_value = "0", value_delimiter = ',')]
275 pub dpdk_ports: Vec<u16>,
276
277 #[arg(long, default_value = "10.0.0.1")]
279 pub dpdk_ip: String,
280
281 #[arg(long, default_value_t = 24)]
283 pub dpdk_prefix_len: u8,
284
285 #[arg(long)]
287 pub dpdk_gateway: Option<String>,
288
289 #[arg(long)]
295 pub dpdk_peer_ip: Option<String>,
296
297 #[arg(long)]
302 pub dpdk_gateway_mac: Option<String>,
303
304 #[arg(long, default_value_t = 1500)]
307 pub dpdk_mtu: usize,
308
309 #[arg(long)]
313 pub dpdk_vlan: Option<u16>,
314
315 #[arg(long)]
320 pub event_bind: Option<SocketAddr>,
321
322 #[arg(long, default_value = "127.0.0.1:9878")]
326 pub health_bind: Option<SocketAddr>,
327
328 #[arg(long)]
341 pub admin_bind: Option<SocketAddr>,
342
343 #[arg(long, default_value_t = 3_000_000)]
348 pub snapshot_interval_ms: u64,
349
350 #[arg(long)]
353 pub snapshot_path: Option<PathBuf>,
354
355 #[arg(long, default_value_t = 250)]
371 pub tick_interval_ms: u64,
372
373 #[arg(long, default_value_t = false)]
386 pub no_mlock: bool,
387}
388
389impl Default for ServerConfig {
393 fn default() -> Self {
394 Self {
399 bind: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9876),
400 journal: PathBuf::from("melin.journal"),
401 snapshot: None,
402 cores: PipelineCores {
403 journal: 1,
404 matching: 2,
405 response: 3,
406 reader: 4,
407 repl_sender: 5,
408 event_publisher: 6,
409 shadow: 7,
410 repl_handler_0: 8,
411 repl_handler_1: 9,
412 },
413 group_commit_us: 0,
414 heartbeat_interval_secs: 10,
415 connection_timeout_secs: 30,
416 max_connections: 1024,
417 accounts: 2,
418 instruments: 2,
419 authorized_keys: PathBuf::from("authorized_keys"),
420 max_journal_mib: 256,
421 journal_writer: melin_journal::JournalWriterMode::default(),
422 max_orders_per_account: 10_000,
423 max_orders_per_second: 1_000,
424 max_orders_burst: 5_000,
425
426 replication_bind: None,
427 standalone: false,
428 replica_of: None,
429 replication_key: None,
430 replication_batch_size: 128,
431 max_journal_batch: 1024,
432 replication_heartbeat_secs: 5,
433 replication_pipeline_depth: DEFAULT_REPLICATION_PIPELINE_DEPTH,
434 replication_ring_size: 256,
435 durability_mode: crate::durability_policy::DurabilityMode::Hybrid,
436 yield_idle: false,
437 dpdk_eal_args: String::new(),
438 dpdk_peer_ip: None,
439 dpdk_gateway_mac: None,
440 dpdk_ports: vec![0],
441 dpdk_ip: "10.0.0.1".into(),
442 dpdk_prefix_len: 24,
443 dpdk_gateway: None,
444 dpdk_mtu: 1500,
445 dpdk_vlan: None,
446 event_bind: None,
447 health_bind: Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9878)),
448 admin_bind: None,
449 snapshot_interval_ms: 3_000_000,
450 snapshot_path: None,
451 tick_interval_ms: 250,
452 no_mlock: false,
453 }
454 }
455}
456
457impl ServerConfig {
458 pub fn group_commit_delay(&self) -> std::time::Duration {
460 std::time::Duration::from_micros(self.group_commit_us)
461 }
462
463 pub fn heartbeat_interval(&self) -> Option<std::time::Duration> {
465 if self.heartbeat_interval_secs == 0 {
466 None
467 } else {
468 Some(std::time::Duration::from_secs(self.heartbeat_interval_secs))
469 }
470 }
471
472 pub fn connection_timeout(&self) -> Option<std::time::Duration> {
474 if self.connection_timeout_secs == 0 {
475 None
476 } else {
477 Some(std::time::Duration::from_secs(self.connection_timeout_secs))
478 }
479 }
480
481 pub fn shadow_snapshot_path(&self) -> PathBuf {
484 self.snapshot_path
485 .clone()
486 .unwrap_or_else(|| self.journal.with_extension("snapshot"))
487 }
488
489 pub fn tick_interval(&self) -> Option<std::time::Duration> {
492 if self.tick_interval_ms == 0 {
493 None
494 } else {
495 Some(std::time::Duration::from_millis(self.tick_interval_ms))
496 }
497 }
498}
499
500#[derive(Debug, Clone, Copy)]
507pub struct PipelineCores {
508 pub journal: usize,
509 pub matching: usize,
510 pub response: usize,
511 pub reader: usize,
513 pub repl_sender: usize,
514 pub event_publisher: usize,
515 pub shadow: usize,
516 pub repl_handler_0: usize,
518 pub repl_handler_1: usize,
520}
521
522impl PipelineCores {
523 pub fn compact(num_cpus: usize) -> Result<(Self, usize), String> {
537 if num_cpus < 8 {
541 return Err(format!(
542 "compact layout needs >= 8 logical cores; have {num_cpus}"
543 ));
544 }
545 let cores = PipelineCores {
546 journal: 1,
547 matching: 2,
548 response: 3,
549 reader: 4,
550 repl_sender: 0,
552 event_publisher: 5,
553 shadow: 6,
554 repl_handler_0: 0,
556 repl_handler_1: 0,
557 };
558 Ok((cores, 7))
559 }
560}
561
562fn parse_cores(s: &str) -> Result<PipelineCores, String> {
564 let parts: Vec<&str> = s.split(',').collect();
565 if parts.len() != 9 {
566 return Err(format!(
567 "expected 9 comma-separated core IDs (journal,matching,response,reader,repl-sender,event-publisher,shadow,repl-handler-0,repl-handler-1), got {}",
568 parts.len()
569 ));
570 }
571 let parse = |p: &str| {
572 p.parse::<usize>()
573 .map_err(|_| format!("invalid core ID: {p}"))
574 };
575 Ok(PipelineCores {
576 journal: parse(parts[0])?,
577 matching: parse(parts[1])?,
578 response: parse(parts[2])?,
579 reader: parse(parts[3])?,
580 repl_sender: parse(parts[4])?,
581 event_publisher: parse(parts[5])?,
582 shadow: parse(parts[6])?,
583 repl_handler_0: parse(parts[7])?,
584 repl_handler_1: parse(parts[8])?,
585 })
586}
587
588pub fn run<A, F, D, E>(
599 config: ServerConfig,
600 factory: F,
601 decoder: D,
602 encoder: E,
603 event_publisher: Option<EventPublisherFn<A>>,
604) -> Result<(), Box<dyn std::error::Error>>
605where
606 A: Application + Send + 'static,
607 A::Event: Send + Sync + 'static,
608 A::Report: Send + 'static,
609 A::QueryResponse: Send + 'static,
610 F: AppFactory<App = A> + 'static,
611 D: RequestDecoder<Event = A::Event> + 'static,
612 E: ResponseEncoder<Report = A::Report, Query = A::QueryResponse> + 'static,
613{
614 let shutdown = Arc::new(AtomicBool::new(false));
615 crate::process::install_shutdown_handler(&shutdown);
616
617 if !config.no_mlock {
618 crate::process::try_lock_memory();
619 }
620
621 #[cfg(feature = "dpdk")]
622 {
623 run_dpdk(
624 config,
625 Arc::new(factory),
626 Arc::new(decoder),
627 Arc::new(encoder),
628 event_publisher,
629 shutdown,
630 )
631 }
632
633 #[cfg(not(feature = "dpdk"))]
634 {
635 let listener = melin_wire_protocol::tcp::BlockingTcpListener::bind(config.bind)?;
636 run_tcp(
637 listener,
638 config,
639 Arc::new(factory),
640 Arc::new(decoder),
641 Arc::new(encoder),
642 event_publisher,
643 shutdown,
644 )
645 }
646}
647
648pub fn run_with_listener<A, L, F, D, E>(
657 listener: L,
658 config: ServerConfig,
659 factory: F,
660 decoder: D,
661 encoder: E,
662 event_publisher: Option<EventPublisherFn<A>>,
663 shutdown: Arc<AtomicBool>,
664) -> Result<(), Box<dyn std::error::Error>>
665where
666 A: Application + Send + 'static,
667 A::Event: Send + Sync + 'static,
668 A::Report: Send + 'static,
669 A::QueryResponse: Send + 'static,
670 L: BlockingTransportListener,
671 F: AppFactory<App = A> + 'static,
672 D: RequestDecoder<Event = A::Event> + 'static,
673 E: ResponseEncoder<Report = A::Report, Query = A::QueryResponse> + 'static,
674{
675 run_tcp(
676 listener,
677 config,
678 Arc::new(factory),
679 Arc::new(decoder),
680 Arc::new(encoder),
681 event_publisher,
682 shutdown,
683 )
684}
685
686fn run_tcp<A, L>(
687 listener: L,
688 config: ServerConfig,
689 factory: Arc<dyn AppFactory<App = A>>,
690 decoder: RequestDecoderArc<A>,
691 encoder: ResponseEncoderArc<A>,
692 event_publisher: Option<EventPublisherFn<A>>,
693 shutdown: Arc<AtomicBool>,
694) -> Result<(), Box<dyn std::error::Error>>
695where
696 A: Application + Send + 'static,
697 A::Event: Send + Sync + 'static,
698 A::Report: Send + 'static,
699 A::QueryResponse: Send + 'static,
700 L: BlockingTransportListener,
701{
702 match config.journal_writer {
703 melin_journal::JournalWriterMode::Buffered => run_impl::<A, L, BufferedWriter<A::Event>>(
704 listener,
705 config,
706 factory,
707 decoder,
708 encoder,
709 event_publisher,
710 shutdown,
711 ),
712 melin_journal::JournalWriterMode::Sector => run_impl::<A, L, SectorWriter<A::Event>>(
713 listener,
714 config,
715 factory,
716 decoder,
717 encoder,
718 event_publisher,
719 shutdown,
720 ),
721 }
722}
723
724fn run_impl<A, L, W>(
725 listener: L,
726 config: ServerConfig,
727 factory: Arc<dyn AppFactory<App = A>>,
728 decoder: RequestDecoderArc<A>,
729 encoder: ResponseEncoderArc<A>,
730 event_publisher: Option<EventPublisherFn<A>>,
731 shutdown: Arc<AtomicBool>,
732) -> Result<(), Box<dyn std::error::Error>>
733where
734 A: Application + Send + 'static,
735 A::Event: Send + Sync + 'static,
736 A::Report: Send + 'static,
737 A::QueryResponse: Send + 'static,
738 L: BlockingTransportListener,
739 W: JournalWrite<A::Event> + Send + 'static,
740 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
741{
742 let durability_mode_atomic = Arc::new(AtomicU8::new(config.durability_mode.as_u8()));
749
750 if let Some(primary_addr) = config.replica_of {
754 info!(primary = %primary_addr, "starting in replica mode");
755
756 let replication_key_path = config.replication_key.as_ref().ok_or_else(|| {
758 std::io::Error::other("--replication-key is required in replica mode (--replica-of)")
759 })?;
760 let signing_key = {
761 let seed = std::fs::read(replication_key_path).map_err(|e| {
762 std::io::Error::other(format!(
763 "failed to read replication key {}: {e}",
764 replication_key_path.display()
765 ))
766 })?;
767 if seed.len() != 32 {
768 return Err(format!(
769 "replication key must be 32 bytes, got {} ({})",
770 seed.len(),
771 replication_key_path.display()
772 )
773 .into());
774 }
775 let mut bytes = [0u8; 32];
776 bytes.copy_from_slice(&seed);
777 ed25519_dalek::SigningKey::from_bytes(&bytes)
778 };
779
780 let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
783 info!(
784 keys = authorized_keys.len(),
785 path = %config.authorized_keys.display(),
786 "loaded authorized keys (replica mode)"
787 );
788
789 let promote_flag = Arc::new(AtomicBool::new(false));
795 let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
796 let _admin_handle = config.admin_bind.map(|addr| {
797 crate::admin::spawn(
798 addr,
799 Some(Arc::clone(&promote_flag)),
800 rotate_flag.clone(),
801 Some(Arc::clone(&durability_mode_atomic)),
802 Arc::clone(&shutdown),
803 Arc::clone(&authorized_keys),
804 )
805 });
806
807 let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(0));
813
814 match crate::replication::run_receiver::<A, W>(
821 primary_addr,
822 &config.journal,
823 &signing_key,
824 &shutdown,
825 &promote_flag,
826 config.snapshot_interval_ms,
827 config.shadow_snapshot_path(),
828 config.cores,
829 config.group_commit_delay(),
830 config.replication_pipeline_depth,
831 !config.yield_idle,
832 Arc::clone(&factory),
833 Arc::clone(&fence_state),
834 )? {
835 None => return Ok(()), Some((mut exchange, writer)) => {
837 info!("replica promoted — transitioning to primary");
840 <A as Application>::prefault(&mut exchange);
841
842 if let Some(ref flag) = rotate_flag {
848 flag.store(false, Ordering::Release);
849 }
850
851 return run_as_primary::<A, L, W>(
852 exchange,
853 writer,
854 listener,
855 &config,
856 &*factory,
857 decoder,
858 encoder,
859 event_publisher,
860 shutdown,
861 authorized_keys,
862 false, rotate_flag,
864 durability_mode_atomic,
865 fence_state,
866 true, );
868 }
869 }
870 }
871 let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
873 info!(
874 keys = authorized_keys.len(),
875 path = %config.authorized_keys.display(),
876 "loaded authorized keys"
877 );
878
879 let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
884 let _admin_handle = config.admin_bind.map(|addr| {
885 crate::admin::spawn(
886 addr,
887 None,
888 rotate_flag.clone(),
889 Some(Arc::clone(&durability_mode_atomic)),
890 Arc::clone(&shutdown),
891 Arc::clone(&authorized_keys),
892 )
893 });
894
895 let (mut exchange, writer, needs_seeding, recovered_epoch) =
898 init_engine::<A, W>(&config, &*factory)?;
899
900 <A as Application>::prefault(&mut exchange);
904
905 let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(
908 recovered_epoch,
909 ));
910
911 run_as_primary::<A, L, W>(
912 exchange,
913 writer,
914 listener,
915 &config,
916 &*factory,
917 decoder,
918 encoder,
919 event_publisher,
920 shutdown,
921 authorized_keys,
922 needs_seeding,
923 rotate_flag,
924 durability_mode_atomic,
925 fence_state,
926 false, )
928}
929
930pub use crate::ControlEvent;
937
938struct PipelineHandles<A: Send + 'static, W: Send + 'static> {
943 journal: std::thread::JoinHandle<Result<W, JournalError>>,
944 matching: std::thread::JoinHandle<A>,
945 response: std::thread::JoinHandle<()>,
946 replication: Option<std::thread::JoinHandle<()>>,
947 event_publisher: Option<std::thread::JoinHandle<()>>,
948 shadow: Option<std::thread::JoinHandle<()>>,
949 health: Option<std::thread::JoinHandle<()>>,
950 }
954
955fn shutdown_pipeline_stages<A: Send + 'static, W: Send + 'static>(
961 handles: PipelineHandles<A, W>,
962 extras: Vec<(String, std::thread::Result<()>)>,
963 pipeline_healthy: &AtomicBool,
964 shutdown: &AtomicBool,
965) -> Result<(), Box<dyn std::error::Error>> {
966 info!("shutdown: draining pipeline");
967 pipeline_healthy.store(false, Ordering::Relaxed);
968 shutdown.store(true, Ordering::Relaxed);
969
970 let mut thread_panicked = false;
971 let mut check_join = |name: &str, result: std::thread::Result<()>| {
972 if let Err(panic) = result {
973 let msg = panic
974 .downcast_ref::<&str>()
975 .copied()
976 .or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
977 .unwrap_or("<non-string panic>");
978 error!(thread = name, message = msg, "pipeline thread panicked");
979 thread_panicked = true;
980 }
981 };
982
983 let journal_result = handles.journal.join();
984 let journal_failed = matches!(&journal_result, Ok(Err(_)));
985 if let Ok(Err(ref e)) = journal_result {
986 error!(thread = "journal", error = %e, "journal stage returned error");
987 }
988 check_join("journal", journal_result.map(|_| ()));
989 check_join("matching", handles.matching.join().map(|_| ()));
990 check_join("response", handles.response.join());
991 for (name, r) in extras {
992 check_join(&name, r);
993 }
994 if let Some(h) = handles.replication {
995 check_join("replication-sender", h.join());
996 }
997 if let Some(h) = handles.event_publisher {
998 check_join("event-publisher", h.join());
999 }
1000 if let Some(h) = handles.shadow {
1001 check_join("shadow", h.join());
1002 }
1003 if let Some(h) = handles.health {
1004 check_join("health", h.join());
1005 }
1006
1007 melin_transport_core::trace::print_report_all();
1010
1011 if thread_panicked || journal_failed {
1012 error!("shutdown complete (with pipeline failure)");
1013 return Err("pipeline failure".into());
1014 }
1015
1016 info!("shutdown complete");
1017 Ok(())
1018}
1019
1020#[allow(clippy::too_many_arguments)]
1030fn run_as_primary<A, L, W>(
1031 exchange: A,
1032 writer: W,
1033 mut listener: L,
1034 config: &ServerConfig,
1035 factory: &dyn AppFactory<App = A>,
1036 decoder: RequestDecoderArc<A>,
1037 encoder: ResponseEncoderArc<A>,
1038 event_publisher: Option<EventPublisherFn<A>>,
1039 shutdown: Arc<AtomicBool>,
1040 authorized_keys: Arc<AuthorizedKeys>,
1041 needs_seeding: bool,
1042 rotate_flag: Option<Arc<AtomicBool>>,
1043 durability_mode_atomic: Arc<AtomicU8>,
1044 fence_state: Arc<melin_transport_core::fence::FenceState>,
1045 promoted: bool,
1046) -> Result<(), Box<dyn std::error::Error>>
1047where
1048 A: Application + Send + 'static,
1049 A::Event: Send + Sync + 'static,
1050 A::Report: Send + 'static,
1051 A::QueryResponse: Send + 'static,
1052 L: BlockingTransportListener,
1053 W: JournalWrite<A::Event> + Send + 'static,
1054 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
1055{
1056 let active_connections = Arc::new(AtomicU64::new(0));
1061
1062 let enable_replication = config.replication_bind.is_some();
1067 if enable_replication && config.standalone {
1068 return Err("--replication-bind and --standalone are mutually exclusive".into());
1069 }
1070 if config.standalone
1074 && config.durability_mode != crate::durability_policy::DurabilityMode::Local
1075 {
1076 return Err(format!(
1077 "--standalone requires --durability-mode local; got `{}` (this mode needs at least one connected replica)",
1078 config.durability_mode,
1079 )
1080 .into());
1081 }
1082 let enable_shadow = config.snapshot_interval_ms > 0;
1086 let shadow_exchange = if enable_shadow {
1087 Some(<A as Application>::clone_via_snapshot(&exchange)?)
1088 } else {
1089 None
1090 };
1091
1092 let enable_event_publisher = event_publisher.is_some() && config.event_bind.is_some();
1100 let Pipeline {
1101 input_producer,
1102 journal_stage,
1103 matching_stage,
1104 mut output_consumers,
1105 events_processed,
1106 input_cursor,
1107 replication_consumers,
1108 replicas_connected,
1109 shadow_consumer,
1110 chain_hash_lock,
1111 replication_ring_progress,
1112 cursors,
1113 } = build_pipeline_with_replication(
1114 exchange,
1115 writer,
1116 config.group_commit_delay(),
1117 Arc::clone(&active_connections),
1118 enable_replication,
1119 config.max_journal_batch,
1120 config.replication_ring_size,
1121 !config.yield_idle,
1122 enable_event_publisher,
1123 enable_shadow,
1124 Arc::clone(&fence_state),
1125 );
1126 let journal_cursor = cursors.journal_ring_arc();
1131 let matching_cursor = cursors.matching_ring_arc();
1132 let fastest_replica_cursor = Arc::new(AtomicU64::new(
1139 melin_transport_core::PipelineCursors::NO_REPLICA,
1140 ));
1141
1142 let output_consumer = output_consumers.remove(0);
1145 let event_publisher_consumer = if enable_event_publisher {
1146 Some(output_consumers.remove(0))
1147 } else {
1148 None
1149 };
1150
1151 let (control_tx, control_rx) = std::sync::mpsc::channel();
1153
1154 let connection_timeout = config.connection_timeout();
1160 let heartbeat_interval = config.heartbeat_interval();
1161
1162 let mut input_producer = input_producer;
1168
1169 let cores = config.cores;
1171
1172 let mut journal_stage = journal_stage;
1179 let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
1180 journal_stage.set_rotation(max_journal_bytes, rotate_flag.clone());
1181 if config.max_journal_mib > 0 {
1182 info!(
1183 max_journal_mib = config.max_journal_mib,
1184 "runtime journal rotation enabled (size threshold)"
1185 );
1186 }
1187
1188 let journal_utilization = journal_stage.utilization();
1190 let matching_utilization = matching_stage.utilization();
1191 let response_utilization = Arc::new(melin_transport_core::pipeline::StageUtilization::new());
1192
1193 let s1 = Arc::clone(&shutdown);
1199 let shutdown_for_journal = Arc::clone(&shutdown);
1200 let journal_handle = std::thread::Builder::new()
1201 .name("journal".into())
1202 .spawn(move || {
1203 melin_app::affinity::pin_thread("journal", cores.journal);
1204 let result = journal_stage.run(&s1);
1205 let was_shutdown = shutdown_for_journal.load(Ordering::Relaxed);
1206 match &result {
1207 Ok(_) if was_shutdown => info!("journal thread exited cleanly on shutdown"),
1208 Ok(_) => error!("journal thread returned without shutdown signal"),
1209 Err(e) => error!(error = %e, "journal thread returned Err"),
1210 }
1211 result
1212 })
1213 .map_err(|e| format!("spawn journal thread: {e}"))?;
1214
1215 let s2 = Arc::clone(&shutdown);
1216 let shutdown_for_matching = Arc::clone(&shutdown);
1217 let matching_handle = std::thread::Builder::new()
1218 .name("matching".into())
1219 .spawn(move || {
1220 melin_app::affinity::pin_thread("matching", cores.matching);
1221 let app = matching_stage.run(&s2);
1222 let was_shutdown = shutdown_for_matching.load(Ordering::Relaxed);
1223 if was_shutdown {
1224 info!("matching thread exited cleanly on shutdown");
1225 } else {
1226 error!("matching thread returned without shutdown signal");
1227 }
1228 app
1229 })
1230 .map_err(|e| format!("spawn matching thread: {e}"))?;
1231
1232 let replication_metrics = build_replication_metrics(
1233 replication_consumers.is_some(),
1234 &durability_mode_atomic,
1235 config.durability_mode,
1236 );
1237
1238 let replica_active: Option<[Arc<AtomicBool>; 2]> =
1242 replication_ring_progress.as_ref().map(|rp| {
1243 [
1244 Arc::clone(&rp.active_flags[0]),
1245 Arc::clone(&rp.active_flags[1]),
1246 ]
1247 });
1248
1249 let journal_persisted_wire_seq_response = cursors.durable_wire_seq();
1251 let replication_metrics_response = replication_metrics.as_ref().map(Arc::clone);
1252 let replica_active_response = replica_active.clone();
1253 let durability_mode_response = Arc::clone(&durability_mode_atomic);
1254 let s3 = Arc::clone(&shutdown);
1255 let shutdown_for_response = Arc::clone(&shutdown);
1256 let busy_spin = !config.yield_idle;
1257 let response_utilization_thread = Arc::clone(&response_utilization);
1258 let response_fence = Arc::clone(&fence_state);
1259 let response_handle = std::thread::Builder::new()
1260 .name("response".into())
1261 .spawn(move || {
1262 melin_app::affinity::pin_thread("response", cores.response);
1263 crate::response::run::<A>(
1264 output_consumer,
1265 control_rx,
1266 crate::response::Response::<A> {
1267 journal_persisted_wire_seq: journal_persisted_wire_seq_response,
1268 durability_mode: durability_mode_response,
1269 replication_metrics: replication_metrics_response,
1270 replica_active: replica_active_response,
1271 heartbeat_interval,
1272 busy_spin,
1273 utilization: response_utilization_thread,
1274 encoder,
1275 fence_state: response_fence,
1276 },
1277 &s3,
1278 );
1279 let was_shutdown = shutdown_for_response.load(Ordering::Relaxed);
1280 if was_shutdown {
1281 info!("response thread exited cleanly on shutdown");
1282 } else {
1283 error!("response thread returned without shutdown signal");
1284 }
1285 })
1286 .map_err(|e| format!("spawn response thread: {e}"))?;
1287
1288 let replica_ready = Arc::new(AtomicBool::new(false));
1293 let replication_handle = if let Some((repl_consumer_1, repl_consumer_2)) = replication_consumers
1300 {
1301 let repl_bind = config
1302 .replication_bind
1303 .ok_or("replication_bind must be set when replication is enabled")?;
1304 let s_repl = Arc::clone(&shutdown);
1305 let repl_cursor = cursors.replica_quorum_cursor_arc();
1306 let fastest_repl_cursor = Arc::clone(&fastest_replica_cursor);
1307 let ready_flag = Arc::clone(&replica_ready);
1308 let connected_counter = replicas_connected
1309 .clone()
1310 .ok_or("replicas_connected must be Some when replication is enabled")?;
1311
1312 let batch_size = config.replication_batch_size;
1313 let heartbeat_secs = config.replication_heartbeat_secs;
1314 let journal_path = config.journal.clone();
1315 let repl_auth_keys = Arc::clone(&authorized_keys);
1316 let evict_flags = replication_ring_progress
1317 .as_ref()
1318 .map(|rp| {
1319 [
1320 Arc::clone(&rp.evict_flags[0]),
1321 Arc::clone(&rp.evict_flags[1]),
1322 ]
1323 })
1324 .unwrap_or_else(|| {
1325 [
1326 Arc::new(AtomicBool::new(false)),
1327 Arc::new(AtomicBool::new(false)),
1328 ]
1329 });
1330 let active_flags = replication_ring_progress
1331 .as_ref()
1332 .map(|rp| {
1333 [
1334 Arc::clone(&rp.active_flags[0]),
1335 Arc::clone(&rp.active_flags[1]),
1336 ]
1337 })
1338 .unwrap_or_else(|| {
1339 [
1340 Arc::new(AtomicBool::new(false)),
1341 Arc::new(AtomicBool::new(false)),
1342 ]
1343 });
1344 let repl_metrics = replication_metrics
1345 .clone()
1346 .ok_or("replication_metrics must be Some when replication is enabled")?;
1347 let handler_cores = [cores.repl_handler_0, cores.repl_handler_1];
1348 let sender_fence = Arc::clone(&fence_state);
1349 let repl_sender_handle = std::thread::Builder::new()
1350 .name("repl-sender".into())
1351 .spawn(move || {
1352 melin_app::affinity::pin_thread("repl-sender", cores.repl_sender);
1353 crate::replication::run_sender::<A>(
1354 crate::replication::Sender {
1355 bind_addr: repl_bind,
1356 repl_consumer_1,
1357 repl_consumer_2,
1358 replication_cursor: repl_cursor,
1359 fastest_replica_cursor: fastest_repl_cursor,
1360 journal_path,
1361 authorized_keys: repl_auth_keys,
1362 evict_flags,
1363 active_flags,
1364 metrics: repl_metrics,
1365 handler_cores,
1366 batch_size,
1367 heartbeat_secs,
1368 busy_spin,
1369 fence_state: sender_fence,
1370 },
1371 &s_repl,
1372 &ready_flag,
1373 &connected_counter,
1374 );
1375 })
1376 .map_err(|e| format!("spawn replication sender thread: {e}"))?;
1377
1378 info!(addr = %repl_bind, "replication listener started");
1379 Some(repl_sender_handle)
1380 } else {
1381 if !config.standalone && config.replica_of.is_none() {
1382 info!("running in standalone mode (no replication)");
1383 }
1384 None
1385 };
1386
1387 let event_publisher_handle = spawn_event_publisher::<A>(
1393 event_publisher_consumer,
1394 event_publisher,
1395 config,
1396 &cores,
1397 &authorized_keys,
1398 &shutdown,
1399 busy_spin,
1400 )?;
1401
1402 let shadow_handle = spawn_shadow_stage::<A>(
1403 shadow_consumer,
1404 shadow_exchange,
1405 chain_hash_lock,
1406 config,
1407 &cores,
1408 &shutdown,
1409 busy_spin,
1410 fence_state.epoch(),
1411 )?;
1412
1413 let pipeline_healthy = Arc::new(AtomicBool::new(true));
1433 let health_handle = spawn_health_endpoint(
1434 config,
1435 &active_connections,
1436 &events_processed,
1437 &cursors,
1438 input_cursor,
1439 &pipeline_healthy,
1440 &replicas_connected,
1441 &fence_state,
1442 &replication_metrics,
1443 &replica_active,
1444 &replication_ring_progress,
1445 &fastest_replica_cursor,
1446 &journal_utilization,
1447 &matching_utilization,
1448 &response_utilization,
1449 &shutdown,
1450 )?;
1451
1452 if enable_replication && needs_seeding {
1453 info!("waiting for replica to connect before seeding...");
1454 while !replica_ready.load(Ordering::Acquire) {
1455 if shutdown.load(Ordering::Relaxed) {
1456 break;
1457 }
1458 std::thread::sleep(std::time::Duration::from_millis(100));
1459 }
1460 }
1461 if needs_seeding {
1462 use melin_app::unix_epoch_nanos;
1463 use melin_journal::JournalEvent;
1464 use melin_transport_core::trace::mono_trace_ns;
1465
1466 let seed_start = std::time::Instant::now();
1467 let seed_events = factory.seed_events();
1468 let seed_count = seed_events.len();
1469
1470 let mut last_published_seq = 0u64;
1475 for event in seed_events {
1476 last_published_seq = input_producer.publish(InputSlot {
1477 connection_id: 0,
1478 key_hash: 0,
1479 request_seq: 0,
1480 sequence: 0,
1481 timestamp_ns: unix_epoch_nanos(),
1482 event: JournalEvent::App(event),
1483 publish_ts: mono_trace_ns(),
1484 recv_ts: mono_trace_ns(),
1485 });
1486 }
1487 let publish_elapsed = seed_start.elapsed();
1488
1489 let drain_start = std::time::Instant::now();
1505 if seed_count > 0 {
1506 let last_seed_seq = last_published_seq + 1; info!(
1509 last_seed_seq,
1510 journal = journal_cursor
1511 .get()
1512 .load(std::sync::atomic::Ordering::Relaxed),
1513 matching = matching_cursor
1514 .get()
1515 .load(std::sync::atomic::Ordering::Relaxed),
1516 "seed drain: waiting for pipeline cursors"
1517 );
1518
1519 while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
1520 && (journal_cursor
1521 .get()
1522 .load(std::sync::atomic::Ordering::Acquire)
1523 < last_seed_seq
1524 || matching_cursor
1525 .get()
1526 .load(std::sync::atomic::Ordering::Acquire)
1527 < last_seed_seq)
1528 {
1529 std::hint::spin_loop();
1530 }
1531
1532 info!("seed drain: pipeline cursors reached target");
1533
1534 if let Some(ref ring_progress) = replication_ring_progress {
1539 for i in 0..ring_progress.producer_cursors.len() {
1540 if !ring_progress.active_flags[i].load(std::sync::atomic::Ordering::Relaxed) {
1541 continue;
1542 }
1543 let target = ring_progress.producer_cursors[i].load();
1544 while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
1545 && ring_progress.consumer_cursors[i]
1546 .get()
1547 .load(std::sync::atomic::Ordering::Acquire)
1548 < target
1549 {
1550 std::hint::spin_loop();
1551 }
1552 }
1553 }
1554
1555 info!("seed drain: replication rings drained");
1556 }
1557 let drain_elapsed = drain_start.elapsed();
1558
1559 info!(
1560 seed_events = seed_count,
1561 publish_ms = publish_elapsed.as_millis(),
1562 drain_ms = drain_elapsed.as_millis(),
1563 total_ms = seed_start.elapsed().as_millis(),
1564 "seeded application state through pipeline"
1565 );
1566 }
1567
1568 if promoted {
1578 use melin_app::unix_epoch_nanos;
1579 use melin_journal::JournalEvent;
1580 use melin_transport_core::trace::mono_trace_ns;
1581
1582 let new_epoch = fence_state.epoch().saturating_add(1);
1583 info!(new_epoch, "promotion: injecting epoch bump");
1584 input_producer.publish(InputSlot {
1585 connection_id: 0,
1586 key_hash: 0,
1587 request_seq: 0,
1588 sequence: 0,
1589 timestamp_ns: unix_epoch_nanos(),
1590 event: JournalEvent::EpochBump { epoch: new_epoch },
1591 publish_ts: mono_trace_ns(),
1592 recv_ts: mono_trace_ns(),
1593 });
1594 while fence_state.epoch() < new_epoch {
1598 if shutdown.load(Ordering::Relaxed) {
1599 break;
1600 }
1601 std::hint::spin_loop();
1602 }
1603 }
1604
1605 let reader_shutdown = Arc::new(AtomicBool::new(false));
1614 let mut reader_handle = crate::reader::spawn_reader::<A, _>(
1615 input_producer,
1616 decoder,
1617 control_tx.clone(),
1618 config.cores.reader,
1619 connection_timeout,
1620 config.tick_interval(),
1621 Arc::clone(&reader_shutdown),
1622 );
1623
1624 listener.set_nonblocking(true);
1634
1635 info!(addr = %config.bind, "listening");
1636
1637 let next_connection_id = AtomicU64::new(1);
1641
1642 loop {
1646 if shutdown.load(Ordering::Relaxed) {
1647 info!("shutdown signal received");
1648 break;
1649 }
1650
1651 let event_pub_died = event_publisher_handle
1657 .as_ref()
1658 .is_some_and(|h| h.is_finished());
1659 let shadow_died = shadow_handle.as_ref().is_some_and(|h| h.is_finished());
1660 if (journal_handle.is_finished()
1661 || matching_handle.is_finished()
1662 || response_handle.is_finished()
1663 || event_pub_died
1664 || shadow_died)
1665 && !shutdown.load(Ordering::Relaxed)
1666 {
1667 error!("pipeline thread died, initiating shutdown");
1668 pipeline_healthy.store(false, Ordering::Relaxed);
1669 break;
1670 }
1671
1672 let (mut std_read, mut std_write, addr) = match listener.accept() {
1673 Ok(conn) => conn,
1674 Err(e) => {
1675 if e.kind() == std::io::ErrorKind::WouldBlock {
1676 std::thread::sleep(std::time::Duration::from_millis(100));
1678 continue;
1679 }
1680 error!(error = %e, "accept error");
1681 continue;
1682 }
1683 };
1684
1685 if config.max_connections > 0
1689 && active_connections.load(Ordering::Relaxed) >= config.max_connections
1690 {
1691 warn!(addr = %addr, "connection rejected: max_connections reached");
1692 drop(std_read);
1693 drop(std_write);
1694 continue;
1695 }
1696
1697 let connection_id = ConnectionId(next_connection_id.fetch_add(1, Ordering::Relaxed));
1698
1699 debug!(connection_id = connection_id.0, addr = %addr, "new connection");
1700
1701 if let Err(e) = set_read_timeout(&std_read, Some(std::time::Duration::from_secs(5))) {
1704 debug!(connection_id = connection_id.0, error = %e, "failed to set auth timeout");
1705 }
1706
1707 let (permission, public_key_bytes) = match authenticate_connection(
1713 connection_id,
1714 addr,
1715 &mut std_read,
1716 &mut std_write,
1717 &authorized_keys,
1718 ) {
1719 Ok(pair) => pair,
1720 Err(e) => {
1721 debug!(connection_id = connection_id.0, addr = %addr, error = %e, "auth failed, dropping");
1722 continue;
1723 }
1724 };
1725
1726 let key_hash = {
1730 let mut hasher = rustc_hash::FxHasher::default();
1731 public_key_bytes.hash(&mut hasher);
1732 hasher.finish()
1733 };
1734
1735 active_connections.fetch_add(1, Ordering::Relaxed);
1736
1737 if let Err(e) = set_read_timeout(&std_read, None) {
1742 debug!(connection_id = connection_id.0, error = %e, "failed to clear auth timeout");
1743 }
1744
1745 if let Err(e) = set_busy_poll(&std_read, BUSY_POLL_US) {
1751 debug!(
1756 connection_id = connection_id.0,
1757 error = %e,
1758 "failed to set SO_BUSY_POLL on client socket"
1759 );
1760 }
1761
1762 if let Err(e) = set_write_timeout(&std_write, Some(std::time::Duration::from_secs(5))) {
1767 debug!(connection_id = connection_id.0, error = %e, "failed to set write timeout");
1768 }
1769
1770 let fd = std_write.as_raw_fd();
1774 let boxed_writer: Box<dyn std::io::Write + Send> = Box::new(std_write);
1775 let control_event = ControlEvent::Connected {
1776 connection_id: connection_id.0,
1777 fd,
1778 writer: BlockingFrameWriter::new(boxed_writer),
1779 };
1780 if control_tx.send(control_event).is_err() {
1781 info!("response thread gone, shutting down");
1782 break;
1783 }
1784
1785 reader_handle.register(crate::reader::ReaderRegistration {
1787 connection_id,
1788 reader: std_read,
1789 addr,
1790 permission,
1791 key_hash,
1792 });
1793 }
1794
1795 info!("shutdown: stopping reader thread");
1798 reader_handle.shutdown();
1799 reader_handle.join();
1800
1801 shutdown_pipeline_stages(
1803 PipelineHandles {
1804 journal: journal_handle,
1805 matching: matching_handle,
1806 response: response_handle,
1807 replication: replication_handle,
1808 event_publisher: event_publisher_handle,
1809 shadow: shadow_handle,
1810 health: health_handle,
1811 },
1812 Vec::new(),
1813 &pipeline_healthy,
1814 &shutdown,
1815 )
1816}
1817
1818#[cfg(feature = "dpdk")]
1832fn run_dpdk<A>(
1833 config: ServerConfig,
1834 factory: Arc<dyn AppFactory<App = A>>,
1835 decoder: RequestDecoderArc<A>,
1836 encoder: ResponseEncoderArc<A>,
1837 event_publisher: Option<EventPublisherFn<A>>,
1838 shutdown: Arc<AtomicBool>,
1839) -> Result<(), Box<dyn std::error::Error>>
1840where
1841 A: Application + Send + 'static,
1842 A::Event: Send + Sync + 'static,
1843 A::Report: Send + 'static,
1844 A::QueryResponse: Send + 'static,
1845{
1846 let dpdk_config = dpdk_config_from(&config);
1847
1848 match config.journal_writer {
1849 melin_journal::JournalWriterMode::Buffered => run_dpdk_impl::<A, BufferedWriter<A::Event>>(
1850 config,
1851 factory,
1852 decoder,
1853 encoder,
1854 event_publisher,
1855 dpdk_config,
1856 shutdown,
1857 ),
1858 melin_journal::JournalWriterMode::Sector => run_dpdk_impl::<A, SectorWriter<A::Event>>(
1859 config,
1860 factory,
1861 decoder,
1862 encoder,
1863 event_publisher,
1864 dpdk_config,
1865 shutdown,
1866 ),
1867 }
1868}
1869
1870#[cfg(feature = "dpdk")]
1871fn dpdk_config_from(cfg: &ServerConfig) -> melin_dpdk::DpdkConfig {
1872 melin_dpdk::DpdkConfig {
1873 eal_args: cfg
1874 .dpdk_eal_args
1875 .split_whitespace()
1876 .map(String::from)
1877 .collect(),
1878 port_ids: cfg.dpdk_ports.clone(),
1879 ip_addr: cfg.dpdk_ip.parse().expect("invalid --dpdk-ip address"),
1880 prefix_len: cfg.dpdk_prefix_len,
1881 gateway: cfg
1882 .dpdk_gateway
1883 .as_deref()
1884 .map(|s| s.parse().expect("invalid --dpdk-gateway address")),
1885 listen_port: cfg.bind.port(),
1886 mtu: cfg.dpdk_mtu,
1887 vlan_id: cfg.dpdk_vlan,
1888 peer_ip: cfg
1889 .dpdk_peer_ip
1890 .as_deref()
1891 .map(|s| s.parse().expect("invalid --dpdk-peer-ip address")),
1892 gateway_mac: cfg.dpdk_gateway_mac.as_deref().map(melin_dpdk::parse_mac),
1893 num_queues: 1,
1894 }
1895}
1896
1897#[cfg(feature = "dpdk")]
1898fn run_dpdk_impl<A, W>(
1899 config: ServerConfig,
1900 factory: Arc<dyn AppFactory<App = A>>,
1901 decoder: RequestDecoderArc<A>,
1902 encoder: ResponseEncoderArc<A>,
1903 event_publisher: Option<EventPublisherFn<A>>,
1904 dpdk_config: melin_dpdk::DpdkConfig,
1905 shutdown: Arc<AtomicBool>,
1906) -> Result<(), Box<dyn std::error::Error>>
1907where
1908 A: Application + Send + 'static,
1909 A::Event: Send + Sync + 'static,
1910 A::Report: Send + 'static,
1911 A::QueryResponse: Send + 'static,
1912 W: JournalWrite<A::Event> + Send + 'static,
1913 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
1914{
1915 let durability_mode_atomic = Arc::new(AtomicU8::new(config.durability_mode.as_u8()));
1919 let shared = melin_dpdk::DpdkShared::init(&dpdk_config)?;
1921 let num_dpdk_threads = shared.num_queues as usize;
1923
1924 if let Some(primary_addr) = config.replica_of {
1929 info!(primary = %primary_addr, "starting in replica mode (DPDK)");
1930
1931 let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
1934 info!(
1935 keys = authorized_keys.len(),
1936 path = %config.authorized_keys.display(),
1937 "loaded authorized keys (DPDK replica mode)"
1938 );
1939
1940 let replication_key_path = config.replication_key.as_ref().ok_or_else(|| {
1943 std::io::Error::other("--replication-key is required in replica mode (--replica-of)")
1944 })?;
1945 let signing_key = {
1946 let seed = std::fs::read(replication_key_path).map_err(|e| {
1947 std::io::Error::other(format!(
1948 "failed to read replication key {}: {e}",
1949 replication_key_path.display()
1950 ))
1951 })?;
1952 if seed.len() != 32 {
1953 return Err(format!(
1954 "replication key must be 32 bytes, got {} ({})",
1955 seed.len(),
1956 replication_key_path.display()
1957 )
1958 .into());
1959 }
1960 let mut bytes = [0u8; 32];
1961 bytes.copy_from_slice(&seed);
1962 ed25519_dalek::SigningKey::from_bytes(&bytes)
1963 };
1964
1965 let promote_flag = Arc::new(AtomicBool::new(false));
1968 let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
1969 let _admin_handle = config.admin_bind.map(|addr| {
1970 crate::admin::spawn(
1971 addr,
1972 Some(Arc::clone(&promote_flag)),
1973 rotate_flag.clone(),
1974 Some(Arc::clone(&durability_mode_atomic)),
1975 Arc::clone(&shutdown),
1976 Arc::clone(&authorized_keys),
1977 )
1978 });
1979
1980 let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(0));
1986
1987 let mut repl_transport = melin_dpdk::DpdkTransport::from_shared_with_port(
1994 &shared,
1995 &dpdk_config,
1996 0,
1997 39999, )?;
1999 repl_transport.send_gratuitous_arp();
2000
2001 let primary_ipv4 = match primary_addr.ip() {
2002 std::net::IpAddr::V4(ip) => ip,
2003 std::net::IpAddr::V6(_) => {
2004 return Err("DPDK replication requires IPv4".into());
2005 }
2006 };
2007
2008 match crate::replication::run_receiver_dpdk::<A, W>(
2009 repl_transport,
2010 primary_ipv4,
2011 primary_addr.port(),
2012 &signing_key,
2013 &config.journal,
2014 &shutdown,
2015 &promote_flag,
2016 config.snapshot_interval_ms,
2017 config.shadow_snapshot_path(),
2018 config.cores,
2019 config.group_commit_delay(),
2020 config.replication_pipeline_depth,
2021 !config.yield_idle,
2022 Arc::clone(&factory),
2023 Arc::clone(&fence_state),
2024 )? {
2025 None => return Ok(()), Some((mut exchange, writer)) => {
2027 info!("replica promoted (DPDK) — transitioning to primary");
2029 <A as Application>::prefault(&mut exchange);
2030
2031 if let Some(ref flag) = rotate_flag {
2034 flag.store(false, Ordering::Release);
2035 }
2036
2037 warn!("DPDK primary promotion not yet implemented — falling back to kernel TCP");
2040 let listener = melin_wire_protocol::tcp::BlockingTcpListener::bind(config.bind)?;
2041 return run_as_primary::<A, _, W>(
2042 exchange,
2043 writer,
2044 listener,
2045 &config,
2046 &*factory,
2047 decoder,
2048 encoder,
2049 event_publisher,
2050 shutdown,
2051 authorized_keys,
2052 false,
2053 rotate_flag,
2054 durability_mode_atomic,
2055 fence_state,
2056 true, );
2058 }
2059 }
2060 }
2061
2062 let num_client_queues = num_dpdk_threads;
2068 let mut transports = Vec::with_capacity(num_client_queues);
2069 for q in 0..num_client_queues {
2070 let mut transport =
2071 melin_dpdk::DpdkTransport::from_shared(&shared, &dpdk_config, q as u16)?;
2072 if q == 0 {
2077 transport.send_gratuitous_arp();
2078 }
2079 transports.push(transport);
2080 }
2081
2082 let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
2084 info!(
2085 keys = authorized_keys.len(),
2086 path = %config.authorized_keys.display(),
2087 "loaded authorized keys"
2088 );
2089
2090 let (mut exchange, writer, needs_seeding, recovered_epoch) =
2092 init_engine::<A, W>(&config, &*factory)?;
2093 <A as Application>::prefault(&mut exchange);
2094
2095 let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(
2097 recovered_epoch,
2098 ));
2099
2100 let enable_shadow = config.snapshot_interval_ms > 0;
2103 let shadow_exchange = if enable_shadow {
2104 Some(<A as Application>::clone_via_snapshot(&exchange)?)
2105 } else {
2106 None
2107 };
2108
2109 let active_connections = Arc::new(AtomicU64::new(0));
2110
2111 let enable_replication = config.replication_bind.is_some();
2113 if enable_replication && config.standalone {
2114 return Err("--replication-bind and --standalone are mutually exclusive".into());
2115 }
2116 if config.standalone
2120 && config.durability_mode != crate::durability_policy::DurabilityMode::Local
2121 {
2122 return Err(format!(
2123 "--standalone requires --durability-mode local; got `{}` (this mode needs at least one connected replica)",
2124 config.durability_mode,
2125 )
2126 .into());
2127 }
2128
2129 let enable_event_publisher = event_publisher.is_some() && config.event_bind.is_some();
2136 let enable_shadow = config.snapshot_interval_ms > 0;
2137 let Pipeline {
2138 input_producer,
2139 journal_stage,
2140 matching_stage,
2141 mut output_consumers,
2142 events_processed,
2143 input_cursor,
2144 replication_consumers,
2145 replicas_connected,
2146 shadow_consumer,
2147 chain_hash_lock,
2148 replication_ring_progress,
2149 cursors,
2150 } = build_pipeline_with_replication(
2151 exchange,
2152 writer,
2153 config.group_commit_delay(),
2154 Arc::clone(&active_connections),
2155 enable_replication,
2156 config.max_journal_batch,
2157 config.replication_ring_size,
2158 !config.yield_idle,
2159 enable_event_publisher,
2160 enable_shadow,
2161 Arc::clone(&fence_state),
2162 );
2163 let journal_cursor = cursors.journal_ring_arc();
2168 let matching_cursor = cursors.matching_ring_arc();
2169
2170 let heartbeat_interval = config.heartbeat_interval();
2171
2172 let mut input_producer = input_producer;
2176
2177 let tick_cadence = config.tick_interval();
2182
2183 let fastest_replica_cursor = Arc::new(AtomicU64::new(
2185 melin_transport_core::PipelineCursors::NO_REPLICA,
2186 ));
2187
2188 let (control_tx, control_rx) = std::sync::mpsc::channel();
2190
2191 let mut tx_producers = Vec::with_capacity(num_dpdk_threads);
2199 let mut tx_consumers = Vec::with_capacity(num_dpdk_threads);
2200 for _ in 0..num_dpdk_threads {
2201 let (tx_out, tx_rx) = melin_pipeline::spsc::channel::<crate::dpdk_response::TxFrame>(4096);
2202 tx_producers.push(tx_out);
2203 tx_consumers.push(tx_rx);
2204 }
2205
2206 let cores = config.cores;
2208
2209 let mut journal_stage = journal_stage;
2213 let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
2214 let _admin_handle = config.admin_bind.map(|addr| {
2215 crate::admin::spawn(
2216 addr,
2217 None,
2218 rotate_flag.clone(),
2219 Some(Arc::clone(&durability_mode_atomic)),
2220 Arc::clone(&shutdown),
2221 Arc::clone(&authorized_keys),
2222 )
2223 });
2224 let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
2225 journal_stage.set_rotation(max_journal_bytes, rotate_flag.clone());
2226 if config.max_journal_mib > 0 {
2227 info!(
2228 max_journal_mib = config.max_journal_mib,
2229 "runtime journal rotation enabled (size threshold, DPDK)"
2230 );
2231 }
2232
2233 let journal_utilization = journal_stage.utilization();
2235 let matching_utilization = matching_stage.utilization();
2236 let response_utilization = Arc::new(melin_transport_core::pipeline::StageUtilization::new());
2237
2238 let s1 = Arc::clone(&shutdown);
2239 let journal_handle = std::thread::Builder::new()
2240 .name("journal".into())
2241 .spawn(move || {
2242 melin_app::affinity::pin_thread("journal", cores.journal);
2243 journal_stage.run(&s1)
2244 })
2245 .map_err(|e| format!("spawn journal thread: {e}"))?;
2246
2247 let s2 = Arc::clone(&shutdown);
2248 let matching_handle = std::thread::Builder::new()
2249 .name("matching".into())
2250 .spawn(move || {
2251 melin_app::affinity::pin_thread("matching", cores.matching);
2252 matching_stage.run(&s2)
2253 })
2254 .map_err(|e| format!("spawn matching thread: {e}"))?;
2255
2256 let replication_metrics = build_replication_metrics(
2257 replication_consumers.is_some(),
2258 &durability_mode_atomic,
2259 config.durability_mode,
2260 );
2261
2262 let replica_active: Option<[Arc<AtomicBool>; 2]> =
2263 replication_ring_progress.as_ref().map(|rp| {
2264 [
2265 Arc::clone(&rp.active_flags[0]),
2266 Arc::clone(&rp.active_flags[1]),
2267 ]
2268 });
2269
2270 let output_consumer = output_consumers.remove(0);
2272 let journal_persisted_wire_seq_response = cursors.durable_wire_seq();
2274 let replication_metrics_response = replication_metrics.as_ref().map(Arc::clone);
2275 let replica_active_response = replica_active.clone();
2276 let durability_mode_response = Arc::clone(&durability_mode_atomic);
2277 let active_connections_response = Arc::clone(&active_connections);
2278 let s3 = Arc::clone(&shutdown);
2279 let response_utilization_thread = Arc::clone(&response_utilization);
2280 let busy_spin = !config.yield_idle;
2281 let response_handle = std::thread::Builder::new()
2282 .name("response".into())
2283 .spawn(move || {
2284 melin_app::affinity::pin_thread("response", cores.response);
2285 crate::dpdk_response::run::<A>(
2286 output_consumer,
2287 control_rx,
2288 journal_persisted_wire_seq_response,
2289 durability_mode_response,
2290 replication_metrics_response,
2291 replica_active_response,
2292 &s3,
2293 heartbeat_interval,
2294 active_connections_response,
2295 tx_producers,
2296 response_utilization_thread,
2297 busy_spin,
2298 encoder,
2299 );
2300 })
2301 .map_err(|e| format!("spawn response thread: {e}"))?;
2302
2303 let shadow_handle = spawn_shadow_stage::<A>(
2304 shadow_consumer,
2305 shadow_exchange,
2306 chain_hash_lock,
2307 &config,
2308 &cores,
2309 &shutdown,
2310 busy_spin,
2311 fence_state.epoch(),
2312 )?;
2313
2314 let replica_ready = Arc::new(AtomicBool::new(false));
2319 let (repl_driver, repl_listen_port) = if let Some((repl_consumer_1, repl_consumer_2)) =
2324 replication_consumers
2325 {
2326 let repl_bind = config
2327 .replication_bind
2328 .ok_or("replication_bind must be set when replication is enabled")?;
2329 let repl_port = repl_bind.port();
2330
2331 let repl_cursor = cursors.replica_quorum_cursor_arc();
2332 let fastest_repl_cursor = Arc::clone(&fastest_replica_cursor);
2333 let ready_flag = Arc::clone(&replica_ready);
2334 let batch_size = config.replication_batch_size;
2335 let heartbeat_secs = config.replication_heartbeat_secs;
2336 let repl_metrics = replication_metrics
2337 .clone()
2338 .ok_or("replication_metrics must be Some when replication is enabled")?;
2339 let connected_counter = replicas_connected
2340 .clone()
2341 .ok_or("replicas_connected must be Some when replication is enabled")?;
2342 let dpdk_active_flags: [Arc<AtomicBool>; 2] = replication_ring_progress
2343 .as_ref()
2344 .map(|rp| {
2345 [
2346 Arc::clone(&rp.active_flags[0]),
2347 Arc::clone(&rp.active_flags[1]),
2348 ]
2349 })
2350 .unwrap_or_else(|| {
2351 [
2352 Arc::new(AtomicBool::new(false)),
2353 Arc::new(AtomicBool::new(false)),
2354 ]
2355 });
2356 let dpdk_evict_flags: [Arc<AtomicBool>; 2] = replication_ring_progress
2357 .as_ref()
2358 .map(|rp| {
2359 [
2360 Arc::clone(&rp.evict_flags[0]),
2361 Arc::clone(&rp.evict_flags[1]),
2362 ]
2363 })
2364 .unwrap_or_else(|| {
2365 [
2366 Arc::new(AtomicBool::new(false)),
2367 Arc::new(AtomicBool::new(false)),
2368 ]
2369 });
2370 let journal_path = config.journal.clone();
2371
2372 const REPL_TX_BUF: usize = 512 * 1024;
2378 const REPL_TX_QUEUE: usize = 512 * 1024;
2379 const REPL_RX_BUF: usize = 64 * 1024;
2380 transports[0]
2381 .add_listener_with_buffers(repl_port, REPL_RX_BUF, REPL_TX_BUF, REPL_TX_QUEUE)
2382 .map_err(|e| format!("add replication listener: {e}"))?;
2383
2384 let driver = crate::replication::DpdkReplicationDriver::new(
2385 [repl_consumer_1, repl_consumer_2],
2386 repl_cursor,
2387 fastest_repl_cursor,
2388 journal_path,
2389 ready_flag,
2390 connected_counter,
2391 dpdk_evict_flags,
2392 dpdk_active_flags,
2393 repl_metrics,
2394 batch_size,
2395 heartbeat_secs,
2396 Arc::clone(&fence_state),
2397 Arc::clone(&authorized_keys),
2398 );
2399 info!(addr = %repl_bind, "DPDK replication sender started (single-queue, in client poll thread)");
2404 (Some(driver), repl_port)
2405 } else {
2406 if !config.standalone && config.replica_of.is_none() {
2407 info!("running in standalone mode (no replication)");
2408 }
2409 (None, 0)
2410 };
2411 let replication_handle: Option<std::thread::JoinHandle<()>> = None;
2415
2416 let _ = (&enable_replication, &replica_ready); if needs_seeding {
2434 use melin_app::unix_epoch_nanos;
2435 use melin_journal::JournalEvent;
2436 use melin_transport_core::trace::mono_trace_ns;
2437
2438 let seed_events = factory.seed_events();
2439 let seed_count = seed_events.len();
2440
2441 let mut last_published_seq = 0u64;
2444 for event in seed_events {
2445 last_published_seq = input_producer.publish(InputSlot {
2446 connection_id: 0,
2447 key_hash: 0,
2448 request_seq: 0,
2449 sequence: 0,
2450 timestamp_ns: unix_epoch_nanos(),
2451 event: JournalEvent::App(event),
2452 publish_ts: mono_trace_ns(),
2453 recv_ts: mono_trace_ns(),
2454 });
2455 }
2456
2457 if seed_count > 0 {
2461 let last_seed_seq = last_published_seq + 1;
2462 while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
2463 && (journal_cursor
2464 .get()
2465 .load(std::sync::atomic::Ordering::Acquire)
2466 < last_seed_seq
2467 || matching_cursor
2468 .get()
2469 .load(std::sync::atomic::Ordering::Acquire)
2470 < last_seed_seq)
2471 {
2472 std::hint::spin_loop();
2473 }
2474 if let Some(ref ring_progress) = replication_ring_progress {
2475 for i in 0..ring_progress.producer_cursors.len() {
2476 if !ring_progress.active_flags[i].load(std::sync::atomic::Ordering::Relaxed) {
2477 continue;
2478 }
2479 let target = ring_progress.producer_cursors[i].load();
2480 while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
2481 && ring_progress.consumer_cursors[i]
2482 .get()
2483 .load(std::sync::atomic::Ordering::Acquire)
2484 < target
2485 {
2486 std::hint::spin_loop();
2487 }
2488 }
2489 }
2490 }
2491
2492 info!(
2493 accounts = config.accounts,
2494 instruments = config.instruments,
2495 "seeded test data through pipeline"
2496 );
2497 }
2498
2499 let pipeline_healthy = Arc::new(AtomicBool::new(true));
2505 let health_handle = spawn_health_endpoint(
2506 &config,
2507 &active_connections,
2508 &events_processed,
2509 &cursors,
2510 input_cursor,
2511 &pipeline_healthy,
2512 &replicas_connected,
2513 &fence_state,
2514 &replication_metrics,
2515 &replica_active,
2516 &replication_ring_progress,
2517 &fastest_replica_cursor,
2518 &journal_utilization,
2519 &matching_utilization,
2520 &response_utilization,
2521 &shutdown,
2522 )?;
2523
2524 info!(
2525 ip = %dpdk_config.ip_addr,
2526 port = dpdk_config.listen_port,
2527 num_dpdk_threads,
2528 "DPDK transport listening"
2529 );
2530
2531 let connection_timeout = config.connection_timeout();
2532 let max_conns = config.max_connections;
2533 let reader_core = config.cores.reader;
2534
2535 assert_eq!(
2539 transports.len(),
2540 1,
2541 "expected exactly one client DPDK transport"
2542 );
2543 let transport_0 = transports.pop().expect("one client transport");
2544 let tx_rx_0 = tx_consumers.remove(0);
2545 melin_app::affinity::pin_thread("dpdk-poll-0", reader_core);
2546 crate::dpdk_transport::run_dpdk_poll::<A>(
2547 transport_0,
2548 input_producer,
2549 decoder,
2550 control_tx,
2551 tx_rx_0,
2552 &shutdown,
2553 authorized_keys,
2554 connection_timeout,
2555 tick_cadence,
2556 max_conns,
2557 Arc::clone(&active_connections),
2558 0,
2559 repl_driver,
2560 repl_listen_port,
2561 );
2562
2563 let dpdk_extras: Vec<(String, std::thread::Result<()>)> = Vec::new();
2566
2567 shutdown_pipeline_stages(
2568 PipelineHandles {
2569 journal: journal_handle,
2570 matching: matching_handle,
2571 response: response_handle,
2572 replication: replication_handle,
2573 event_publisher: None,
2574 shadow: shadow_handle,
2575 health: health_handle,
2576 },
2577 dpdk_extras,
2578 &pipeline_healthy,
2579 &shutdown,
2580 )
2581}
2582
2583#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2599enum BootstrapSource {
2600 SnapshotAndJournal,
2603 SnapshotOnly,
2606 JournalOnly,
2608 Fresh,
2610}
2611
2612fn choose_bootstrap(
2621 snapshot_exists: bool,
2622 live_exists: bool,
2623 archives_exist: bool,
2624) -> BootstrapSource {
2625 let lineage_exists = live_exists || archives_exist;
2626 match (snapshot_exists, lineage_exists) {
2627 (true, true) => BootstrapSource::SnapshotAndJournal,
2628 (true, false) => BootstrapSource::SnapshotOnly,
2629 (false, true) => BootstrapSource::JournalOnly,
2630 (false, false) => BootstrapSource::Fresh,
2631 }
2632}
2633
2634pub(crate) fn init_engine<A, W>(
2645 config: &ServerConfig,
2646 factory: &dyn AppFactory<App = A>,
2647) -> Result<(A, W, bool, u64), Box<dyn std::error::Error>>
2648where
2649 A: Application,
2650 W: JournalWrite<A::Event>,
2651{
2652 let derived_snap = config.journal.with_extension("snapshot");
2655 let snap_path = config.snapshot.as_deref().or_else(|| {
2656 if derived_snap.exists() {
2657 Some(derived_snap.as_path())
2658 } else {
2659 None
2660 }
2661 });
2662
2663 let journal_exists = config.journal.exists();
2672 let archives_exist = !melin_journal::segment::list_archives(&config.journal)?.is_empty();
2673 let snapshot_exists = snap_path.is_some_and(|p| p.exists());
2674 let writer_mode = config.journal_writer;
2675 let mut engine: JournaledApp<A, W> = match choose_bootstrap(
2676 snapshot_exists,
2677 journal_exists,
2678 archives_exist,
2679 ) {
2680 BootstrapSource::SnapshotAndJournal => {
2681 let snap_path = snap_path.expect("snapshot_exists implies snap_path");
2682 info!(snapshot = %snap_path.display(), writer_mode = %writer_mode, "recovering from snapshot + journal");
2683 JournaledApp::<A, W>::recover_from_snapshot(snap_path, &config.journal)?
2684 }
2685 BootstrapSource::SnapshotOnly => {
2686 let snap_path = snap_path.expect("snapshot_exists implies snap_path");
2690 info!(
2691 snapshot = %snap_path.display(),
2692 writer_mode = %writer_mode,
2693 "recovering from snapshot only (no journal segments on disk)"
2694 );
2695 let (app, snap_sequence, snap_chain_hash, snap_epoch) =
2696 melin_transport_core::snapshot::load::<A>(snap_path)?;
2697 let writer = W::create_continuing(&config.journal, snap_sequence + 1, snap_chain_hash)?;
2698 JournaledApp::<A, W>::from_parts(app, writer, snap_epoch)
2699 }
2700 BootstrapSource::JournalOnly => {
2701 info!(writer_mode = %writer_mode, "recovering from journal");
2702 let mut app = factory.empty();
2703 factory.prefault(&mut app);
2704 JournaledApp::<A, W>::recover(app, &config.journal)?
2705 }
2706 BootstrapSource::Fresh => {
2707 info!(writer_mode = %writer_mode, "creating new journal");
2708 let mut app = factory.empty();
2709 factory.prefault(&mut app);
2710 JournaledApp::<A, W>::create(app, &config.journal)?
2711 }
2712 };
2713
2714 let needs_seeding = !journal_exists && !archives_exist;
2717
2718 factory.apply_operator_policy(engine.app_mut());
2734
2735 if config.max_journal_mib > 0 {
2741 let threshold = config.max_journal_mib * 1024 * 1024;
2742 let current_size = engine.journal_size();
2743 if current_size > threshold {
2744 info!(
2745 current_mib = current_size / (1024 * 1024),
2746 threshold_mib = config.max_journal_mib,
2747 "journal exceeds threshold, rotating segment"
2748 );
2749 engine.rotate_segment()?;
2750 info!("journal segment rotated successfully");
2751 }
2752 }
2753
2754 let recovered_epoch = engine.recovered_epoch();
2755 let (app, writer) = engine.into_parts();
2756 Ok((app, writer, needs_seeding, recovered_epoch))
2757}
2758
2759fn build_replication_metrics(
2764 has_replication: bool,
2765 durability_mode_atomic: &AtomicU8,
2766 config_mode: crate::durability_policy::DurabilityMode,
2767) -> Option<Arc<crate::replication::ReplicationMetrics>> {
2768 let metrics = if has_replication {
2769 Some(Arc::new(crate::replication::ReplicationMetrics::default()))
2770 } else {
2771 None
2772 };
2773
2774 let active_mode = crate::durability_policy::DurabilityMode::from_u8(
2775 durability_mode_atomic.load(Ordering::Relaxed),
2776 )
2777 .unwrap_or(config_mode);
2778 info!(
2779 mode = %active_mode,
2780 policy = %active_mode.to_policy(),
2781 "durability mode active"
2782 );
2783
2784 metrics
2785}
2786
2787fn spawn_shadow_stage<A: Application + Send + 'static>(
2788 shadow_consumer: Option<Consumer<InputSlot<A::Event>>>,
2789 shadow_exchange: Option<A>,
2790 chain_hash_lock: Option<
2791 Arc<melin_pipeline::seqlock::SeqLock<melin_transport_core::pipeline::FsyncState>>,
2792 >,
2793 config: &ServerConfig,
2794 cores: &PipelineCores,
2795 shutdown: &Arc<AtomicBool>,
2796 busy_spin: bool,
2797 initial_epoch: u64,
2798) -> Result<Option<std::thread::JoinHandle<()>>, Box<dyn std::error::Error>>
2799where
2800 A::Event: Send + Sync + 'static,
2801{
2802 let Some(shadow_cons) = shadow_consumer else {
2803 return Ok(None);
2804 };
2805 let snap_path = config.shadow_snapshot_path();
2806 let interval = std::time::Duration::from_millis(config.snapshot_interval_ms);
2807 let chain_hash =
2808 chain_hash_lock.ok_or("chain hash lock must be Some when shadow is enabled")?;
2809 let shadow_ex = shadow_exchange.ok_or("shadow exchange must be Some when shadow is enabled")?;
2810 let s_shadow = Arc::clone(shutdown);
2811 let shadow_core = cores.shadow;
2812 let shadow_initial_epoch = initial_epoch;
2813 let handle = std::thread::Builder::new()
2814 .name("shadow".into())
2815 .spawn(move || {
2816 melin_app::affinity::pin_thread("shadow", shadow_core);
2817 melin_transport_core::shadow::run(
2818 shadow_cons,
2819 shadow_ex,
2820 snap_path,
2821 interval,
2822 chain_hash,
2823 &s_shadow,
2824 busy_spin,
2825 shadow_initial_epoch,
2826 );
2827 })
2828 .map_err(|e| format!("spawn shadow thread: {e}"))?;
2829
2830 info!(
2831 interval_ms = config.snapshot_interval_ms,
2832 path = %config.shadow_snapshot_path().display(),
2833 "shadow snapshot stage started"
2834 );
2835 Ok(Some(handle))
2836}
2837
2838#[allow(clippy::too_many_arguments)]
2839fn spawn_health_endpoint(
2840 config: &ServerConfig,
2841 active_connections: &Arc<AtomicU64>,
2842 events_processed: &Arc<AtomicU64>,
2843 cursors: &melin_transport_core::PipelineCursors,
2844 input_cursor: Box<dyn melin_pipeline::ring::QueueCursor>,
2845 pipeline_healthy: &Arc<AtomicBool>,
2846 replicas_connected: &Option<Arc<std::sync::atomic::AtomicU32>>,
2847 fence_state: &Arc<melin_transport_core::fence::FenceState>,
2848 replication_metrics: &Option<Arc<crate::replication::ReplicationMetrics>>,
2849 replica_active: &Option<[Arc<AtomicBool>; 2]>,
2850 replication_ring_progress: &Option<melin_transport_core::pipeline::ReplicationRingProgress>,
2851 fastest_replica_cursor: &Arc<AtomicU64>,
2852 journal_utilization: &Arc<melin_transport_core::pipeline::StageUtilization>,
2853 matching_utilization: &Arc<melin_transport_core::pipeline::StageUtilization>,
2854 response_utilization: &Arc<melin_transport_core::pipeline::StageUtilization>,
2855 shutdown: &Arc<AtomicBool>,
2856) -> Result<Option<std::thread::JoinHandle<()>>, Box<dyn std::error::Error>> {
2857 let Some(health_addr) = config.health_bind else {
2858 return Ok(None);
2859 };
2860
2861 let (repl_ring_producers, repl_ring_consumers) = replication_ring_progress
2862 .as_ref()
2863 .map(|rp| {
2864 (
2865 Some([
2866 Arc::clone(&rp.producer_cursors[0]),
2867 Arc::clone(&rp.producer_cursors[1]),
2868 ]),
2869 Some([
2870 Arc::clone(&rp.consumer_cursors[0]),
2871 Arc::clone(&rp.consumer_cursors[1]),
2872 ]),
2873 )
2874 })
2875 .unwrap_or((None, None));
2876 let fastest_repl_cursor_health = replication_metrics
2877 .as_ref()
2878 .map(|_| Arc::clone(fastest_replica_cursor));
2879 Ok(Some(melin_transport_core::health::spawn(
2880 health_addr,
2881 melin_transport_core::health::HealthState {
2882 active_connections: Arc::clone(active_connections),
2883 events_processed: Arc::clone(events_processed),
2884 cursors: cursors.clone(),
2885 input_cursor,
2886 pipeline_healthy: Arc::clone(pipeline_healthy),
2887 replicas_connected: replicas_connected.clone(),
2888 fence_state: Some(Arc::clone(fence_state)),
2889 replication_metrics: replication_metrics.clone(),
2890 replica_active: replica_active.clone(),
2891 replication_ring_producer_cursors: repl_ring_producers,
2892 replication_ring_consumer_cursors: repl_ring_consumers,
2893 fastest_replica_cursor: fastest_repl_cursor_health,
2894 journal_utilization: Arc::clone(journal_utilization),
2895 matching_utilization: Arc::clone(matching_utilization),
2896 response_utilization: Arc::clone(response_utilization),
2897 },
2898 Arc::clone(shutdown),
2899 )?))
2900}
2901
2902fn spawn_event_publisher<A: Application>(
2910 consumer: Option<Consumer<OutputSlot<A>>>,
2911 run_fn: Option<EventPublisherFn<A>>,
2912 config: &ServerConfig,
2913 cores: &PipelineCores,
2914 authorized_keys: &Arc<AuthorizedKeys>,
2915 shutdown: &Arc<AtomicBool>,
2916 busy_spin: bool,
2917) -> Result<Option<std::thread::JoinHandle<()>>, Box<dyn std::error::Error>>
2918where
2919 A::Report: Send + 'static,
2920 A::QueryResponse: Send + 'static,
2921{
2922 let (Some(event_consumer), Some(run_fn)) = (consumer, run_fn) else {
2923 return Ok(None);
2924 };
2925 let event_bind = config
2926 .event_bind
2927 .ok_or("event_bind must be set when event publisher is enabled")?;
2928 let s_event = Arc::clone(shutdown);
2929 let event_keys = Arc::clone(authorized_keys);
2930 let event_core = cores.event_publisher;
2931 let event_handle = std::thread::Builder::new()
2932 .name("event-publisher".into())
2933 .spawn(move || {
2934 melin_app::affinity::pin_thread("event-publisher", event_core);
2935 run_fn(event_consumer, event_bind, event_keys, &s_event, busy_spin);
2936 })
2937 .map_err(|e| format!("spawn event publisher thread: {e}"))?;
2938 info!(addr = %event_bind, "event publisher started");
2939 Ok(Some(event_handle))
2940}
2941
2942fn authenticate_connection<R: std::io::Read, W: std::io::Write>(
2953 connection_id: ConnectionId,
2954 addr: SocketAddr,
2955 reader: &mut R,
2956 writer: &mut W,
2957 authorized_keys: &AuthorizedKeys,
2958) -> Result<(Permission, [u8; 32]), Box<dyn std::error::Error>> {
2959 use std::io;
2960
2961 use ed25519_dalek::{Verifier, VerifyingKey};
2962 use melin_wire_protocol::control::TransportResponse;
2963 use melin_wire_protocol::control_codec;
2964
2965 let mut nonce = [0u8; 32];
2968 getrandom::fill(&mut nonce).map_err(|e| io::Error::other(format!("getrandom failed: {e}")))?;
2969
2970 let mut buf = [0u8; 128];
2971 let written =
2972 control_codec::encode_transport_response(&TransportResponse::Challenge { nonce }, &mut buf)
2973 .map_err(|e| io::Error::other(format!("encode Challenge: {e}")))?;
2974 writer.write_all(&buf[..written])?;
2975 writer.flush()?;
2976
2977 let mut len_buf = [0u8; 4];
2982 reader
2983 .read_exact(&mut len_buf)
2984 .map_err(|e| io::Error::other(format!("read auth frame length: {e}")))?;
2985 let frame_len = u32::from_le_bytes(len_buf) as usize;
2986 if frame_len > 256 {
2988 send_auth_failed(writer);
2989 return Err(io::Error::other(format!("auth frame too large: {frame_len}")).into());
2990 }
2991 let mut frame_buf = [0u8; 256];
2992 reader
2993 .read_exact(&mut frame_buf[..frame_len])
2994 .map_err(|e| io::Error::other(format!("read auth frame payload: {e}")))?;
2995
2996 let (_seq, cr) = match control_codec::decode_challenge_response(&frame_buf[..frame_len]) {
2997 Ok(pair) => pair,
2998 Err(e) => {
2999 send_auth_failed(writer);
3000 return Err(io::Error::other(format!("decode ChallengeResponse: {e}")).into());
3001 }
3002 };
3003
3004 let (signature_bytes, public_key_bytes) = (cr.signature, cr.public_key);
3005
3006 let permission = match authorized_keys.lookup(&public_key_bytes) {
3008 Some(perm) => perm,
3009 None => {
3010 send_auth_failed(writer);
3011 return Err("unknown public key".into());
3012 }
3013 };
3014
3015 let verifying_key = VerifyingKey::from_bytes(&public_key_bytes).map_err(|e| {
3018 send_auth_failed(writer);
3019 io::Error::other(format!("invalid public key: {e}"))
3020 })?;
3021 let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes);
3022 verifying_key.verify(&nonce, &signature).map_err(|e| {
3023 send_auth_failed(writer);
3024 io::Error::other(format!("signature verification failed: {e}"))
3025 })?;
3026
3027 let written =
3029 control_codec::encode_transport_response(&TransportResponse::ServerReady, &mut buf)
3030 .map_err(|e| io::Error::other(format!("encode ServerReady: {e}")))?;
3031 writer.write_all(&buf[..written])?;
3032 writer.flush()?;
3033
3034 debug!(
3035 connection_id = connection_id.0,
3036 addr = %addr,
3037 permission = ?permission,
3038 "authenticated"
3039 );
3040
3041 Ok((permission, public_key_bytes))
3042}
3043
3044fn set_read_timeout<F: std::os::unix::io::AsRawFd>(
3049 fd: &F,
3050 timeout: Option<std::time::Duration>,
3051) -> std::io::Result<()> {
3052 let tv = match timeout {
3053 Some(d) => libc::timeval {
3054 tv_sec: d.as_secs() as libc::time_t,
3055 tv_usec: d.subsec_micros() as libc::suseconds_t,
3056 },
3057 None => libc::timeval {
3058 tv_sec: 0,
3059 tv_usec: 0,
3060 },
3061 };
3062 let ret = unsafe {
3063 libc::setsockopt(
3064 fd.as_raw_fd(),
3065 libc::SOL_SOCKET,
3066 libc::SO_RCVTIMEO,
3067 &tv as *const libc::timeval as *const libc::c_void,
3068 std::mem::size_of::<libc::timeval>() as libc::socklen_t,
3069 )
3070 };
3071 if ret < 0 {
3072 return Err(std::io::Error::last_os_error());
3073 }
3074 Ok(())
3075}
3076
3077pub(crate) fn set_busy_poll<F: std::os::unix::io::AsRawFd>(
3094 fd: &F,
3095 micros: i32,
3096) -> std::io::Result<()> {
3097 let val: libc::c_int = micros;
3101 let ret = unsafe {
3102 libc::setsockopt(
3103 fd.as_raw_fd(),
3104 libc::SOL_SOCKET,
3105 libc::SO_BUSY_POLL,
3106 &val as *const libc::c_int as *const libc::c_void,
3107 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
3108 )
3109 };
3110 if ret < 0 {
3111 return Err(std::io::Error::last_os_error());
3112 }
3113 Ok(())
3114}
3115
3116pub(crate) const BUSY_POLL_US: i32 = 50;
3121
3122fn set_write_timeout<F: std::os::unix::io::AsRawFd>(
3125 fd: &F,
3126 timeout: Option<std::time::Duration>,
3127) -> std::io::Result<()> {
3128 let tv = match timeout {
3129 Some(d) => libc::timeval {
3130 tv_sec: d.as_secs() as libc::time_t,
3131 tv_usec: d.subsec_micros() as libc::suseconds_t,
3132 },
3133 None => libc::timeval {
3134 tv_sec: 0,
3135 tv_usec: 0,
3136 },
3137 };
3138 let ret = unsafe {
3139 libc::setsockopt(
3140 fd.as_raw_fd(),
3141 libc::SOL_SOCKET,
3142 libc::SO_SNDTIMEO,
3143 &tv as *const libc::timeval as *const libc::c_void,
3144 std::mem::size_of::<libc::timeval>() as libc::socklen_t,
3145 )
3146 };
3147 if ret < 0 {
3148 return Err(std::io::Error::last_os_error());
3149 }
3150 Ok(())
3151}
3152
3153fn send_auth_failed(writer: &mut impl std::io::Write) {
3155 use melin_wire_protocol::control::TransportResponse;
3156 use melin_wire_protocol::control_codec;
3157
3158 let mut buf = [0u8; 8];
3159 if let Ok(written) =
3160 control_codec::encode_transport_response(&TransportResponse::AuthFailed, &mut buf)
3161 {
3162 let _ = writer.write_all(&buf[..written]);
3163 let _ = writer.flush();
3164 }
3165}
3166
3167#[cfg(test)]
3168mod tests {
3169 use std::io::{Read, Write};
3170 use std::os::unix::net::UnixStream;
3171
3172 use ed25519_dalek::{Signer, SigningKey};
3173 use melin_app::auth::AuthorizedKeys;
3174 use melin_app::auth::Permission;
3175 use melin_wire_protocol::control::ConnectionId;
3176 use melin_wire_protocol::control_codec::{
3177 TAG_AUTH_FAILED, TAG_CHALLENGE, TAG_CHALLENGE_RESPONSE, TAG_RESPONSE_HEARTBEAT,
3178 TAG_SERVER_READY,
3179 };
3180
3181 use super::authenticate_connection;
3182 use super::{BootstrapSource, choose_bootstrap};
3183
3184 #[test]
3190 fn bootstrap_decision_routes_archives_through_recovery() {
3191 use BootstrapSource::*;
3192 let matrix = [
3194 ((false, false, false), Fresh),
3195 ((false, false, true), JournalOnly), ((false, true, false), JournalOnly),
3197 ((false, true, true), JournalOnly),
3198 ((true, false, false), SnapshotOnly),
3199 ((true, false, true), SnapshotAndJournal), ((true, true, false), SnapshotAndJournal),
3201 ((true, true, true), SnapshotAndJournal),
3202 ];
3203 for ((snap, live, arch), expected) in matrix {
3204 assert_eq!(
3205 choose_bootstrap(snap, live, arch),
3206 expected,
3207 "snapshot={snap} live={live} archives={arch}"
3208 );
3209 }
3210 }
3211
3212 fn test_key() -> SigningKey {
3214 SigningKey::from_bytes(&[0xAA; 32])
3215 }
3216
3217 fn keys_with_test_key(perm: &str) -> AuthorizedKeys {
3219 let pub_bytes = test_key().verifying_key().to_bytes();
3223 let pub_b64 = base64_encode(&pub_bytes);
3224 AuthorizedKeys::parse(&format!("{perm} {pub_b64} test\n")).unwrap()
3225 }
3226
3227 fn base64_encode(bytes: &[u8]) -> String {
3230 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3231 let mut out = String::new();
3232 for chunk in bytes.chunks(3) {
3233 let b0 = chunk[0] as u32;
3234 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
3235 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
3236 let n = (b0 << 16) | (b1 << 8) | b2;
3237 out.push(ALPHABET[(n >> 18 & 0x3F) as usize] as char);
3238 out.push(ALPHABET[(n >> 12 & 0x3F) as usize] as char);
3239 if chunk.len() > 1 {
3240 out.push(ALPHABET[(n >> 6 & 0x3F) as usize] as char);
3241 } else {
3242 out.push('=');
3243 }
3244 if chunk.len() > 2 {
3245 out.push(ALPHABET[(n & 0x3F) as usize] as char);
3246 } else {
3247 out.push('=');
3248 }
3249 }
3250 out
3251 }
3252
3253 fn run_server_auth(
3256 mut stream: UnixStream,
3257 keys: AuthorizedKeys,
3258 ) -> std::thread::JoinHandle<Result<Permission, String>> {
3259 std::thread::spawn(move || {
3260 let mut writer = stream.try_clone().unwrap();
3262 authenticate_connection(
3263 ConnectionId(1),
3264 "127.0.0.1:0".parse().unwrap(),
3265 &mut stream,
3266 &mut writer,
3267 &keys,
3268 )
3269 .map(|(perm, _pk)| perm)
3270 .map_err(|e| e.to_string())
3271 })
3272 }
3273
3274 fn read_challenge(stream: &mut UnixStream) -> [u8; 32] {
3277 let mut len_buf = [0u8; 4];
3278 stream.read_exact(&mut len_buf).unwrap();
3279 let len = u32::from_le_bytes(len_buf) as usize;
3280 let mut payload = vec![0u8; len];
3281 stream.read_exact(&mut payload).unwrap();
3282 assert_eq!(payload[0], TAG_CHALLENGE, "expected Challenge");
3283 payload[1..33].try_into().unwrap()
3284 }
3285
3286 fn write_challenge_response(
3290 stream: &mut UnixStream,
3291 signature: [u8; 64],
3292 public_key: [u8; 32],
3293 ) {
3294 let mut frame = Vec::with_capacity(105);
3295 frame.extend_from_slice(&0u64.to_le_bytes()); frame.push(TAG_CHALLENGE_RESPONSE);
3297 frame.extend_from_slice(&signature);
3298 frame.extend_from_slice(&public_key);
3299 stream
3300 .write_all(&(frame.len() as u32).to_le_bytes())
3301 .unwrap();
3302 stream.write_all(&frame).unwrap();
3303 stream.flush().unwrap();
3304 }
3305
3306 fn client_sign_challenge(stream: &mut UnixStream, key: &SigningKey) {
3309 let nonce = read_challenge(stream);
3310 let sig = key.sign(&nonce);
3311 write_challenge_response(stream, sig.to_bytes(), key.verifying_key().to_bytes());
3312 }
3313
3314 fn client_sign_challenge_bad(stream: &mut UnixStream, key: &SigningKey) {
3316 let nonce = read_challenge(stream);
3317 let mut sig_bytes = key.sign(&nonce).to_bytes();
3318 sig_bytes[0] ^= 0xFF;
3319 write_challenge_response(stream, sig_bytes, key.verifying_key().to_bytes());
3320 }
3321
3322 fn read_response_tag(stream: &mut UnixStream) -> u8 {
3324 let mut len_buf = [0u8; 4];
3325 stream.read_exact(&mut len_buf).unwrap();
3326 let len = u32::from_le_bytes(len_buf) as usize;
3327 let mut buf = vec![0u8; len];
3328 stream.read_exact(&mut buf).unwrap();
3329 buf[0]
3330 }
3331
3332 #[test]
3333 fn auth_success_returns_permission() {
3334 let keys = keys_with_test_key("trader");
3335 let key = test_key();
3336 let (s1, mut s2) = UnixStream::pair().unwrap();
3337
3338 let handle = run_server_auth(s1, keys);
3339
3340 client_sign_challenge(&mut s2, &key);
3341 let resp = read_response_tag(&mut s2);
3342 assert_eq!(resp, TAG_SERVER_READY);
3343
3344 let result = handle.join().unwrap();
3345 assert_eq!(result.unwrap(), Permission::Trader);
3346 }
3347
3348 #[test]
3349 fn auth_admin_permission() {
3350 let keys = keys_with_test_key("operator");
3351 let key = test_key();
3352 let (s1, mut s2) = UnixStream::pair().unwrap();
3353
3354 let handle = run_server_auth(s1, keys);
3355
3356 client_sign_challenge(&mut s2, &key);
3357 let resp = read_response_tag(&mut s2);
3358 assert_eq!(resp, TAG_SERVER_READY);
3359
3360 assert_eq!(handle.join().unwrap().unwrap(), Permission::Operator);
3361 }
3362
3363 #[test]
3364 fn auth_unknown_key_sends_auth_failed() {
3365 let keys = AuthorizedKeys::parse("").unwrap();
3366 let key = test_key();
3367 let (s1, mut s2) = UnixStream::pair().unwrap();
3368
3369 let handle = run_server_auth(s1, keys);
3370
3371 client_sign_challenge(&mut s2, &key);
3372 let resp = read_response_tag(&mut s2);
3373 assert_eq!(resp, TAG_AUTH_FAILED);
3374
3375 assert!(handle.join().unwrap().is_err());
3376 }
3377
3378 #[test]
3379 fn auth_bad_signature_sends_auth_failed() {
3380 let keys = keys_with_test_key("operator");
3381 let key = test_key();
3382 let (s1, mut s2) = UnixStream::pair().unwrap();
3383
3384 let handle = run_server_auth(s1, keys);
3385
3386 client_sign_challenge_bad(&mut s2, &key);
3387 let resp = read_response_tag(&mut s2);
3388 assert_eq!(resp, TAG_AUTH_FAILED);
3389
3390 assert!(handle.join().unwrap().is_err());
3391 }
3392
3393 #[test]
3394 fn auth_wrong_message_type_sends_auth_failed() {
3395 let keys = keys_with_test_key("trader");
3396 let (s1, mut s2) = UnixStream::pair().unwrap();
3397
3398 let handle = run_server_auth(s1, keys);
3399
3400 read_challenge(&mut s2);
3402
3403 let mut frame = vec![0u8; 105];
3408 frame[8] = TAG_RESPONSE_HEARTBEAT; s2.write_all(&(frame.len() as u32).to_le_bytes()).unwrap();
3410 s2.write_all(&frame).unwrap();
3411 s2.flush().unwrap();
3412
3413 let resp = read_response_tag(&mut s2);
3414 assert_eq!(resp, TAG_AUTH_FAILED);
3415
3416 assert!(handle.join().unwrap().is_err());
3417 }
3418
3419 #[test]
3420 fn auth_client_disconnects_is_error() {
3421 let keys = keys_with_test_key("trader");
3422 let (s1, s2) = UnixStream::pair().unwrap();
3423
3424 let handle = run_server_auth(s1, keys);
3425
3426 drop(s2);
3428
3429 assert!(handle.join().unwrap().is_err());
3430 }
3431
3432 #[test]
3433 fn auth_different_key_than_authorized_is_rejected() {
3434 let keys = keys_with_test_key("trader");
3436 let wrong_key = SigningKey::from_bytes(&[0xCC; 32]);
3437 let (s1, mut s2) = UnixStream::pair().unwrap();
3438
3439 let handle = run_server_auth(s1, keys);
3440
3441 client_sign_challenge(&mut s2, &wrong_key);
3442 let resp = read_response_tag(&mut s2);
3443 assert_eq!(resp, TAG_AUTH_FAILED);
3444
3445 assert!(handle.join().unwrap().is_err());
3446 }
3447
3448 #[test]
3449 fn auth_oversized_frame_sends_auth_failed() {
3450 let keys = keys_with_test_key("trader");
3451 let (s1, mut s2) = UnixStream::pair().unwrap();
3452
3453 let handle = run_server_auth(s1, keys);
3454
3455 read_challenge(&mut s2);
3457
3458 let fake_len: u32 = 1000;
3460 s2.write_all(&fake_len.to_le_bytes()).unwrap();
3461 s2.flush().unwrap();
3462
3463 let resp = read_response_tag(&mut s2);
3465 assert_eq!(resp, TAG_AUTH_FAILED);
3466
3467 assert!(handle.join().unwrap().is_err());
3468 }
3469
3470 #[test]
3471 fn auth_zero_length_frame_sends_auth_failed() {
3472 let keys = keys_with_test_key("trader");
3473 let (s1, mut s2) = UnixStream::pair().unwrap();
3474
3475 let handle = run_server_auth(s1, keys);
3476
3477 read_challenge(&mut s2);
3479
3480 let zero_len: u32 = 0;
3482 s2.write_all(&zero_len.to_le_bytes()).unwrap();
3483 s2.flush().unwrap();
3484
3485 let resp = read_response_tag(&mut s2);
3487 assert_eq!(resp, TAG_AUTH_FAILED);
3488
3489 assert!(handle.join().unwrap().is_err());
3490 }
3491
3492 #[test]
3493 fn auth_readonly_permission() {
3494 let keys = keys_with_test_key("readonly");
3495 let key = test_key();
3496 let (s1, mut s2) = UnixStream::pair().unwrap();
3497
3498 let handle = run_server_auth(s1, keys);
3499
3500 client_sign_challenge(&mut s2, &key);
3501 let resp = read_response_tag(&mut s2);
3502 assert_eq!(resp, TAG_SERVER_READY);
3503
3504 let perm = handle.join().unwrap().unwrap();
3505 assert_eq!(perm, Permission::ReadOnly);
3506 assert!(!perm.can_trade());
3507 }
3508}