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,6,7,8,9,10", value_parser = parse_cores)]
100 pub cores: PipelineCores,
101 #[arg(long, default_value_t = 4)]
107 pub reader_cores: usize,
108 #[arg(long, default_value_t = 0)]
110 pub group_commit_us: u64,
111 #[arg(long, default_value_t = 10)]
114 pub heartbeat_interval_secs: u64,
115 #[arg(long, default_value_t = 30)]
118 pub connection_timeout_secs: u64,
119 #[arg(long, default_value_t = 1024)]
123 pub max_connections: u64,
124 #[arg(long, default_value_t = 100_000)]
127 pub accounts: u32,
128 #[arg(long, default_value_t = 100)]
130 pub instruments: u32,
131 #[arg(long, default_value = "authorized_keys")]
136 pub authorized_keys: PathBuf,
137 #[arg(long, default_value_t = 256)]
141 pub max_journal_mib: u64,
142 #[arg(
151 long,
152 default_value_t = melin_journal::JournalWriterMode::default(),
153 value_parser = melin_journal::JournalWriterMode::parse,
154 )]
155 pub journal_writer: melin_journal::JournalWriterMode,
156 #[arg(long, default_value_t = 10_000)]
164 pub max_orders_per_account: u32,
165 #[arg(long, default_value_t = 1_000)]
177 pub max_orders_per_second: u32,
178 #[arg(long, default_value_t = 5_000)]
185 pub max_orders_burst: u32,
186
187 #[arg(long)]
190 pub replication_bind: Option<std::net::SocketAddr>,
191
192 #[arg(long, default_value_t = false)]
196 pub standalone: bool,
197
198 #[arg(long)]
202 pub replica_of: Option<std::net::SocketAddr>,
203
204 #[arg(long)]
209 pub replication_key: Option<std::path::PathBuf>,
210
211 #[arg(long, default_value_t = 128)]
215 pub replication_batch_size: usize,
216
217 #[arg(long, default_value_t = 4096)]
221 pub max_journal_batch: usize,
222
223 #[arg(long, default_value_t = 5)]
227 pub replication_heartbeat_secs: u64,
228
229 #[arg(long, default_value_t = DEFAULT_REPLICATION_PIPELINE_DEPTH)]
235 pub replication_pipeline_depth: usize,
236
237 #[arg(long, default_value_t = 256)]
241 pub replication_ring_size: usize,
242
243 #[arg(long, value_enum, default_value_t = crate::durability_policy::DurabilityMode::Hybrid)]
262 pub durability_mode: crate::durability_policy::DurabilityMode,
263
264 #[arg(long, default_value_t = false)]
269 pub yield_idle: bool,
270
271 #[arg(long, default_value = "", allow_hyphen_values = true)]
275 pub dpdk_eal_args: String,
276
277 #[arg(long, default_value = "0", value_delimiter = ',')]
280 pub dpdk_ports: Vec<u16>,
281
282 #[arg(long, default_value = "10.0.0.1")]
284 pub dpdk_ip: String,
285
286 #[arg(long, default_value_t = 24)]
288 pub dpdk_prefix_len: u8,
289
290 #[arg(long)]
292 pub dpdk_gateway: Option<String>,
293
294 #[arg(long, default_value_t = 1500)]
297 pub dpdk_mtu: usize,
298
299 #[arg(long)]
303 pub dpdk_vlan: Option<u16>,
304
305 #[arg(long)]
310 pub event_bind: Option<SocketAddr>,
311
312 #[arg(long, default_value = "127.0.0.1:9878")]
316 pub health_bind: Option<SocketAddr>,
317
318 #[arg(long)]
331 pub admin_bind: Option<SocketAddr>,
332
333 #[arg(long, default_value_t = 3_000_000)]
338 pub snapshot_interval_ms: u64,
339
340 #[arg(long)]
343 pub snapshot_path: Option<PathBuf>,
344
345 #[arg(long, default_value_t = 250)]
361 pub tick_interval_ms: u64,
362
363 #[arg(long, default_value_t = false)]
376 pub no_mlock: bool,
377}
378
379impl Default for ServerConfig {
383 fn default() -> Self {
384 Self {
389 bind: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9876),
390 journal: PathBuf::from("melin.journal"),
391 snapshot: None,
392 cores: PipelineCores {
393 journal: 1,
394 matching: 2,
395 response: 3,
396 repl_sender: 6,
397 event_publisher: 7,
398 shadow: 8,
399 repl_handler_0: 9,
400 repl_handler_1: 10,
401 },
402 reader_cores: 4,
403 group_commit_us: 0,
404 heartbeat_interval_secs: 10,
405 connection_timeout_secs: 30,
406 max_connections: 1024,
407 accounts: 2,
408 instruments: 2,
409 authorized_keys: PathBuf::from("authorized_keys"),
410 max_journal_mib: 256,
411 journal_writer: melin_journal::JournalWriterMode::default(),
412 max_orders_per_account: 10_000,
413 max_orders_per_second: 1_000,
414 max_orders_burst: 5_000,
415
416 replication_bind: None,
417 standalone: false,
418 replica_of: None,
419 replication_key: None,
420 replication_batch_size: 128,
421 max_journal_batch: 1024,
422 replication_heartbeat_secs: 5,
423 replication_pipeline_depth: DEFAULT_REPLICATION_PIPELINE_DEPTH,
424 replication_ring_size: 256,
425 durability_mode: crate::durability_policy::DurabilityMode::Hybrid,
426 yield_idle: false,
427 dpdk_eal_args: String::new(),
428 dpdk_ports: vec![0],
429 dpdk_ip: "10.0.0.1".into(),
430 dpdk_prefix_len: 24,
431 dpdk_gateway: None,
432 dpdk_mtu: 1500,
433 dpdk_vlan: None,
434 event_bind: None,
435 health_bind: Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9878)),
436 admin_bind: None,
437 snapshot_interval_ms: 3_000_000,
438 snapshot_path: None,
439 tick_interval_ms: 250,
440 no_mlock: false,
441 }
442 }
443}
444
445impl ServerConfig {
446 pub fn group_commit_delay(&self) -> std::time::Duration {
448 std::time::Duration::from_micros(self.group_commit_us)
449 }
450
451 pub fn heartbeat_interval(&self) -> Option<std::time::Duration> {
453 if self.heartbeat_interval_secs == 0 {
454 None
455 } else {
456 Some(std::time::Duration::from_secs(self.heartbeat_interval_secs))
457 }
458 }
459
460 pub fn connection_timeout(&self) -> Option<std::time::Duration> {
462 if self.connection_timeout_secs == 0 {
463 None
464 } else {
465 Some(std::time::Duration::from_secs(self.connection_timeout_secs))
466 }
467 }
468
469 pub fn shadow_snapshot_path(&self) -> PathBuf {
472 self.snapshot_path
473 .clone()
474 .unwrap_or_else(|| self.journal.with_extension("snapshot"))
475 }
476
477 pub fn tick_interval(&self) -> Option<std::time::Duration> {
480 if self.tick_interval_ms == 0 {
481 None
482 } else {
483 Some(std::time::Duration::from_millis(self.tick_interval_ms))
484 }
485 }
486}
487
488#[derive(Debug, Clone, Copy)]
495pub struct PipelineCores {
496 pub journal: usize,
497 pub matching: usize,
498 pub response: usize,
499 pub repl_sender: usize,
500 pub event_publisher: usize,
501 pub shadow: usize,
502 pub repl_handler_0: usize,
504 pub repl_handler_1: usize,
506}
507
508impl PipelineCores {
509 pub fn compact(num_cpus: usize) -> Result<(Self, usize), String> {
523 if num_cpus < 8 {
527 return Err(format!(
528 "compact layout needs >= 8 logical cores; have {num_cpus}"
529 ));
530 }
531 let cores = PipelineCores {
532 journal: 1,
533 matching: 2,
534 response: 3,
535 repl_sender: 4,
536 event_publisher: 5,
537 shadow: 6,
538 repl_handler_0: 0,
540 repl_handler_1: 0,
541 };
542 Ok((cores, 7))
543 }
544}
545
546fn parse_cores(s: &str) -> Result<PipelineCores, String> {
548 let parts: Vec<&str> = s.split(',').collect();
549 if parts.len() != 8 {
550 return Err(format!(
551 "expected 8 comma-separated core IDs (journal,matching,response,repl-sender,event-publisher,shadow,repl-handler-0,repl-handler-1), got {}",
552 parts.len()
553 ));
554 }
555 let parse = |p: &str| {
556 p.parse::<usize>()
557 .map_err(|_| format!("invalid core ID: {p}"))
558 };
559 Ok(PipelineCores {
560 journal: parse(parts[0])?,
561 matching: parse(parts[1])?,
562 response: parse(parts[2])?,
563 repl_sender: parse(parts[3])?,
564 event_publisher: parse(parts[4])?,
565 shadow: parse(parts[5])?,
566 repl_handler_0: parse(parts[6])?,
567 repl_handler_1: parse(parts[7])?,
568 })
569}
570
571pub fn run<A, F, D, E>(
582 config: ServerConfig,
583 factory: F,
584 decoder: D,
585 encoder: E,
586 event_publisher: Option<EventPublisherFn<A>>,
587) -> Result<(), Box<dyn std::error::Error>>
588where
589 A: Application + Send + 'static,
590 A::Event: Send + Sync + 'static,
591 A::Report: Send + 'static,
592 A::QueryResponse: Send + 'static,
593 F: AppFactory<App = A> + 'static,
594 D: RequestDecoder<Event = A::Event> + 'static,
595 E: ResponseEncoder<Report = A::Report, Query = A::QueryResponse> + 'static,
596{
597 let shutdown = Arc::new(AtomicBool::new(false));
598 crate::process::install_shutdown_handler(&shutdown);
599
600 if !config.no_mlock {
601 crate::process::try_lock_memory();
602 }
603
604 #[cfg(feature = "dpdk")]
605 {
606 run_dpdk(
607 config,
608 Arc::new(factory),
609 Arc::new(decoder),
610 Arc::new(encoder),
611 event_publisher,
612 shutdown,
613 )
614 }
615
616 #[cfg(not(feature = "dpdk"))]
617 {
618 let listener = melin_wire_protocol::tcp::BlockingTcpListener::bind(config.bind)?;
619 run_tcp(
620 listener,
621 config,
622 Arc::new(factory),
623 Arc::new(decoder),
624 Arc::new(encoder),
625 event_publisher,
626 shutdown,
627 )
628 }
629}
630
631pub fn run_with_listener<A, L, F, D, E>(
640 listener: L,
641 config: ServerConfig,
642 factory: F,
643 decoder: D,
644 encoder: E,
645 event_publisher: Option<EventPublisherFn<A>>,
646 shutdown: Arc<AtomicBool>,
647) -> Result<(), Box<dyn std::error::Error>>
648where
649 A: Application + Send + 'static,
650 A::Event: Send + Sync + 'static,
651 A::Report: Send + 'static,
652 A::QueryResponse: Send + 'static,
653 L: BlockingTransportListener,
654 F: AppFactory<App = A> + 'static,
655 D: RequestDecoder<Event = A::Event> + 'static,
656 E: ResponseEncoder<Report = A::Report, Query = A::QueryResponse> + 'static,
657{
658 run_tcp(
659 listener,
660 config,
661 Arc::new(factory),
662 Arc::new(decoder),
663 Arc::new(encoder),
664 event_publisher,
665 shutdown,
666 )
667}
668
669fn run_tcp<A, L>(
670 listener: L,
671 config: ServerConfig,
672 factory: Arc<dyn AppFactory<App = A>>,
673 decoder: RequestDecoderArc<A>,
674 encoder: ResponseEncoderArc<A>,
675 event_publisher: Option<EventPublisherFn<A>>,
676 shutdown: Arc<AtomicBool>,
677) -> Result<(), Box<dyn std::error::Error>>
678where
679 A: Application + Send + 'static,
680 A::Event: Send + Sync + 'static,
681 A::Report: Send + 'static,
682 A::QueryResponse: Send + 'static,
683 L: BlockingTransportListener,
684{
685 match config.journal_writer {
686 melin_journal::JournalWriterMode::Buffered => run_impl::<A, L, BufferedWriter<A::Event>>(
687 listener,
688 config,
689 factory,
690 decoder,
691 encoder,
692 event_publisher,
693 shutdown,
694 ),
695 melin_journal::JournalWriterMode::Sector => run_impl::<A, L, SectorWriter<A::Event>>(
696 listener,
697 config,
698 factory,
699 decoder,
700 encoder,
701 event_publisher,
702 shutdown,
703 ),
704 }
705}
706
707fn run_impl<A, L, W>(
708 listener: L,
709 config: ServerConfig,
710 factory: Arc<dyn AppFactory<App = A>>,
711 decoder: RequestDecoderArc<A>,
712 encoder: ResponseEncoderArc<A>,
713 event_publisher: Option<EventPublisherFn<A>>,
714 shutdown: Arc<AtomicBool>,
715) -> Result<(), Box<dyn std::error::Error>>
716where
717 A: Application + Send + 'static,
718 A::Event: Send + Sync + 'static,
719 A::Report: Send + 'static,
720 A::QueryResponse: Send + 'static,
721 L: BlockingTransportListener,
722 W: JournalWrite<A::Event> + Send + 'static,
723 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
724{
725 let durability_mode_atomic = Arc::new(AtomicU8::new(config.durability_mode.as_u8()));
732
733 if let Some(primary_addr) = config.replica_of {
737 info!(primary = %primary_addr, "starting in replica mode");
738
739 let replication_key_path = config.replication_key.as_ref().ok_or_else(|| {
741 std::io::Error::other("--replication-key is required in replica mode (--replica-of)")
742 })?;
743 let signing_key = {
744 let seed = std::fs::read(replication_key_path).map_err(|e| {
745 std::io::Error::other(format!(
746 "failed to read replication key {}: {e}",
747 replication_key_path.display()
748 ))
749 })?;
750 if seed.len() != 32 {
751 return Err(format!(
752 "replication key must be 32 bytes, got {} ({})",
753 seed.len(),
754 replication_key_path.display()
755 )
756 .into());
757 }
758 let mut bytes = [0u8; 32];
759 bytes.copy_from_slice(&seed);
760 ed25519_dalek::SigningKey::from_bytes(&bytes)
761 };
762
763 let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
766 info!(
767 keys = authorized_keys.len(),
768 path = %config.authorized_keys.display(),
769 "loaded authorized keys (replica mode)"
770 );
771
772 let promote_flag = Arc::new(AtomicBool::new(false));
778 let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
779 let _admin_handle = config.admin_bind.map(|addr| {
780 crate::admin::spawn(
781 addr,
782 Some(Arc::clone(&promote_flag)),
783 rotate_flag.clone(),
784 Some(Arc::clone(&durability_mode_atomic)),
785 Arc::clone(&shutdown),
786 Arc::clone(&authorized_keys),
787 )
788 });
789
790 let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
793 let rotation = match (max_journal_bytes, rotate_flag.as_ref()) {
794 (0, None) => None,
795 (b, f) => Some((
796 b,
797 f.cloned()
798 .unwrap_or_else(|| Arc::new(AtomicBool::new(false))),
799 )),
800 };
801
802 match crate::replication::run_receiver::<A, W>(
803 primary_addr,
804 &config.journal,
805 &signing_key,
806 &shutdown,
807 &promote_flag,
808 config.snapshot_interval_ms,
809 config.shadow_snapshot_path(),
810 config.cores,
811 config.reader_cores,
812 config.group_commit_delay(),
813 config.replication_pipeline_depth,
814 !config.yield_idle,
815 rotation,
816 Arc::clone(&factory),
817 )? {
818 None => return Ok(()), Some((mut exchange, writer)) => {
820 info!("replica promoted — transitioning to primary");
822 <A as Application>::prefault(&mut exchange);
823
824 return run_as_primary::<A, L, W>(
825 exchange,
826 writer,
827 listener,
828 &config,
829 &*factory,
830 decoder,
831 encoder,
832 event_publisher,
833 shutdown,
834 authorized_keys,
835 false, rotate_flag,
837 durability_mode_atomic,
838 );
839 }
840 }
841 }
842 let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
844 info!(
845 keys = authorized_keys.len(),
846 path = %config.authorized_keys.display(),
847 "loaded authorized keys"
848 );
849
850 let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
855 let _admin_handle = config.admin_bind.map(|addr| {
856 crate::admin::spawn(
857 addr,
858 None,
859 rotate_flag.clone(),
860 Some(Arc::clone(&durability_mode_atomic)),
861 Arc::clone(&shutdown),
862 Arc::clone(&authorized_keys),
863 )
864 });
865
866 let (mut exchange, writer, needs_seeding) = init_engine::<A, W>(&config, &*factory)?;
869
870 <A as Application>::prefault(&mut exchange);
874
875 run_as_primary::<A, L, W>(
876 exchange,
877 writer,
878 listener,
879 &config,
880 &*factory,
881 decoder,
882 encoder,
883 event_publisher,
884 shutdown,
885 authorized_keys,
886 needs_seeding,
887 rotate_flag,
888 durability_mode_atomic,
889 )
890}
891
892pub use crate::ControlEvent;
899
900struct PipelineHandles<A: Send + 'static, W: Send + 'static> {
905 journal: std::thread::JoinHandle<Result<W, JournalError>>,
906 matching: std::thread::JoinHandle<A>,
907 response: std::thread::JoinHandle<()>,
908 replication: Option<std::thread::JoinHandle<()>>,
909 event_publisher: Option<std::thread::JoinHandle<()>>,
910 shadow: Option<std::thread::JoinHandle<()>>,
911 health: Option<std::thread::JoinHandle<()>>,
912 }
916
917fn shutdown_pipeline_stages<A: Send + 'static, W: Send + 'static>(
923 handles: PipelineHandles<A, W>,
924 extras: Vec<(String, std::thread::Result<()>)>,
925 pipeline_healthy: &AtomicBool,
926 shutdown: &AtomicBool,
927) -> Result<(), Box<dyn std::error::Error>> {
928 info!("shutdown: draining pipeline");
929 pipeline_healthy.store(false, Ordering::Relaxed);
930 shutdown.store(true, Ordering::Relaxed);
931
932 let mut thread_panicked = false;
933 let mut check_join = |name: &str, result: std::thread::Result<()>| {
934 if let Err(panic) = result {
935 let msg = panic
936 .downcast_ref::<&str>()
937 .copied()
938 .or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
939 .unwrap_or("<non-string panic>");
940 error!(thread = name, message = msg, "pipeline thread panicked");
941 thread_panicked = true;
942 }
943 };
944
945 let journal_result = handles.journal.join();
946 let journal_failed = matches!(&journal_result, Ok(Err(_)));
947 if let Ok(Err(ref e)) = journal_result {
948 error!(thread = "journal", error = %e, "journal stage returned error");
949 }
950 check_join("journal", journal_result.map(|_| ()));
951 check_join("matching", handles.matching.join().map(|_| ()));
952 check_join("response", handles.response.join());
953 for (name, r) in extras {
954 check_join(&name, r);
955 }
956 if let Some(h) = handles.replication {
957 check_join("replication-sender", h.join());
958 }
959 if let Some(h) = handles.event_publisher {
960 check_join("event-publisher", h.join());
961 }
962 if let Some(h) = handles.shadow {
963 check_join("shadow", h.join());
964 }
965 if let Some(h) = handles.health {
966 check_join("health", h.join());
967 }
968
969 melin_transport_core::trace::print_report_all();
972
973 if thread_panicked || journal_failed {
974 error!("shutdown complete (with pipeline failure)");
975 return Err("pipeline failure".into());
976 }
977
978 info!("shutdown complete");
979 Ok(())
980}
981
982#[allow(clippy::too_many_arguments)]
992fn run_as_primary<A, L, W>(
993 exchange: A,
994 writer: W,
995 mut listener: L,
996 config: &ServerConfig,
997 factory: &dyn AppFactory<App = A>,
998 decoder: RequestDecoderArc<A>,
999 encoder: ResponseEncoderArc<A>,
1000 event_publisher: Option<EventPublisherFn<A>>,
1001 shutdown: Arc<AtomicBool>,
1002 authorized_keys: Arc<AuthorizedKeys>,
1003 needs_seeding: bool,
1004 rotate_flag: Option<Arc<AtomicBool>>,
1005 durability_mode_atomic: Arc<AtomicU8>,
1006) -> Result<(), Box<dyn std::error::Error>>
1007where
1008 A: Application + Send + 'static,
1009 A::Event: Send + Sync + 'static,
1010 A::Report: Send + 'static,
1011 A::QueryResponse: Send + 'static,
1012 L: BlockingTransportListener,
1013 W: JournalWrite<A::Event> + Send + 'static,
1014 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
1015{
1016 let active_connections = Arc::new(AtomicU64::new(0));
1021
1022 let enable_replication = config.replication_bind.is_some();
1027 if enable_replication && config.standalone {
1028 return Err("--replication-bind and --standalone are mutually exclusive".into());
1029 }
1030 if config.standalone
1034 && config.durability_mode != crate::durability_policy::DurabilityMode::Local
1035 {
1036 return Err(format!(
1037 "--standalone requires --durability-mode local; got `{}` (this mode needs at least one connected replica)",
1038 config.durability_mode,
1039 )
1040 .into());
1041 }
1042 let genesis_entry = if enable_replication {
1047 writer
1048 .read_genesis_entry()
1049 .map_err(|e| format!("failed to read genesis entry: {e}"))?
1050 } else {
1051 Vec::new()
1052 };
1053
1054 let enable_shadow = config.snapshot_interval_ms > 0;
1058 let shadow_exchange = if enable_shadow {
1059 Some(<A as Application>::clone_via_snapshot(&exchange)?)
1060 } else {
1061 None
1062 };
1063
1064 let enable_event_publisher = event_publisher.is_some() && config.event_bind.is_some();
1072 let Pipeline {
1073 input_producer,
1074 journal_stage,
1075 matching_stage,
1076 mut output_consumers,
1077 journal_cursor,
1078 matching_cursor,
1079 events_processed,
1080 input_cursor,
1081 replication_consumers,
1082 replication_cursor,
1083 replicas_connected,
1084 shadow_consumer,
1085 chain_hash_lock,
1086 replication_ring_progress,
1087 last_seq,
1088 } = build_pipeline_with_replication(
1089 exchange,
1090 writer,
1091 config.group_commit_delay(),
1092 Arc::clone(&active_connections),
1093 enable_replication,
1094 config.max_journal_batch,
1095 config.replication_ring_size,
1096 !config.yield_idle,
1097 enable_event_publisher,
1098 enable_shadow,
1099 );
1100 let fastest_replica_cursor = Arc::new(AtomicU64::new(u64::MAX));
1107
1108 let output_consumer = output_consumers.remove(0);
1111 let event_publisher_consumer = if enable_event_publisher {
1112 Some(output_consumers.remove(0))
1113 } else {
1114 None
1115 };
1116
1117 let (control_tx, control_rx) = std::sync::mpsc::channel();
1119
1120 let connection_timeout = config.connection_timeout();
1126 let heartbeat_interval = config.heartbeat_interval();
1127
1128 let mut input_producer = input_producer;
1134
1135 let cores = config.cores;
1137
1138 let mut journal_stage = journal_stage;
1145 let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
1146 journal_stage.set_rotation(max_journal_bytes, rotate_flag.clone());
1147 if config.max_journal_mib > 0 {
1148 info!(
1149 max_journal_mib = config.max_journal_mib,
1150 "runtime journal rotation enabled (size threshold)"
1151 );
1152 }
1153
1154 let journal_utilization = journal_stage.utilization();
1156 let matching_utilization = matching_stage.utilization();
1157 let response_utilization = Arc::new(melin_transport_core::pipeline::StageUtilization::new());
1158
1159 let s1 = Arc::clone(&shutdown);
1165 let shutdown_for_journal = Arc::clone(&shutdown);
1166 let journal_handle = std::thread::Builder::new()
1167 .name("journal".into())
1168 .spawn(move || {
1169 melin_app::affinity::pin_thread("journal", cores.journal);
1170 let result = journal_stage.run(&s1);
1171 let was_shutdown = shutdown_for_journal.load(Ordering::Relaxed);
1172 match &result {
1173 Ok(_) if was_shutdown => info!("journal thread exited cleanly on shutdown"),
1174 Ok(_) => error!("journal thread returned without shutdown signal"),
1175 Err(e) => error!(error = %e, "journal thread returned Err"),
1176 }
1177 result
1178 })
1179 .map_err(|e| format!("spawn journal thread: {e}"))?;
1180
1181 let s2 = Arc::clone(&shutdown);
1182 let shutdown_for_matching = Arc::clone(&shutdown);
1183 let matching_handle = std::thread::Builder::new()
1184 .name("matching".into())
1185 .spawn(move || {
1186 melin_app::affinity::pin_thread("matching", cores.matching);
1187 let app = matching_stage.run(&s2);
1188 let was_shutdown = shutdown_for_matching.load(Ordering::Relaxed);
1189 if was_shutdown {
1190 info!("matching thread exited cleanly on shutdown");
1191 } else {
1192 error!("matching thread returned without shutdown signal");
1193 }
1194 app
1195 })
1196 .map_err(|e| format!("spawn matching thread: {e}"))?;
1197
1198 let replication_metrics = build_replication_metrics(
1199 replication_consumers.is_some(),
1200 &durability_mode_atomic,
1201 config.durability_mode,
1202 );
1203
1204 let replica_active: Option<[Arc<AtomicBool>; 2]> =
1208 replication_ring_progress.as_ref().map(|rp| {
1209 [
1210 Arc::clone(&rp.active_flags[0]),
1211 Arc::clone(&rp.active_flags[1]),
1212 ]
1213 });
1214
1215 let journal_persisted_wire_seq_response = Arc::clone(&last_seq);
1218 let replication_metrics_response = replication_metrics.as_ref().map(Arc::clone);
1219 let replica_active_response = replica_active.clone();
1220 let durability_mode_response = Arc::clone(&durability_mode_atomic);
1221 let s3 = Arc::clone(&shutdown);
1222 let shutdown_for_response = Arc::clone(&shutdown);
1223 let busy_spin = !config.yield_idle;
1224 let response_utilization_thread = Arc::clone(&response_utilization);
1225 let response_handle = std::thread::Builder::new()
1226 .name("response".into())
1227 .spawn(move || {
1228 melin_app::affinity::pin_thread("response", cores.response);
1229 crate::response::run::<A>(
1230 output_consumer,
1231 control_rx,
1232 crate::response::Response::<A> {
1233 journal_persisted_wire_seq: journal_persisted_wire_seq_response,
1234 durability_mode: durability_mode_response,
1235 replication_metrics: replication_metrics_response,
1236 replica_active: replica_active_response,
1237 heartbeat_interval,
1238 busy_spin,
1239 utilization: response_utilization_thread,
1240 encoder,
1241 },
1242 &s3,
1243 );
1244 let was_shutdown = shutdown_for_response.load(Ordering::Relaxed);
1245 if was_shutdown {
1246 info!("response thread exited cleanly on shutdown");
1247 } else {
1248 error!("response thread returned without shutdown signal");
1249 }
1250 })
1251 .map_err(|e| format!("spawn response thread: {e}"))?;
1252
1253 let replica_ready = Arc::new(AtomicBool::new(false));
1258 let replication_handle = if let Some((repl_consumer_1, repl_consumer_2)) = replication_consumers
1265 {
1266 let repl_bind = config
1267 .replication_bind
1268 .ok_or("replication_bind must be set when replication is enabled")?;
1269 let s_repl = Arc::clone(&shutdown);
1270 let repl_cursor = Arc::clone(&replication_cursor);
1271 let fastest_repl_cursor = Arc::clone(&fastest_replica_cursor);
1272 let ready_flag = Arc::clone(&replica_ready);
1273 let connected_counter = replicas_connected
1274 .clone()
1275 .ok_or("replicas_connected must be Some when replication is enabled")?;
1276
1277 let batch_size = config.replication_batch_size;
1278 let heartbeat_secs = config.replication_heartbeat_secs;
1279 let journal_path = config.journal.clone();
1280 let repl_auth_keys = Arc::clone(&authorized_keys);
1281 let evict_flags = replication_ring_progress
1282 .as_ref()
1283 .map(|rp| {
1284 [
1285 Arc::clone(&rp.evict_flags[0]),
1286 Arc::clone(&rp.evict_flags[1]),
1287 ]
1288 })
1289 .unwrap_or_else(|| {
1290 [
1291 Arc::new(AtomicBool::new(false)),
1292 Arc::new(AtomicBool::new(false)),
1293 ]
1294 });
1295 let active_flags = replication_ring_progress
1296 .as_ref()
1297 .map(|rp| {
1298 [
1299 Arc::clone(&rp.active_flags[0]),
1300 Arc::clone(&rp.active_flags[1]),
1301 ]
1302 })
1303 .unwrap_or_else(|| {
1304 [
1305 Arc::new(AtomicBool::new(false)),
1306 Arc::new(AtomicBool::new(false)),
1307 ]
1308 });
1309 let repl_metrics = replication_metrics
1310 .clone()
1311 .ok_or("replication_metrics must be Some when replication is enabled")?;
1312 let handler_cores = [cores.repl_handler_0, cores.repl_handler_1];
1313 let repl_sender_handle = std::thread::Builder::new()
1314 .name("repl-sender".into())
1315 .spawn(move || {
1316 melin_app::affinity::pin_thread("repl-sender", cores.repl_sender);
1317 crate::replication::run_sender::<A>(
1318 crate::replication::Sender {
1319 bind_addr: repl_bind,
1320 repl_consumer_1,
1321 repl_consumer_2,
1322 replication_cursor: repl_cursor,
1323 fastest_replica_cursor: fastest_repl_cursor,
1324 genesis_entry,
1325 journal_path,
1326 authorized_keys: repl_auth_keys,
1327 evict_flags,
1328 active_flags,
1329 metrics: repl_metrics,
1330 handler_cores,
1331 batch_size,
1332 heartbeat_secs,
1333 busy_spin,
1334 },
1335 &s_repl,
1336 &ready_flag,
1337 &connected_counter,
1338 );
1339 })
1340 .map_err(|e| format!("spawn replication sender thread: {e}"))?;
1341
1342 info!(addr = %repl_bind, "replication listener started");
1343 Some(repl_sender_handle)
1344 } else {
1345 if !config.standalone && config.replica_of.is_none() {
1346 info!("running in standalone mode (no replication)");
1347 }
1348 None
1349 };
1350
1351 let event_publisher_handle = spawn_event_publisher::<A>(
1357 event_publisher_consumer,
1358 event_publisher,
1359 config,
1360 &cores,
1361 &authorized_keys,
1362 &shutdown,
1363 busy_spin,
1364 )?;
1365
1366 let shadow_handle = spawn_shadow_stage::<A>(
1367 shadow_consumer,
1368 shadow_exchange,
1369 chain_hash_lock,
1370 config,
1371 &cores,
1372 &shutdown,
1373 busy_spin,
1374 )?;
1375
1376 let pipeline_healthy = Arc::new(AtomicBool::new(true));
1396 let health_handle = spawn_health_endpoint(
1397 config,
1398 &active_connections,
1399 &events_processed,
1400 &journal_cursor,
1401 &matching_cursor,
1402 input_cursor,
1403 &replication_cursor,
1404 &pipeline_healthy,
1405 &replicas_connected,
1406 &replication_metrics,
1407 &replication_ring_progress,
1408 &fastest_replica_cursor,
1409 &journal_utilization,
1410 &matching_utilization,
1411 &response_utilization,
1412 &shutdown,
1413 )?;
1414
1415 if enable_replication && needs_seeding {
1416 info!("waiting for replica to connect before seeding...");
1417 while !replica_ready.load(Ordering::Acquire) {
1418 if shutdown.load(Ordering::Relaxed) {
1419 break;
1420 }
1421 std::thread::sleep(std::time::Duration::from_millis(100));
1422 }
1423 }
1424 if needs_seeding {
1425 use melin_app::unix_epoch_nanos;
1426 use melin_journal::JournalEvent;
1427 use melin_transport_core::trace::mono_trace_ns;
1428
1429 let seed_start = std::time::Instant::now();
1430 let seed_events = factory.seed_events();
1431 let seed_count = seed_events.len();
1432
1433 let mut last_published_seq = 0u64;
1438 for event in seed_events {
1439 last_published_seq = input_producer.publish(InputSlot {
1440 connection_id: 0,
1441 key_hash: 0,
1442 request_seq: 0,
1443 sequence: 0,
1444 timestamp_ns: unix_epoch_nanos(),
1445 event: JournalEvent::App(event),
1446 publish_ts: mono_trace_ns(),
1447 recv_ts: mono_trace_ns(),
1448 });
1449 }
1450 let publish_elapsed = seed_start.elapsed();
1451
1452 let drain_start = std::time::Instant::now();
1464 let last_seed_seq = last_published_seq + 1; info!(
1467 last_seed_seq,
1468 journal = journal_cursor
1469 .get()
1470 .load(std::sync::atomic::Ordering::Relaxed),
1471 matching = matching_cursor
1472 .get()
1473 .load(std::sync::atomic::Ordering::Relaxed),
1474 "seed drain: waiting for pipeline cursors"
1475 );
1476
1477 while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
1478 && (journal_cursor
1479 .get()
1480 .load(std::sync::atomic::Ordering::Acquire)
1481 < last_seed_seq
1482 || matching_cursor
1483 .get()
1484 .load(std::sync::atomic::Ordering::Acquire)
1485 < last_seed_seq)
1486 {
1487 std::hint::spin_loop();
1488 }
1489
1490 info!("seed drain: pipeline cursors reached target");
1491
1492 if let Some(ref ring_progress) = replication_ring_progress {
1497 for i in 0..ring_progress.producer_cursors.len() {
1498 if !ring_progress.active_flags[i].load(std::sync::atomic::Ordering::Relaxed) {
1499 continue;
1500 }
1501 let target = ring_progress.producer_cursors[i].load();
1502 while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
1503 && ring_progress.consumer_cursors[i]
1504 .get()
1505 .load(std::sync::atomic::Ordering::Acquire)
1506 < target
1507 {
1508 std::hint::spin_loop();
1509 }
1510 }
1511 }
1512
1513 info!("seed drain: replication rings drained");
1514 let drain_elapsed = drain_start.elapsed();
1515
1516 info!(
1517 seed_events = seed_count,
1518 publish_ms = publish_elapsed.as_millis(),
1519 drain_ms = drain_elapsed.as_millis(),
1520 total_ms = seed_start.elapsed().as_millis(),
1521 "seeded application state through pipeline"
1522 );
1523 }
1524
1525 let reader_shutdown = Arc::new(AtomicBool::new(false));
1534 let mut reader_handle = crate::reader::spawn_reader::<A, _>(
1535 input_producer,
1536 decoder,
1537 control_tx.clone(),
1538 config.reader_cores,
1539 connection_timeout,
1540 config.tick_interval(),
1541 Arc::clone(&reader_shutdown),
1542 );
1543
1544 listener.set_nonblocking(true);
1554
1555 info!(addr = %config.bind, "listening");
1556
1557 let next_connection_id = AtomicU64::new(1);
1561
1562 loop {
1566 if shutdown.load(Ordering::Relaxed) {
1567 info!("shutdown signal received");
1568 break;
1569 }
1570
1571 let event_pub_died = event_publisher_handle
1577 .as_ref()
1578 .is_some_and(|h| h.is_finished());
1579 let shadow_died = shadow_handle.as_ref().is_some_and(|h| h.is_finished());
1580 if (journal_handle.is_finished()
1581 || matching_handle.is_finished()
1582 || response_handle.is_finished()
1583 || event_pub_died
1584 || shadow_died)
1585 && !shutdown.load(Ordering::Relaxed)
1586 {
1587 error!("pipeline thread died, initiating shutdown");
1588 pipeline_healthy.store(false, Ordering::Relaxed);
1589 break;
1590 }
1591
1592 let (mut std_read, mut std_write, addr) = match listener.accept() {
1593 Ok(conn) => conn,
1594 Err(e) => {
1595 if e.kind() == std::io::ErrorKind::WouldBlock {
1596 std::thread::sleep(std::time::Duration::from_millis(100));
1598 continue;
1599 }
1600 error!(error = %e, "accept error");
1601 continue;
1602 }
1603 };
1604
1605 if config.max_connections > 0
1609 && active_connections.load(Ordering::Relaxed) >= config.max_connections
1610 {
1611 warn!(addr = %addr, "connection rejected: max_connections reached");
1612 drop(std_read);
1613 drop(std_write);
1614 continue;
1615 }
1616
1617 let connection_id = ConnectionId(next_connection_id.fetch_add(1, Ordering::Relaxed));
1618
1619 debug!(connection_id = connection_id.0, addr = %addr, "new connection");
1620
1621 if let Err(e) = set_read_timeout(&std_read, Some(std::time::Duration::from_secs(5))) {
1624 debug!(connection_id = connection_id.0, error = %e, "failed to set auth timeout");
1625 }
1626
1627 let (permission, public_key_bytes) = match authenticate_connection(
1633 connection_id,
1634 addr,
1635 &mut std_read,
1636 &mut std_write,
1637 &authorized_keys,
1638 ) {
1639 Ok(pair) => pair,
1640 Err(e) => {
1641 debug!(connection_id = connection_id.0, addr = %addr, error = %e, "auth failed, dropping");
1642 continue;
1643 }
1644 };
1645
1646 let key_hash = {
1650 let mut hasher = rustc_hash::FxHasher::default();
1651 public_key_bytes.hash(&mut hasher);
1652 hasher.finish()
1653 };
1654
1655 active_connections.fetch_add(1, Ordering::Relaxed);
1656
1657 if let Err(e) = set_read_timeout(&std_read, None) {
1662 debug!(connection_id = connection_id.0, error = %e, "failed to clear auth timeout");
1663 }
1664
1665 if let Err(e) = set_busy_poll(&std_read, BUSY_POLL_US) {
1671 debug!(
1676 connection_id = connection_id.0,
1677 error = %e,
1678 "failed to set SO_BUSY_POLL on client socket"
1679 );
1680 }
1681
1682 if let Err(e) = set_write_timeout(&std_write, Some(std::time::Duration::from_secs(5))) {
1687 debug!(connection_id = connection_id.0, error = %e, "failed to set write timeout");
1688 }
1689
1690 let fd = std_write.as_raw_fd();
1694 let boxed_writer: Box<dyn std::io::Write + Send> = Box::new(std_write);
1695 let control_event = ControlEvent::Connected {
1696 connection_id: connection_id.0,
1697 fd,
1698 writer: BlockingFrameWriter::new(boxed_writer),
1699 };
1700 if control_tx.send(control_event).is_err() {
1701 info!("response thread gone, shutting down");
1702 break;
1703 }
1704
1705 reader_handle.register(crate::reader::ReaderRegistration {
1707 connection_id,
1708 reader: std_read,
1709 addr,
1710 permission,
1711 key_hash,
1712 });
1713 }
1714
1715 info!("shutdown: stopping reader thread");
1718 reader_handle.shutdown();
1719 reader_handle.join();
1720
1721 shutdown_pipeline_stages(
1723 PipelineHandles {
1724 journal: journal_handle,
1725 matching: matching_handle,
1726 response: response_handle,
1727 replication: replication_handle,
1728 event_publisher: event_publisher_handle,
1729 shadow: shadow_handle,
1730 health: health_handle,
1731 },
1732 Vec::new(),
1733 &pipeline_healthy,
1734 &shutdown,
1735 )
1736}
1737
1738#[cfg(feature = "dpdk")]
1752fn run_dpdk<A>(
1753 config: ServerConfig,
1754 factory: Arc<dyn AppFactory<App = A>>,
1755 decoder: RequestDecoderArc<A>,
1756 encoder: ResponseEncoderArc<A>,
1757 event_publisher: Option<EventPublisherFn<A>>,
1758 shutdown: Arc<AtomicBool>,
1759) -> Result<(), Box<dyn std::error::Error>>
1760where
1761 A: Application + Send + 'static,
1762 A::Event: Send + Sync + 'static,
1763 A::Report: Send + 'static,
1764 A::QueryResponse: Send + 'static,
1765{
1766 let dpdk_config = dpdk_config_from(&config);
1767
1768 match config.journal_writer {
1769 melin_journal::JournalWriterMode::Buffered => run_dpdk_impl::<A, BufferedWriter<A::Event>>(
1770 config,
1771 factory,
1772 decoder,
1773 encoder,
1774 event_publisher,
1775 dpdk_config,
1776 shutdown,
1777 ),
1778 melin_journal::JournalWriterMode::Sector => run_dpdk_impl::<A, SectorWriter<A::Event>>(
1779 config,
1780 factory,
1781 decoder,
1782 encoder,
1783 event_publisher,
1784 dpdk_config,
1785 shutdown,
1786 ),
1787 }
1788}
1789
1790#[cfg(feature = "dpdk")]
1791fn dpdk_config_from(cfg: &ServerConfig) -> melin_dpdk::DpdkConfig {
1792 melin_dpdk::DpdkConfig {
1793 eal_args: cfg
1794 .dpdk_eal_args
1795 .split_whitespace()
1796 .map(String::from)
1797 .collect(),
1798 port_ids: cfg.dpdk_ports.clone(),
1799 ip_addr: cfg.dpdk_ip.parse().expect("invalid --dpdk-ip address"),
1800 prefix_len: cfg.dpdk_prefix_len,
1801 gateway: cfg
1802 .dpdk_gateway
1803 .as_deref()
1804 .map(|s| s.parse().expect("invalid --dpdk-gateway address")),
1805 listen_port: cfg.bind.port(),
1806 mtu: cfg.dpdk_mtu,
1807 vlan_id: cfg.dpdk_vlan,
1808 num_queues: 1,
1809 }
1810}
1811
1812#[cfg(feature = "dpdk")]
1813fn run_dpdk_impl<A, W>(
1814 config: ServerConfig,
1815 factory: Arc<dyn AppFactory<App = A>>,
1816 decoder: RequestDecoderArc<A>,
1817 encoder: ResponseEncoderArc<A>,
1818 event_publisher: Option<EventPublisherFn<A>>,
1819 dpdk_config: melin_dpdk::DpdkConfig,
1820 shutdown: Arc<AtomicBool>,
1821) -> Result<(), Box<dyn std::error::Error>>
1822where
1823 A: Application + Send + 'static,
1824 A::Event: Send + Sync + 'static,
1825 A::Report: Send + 'static,
1826 A::QueryResponse: Send + 'static,
1827 W: JournalWrite<A::Event> + Send + 'static,
1828 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
1829{
1830 let durability_mode_atomic = Arc::new(AtomicU8::new(config.durability_mode.as_u8()));
1834 let shared = melin_dpdk::DpdkShared::init(&dpdk_config)?;
1836 let num_dpdk_threads = shared.num_queues as usize;
1838
1839 if let Some(primary_addr) = config.replica_of {
1844 info!(primary = %primary_addr, "starting in replica mode (DPDK)");
1845
1846 let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
1849 info!(
1850 keys = authorized_keys.len(),
1851 path = %config.authorized_keys.display(),
1852 "loaded authorized keys (DPDK replica mode)"
1853 );
1854
1855 let promote_flag = Arc::new(AtomicBool::new(false));
1858 let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
1859 let _admin_handle = config.admin_bind.map(|addr| {
1860 crate::admin::spawn(
1861 addr,
1862 Some(Arc::clone(&promote_flag)),
1863 rotate_flag.clone(),
1864 Some(Arc::clone(&durability_mode_atomic)),
1865 Arc::clone(&shutdown),
1866 Arc::clone(&authorized_keys),
1867 )
1868 });
1869
1870 let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
1871 let rotation = match (max_journal_bytes, rotate_flag.as_ref()) {
1872 (0, None) => None,
1873 (b, f) => Some((
1874 b,
1875 f.cloned()
1876 .unwrap_or_else(|| Arc::new(AtomicBool::new(false))),
1877 )),
1878 };
1879
1880 let mut repl_transport = melin_dpdk::DpdkTransport::from_shared_with_port(
1884 &shared,
1885 &dpdk_config,
1886 0,
1887 39999, )?;
1889 repl_transport.send_gratuitous_arp();
1890
1891 let primary_ipv4 = match primary_addr.ip() {
1892 std::net::IpAddr::V4(ip) => ip,
1893 std::net::IpAddr::V6(_) => {
1894 return Err("DPDK replication requires IPv4".into());
1895 }
1896 };
1897
1898 match crate::replication::run_receiver_dpdk::<A, W>(
1899 repl_transport,
1900 primary_ipv4,
1901 primary_addr.port(),
1902 &config.journal,
1903 &shutdown,
1904 &promote_flag,
1905 config.snapshot_interval_ms,
1906 config.shadow_snapshot_path(),
1907 config.cores,
1908 config.reader_cores,
1909 config.group_commit_delay(),
1910 config.replication_pipeline_depth,
1911 !config.yield_idle,
1912 rotation,
1913 Arc::clone(&factory),
1914 )? {
1915 None => return Ok(()), Some((mut exchange, writer)) => {
1917 info!("replica promoted (DPDK) — transitioning to primary");
1919 <A as Application>::prefault(&mut exchange);
1920
1921 warn!("DPDK primary promotion not yet implemented — falling back to kernel TCP");
1924 let listener = melin_wire_protocol::tcp::BlockingTcpListener::bind(config.bind)?;
1925 return run_as_primary::<A, _, W>(
1926 exchange,
1927 writer,
1928 listener,
1929 &config,
1930 &*factory,
1931 decoder,
1932 encoder,
1933 event_publisher,
1934 shutdown,
1935 authorized_keys,
1936 false,
1937 rotate_flag,
1938 durability_mode_atomic,
1939 );
1940 }
1941 }
1942 }
1943
1944 let num_client_queues = num_dpdk_threads;
1950 let mut transports = Vec::with_capacity(num_client_queues);
1951 for q in 0..num_client_queues {
1952 let mut transport =
1953 melin_dpdk::DpdkTransport::from_shared(&shared, &dpdk_config, q as u16)?;
1954 if q == 0 {
1959 transport.send_gratuitous_arp();
1960 }
1961 transports.push(transport);
1962 }
1963
1964 let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
1966 info!(
1967 keys = authorized_keys.len(),
1968 path = %config.authorized_keys.display(),
1969 "loaded authorized keys"
1970 );
1971
1972 let (mut exchange, writer, needs_seeding) = init_engine::<A, W>(&config, &*factory)?;
1974 <A as Application>::prefault(&mut exchange);
1975
1976 let enable_shadow = config.snapshot_interval_ms > 0;
1979 let shadow_exchange = if enable_shadow {
1980 Some(<A as Application>::clone_via_snapshot(&exchange)?)
1981 } else {
1982 None
1983 };
1984
1985 let active_connections = Arc::new(AtomicU64::new(0));
1986
1987 let enable_replication = config.replication_bind.is_some();
1989 if enable_replication && config.standalone {
1990 return Err("--replication-bind and --standalone are mutually exclusive".into());
1991 }
1992 if config.standalone
1996 && config.durability_mode != crate::durability_policy::DurabilityMode::Local
1997 {
1998 return Err(format!(
1999 "--standalone requires --durability-mode local; got `{}` (this mode needs at least one connected replica)",
2000 config.durability_mode,
2001 )
2002 .into());
2003 }
2004
2005 let genesis_entry = if enable_replication {
2006 writer
2007 .read_genesis_entry()
2008 .map_err(|e| format!("failed to read genesis entry: {e}"))?
2009 } else {
2010 Vec::new()
2011 };
2012
2013 let enable_event_publisher = event_publisher.is_some() && config.event_bind.is_some();
2020 let enable_shadow = config.snapshot_interval_ms > 0;
2021 let Pipeline {
2022 input_producer,
2023 journal_stage,
2024 matching_stage,
2025 mut output_consumers,
2026 journal_cursor,
2027 matching_cursor,
2028 events_processed,
2029 input_cursor,
2030 replication_consumers,
2031 replication_cursor,
2032 replicas_connected,
2033 shadow_consumer,
2034 chain_hash_lock,
2035 replication_ring_progress,
2036 last_seq,
2037 } = build_pipeline_with_replication(
2038 exchange,
2039 writer,
2040 config.group_commit_delay(),
2041 Arc::clone(&active_connections),
2042 enable_replication,
2043 config.max_journal_batch,
2044 config.replication_ring_size,
2045 !config.yield_idle,
2046 enable_event_publisher,
2047 enable_shadow,
2048 );
2049
2050 let heartbeat_interval = config.heartbeat_interval();
2051
2052 let mut input_producer = input_producer;
2056
2057 let tick_cadence = config.tick_interval();
2062
2063 let fastest_replica_cursor = Arc::new(AtomicU64::new(u64::MAX));
2065
2066 let (control_tx, control_rx) = std::sync::mpsc::channel();
2068
2069 let mut tx_producers = Vec::with_capacity(num_dpdk_threads);
2077 let mut tx_consumers = Vec::with_capacity(num_dpdk_threads);
2078 for _ in 0..num_dpdk_threads {
2079 let (tx_out, tx_rx) = melin_pipeline::spsc::channel::<crate::dpdk_response::TxFrame>(4096);
2080 tx_producers.push(tx_out);
2081 tx_consumers.push(tx_rx);
2082 }
2083
2084 let cores = config.cores;
2086
2087 let mut journal_stage = journal_stage;
2091 let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
2092 let _admin_handle = config.admin_bind.map(|addr| {
2093 crate::admin::spawn(
2094 addr,
2095 None,
2096 rotate_flag.clone(),
2097 Some(Arc::clone(&durability_mode_atomic)),
2098 Arc::clone(&shutdown),
2099 Arc::clone(&authorized_keys),
2100 )
2101 });
2102 let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
2103 journal_stage.set_rotation(max_journal_bytes, rotate_flag.clone());
2104 if config.max_journal_mib > 0 {
2105 info!(
2106 max_journal_mib = config.max_journal_mib,
2107 "runtime journal rotation enabled (size threshold, DPDK)"
2108 );
2109 }
2110
2111 let journal_utilization = journal_stage.utilization();
2113 let matching_utilization = matching_stage.utilization();
2114 let response_utilization = Arc::new(melin_transport_core::pipeline::StageUtilization::new());
2115
2116 let s1 = Arc::clone(&shutdown);
2117 let journal_handle = std::thread::Builder::new()
2118 .name("journal".into())
2119 .spawn(move || {
2120 melin_app::affinity::pin_thread("journal", cores.journal);
2121 journal_stage.run(&s1)
2122 })
2123 .map_err(|e| format!("spawn journal thread: {e}"))?;
2124
2125 let s2 = Arc::clone(&shutdown);
2126 let matching_handle = std::thread::Builder::new()
2127 .name("matching".into())
2128 .spawn(move || {
2129 melin_app::affinity::pin_thread("matching", cores.matching);
2130 matching_stage.run(&s2)
2131 })
2132 .map_err(|e| format!("spawn matching thread: {e}"))?;
2133
2134 let replication_metrics = build_replication_metrics(
2135 replication_consumers.is_some(),
2136 &durability_mode_atomic,
2137 config.durability_mode,
2138 );
2139
2140 let replica_active: Option<[Arc<AtomicBool>; 2]> =
2141 replication_ring_progress.as_ref().map(|rp| {
2142 [
2143 Arc::clone(&rp.active_flags[0]),
2144 Arc::clone(&rp.active_flags[1]),
2145 ]
2146 });
2147
2148 let output_consumer = output_consumers.remove(0);
2150 let journal_persisted_wire_seq_response = Arc::clone(&last_seq);
2151 let replication_metrics_response = replication_metrics.as_ref().map(Arc::clone);
2152 let replica_active_response = replica_active.clone();
2153 let durability_mode_response = Arc::clone(&durability_mode_atomic);
2154 let active_connections_response = Arc::clone(&active_connections);
2155 let s3 = Arc::clone(&shutdown);
2156 let response_utilization_thread = Arc::clone(&response_utilization);
2157 let busy_spin = !config.yield_idle;
2158 let response_handle = std::thread::Builder::new()
2159 .name("response".into())
2160 .spawn(move || {
2161 melin_app::affinity::pin_thread("response", cores.response);
2162 crate::dpdk_response::run::<A>(
2163 output_consumer,
2164 control_rx,
2165 journal_persisted_wire_seq_response,
2166 durability_mode_response,
2167 replication_metrics_response,
2168 replica_active_response,
2169 &s3,
2170 heartbeat_interval,
2171 active_connections_response,
2172 tx_producers,
2173 response_utilization_thread,
2174 busy_spin,
2175 encoder,
2176 );
2177 })
2178 .map_err(|e| format!("spawn response thread: {e}"))?;
2179
2180 let shadow_handle = spawn_shadow_stage::<A>(
2181 shadow_consumer,
2182 shadow_exchange,
2183 chain_hash_lock,
2184 &config,
2185 &cores,
2186 &shutdown,
2187 busy_spin,
2188 )?;
2189
2190 let replica_ready = Arc::new(AtomicBool::new(false));
2195 let (repl_driver, repl_listen_port) = if let Some((repl_consumer_1, repl_consumer_2)) =
2200 replication_consumers
2201 {
2202 let repl_bind = config
2203 .replication_bind
2204 .ok_or("replication_bind must be set when replication is enabled")?;
2205 let repl_port = repl_bind.port();
2206
2207 let repl_cursor = Arc::clone(&replication_cursor);
2208 let fastest_repl_cursor = Arc::clone(&fastest_replica_cursor);
2209 let ready_flag = Arc::clone(&replica_ready);
2210 let batch_size = config.replication_batch_size;
2211 let heartbeat_secs = config.replication_heartbeat_secs;
2212 let repl_metrics = replication_metrics
2213 .clone()
2214 .ok_or("replication_metrics must be Some when replication is enabled")?;
2215 let connected_counter = replicas_connected
2216 .clone()
2217 .ok_or("replicas_connected must be Some when replication is enabled")?;
2218 let dpdk_active_flags: [Arc<AtomicBool>; 2] = replication_ring_progress
2219 .as_ref()
2220 .map(|rp| {
2221 [
2222 Arc::clone(&rp.active_flags[0]),
2223 Arc::clone(&rp.active_flags[1]),
2224 ]
2225 })
2226 .unwrap_or_else(|| {
2227 [
2228 Arc::new(AtomicBool::new(false)),
2229 Arc::new(AtomicBool::new(false)),
2230 ]
2231 });
2232 let dpdk_evict_flags: [Arc<AtomicBool>; 2] = replication_ring_progress
2233 .as_ref()
2234 .map(|rp| {
2235 [
2236 Arc::clone(&rp.evict_flags[0]),
2237 Arc::clone(&rp.evict_flags[1]),
2238 ]
2239 })
2240 .unwrap_or_else(|| {
2241 [
2242 Arc::new(AtomicBool::new(false)),
2243 Arc::new(AtomicBool::new(false)),
2244 ]
2245 });
2246 let journal_path = config.journal.clone();
2247
2248 const REPL_TX_BUF: usize = 512 * 1024;
2254 const REPL_TX_QUEUE: usize = 512 * 1024;
2255 const REPL_RX_BUF: usize = 64 * 1024;
2256 transports[0]
2257 .add_listener_with_buffers(repl_port, REPL_RX_BUF, REPL_TX_BUF, REPL_TX_QUEUE)
2258 .map_err(|e| format!("add replication listener: {e}"))?;
2259
2260 let driver = crate::replication::DpdkReplicationDriver::new(
2261 [repl_consumer_1, repl_consumer_2],
2262 repl_cursor,
2263 fastest_repl_cursor,
2264 genesis_entry,
2265 journal_path,
2266 ready_flag,
2267 connected_counter,
2268 dpdk_evict_flags,
2269 dpdk_active_flags,
2270 repl_metrics,
2271 batch_size,
2272 heartbeat_secs,
2273 );
2274 info!(addr = %repl_bind, "DPDK replication sender started (single-queue, in client poll thread)");
2279 (Some(driver), repl_port)
2280 } else {
2281 if !config.standalone && config.replica_of.is_none() {
2282 info!("running in standalone mode (no replication)");
2283 }
2284 (None, 0)
2285 };
2286 let replication_handle: Option<std::thread::JoinHandle<()>> = None;
2290
2291 let _ = (&enable_replication, &replica_ready); if needs_seeding {
2309 use melin_app::unix_epoch_nanos;
2310 use melin_journal::JournalEvent;
2311 use melin_transport_core::trace::mono_trace_ns;
2312
2313 let seed_events = factory.seed_events();
2314
2315 let mut last_published_seq = 0u64;
2318 for event in seed_events {
2319 last_published_seq = input_producer.publish(InputSlot {
2320 connection_id: 0,
2321 key_hash: 0,
2322 request_seq: 0,
2323 sequence: 0,
2324 timestamp_ns: unix_epoch_nanos(),
2325 event: JournalEvent::App(event),
2326 publish_ts: mono_trace_ns(),
2327 recv_ts: mono_trace_ns(),
2328 });
2329 }
2330
2331 let last_seed_seq = last_published_seq + 1;
2334 while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
2335 && (journal_cursor
2336 .get()
2337 .load(std::sync::atomic::Ordering::Acquire)
2338 < last_seed_seq
2339 || matching_cursor
2340 .get()
2341 .load(std::sync::atomic::Ordering::Acquire)
2342 < last_seed_seq)
2343 {
2344 std::hint::spin_loop();
2345 }
2346 if let Some(ref ring_progress) = replication_ring_progress {
2347 for i in 0..ring_progress.producer_cursors.len() {
2348 if !ring_progress.active_flags[i].load(std::sync::atomic::Ordering::Relaxed) {
2349 continue;
2350 }
2351 let target = ring_progress.producer_cursors[i].load();
2352 while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
2353 && ring_progress.consumer_cursors[i]
2354 .get()
2355 .load(std::sync::atomic::Ordering::Acquire)
2356 < target
2357 {
2358 std::hint::spin_loop();
2359 }
2360 }
2361 }
2362
2363 info!(
2364 accounts = config.accounts,
2365 instruments = config.instruments,
2366 "seeded test data through pipeline"
2367 );
2368 }
2369
2370 let pipeline_healthy = Arc::new(AtomicBool::new(true));
2376 let health_handle = spawn_health_endpoint(
2377 &config,
2378 &active_connections,
2379 &events_processed,
2380 &journal_cursor,
2381 &matching_cursor,
2382 input_cursor,
2383 &replication_cursor,
2384 &pipeline_healthy,
2385 &replicas_connected,
2386 &replication_metrics,
2387 &replication_ring_progress,
2388 &fastest_replica_cursor,
2389 &journal_utilization,
2390 &matching_utilization,
2391 &response_utilization,
2392 &shutdown,
2393 )?;
2394
2395 info!(
2396 ip = %dpdk_config.ip_addr,
2397 port = dpdk_config.listen_port,
2398 num_dpdk_threads,
2399 "DPDK transport listening"
2400 );
2401
2402 let connection_timeout = config.connection_timeout();
2403 let max_conns = config.max_connections;
2404 let reader_cores = config.reader_cores;
2405
2406 assert_eq!(
2410 transports.len(),
2411 1,
2412 "expected exactly one client DPDK transport"
2413 );
2414 let transport_0 = transports.pop().expect("one client transport");
2415 let tx_rx_0 = tx_consumers.remove(0);
2416 melin_app::affinity::pin_thread("dpdk-poll-0", reader_cores);
2417 crate::dpdk_transport::run_dpdk_poll::<A>(
2418 transport_0,
2419 input_producer,
2420 decoder,
2421 control_tx,
2422 tx_rx_0,
2423 &shutdown,
2424 authorized_keys,
2425 connection_timeout,
2426 tick_cadence,
2427 max_conns,
2428 Arc::clone(&active_connections),
2429 0,
2430 repl_driver,
2431 repl_listen_port,
2432 );
2433
2434 let dpdk_extras: Vec<(String, std::thread::Result<()>)> = Vec::new();
2437
2438 shutdown_pipeline_stages(
2439 PipelineHandles {
2440 journal: journal_handle,
2441 matching: matching_handle,
2442 response: response_handle,
2443 replication: replication_handle,
2444 event_publisher: None,
2445 shadow: shadow_handle,
2446 health: health_handle,
2447 },
2448 dpdk_extras,
2449 &pipeline_healthy,
2450 &shutdown,
2451 )
2452}
2453
2454pub(crate) fn init_engine<A, W>(
2477 config: &ServerConfig,
2478 factory: &dyn AppFactory<App = A>,
2479) -> Result<(A, W, bool), Box<dyn std::error::Error>>
2480where
2481 A: Application,
2482 W: JournalWrite<A::Event>,
2483{
2484 let derived_snap = config.journal.with_extension("snapshot");
2487 let snap_path = config.snapshot.as_deref().or_else(|| {
2488 if derived_snap.exists() {
2489 Some(derived_snap.as_path())
2490 } else {
2491 None
2492 }
2493 });
2494
2495 let journal_exists = config.journal.exists();
2496 let writer_mode = config.journal_writer;
2497 let mut engine: JournaledApp<A, W> = if let Some(snap_path) = snap_path
2498 && snap_path.exists()
2499 && journal_exists
2500 {
2501 info!(snapshot = %snap_path.display(), writer_mode = %writer_mode, "recovering from snapshot + journal");
2502 JournaledApp::<A, W>::recover_from_snapshot(snap_path, &config.journal)?
2503 } else if let Some(snap_path) = snap_path
2504 && snap_path.exists()
2505 && !journal_exists
2506 {
2507 info!(
2511 snapshot = %snap_path.display(),
2512 writer_mode = %writer_mode,
2513 "recovering from snapshot only (journal missing, post-rotation crash?)"
2514 );
2515 let (app, snap_sequence, snap_chain_hash) =
2516 melin_transport_core::snapshot::load::<A>(snap_path)?;
2517 let writer = W::create_continuing(&config.journal, snap_sequence + 1, snap_chain_hash)?;
2518 JournaledApp::<A, W>::from_parts(app, writer)
2519 } else if journal_exists {
2520 info!(writer_mode = %writer_mode, "recovering from journal");
2521 let mut app = factory.empty();
2522 factory.prefault(&mut app);
2523 JournaledApp::<A, W>::recover(app, &config.journal)?
2524 } else {
2525 info!(writer_mode = %writer_mode, "creating new journal");
2526 let mut app = factory.empty();
2527 factory.prefault(&mut app);
2528 JournaledApp::<A, W>::create(app, &config.journal)?
2529 };
2530
2531 let needs_seeding = !journal_exists;
2532
2533 factory.apply_operator_policy(engine.app_mut());
2549
2550 if config.max_journal_mib > 0 {
2556 let threshold = config.max_journal_mib * 1024 * 1024;
2557 let current_size = engine.journal_size();
2558 if current_size > threshold {
2559 info!(
2560 current_mib = current_size / (1024 * 1024),
2561 threshold_mib = config.max_journal_mib,
2562 "journal exceeds threshold, rotating segment"
2563 );
2564 engine.rotate_segment()?;
2565 info!("journal segment rotated successfully");
2566 }
2567 }
2568
2569 let (app, writer) = engine.into_parts();
2570 Ok((app, writer, needs_seeding))
2571}
2572
2573fn build_replication_metrics(
2578 has_replication: bool,
2579 durability_mode_atomic: &AtomicU8,
2580 config_mode: crate::durability_policy::DurabilityMode,
2581) -> Option<Arc<crate::replication::ReplicationMetrics>> {
2582 let metrics = if has_replication {
2583 Some(Arc::new(crate::replication::ReplicationMetrics::default()))
2584 } else {
2585 None
2586 };
2587
2588 let active_mode = crate::durability_policy::DurabilityMode::from_u8(
2589 durability_mode_atomic.load(Ordering::Relaxed),
2590 )
2591 .unwrap_or(config_mode);
2592 info!(
2593 mode = %active_mode,
2594 policy = %active_mode.to_policy(),
2595 "durability mode active"
2596 );
2597
2598 metrics
2599}
2600
2601fn spawn_shadow_stage<A: Application + Send + 'static>(
2602 shadow_consumer: Option<Consumer<InputSlot<A::Event>>>,
2603 shadow_exchange: Option<A>,
2604 chain_hash_lock: Option<
2605 Arc<melin_pipeline::seqlock::SeqLock<melin_transport_core::pipeline::FsyncState>>,
2606 >,
2607 config: &ServerConfig,
2608 cores: &PipelineCores,
2609 shutdown: &Arc<AtomicBool>,
2610 busy_spin: bool,
2611) -> Result<Option<std::thread::JoinHandle<()>>, Box<dyn std::error::Error>>
2612where
2613 A::Event: Send + Sync + 'static,
2614{
2615 let Some(shadow_cons) = shadow_consumer else {
2616 return Ok(None);
2617 };
2618 let snap_path = config.shadow_snapshot_path();
2619 let interval = std::time::Duration::from_millis(config.snapshot_interval_ms);
2620 let chain_hash =
2621 chain_hash_lock.ok_or("chain hash lock must be Some when shadow is enabled")?;
2622 let shadow_ex = shadow_exchange.ok_or("shadow exchange must be Some when shadow is enabled")?;
2623 let s_shadow = Arc::clone(shutdown);
2624 let shadow_core = cores.shadow;
2625 let handle = std::thread::Builder::new()
2626 .name("shadow".into())
2627 .spawn(move || {
2628 melin_app::affinity::pin_thread("shadow", shadow_core);
2629 melin_transport_core::shadow::run(
2630 shadow_cons,
2631 shadow_ex,
2632 snap_path,
2633 interval,
2634 chain_hash,
2635 &s_shadow,
2636 busy_spin,
2637 );
2638 })
2639 .map_err(|e| format!("spawn shadow thread: {e}"))?;
2640
2641 info!(
2642 interval_ms = config.snapshot_interval_ms,
2643 path = %config.shadow_snapshot_path().display(),
2644 "shadow snapshot stage started"
2645 );
2646 Ok(Some(handle))
2647}
2648
2649#[allow(clippy::too_many_arguments)]
2650fn spawn_health_endpoint(
2651 config: &ServerConfig,
2652 active_connections: &Arc<AtomicU64>,
2653 events_processed: &Arc<AtomicU64>,
2654 journal_cursor: &Arc<melin_pipeline::padding::Sequence>,
2655 matching_cursor: &Arc<melin_pipeline::padding::Sequence>,
2656 input_cursor: Box<dyn melin_pipeline::ring::QueueCursor>,
2657 replication_cursor: &Arc<AtomicU64>,
2658 pipeline_healthy: &Arc<AtomicBool>,
2659 replicas_connected: &Option<Arc<std::sync::atomic::AtomicU32>>,
2660 replication_metrics: &Option<Arc<crate::replication::ReplicationMetrics>>,
2661 replication_ring_progress: &Option<melin_transport_core::pipeline::ReplicationRingProgress>,
2662 fastest_replica_cursor: &Arc<AtomicU64>,
2663 journal_utilization: &Arc<melin_transport_core::pipeline::StageUtilization>,
2664 matching_utilization: &Arc<melin_transport_core::pipeline::StageUtilization>,
2665 response_utilization: &Arc<melin_transport_core::pipeline::StageUtilization>,
2666 shutdown: &Arc<AtomicBool>,
2667) -> Result<Option<std::thread::JoinHandle<()>>, Box<dyn std::error::Error>> {
2668 let Some(health_addr) = config.health_bind else {
2669 return Ok(None);
2670 };
2671
2672 let (repl_ring_producers, repl_ring_consumers) = replication_ring_progress
2673 .as_ref()
2674 .map(|rp| {
2675 (
2676 Some([
2677 Arc::clone(&rp.producer_cursors[0]),
2678 Arc::clone(&rp.producer_cursors[1]),
2679 ]),
2680 Some([
2681 Arc::clone(&rp.consumer_cursors[0]),
2682 Arc::clone(&rp.consumer_cursors[1]),
2683 ]),
2684 )
2685 })
2686 .unwrap_or((None, None));
2687 let fastest_repl_cursor_health = replication_metrics
2688 .as_ref()
2689 .map(|_| Arc::clone(fastest_replica_cursor));
2690 Ok(Some(melin_transport_core::health::spawn(
2691 health_addr,
2692 melin_transport_core::health::HealthState {
2693 active_connections: Arc::clone(active_connections),
2694 events_processed: Arc::clone(events_processed),
2695 journal_cursor: Arc::clone(journal_cursor),
2696 matching_cursor: Arc::clone(matching_cursor),
2697 input_cursor,
2698 replication_cursor: Arc::clone(replication_cursor),
2699 pipeline_healthy: Arc::clone(pipeline_healthy),
2700 replicas_connected: replicas_connected.clone(),
2701 replication_metrics: replication_metrics.clone(),
2702 replication_ring_producer_cursors: repl_ring_producers,
2703 replication_ring_consumer_cursors: repl_ring_consumers,
2704 fastest_replica_cursor: fastest_repl_cursor_health,
2705 journal_utilization: Arc::clone(journal_utilization),
2706 matching_utilization: Arc::clone(matching_utilization),
2707 response_utilization: Arc::clone(response_utilization),
2708 },
2709 Arc::clone(shutdown),
2710 )?))
2711}
2712
2713fn spawn_event_publisher<A: Application>(
2721 consumer: Option<Consumer<OutputSlot<A>>>,
2722 run_fn: Option<EventPublisherFn<A>>,
2723 config: &ServerConfig,
2724 cores: &PipelineCores,
2725 authorized_keys: &Arc<AuthorizedKeys>,
2726 shutdown: &Arc<AtomicBool>,
2727 busy_spin: bool,
2728) -> Result<Option<std::thread::JoinHandle<()>>, Box<dyn std::error::Error>>
2729where
2730 A::Report: Send + 'static,
2731 A::QueryResponse: Send + 'static,
2732{
2733 let (Some(event_consumer), Some(run_fn)) = (consumer, run_fn) else {
2734 return Ok(None);
2735 };
2736 let event_bind = config
2737 .event_bind
2738 .ok_or("event_bind must be set when event publisher is enabled")?;
2739 let s_event = Arc::clone(shutdown);
2740 let event_keys = Arc::clone(authorized_keys);
2741 let event_core = cores.event_publisher;
2742 let event_handle = std::thread::Builder::new()
2743 .name("event-publisher".into())
2744 .spawn(move || {
2745 melin_app::affinity::pin_thread("event-publisher", event_core);
2746 run_fn(event_consumer, event_bind, event_keys, &s_event, busy_spin);
2747 })
2748 .map_err(|e| format!("spawn event publisher thread: {e}"))?;
2749 info!(addr = %event_bind, "event publisher started");
2750 Ok(Some(event_handle))
2751}
2752
2753fn authenticate_connection<R: std::io::Read, W: std::io::Write>(
2764 connection_id: ConnectionId,
2765 addr: SocketAddr,
2766 reader: &mut R,
2767 writer: &mut W,
2768 authorized_keys: &AuthorizedKeys,
2769) -> Result<(Permission, [u8; 32]), Box<dyn std::error::Error>> {
2770 use std::io;
2771
2772 use ed25519_dalek::{Verifier, VerifyingKey};
2773 use melin_wire_protocol::control::TransportResponse;
2774 use melin_wire_protocol::control_codec;
2775
2776 let mut nonce = [0u8; 32];
2779 getrandom::fill(&mut nonce).map_err(|e| io::Error::other(format!("getrandom failed: {e}")))?;
2780
2781 let mut buf = [0u8; 128];
2782 let written =
2783 control_codec::encode_transport_response(&TransportResponse::Challenge { nonce }, &mut buf)
2784 .map_err(|e| io::Error::other(format!("encode Challenge: {e}")))?;
2785 writer.write_all(&buf[..written])?;
2786 writer.flush()?;
2787
2788 let mut len_buf = [0u8; 4];
2793 reader
2794 .read_exact(&mut len_buf)
2795 .map_err(|e| io::Error::other(format!("read auth frame length: {e}")))?;
2796 let frame_len = u32::from_le_bytes(len_buf) as usize;
2797 if frame_len > 256 {
2799 send_auth_failed(writer);
2800 return Err(io::Error::other(format!("auth frame too large: {frame_len}")).into());
2801 }
2802 let mut frame_buf = [0u8; 256];
2803 reader
2804 .read_exact(&mut frame_buf[..frame_len])
2805 .map_err(|e| io::Error::other(format!("read auth frame payload: {e}")))?;
2806
2807 let (_seq, cr) = match control_codec::decode_challenge_response(&frame_buf[..frame_len]) {
2808 Ok(pair) => pair,
2809 Err(e) => {
2810 send_auth_failed(writer);
2811 return Err(io::Error::other(format!("decode ChallengeResponse: {e}")).into());
2812 }
2813 };
2814
2815 let (signature_bytes, public_key_bytes) = (cr.signature, cr.public_key);
2816
2817 let permission = match authorized_keys.lookup(&public_key_bytes) {
2819 Some(perm) => perm,
2820 None => {
2821 send_auth_failed(writer);
2822 return Err("unknown public key".into());
2823 }
2824 };
2825
2826 let verifying_key = VerifyingKey::from_bytes(&public_key_bytes).map_err(|e| {
2829 send_auth_failed(writer);
2830 io::Error::other(format!("invalid public key: {e}"))
2831 })?;
2832 let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes);
2833 verifying_key.verify(&nonce, &signature).map_err(|e| {
2834 send_auth_failed(writer);
2835 io::Error::other(format!("signature verification failed: {e}"))
2836 })?;
2837
2838 let written =
2840 control_codec::encode_transport_response(&TransportResponse::ServerReady, &mut buf)
2841 .map_err(|e| io::Error::other(format!("encode ServerReady: {e}")))?;
2842 writer.write_all(&buf[..written])?;
2843 writer.flush()?;
2844
2845 debug!(
2846 connection_id = connection_id.0,
2847 addr = %addr,
2848 permission = ?permission,
2849 "authenticated"
2850 );
2851
2852 Ok((permission, public_key_bytes))
2853}
2854
2855fn set_read_timeout<F: std::os::unix::io::AsRawFd>(
2860 fd: &F,
2861 timeout: Option<std::time::Duration>,
2862) -> std::io::Result<()> {
2863 let tv = match timeout {
2864 Some(d) => libc::timeval {
2865 tv_sec: d.as_secs() as libc::time_t,
2866 tv_usec: d.subsec_micros() as libc::suseconds_t,
2867 },
2868 None => libc::timeval {
2869 tv_sec: 0,
2870 tv_usec: 0,
2871 },
2872 };
2873 let ret = unsafe {
2874 libc::setsockopt(
2875 fd.as_raw_fd(),
2876 libc::SOL_SOCKET,
2877 libc::SO_RCVTIMEO,
2878 &tv as *const libc::timeval as *const libc::c_void,
2879 std::mem::size_of::<libc::timeval>() as libc::socklen_t,
2880 )
2881 };
2882 if ret < 0 {
2883 return Err(std::io::Error::last_os_error());
2884 }
2885 Ok(())
2886}
2887
2888pub(crate) fn set_busy_poll<F: std::os::unix::io::AsRawFd>(
2905 fd: &F,
2906 micros: i32,
2907) -> std::io::Result<()> {
2908 let val: libc::c_int = micros;
2912 let ret = unsafe {
2913 libc::setsockopt(
2914 fd.as_raw_fd(),
2915 libc::SOL_SOCKET,
2916 libc::SO_BUSY_POLL,
2917 &val as *const libc::c_int as *const libc::c_void,
2918 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
2919 )
2920 };
2921 if ret < 0 {
2922 return Err(std::io::Error::last_os_error());
2923 }
2924 Ok(())
2925}
2926
2927pub(crate) const BUSY_POLL_US: i32 = 50;
2932
2933fn set_write_timeout<F: std::os::unix::io::AsRawFd>(
2936 fd: &F,
2937 timeout: Option<std::time::Duration>,
2938) -> std::io::Result<()> {
2939 let tv = match timeout {
2940 Some(d) => libc::timeval {
2941 tv_sec: d.as_secs() as libc::time_t,
2942 tv_usec: d.subsec_micros() as libc::suseconds_t,
2943 },
2944 None => libc::timeval {
2945 tv_sec: 0,
2946 tv_usec: 0,
2947 },
2948 };
2949 let ret = unsafe {
2950 libc::setsockopt(
2951 fd.as_raw_fd(),
2952 libc::SOL_SOCKET,
2953 libc::SO_SNDTIMEO,
2954 &tv as *const libc::timeval as *const libc::c_void,
2955 std::mem::size_of::<libc::timeval>() as libc::socklen_t,
2956 )
2957 };
2958 if ret < 0 {
2959 return Err(std::io::Error::last_os_error());
2960 }
2961 Ok(())
2962}
2963
2964fn send_auth_failed(writer: &mut impl std::io::Write) {
2966 use melin_wire_protocol::control::TransportResponse;
2967 use melin_wire_protocol::control_codec;
2968
2969 let mut buf = [0u8; 8];
2970 if let Ok(written) =
2971 control_codec::encode_transport_response(&TransportResponse::AuthFailed, &mut buf)
2972 {
2973 let _ = writer.write_all(&buf[..written]);
2974 let _ = writer.flush();
2975 }
2976}
2977
2978#[cfg(test)]
2979mod tests {
2980 use std::io::{Read, Write};
2981 use std::os::unix::net::UnixStream;
2982
2983 use ed25519_dalek::{Signer, SigningKey};
2984 use melin_app::auth::AuthorizedKeys;
2985 use melin_app::auth::Permission;
2986 use melin_protocol::codec;
2987 use melin_protocol::message::{ConnectionId, Request, ResponseKind};
2988
2989 use super::authenticate_connection;
2990
2991 fn test_key() -> SigningKey {
2993 SigningKey::from_bytes(&[0xAA; 32])
2994 }
2995
2996 fn keys_with_test_key(perm: &str) -> AuthorizedKeys {
2998 let pub_bytes = test_key().verifying_key().to_bytes();
3002 let pub_b64 = base64_encode(&pub_bytes);
3003 AuthorizedKeys::parse(&format!("{perm} {pub_b64} test\n")).unwrap()
3004 }
3005
3006 fn base64_encode(bytes: &[u8]) -> String {
3009 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3010 let mut out = String::new();
3011 for chunk in bytes.chunks(3) {
3012 let b0 = chunk[0] as u32;
3013 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
3014 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
3015 let n = (b0 << 16) | (b1 << 8) | b2;
3016 out.push(ALPHABET[(n >> 18 & 0x3F) as usize] as char);
3017 out.push(ALPHABET[(n >> 12 & 0x3F) as usize] as char);
3018 if chunk.len() > 1 {
3019 out.push(ALPHABET[(n >> 6 & 0x3F) as usize] as char);
3020 } else {
3021 out.push('=');
3022 }
3023 if chunk.len() > 2 {
3024 out.push(ALPHABET[(n & 0x3F) as usize] as char);
3025 } else {
3026 out.push('=');
3027 }
3028 }
3029 out
3030 }
3031
3032 fn run_server_auth(
3035 mut stream: UnixStream,
3036 keys: AuthorizedKeys,
3037 ) -> std::thread::JoinHandle<Result<Permission, String>> {
3038 std::thread::spawn(move || {
3039 let mut writer = stream.try_clone().unwrap();
3041 authenticate_connection(
3042 ConnectionId(1),
3043 "127.0.0.1:0".parse().unwrap(),
3044 &mut stream,
3045 &mut writer,
3046 &keys,
3047 )
3048 .map(|(perm, _pk)| perm)
3049 .map_err(|e| e.to_string())
3050 })
3051 }
3052
3053 fn client_sign_challenge(stream: &mut UnixStream, key: &SigningKey) {
3056 let mut len_buf = [0u8; 4];
3057 let mut payload = [0u8; 128];
3058 stream.read_exact(&mut len_buf).unwrap();
3059 let len = u32::from_le_bytes(len_buf) as usize;
3060 stream.read_exact(&mut payload[..len]).unwrap();
3061
3062 let resp = codec::decode_response(&payload[..len]).unwrap();
3063 let nonce = match resp {
3064 ResponseKind::Challenge { nonce } => nonce,
3065 other => panic!("expected Challenge, got {other:?}"),
3066 };
3067
3068 let sig = key.sign(&nonce);
3069 let request = Request::ChallengeResponse {
3070 signature: sig.to_bytes(),
3071 public_key: key.verifying_key().to_bytes(),
3072 };
3073 let mut buf = [0u8; 256];
3074 let written = codec::encode_request(&request, 0, &mut buf).unwrap();
3075 stream.write_all(&buf[..written]).unwrap();
3076 stream.flush().unwrap();
3077 }
3078
3079 fn client_sign_challenge_bad(stream: &mut UnixStream, key: &SigningKey) {
3081 let mut len_buf = [0u8; 4];
3082 let mut payload = [0u8; 128];
3083 stream.read_exact(&mut len_buf).unwrap();
3084 let len = u32::from_le_bytes(len_buf) as usize;
3085 stream.read_exact(&mut payload[..len]).unwrap();
3086
3087 let resp = codec::decode_response(&payload[..len]).unwrap();
3088 let nonce = match resp {
3089 ResponseKind::Challenge { nonce } => nonce,
3090 other => panic!("expected Challenge, got {other:?}"),
3091 };
3092
3093 let mut sig_bytes = key.sign(&nonce).to_bytes();
3094 sig_bytes[0] ^= 0xFF;
3095
3096 let request = Request::ChallengeResponse {
3097 signature: sig_bytes,
3098 public_key: key.verifying_key().to_bytes(),
3099 };
3100 let mut buf = [0u8; 256];
3101 let written = codec::encode_request(&request, 0, &mut buf).unwrap();
3102 stream.write_all(&buf[..written]).unwrap();
3103 stream.flush().unwrap();
3104 }
3105
3106 fn read_response(stream: &mut UnixStream) -> ResponseKind {
3108 let mut len_buf = [0u8; 4];
3109 stream.read_exact(&mut len_buf).unwrap();
3110 let len = u32::from_le_bytes(len_buf) as usize;
3111 let mut buf = [0u8; 64];
3112 stream.read_exact(&mut buf[..len]).unwrap();
3113 codec::decode_response(&buf[..len]).unwrap()
3114 }
3115
3116 #[test]
3117 fn auth_success_returns_permission() {
3118 let keys = keys_with_test_key("trader");
3119 let key = test_key();
3120 let (s1, mut s2) = UnixStream::pair().unwrap();
3121
3122 let handle = run_server_auth(s1, keys);
3123
3124 client_sign_challenge(&mut s2, &key);
3125 let resp = read_response(&mut s2);
3126 assert!(matches!(resp, ResponseKind::ServerReady));
3127
3128 let result = handle.join().unwrap();
3129 assert_eq!(result.unwrap(), Permission::Trader);
3130 }
3131
3132 #[test]
3133 fn auth_admin_permission() {
3134 let keys = keys_with_test_key("operator");
3135 let key = test_key();
3136 let (s1, mut s2) = UnixStream::pair().unwrap();
3137
3138 let handle = run_server_auth(s1, keys);
3139
3140 client_sign_challenge(&mut s2, &key);
3141 let resp = read_response(&mut s2);
3142 assert!(matches!(resp, ResponseKind::ServerReady));
3143
3144 assert_eq!(handle.join().unwrap().unwrap(), Permission::Operator);
3145 }
3146
3147 #[test]
3148 fn auth_unknown_key_sends_auth_failed() {
3149 let keys = AuthorizedKeys::parse("").unwrap();
3150 let key = test_key();
3151 let (s1, mut s2) = UnixStream::pair().unwrap();
3152
3153 let handle = run_server_auth(s1, keys);
3154
3155 client_sign_challenge(&mut s2, &key);
3156 let resp = read_response(&mut s2);
3157 assert!(matches!(resp, ResponseKind::AuthFailed));
3158
3159 assert!(handle.join().unwrap().is_err());
3160 }
3161
3162 #[test]
3163 fn auth_bad_signature_sends_auth_failed() {
3164 let keys = keys_with_test_key("operator");
3165 let key = test_key();
3166 let (s1, mut s2) = UnixStream::pair().unwrap();
3167
3168 let handle = run_server_auth(s1, keys);
3169
3170 client_sign_challenge_bad(&mut s2, &key);
3171 let resp = read_response(&mut s2);
3172 assert!(matches!(resp, ResponseKind::AuthFailed));
3173
3174 assert!(handle.join().unwrap().is_err());
3175 }
3176
3177 #[test]
3178 fn auth_wrong_message_type_sends_auth_failed() {
3179 let keys = keys_with_test_key("trader");
3180 let (s1, mut s2) = UnixStream::pair().unwrap();
3181
3182 let handle = run_server_auth(s1, keys);
3183
3184 let mut len_buf = [0u8; 4];
3186 let mut payload = [0u8; 128];
3187 s2.read_exact(&mut len_buf).unwrap();
3188 let len = u32::from_le_bytes(len_buf) as usize;
3189 s2.read_exact(&mut payload[..len]).unwrap();
3190
3191 let mut buf = [0u8; 16];
3193 let written = codec::encode_request(&Request::Heartbeat, 0, &mut buf).unwrap();
3194 s2.write_all(&buf[..written]).unwrap();
3195 s2.flush().unwrap();
3196
3197 let resp = read_response(&mut s2);
3198 assert!(matches!(resp, ResponseKind::AuthFailed));
3199
3200 assert!(handle.join().unwrap().is_err());
3201 }
3202
3203 #[test]
3204 fn auth_client_disconnects_is_error() {
3205 let keys = keys_with_test_key("trader");
3206 let (s1, s2) = UnixStream::pair().unwrap();
3207
3208 let handle = run_server_auth(s1, keys);
3209
3210 drop(s2);
3212
3213 assert!(handle.join().unwrap().is_err());
3214 }
3215
3216 #[test]
3217 fn auth_different_key_than_authorized_is_rejected() {
3218 let keys = keys_with_test_key("trader");
3220 let wrong_key = SigningKey::from_bytes(&[0xCC; 32]);
3221 let (s1, mut s2) = UnixStream::pair().unwrap();
3222
3223 let handle = run_server_auth(s1, keys);
3224
3225 client_sign_challenge(&mut s2, &wrong_key);
3226 let resp = read_response(&mut s2);
3227 assert!(matches!(resp, ResponseKind::AuthFailed));
3228
3229 assert!(handle.join().unwrap().is_err());
3230 }
3231
3232 #[test]
3233 fn auth_oversized_frame_sends_auth_failed() {
3234 let keys = keys_with_test_key("trader");
3235 let (s1, mut s2) = UnixStream::pair().unwrap();
3236
3237 let handle = run_server_auth(s1, keys);
3238
3239 let mut len_buf = [0u8; 4];
3241 let mut payload = [0u8; 128];
3242 s2.read_exact(&mut len_buf).unwrap();
3243 let len = u32::from_le_bytes(len_buf) as usize;
3244 s2.read_exact(&mut payload[..len]).unwrap();
3245
3246 let fake_len: u32 = 1000;
3248 s2.write_all(&fake_len.to_le_bytes()).unwrap();
3249 s2.flush().unwrap();
3250
3251 let resp = read_response(&mut s2);
3253 assert!(matches!(resp, ResponseKind::AuthFailed));
3254
3255 assert!(handle.join().unwrap().is_err());
3256 }
3257
3258 #[test]
3259 fn auth_zero_length_frame_sends_auth_failed() {
3260 let keys = keys_with_test_key("trader");
3261 let (s1, mut s2) = UnixStream::pair().unwrap();
3262
3263 let handle = run_server_auth(s1, keys);
3264
3265 let mut len_buf = [0u8; 4];
3267 let mut payload = [0u8; 128];
3268 s2.read_exact(&mut len_buf).unwrap();
3269 let len = u32::from_le_bytes(len_buf) as usize;
3270 s2.read_exact(&mut payload[..len]).unwrap();
3271
3272 let zero_len: u32 = 0;
3274 s2.write_all(&zero_len.to_le_bytes()).unwrap();
3275 s2.flush().unwrap();
3276
3277 let resp = read_response(&mut s2);
3279 assert!(matches!(resp, ResponseKind::AuthFailed));
3280
3281 assert!(handle.join().unwrap().is_err());
3282 }
3283
3284 #[test]
3285 fn auth_readonly_permission() {
3286 let keys = keys_with_test_key("readonly");
3287 let key = test_key();
3288 let (s1, mut s2) = UnixStream::pair().unwrap();
3289
3290 let handle = run_server_auth(s1, keys);
3291
3292 client_sign_challenge(&mut s2, &key);
3293 let resp = read_response(&mut s2);
3294 assert!(matches!(resp, ResponseKind::ServerReady));
3295
3296 let perm = handle.join().unwrap().unwrap();
3297 assert_eq!(perm, Permission::ReadOnly);
3298 assert!(!perm.can_trade());
3299 }
3300}