use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
use std::hash::{Hash, Hasher};
use tracing::{debug, error, info, warn};
use melin_journal::BufferedWriter;
use melin_journal::JournalError;
use melin_journal::JournalWrite;
use melin_transport_core::journaled_app::JournaledApp;
use melin_transport_core::pipeline::{
InputSlot, OutputSlot as GenericOutputSlot, Pipeline as GenericPipeline,
build_pipeline_with_replication,
};
type Pipeline<A> = GenericPipeline<A>;
use crate::reader::RequestDecoderArc;
use crate::response::ResponseEncoderArc;
use melin_app::Application;
use melin_app::app_factory::AppFactory;
use melin_app::auth::AuthorizedKeys;
use melin_app::auth::Permission;
use melin_app::decoder::RequestDecoder;
use melin_app::encoder::ResponseEncoder;
use melin_pipeline::ring::Consumer;
type OutputSlot<A> =
GenericOutputSlot<<A as Application>::Report, <A as Application>::QueryResponse>;
pub type EventPublisherFn<A> = fn(
consumer: Consumer<OutputSlot<A>>,
bind_addr: SocketAddr,
authorized_keys: Arc<AuthorizedKeys>,
shutdown: &AtomicBool,
busy_spin: bool,
);
use melin_wire_protocol::blocking::BlockingFrameWriter;
use melin_wire_protocol::control::ConnectionId;
use melin_wire_protocol::transport::BlockingTransportListener;
const DEFAULT_REPLICATION_PIPELINE_DEPTH: usize = 256;
#[derive(clap::Parser)]
#[command(name = "melin-server", about = "Low-latency matching engine server")]
pub struct ServerConfig {
#[arg(long, default_value = "127.0.0.1:9876")]
pub bind: SocketAddr,
#[arg(long, default_value = "melin.journal")]
pub journal: PathBuf,
#[arg(long)]
pub snapshot: Option<PathBuf>,
#[arg(long, default_value = "1,2,3,4,5,6,7,8,9,10,11", value_parser = parse_cores)]
pub cores: PipelineCores,
#[arg(long, default_value_t = 0)]
pub group_commit_us: u64,
#[arg(long, default_value_t = 10)]
pub heartbeat_interval_secs: u64,
#[arg(long, default_value_t = 30)]
pub connection_timeout_secs: u64,
#[arg(long, default_value_t = 1024)]
pub max_connections: u64,
#[arg(long, default_value_t = 100_000)]
pub accounts: u32,
#[arg(long, default_value_t = 100)]
pub instruments: u32,
#[arg(long, default_value = "authorized_keys")]
pub authorized_keys: PathBuf,
#[arg(long, default_value_t = 256)]
pub max_journal_mib: u64,
#[arg(long, default_value_t = 10_000)]
pub max_orders_per_account: u32,
#[arg(long, default_value_t = 1_000)]
pub max_orders_per_second: u32,
#[arg(long, default_value_t = 5_000)]
pub max_orders_burst: u32,
#[arg(long)]
pub replication_bind: Option<std::net::SocketAddr>,
#[arg(long, default_value_t = false)]
pub standalone: bool,
#[arg(long)]
pub replica_of: Option<std::net::SocketAddr>,
#[arg(long)]
pub replication_key: Option<std::path::PathBuf>,
#[arg(long, default_value_t = 128)]
pub replication_batch_size: usize,
#[arg(long, default_value_t = 4096)]
pub max_journal_batch: usize,
#[arg(long, default_value_t = 5)]
pub replication_heartbeat_secs: u64,
#[arg(long, default_value_t = DEFAULT_REPLICATION_PIPELINE_DEPTH)]
pub replication_pipeline_depth: usize,
#[arg(long, default_value_t = 256)]
pub replication_ring_size: usize,
#[arg(long, value_enum, default_value_t = crate::ack_policy::AckPolicy::DiskAndRam)]
pub ack_policy: crate::ack_policy::AckPolicy,
#[arg(long, default_value_t = false)]
pub yield_idle: bool,
#[arg(long, default_value = "", allow_hyphen_values = true)]
pub dpdk_eal_args: String,
#[arg(long, default_value = "0", value_delimiter = ',')]
pub dpdk_ports: Vec<u16>,
#[arg(long, default_value = "10.0.0.1")]
pub dpdk_ip: String,
#[arg(long, default_value_t = 24)]
pub dpdk_prefix_len: u8,
#[arg(long)]
pub dpdk_gateway: Option<String>,
#[arg(long)]
pub dpdk_peer_ip: Option<String>,
#[arg(long)]
pub dpdk_gateway_mac: Option<String>,
#[arg(long)]
pub dpdk_peer_mac: Option<String>,
#[arg(long, default_value_t = 1500)]
pub dpdk_mtu: usize,
#[arg(long)]
pub dpdk_vlan: Option<u16>,
#[arg(long)]
pub event_bind: Option<SocketAddr>,
#[arg(long, default_value = "127.0.0.1:9878")]
pub health_bind: Option<SocketAddr>,
#[arg(long)]
pub admin_bind: Option<SocketAddr>,
#[arg(long, default_value_t = 3_000_000)]
pub snapshot_interval_ms: u64,
#[arg(long)]
pub snapshot_path: Option<PathBuf>,
#[arg(long, default_value_t = 250)]
pub tick_interval_ms: u64,
#[arg(long, default_value_t = false)]
pub no_mlock: bool,
#[arg(long)]
pub raft_bind: Option<SocketAddr>,
#[arg(long)]
pub raft_node_id: Option<u64>,
#[arg(long)]
pub raft_peer: Vec<String>,
#[arg(long)]
pub raft_dir: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub raft_auto_promote: bool,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
bind: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9876),
journal: PathBuf::from("melin.journal"),
snapshot: None,
cores: PipelineCores {
journal: 1,
matching: 2,
response: 3,
reader: 4,
event_publisher: 6,
shadow: 7,
repl_handler_0: 8,
repl_handler_1: 9,
journal_prep: 10,
journal_disk: 11,
},
group_commit_us: 0,
heartbeat_interval_secs: 10,
connection_timeout_secs: 30,
max_connections: 1024,
accounts: 2,
instruments: 2,
authorized_keys: PathBuf::from("authorized_keys"),
max_journal_mib: 256,
max_orders_per_account: 10_000,
max_orders_per_second: 1_000,
max_orders_burst: 5_000,
replication_bind: None,
standalone: false,
replica_of: None,
replication_key: None,
replication_batch_size: 128,
max_journal_batch: 1024,
replication_heartbeat_secs: 5,
replication_pipeline_depth: DEFAULT_REPLICATION_PIPELINE_DEPTH,
replication_ring_size: 256,
ack_policy: crate::ack_policy::AckPolicy::DiskAndRam,
yield_idle: false,
dpdk_eal_args: String::new(),
dpdk_peer_ip: None,
dpdk_gateway_mac: None,
dpdk_peer_mac: None,
dpdk_ports: vec![0],
dpdk_ip: "10.0.0.1".into(),
dpdk_prefix_len: 24,
dpdk_gateway: None,
dpdk_mtu: 1500,
dpdk_vlan: None,
event_bind: None,
health_bind: Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9878)),
admin_bind: None,
snapshot_interval_ms: 3_000_000,
snapshot_path: None,
tick_interval_ms: 250,
no_mlock: false,
raft_bind: None,
raft_node_id: None,
raft_peer: Vec::new(),
raft_dir: None,
raft_auto_promote: false,
}
}
}
impl ServerConfig {
pub fn group_commit_delay(&self) -> std::time::Duration {
std::time::Duration::from_micros(self.group_commit_us)
}
pub fn heartbeat_interval(&self) -> Option<std::time::Duration> {
if self.heartbeat_interval_secs == 0 {
None
} else {
Some(std::time::Duration::from_secs(self.heartbeat_interval_secs))
}
}
pub fn connection_timeout(&self) -> Option<std::time::Duration> {
if self.connection_timeout_secs == 0 {
None
} else {
Some(std::time::Duration::from_secs(self.connection_timeout_secs))
}
}
pub fn shadow_snapshot_path(&self) -> PathBuf {
self.snapshot_path
.clone()
.unwrap_or_else(|| self.journal.with_extension("snapshot"))
}
pub fn tick_interval(&self) -> Option<std::time::Duration> {
if self.tick_interval_ms == 0 {
None
} else {
Some(std::time::Duration::from_millis(self.tick_interval_ms))
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PipelineCores {
pub journal: usize,
pub matching: usize,
pub response: usize,
pub reader: usize,
pub event_publisher: usize,
pub shadow: usize,
pub repl_handler_0: usize,
pub repl_handler_1: usize,
pub journal_prep: usize,
pub journal_disk: usize,
}
impl PipelineCores {
pub fn compact(num_cpus: usize) -> Result<(Self, usize), String> {
if num_cpus < 8 {
return Err(format!(
"compact layout needs >= 8 logical cores; have {num_cpus}"
));
}
let cores = PipelineCores {
journal: 1,
matching: 2,
response: 3,
reader: 4,
event_publisher: 5,
shadow: 6,
repl_handler_0: 0,
repl_handler_1: 0,
journal_prep: 0,
journal_disk: 0,
};
Ok((cores, 7))
}
}
fn parse_cores(s: &str) -> Result<PipelineCores, String> {
let parts: Vec<&str> = s.split(',').collect();
if !(9..=11).contains(&parts.len()) {
return Err(format!(
"expected 9 to 11 comma-separated core IDs (journal,matching,response,reader,unused,event-publisher,shadow,repl-handler-0,repl-handler-1[,journal-prep[,journal-disk]]), got {}",
parts.len()
));
}
let parse = |p: &str| {
p.parse::<usize>()
.map_err(|_| format!("invalid core ID: {p}"))
};
parse(parts[4])?;
Ok(PipelineCores {
journal: parse(parts[0])?,
matching: parse(parts[1])?,
response: parse(parts[2])?,
reader: parse(parts[3])?,
event_publisher: parse(parts[5])?,
shadow: parse(parts[6])?,
repl_handler_0: parse(parts[7])?,
repl_handler_1: parse(parts[8])?,
journal_prep: parts.get(9).map(|p| parse(p)).transpose()?.unwrap_or(0),
journal_disk: parts.get(10).map(|p| parse(p)).transpose()?.unwrap_or(0),
})
}
pub fn run<A, F, D, E>(
config: ServerConfig,
factory: F,
decoder: D,
encoder: E,
event_publisher: Option<EventPublisherFn<A>>,
) -> Result<(), Box<dyn std::error::Error>>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
F: AppFactory<App = A> + 'static,
D: RequestDecoder<Event = A::Event> + 'static,
E: ResponseEncoder<Report = A::Report, Query = A::QueryResponse> + 'static,
{
let shutdown = Arc::new(AtomicBool::new(false));
crate::process::install_shutdown_handler(&shutdown);
if !config.no_mlock {
crate::process::try_lock_memory();
}
#[cfg(feature = "dpdk")]
{
run_dpdk(
config,
Arc::new(factory),
Arc::new(decoder),
Arc::new(encoder),
event_publisher,
shutdown,
)
}
#[cfg(not(feature = "dpdk"))]
{
let listener = melin_wire_protocol::tcp::BlockingTcpListener::bind(config.bind)?;
run_tcp(
listener,
config,
Arc::new(factory),
Arc::new(decoder),
Arc::new(encoder),
event_publisher,
shutdown,
)
}
}
pub fn run_with_listener<A, L, F, D, E>(
listener: L,
config: ServerConfig,
factory: F,
decoder: D,
encoder: E,
event_publisher: Option<EventPublisherFn<A>>,
shutdown: Arc<AtomicBool>,
) -> Result<(), Box<dyn std::error::Error>>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
L: BlockingTransportListener,
F: AppFactory<App = A> + 'static,
D: RequestDecoder<Event = A::Event> + 'static,
E: ResponseEncoder<Report = A::Report, Query = A::QueryResponse> + 'static,
{
run_tcp(
listener,
config,
Arc::new(factory),
Arc::new(decoder),
Arc::new(encoder),
event_publisher,
shutdown,
)
}
fn run_tcp<A, L>(
listener: L,
config: ServerConfig,
factory: Arc<dyn AppFactory<App = A>>,
decoder: RequestDecoderArc<A>,
encoder: ResponseEncoderArc<A>,
event_publisher: Option<EventPublisherFn<A>>,
shutdown: Arc<AtomicBool>,
) -> Result<(), Box<dyn std::error::Error>>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
L: BlockingTransportListener,
{
run_impl::<A, L>(
listener,
config,
factory,
decoder,
encoder,
event_publisher,
shutdown,
)
}
fn warn_if_chain_disabled() {
#[cfg(not(feature = "hash-chain"))]
warn!(
"built without the `hash-chain` feature: journal tamper evidence, \
segment-continuity and snapshot cross-checks at recovery, and \
cross-node divergence detection are all disabled"
);
}
fn run_impl<A, L>(
listener: L,
config: ServerConfig,
factory: Arc<dyn AppFactory<App = A>>,
decoder: RequestDecoderArc<A>,
encoder: ResponseEncoderArc<A>,
event_publisher: Option<EventPublisherFn<A>>,
shutdown: Arc<AtomicBool>,
) -> Result<(), Box<dyn std::error::Error>>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
L: BlockingTransportListener,
{
warn_if_chain_disabled();
let ack_policy_atomic = Arc::new(AtomicU8::new(config.ack_policy.as_u8()));
if config.replication_bind.is_some() && config.standalone {
return Err("--replication-bind and --standalone are mutually exclusive".into());
}
let repl_listener = config
.replication_bind
.map(bind_replication_listener)
.transpose()?;
if let Some(primary_addr) = config.replica_of {
info!(primary = %primary_addr, "starting in replica mode");
let replication_key_path = config.replication_key.as_ref().ok_or_else(|| {
std::io::Error::other("--replication-key is required in replica mode (--replica-of)")
})?;
let signing_key = {
let seed = std::fs::read(replication_key_path).map_err(|e| {
std::io::Error::other(format!(
"failed to read replication key {}: {e}",
replication_key_path.display()
))
})?;
if seed.len() != 32 {
return Err(format!(
"replication key must be 32 bytes, got {} ({})",
seed.len(),
replication_key_path.display()
)
.into());
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&seed);
ed25519_dalek::SigningKey::from_bytes(&bytes)
};
let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
info!(
keys = authorized_keys.len(),
path = %config.authorized_keys.display(),
"loaded authorized keys (replica mode)"
);
let control = crate::replication::ReplicaControlPlane::new();
let promotion_request = control.promote.clone();
let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
let _admin_handle = config
.admin_bind
.map(|addr| {
crate::admin::spawn(
addr,
Some(promotion_request.clone()),
rotate_flag.clone(),
Some(Arc::clone(&ack_policy_atomic)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
})
.transpose()?;
let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(0));
let journal_tip = control.journal_tip.clone();
let mut raft = match crate::raft::build_raft_config(&config)? {
None => crate::raft::RaftDriverGuard::disabled(&shutdown),
Some(cfg) => crate::raft::spawn_raft_driver(
cfg,
config.raft_auto_promote,
&signing_key,
&authorized_keys,
&fence_state,
journal_tip.clone(),
Arc::clone(&control.tip_ready),
{
let promote = control.promote.clone();
Arc::new(move || promote.is_requested())
},
&shutdown,
)?,
};
if config.raft_auto_promote
&& let Some(wiring) = raft.promotion_wiring()
{
raft.arm_promotion(crate::raft_promotion::spawn_auto_promotion(
wiring.status,
control.clone(),
Arc::clone(&fence_state),
Arc::clone(&ack_policy_atomic),
wiring.peer_tips,
wiring.peer_ids,
wiring.elect_requested,
wiring.elect_enabled,
Arc::clone(&shutdown),
));
}
let raft_status = raft.status();
let mut replica_health = crate::raft::spawn_replica_health(
&config,
&fence_state,
raft_status.as_ref(),
Arc::clone(&control.pipeline_healthy),
)?;
match crate::replication::run_receiver::<A>(
primary_addr,
&config.journal,
&signing_key,
&shutdown,
&control,
config.snapshot_interval_ms,
config.shadow_snapshot_path(),
config.cores,
config.group_commit_delay(),
config.replication_pipeline_depth,
!config.yield_idle,
Arc::clone(&factory),
Arc::clone(&fence_state),
)? {
None => return Ok(()),
Some((mut exchange, writer)) => {
info!("replica promoted — transitioning to primary");
replica_health.stop();
<A as Application>::prefault(&mut exchange);
if let Some(ref flag) = rotate_flag {
flag.store(false, Ordering::Release);
}
return run_as_primary::<A, L>(
exchange,
writer,
listener,
repl_listener,
&config,
&*factory,
decoder,
encoder,
event_publisher,
Arc::clone(&shutdown),
authorized_keys,
false, rotate_flag,
ack_policy_atomic,
fence_state,
promotion_request.pending(), raft_status,
journal_tip,
);
}
}
}
let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
info!(
keys = authorized_keys.len(),
path = %config.authorized_keys.display(),
"loaded authorized keys"
);
let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
let _admin_handle = config
.admin_bind
.map(|addr| {
crate::admin::spawn(
addr,
None,
rotate_flag.clone(),
Some(Arc::clone(&ack_policy_atomic)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
})
.transpose()?;
let (mut exchange, writer, needs_seeding, recovered_epoch) =
init_engine::<A, BufferedWriter<A::Event>>(&config, &*factory)?;
<A as Application>::prefault(&mut exchange);
let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(
recovered_epoch,
));
let journal_tip = melin_transport_core::AdvertisedJournalTip::new(
melin_transport_core::WireSeq::new(writer.next_sequence().saturating_sub(1)),
);
let raft = match crate::raft::build_raft_config(&config)? {
None => crate::raft::RaftDriverGuard::disabled(&shutdown),
Some(cfg) => {
let signing_key = load_replication_key(&config)?;
crate::raft::spawn_raft_driver(
cfg,
config.raft_auto_promote,
&signing_key,
&authorized_keys,
&fence_state,
journal_tip.clone(),
Arc::new(AtomicBool::new(true)),
Arc::new(|| true),
&shutdown,
)?
}
};
let raft_status = raft.status();
run_as_primary::<A, L>(
exchange,
writer,
listener,
repl_listener,
&config,
&*factory,
decoder,
encoder,
event_publisher,
Arc::clone(&shutdown),
authorized_keys,
needs_seeding,
rotate_flag,
ack_policy_atomic,
fence_state,
None, raft_status,
journal_tip,
)
}
fn bind_replication_listener(
addr: std::net::SocketAddr,
) -> Result<crate::replication::ReplicationListener, Box<dyn std::error::Error>> {
let listener = std::net::TcpListener::bind(addr)
.map_err(|e| format!("failed to bind replication listener on {addr}: {e}"))?;
let listener = crate::replication::ReplicationListener::new(listener)
.map_err(|e| format!("failed to set non-blocking on replication listener: {e}"))?;
info!(%addr, "replication listener bound");
Ok(listener)
}
fn load_replication_key(
config: &ServerConfig,
) -> Result<ed25519_dalek::SigningKey, Box<dyn std::error::Error>> {
let path = config.replication_key.as_ref().ok_or_else(|| {
std::io::Error::other("--replication-key is required (replica mode or --raft-bind)")
})?;
let seed = std::fs::read(path).map_err(|e| {
std::io::Error::other(format!(
"failed to read replication key {}: {e}",
path.display()
))
})?;
if seed.len() != 32 {
return Err(format!(
"replication key must be 32 bytes, got {} ({})",
seed.len(),
path.display()
)
.into());
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&seed);
Ok(ed25519_dalek::SigningKey::from_bytes(&bytes))
}
pub use crate::ControlEvent;
struct PipelineHandles<A: Send + 'static, W: Send + 'static> {
journal: std::thread::JoinHandle<Result<W, JournalError>>,
matching: std::thread::JoinHandle<A>,
response: std::thread::JoinHandle<()>,
replication: Option<std::thread::JoinHandle<()>>,
event_publisher: Option<std::thread::JoinHandle<()>>,
shadow: Option<std::thread::JoinHandle<()>>,
health: Option<std::thread::JoinHandle<()>>,
}
fn shutdown_pipeline_stages<A: Send + 'static, W: Send + 'static>(
handles: PipelineHandles<A, W>,
extras: Vec<(String, std::thread::Result<()>)>,
pipeline_healthy: &AtomicBool,
shutdown: &AtomicBool,
) -> Result<(), Box<dyn std::error::Error>> {
info!("shutdown: draining pipeline");
pipeline_healthy.store(false, Ordering::Relaxed);
shutdown.store(true, Ordering::Relaxed);
let mut thread_panicked = false;
let mut check_join = |name: &str, result: std::thread::Result<()>| {
if let Err(panic) = result {
let msg = panic
.downcast_ref::<&str>()
.copied()
.or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
.unwrap_or("<non-string panic>");
error!(thread = name, message = msg, "pipeline thread panicked");
thread_panicked = true;
}
};
let journal_result = handles.journal.join();
let journal_failed = matches!(&journal_result, Ok(Err(_)));
if let Ok(Err(ref e)) = journal_result {
error!(thread = "journal", error = %e, "journal stage returned error");
}
check_join("journal", journal_result.map(|_| ()));
check_join("matching", handles.matching.join().map(|_| ()));
check_join("response", handles.response.join());
for (name, r) in extras {
check_join(&name, r);
}
if let Some(h) = handles.replication {
check_join("replication-sender", h.join());
}
if let Some(h) = handles.event_publisher {
check_join("event-publisher", h.join());
}
if let Some(h) = handles.shadow {
check_join("shadow", h.join());
}
if let Some(h) = handles.health {
check_join("health", h.join());
}
melin_transport_core::trace::print_report_all();
if thread_panicked || journal_failed {
error!("shutdown complete (with pipeline failure)");
return Err("pipeline failure".into());
}
info!("shutdown complete");
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run_as_primary<A, L>(
exchange: A,
writer: BufferedWriter<A::Event>,
mut listener: L,
repl_listener: Option<crate::replication::ReplicationListener>,
config: &ServerConfig,
factory: &dyn AppFactory<App = A>,
decoder: RequestDecoderArc<A>,
encoder: ResponseEncoderArc<A>,
event_publisher: Option<EventPublisherFn<A>>,
shutdown: Arc<AtomicBool>,
authorized_keys: Arc<AuthorizedKeys>,
needs_seeding: bool,
rotate_flag: Option<Arc<AtomicBool>>,
ack_policy_atomic: Arc<AtomicU8>,
fence_state: Arc<melin_transport_core::fence::FenceState>,
promotion: Option<u64>,
raft_status: Option<Arc<melin_transport_core::health::RaftStatus>>,
journal_tip: melin_transport_core::AdvertisedJournalTip,
) -> Result<(), Box<dyn std::error::Error>>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
L: BlockingTransportListener,
{
let active_connections = Arc::new(AtomicU64::new(0));
let enable_replication = config.replication_bind.is_some();
if enable_replication && config.standalone {
return Err("--replication-bind and --standalone are mutually exclusive".into());
}
if config.standalone && config.ack_policy != crate::ack_policy::AckPolicy::Disk {
return Err(format!(
"--standalone requires --ack-policy disk; got `{}` (this policy needs at least one connected replica)",
config.ack_policy,
)
.into());
}
let enable_shadow = config.snapshot_interval_ms > 0;
let shadow_exchange = if enable_shadow {
Some(<A as Application>::clone_via_snapshot(&exchange)?)
} else {
None
};
let enable_event_publisher = event_publisher.is_some() && config.event_bind.is_some();
let Pipeline {
input_producer,
journal_stage,
matching_stage,
mut output_consumers,
events_processed,
input_cursor,
replication_consumers,
replicas_connected,
shadow_consumer,
chain_hash_lock,
replication_ring_progress,
cursors,
} = build_pipeline_with_replication(
exchange,
writer,
config.group_commit_delay(),
Arc::clone(&active_connections),
enable_replication,
config.max_journal_batch,
config.replication_ring_size,
!config.yield_idle,
enable_event_publisher,
enable_shadow,
Arc::clone(&fence_state),
);
let journal_cursor = cursors.journal_ring_arc();
let matching_cursor = cursors.matching_ring_arc();
let output_consumer = output_consumers.remove(0);
let event_publisher_consumer = if enable_event_publisher {
Some(output_consumers.remove(0))
} else {
None
};
let (control_tx, control_rx) = std::sync::mpsc::channel();
let connection_timeout = config.connection_timeout();
let heartbeat_interval = config.heartbeat_interval();
let mut input_producer = input_producer;
let cores = config.cores;
let mut journal_stage = journal_stage;
let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
journal_stage.set_rotation(max_journal_bytes, rotate_flag.clone());
journal_stage.set_preparer_core(config.cores.journal_prep);
journal_stage.set_disk_core(config.cores.journal_disk);
journal_stage.set_advertised_tip_publisher(journal_tip.clone());
if config.max_journal_mib > 0 {
info!(
max_journal_mib = config.max_journal_mib,
"runtime journal rotation enabled (size threshold)"
);
}
let journal_utilization = journal_stage.utilization();
let matching_utilization = matching_stage.utilization();
let response_utilization = Arc::new(melin_transport_core::pipeline::StageUtilization::new());
let s1 = Arc::clone(&shutdown);
let shutdown_for_journal = Arc::clone(&shutdown);
let journal_handle = std::thread::Builder::new()
.name("journal".into())
.spawn(move || {
melin_app::affinity::pin_thread("journal", cores.journal);
let result = journal_stage.run(&s1);
let was_shutdown = shutdown_for_journal.load(Ordering::Relaxed);
match &result {
Ok(_) if was_shutdown => info!("journal thread exited cleanly on shutdown"),
Ok(_) => error!("journal thread returned without shutdown signal"),
Err(e) => error!(error = %e, "journal thread returned Err"),
}
result
})
.map_err(|e| format!("spawn journal thread: {e}"))?;
let s2 = Arc::clone(&shutdown);
let shutdown_for_matching = Arc::clone(&shutdown);
let matching_handle = std::thread::Builder::new()
.name("matching".into())
.spawn(move || {
melin_app::affinity::pin_thread("matching", cores.matching);
let app = matching_stage.run(&s2);
let was_shutdown = shutdown_for_matching.load(Ordering::Relaxed);
if was_shutdown {
info!("matching thread exited cleanly on shutdown");
} else {
error!("matching thread returned without shutdown signal");
}
app
})
.map_err(|e| format!("spawn matching thread: {e}"))?;
let replication_metrics = build_replication_metrics(
replication_consumers.is_some(),
&ack_policy_atomic,
config.ack_policy,
);
let replica_active: Option<[Arc<AtomicBool>; 2]> =
replication_ring_progress.as_ref().map(|rp| {
[
Arc::clone(&rp.active_flags[0]),
Arc::clone(&rp.active_flags[1]),
]
});
let journal_persisted_wire_seq_response = cursors.durable_wire_seq();
let replication_metrics_response = replication_metrics.as_ref().map(Arc::clone);
let replica_active_response = replica_active.clone();
let ack_policy_response = Arc::clone(&ack_policy_atomic);
let s3 = Arc::clone(&shutdown);
let shutdown_for_response = Arc::clone(&shutdown);
let busy_spin = !config.yield_idle;
let response_utilization_thread = Arc::clone(&response_utilization);
let response_fence = Arc::clone(&fence_state);
let active_connections_response = Arc::clone(&active_connections);
let response_handle = std::thread::Builder::new()
.name("response".into())
.spawn(move || {
melin_app::affinity::pin_thread("response", cores.response);
crate::response::run::<A>(
output_consumer,
control_rx,
crate::response::Response::<A> {
journal_persisted_wire_seq: journal_persisted_wire_seq_response,
ack_policy: ack_policy_response,
replication_metrics: replication_metrics_response,
replica_active: replica_active_response,
heartbeat_interval,
busy_spin,
utilization: response_utilization_thread,
encoder,
fence_state: response_fence,
active_connections: active_connections_response,
},
&s3,
);
let was_shutdown = shutdown_for_response.load(Ordering::Relaxed);
if was_shutdown {
info!("response thread exited cleanly on shutdown");
} else {
error!("response thread returned without shutdown signal");
}
})
.map_err(|e| format!("spawn response thread: {e}"))?;
let replica_ready = Arc::new(AtomicBool::new(false));
let replication_handle = if let Some((repl_consumer_1, repl_consumer_2)) = replication_consumers
{
let s_repl = Arc::clone(&shutdown);
let repl_slots = cursors.replica_slot_cursors();
let ready_flag = Arc::clone(&replica_ready);
let connected_counter = replicas_connected
.clone()
.ok_or("replicas_connected must be Some when replication is enabled")?;
let batch_size = config.replication_batch_size;
let heartbeat_secs = config.replication_heartbeat_secs;
let journal_path = config.journal.clone();
let repl_auth_keys = Arc::clone(&authorized_keys);
let evict_flags = replication_ring_progress
.as_ref()
.map(|rp| {
[
Arc::clone(&rp.evict_flags[0]),
Arc::clone(&rp.evict_flags[1]),
]
})
.unwrap_or_else(|| {
[
Arc::new(AtomicBool::new(false)),
Arc::new(AtomicBool::new(false)),
]
});
let active_flags = replication_ring_progress
.as_ref()
.map(|rp| {
[
Arc::clone(&rp.active_flags[0]),
Arc::clone(&rp.active_flags[1]),
]
})
.unwrap_or_else(|| {
[
Arc::new(AtomicBool::new(false)),
Arc::new(AtomicBool::new(false)),
]
});
let repl_metrics = replication_metrics
.clone()
.ok_or("replication_metrics must be Some when replication is enabled")?;
let handler_cores = [cores.repl_handler_0, cores.repl_handler_1];
let sender_fence = Arc::clone(&fence_state);
let sender_ack_policy = Arc::clone(&ack_policy_atomic);
let repl_listener = repl_listener
.ok_or("replication listener must be pre-bound when replication is enabled")?;
let repl_accept_handle = std::thread::Builder::new()
.name("repl-accept".into())
.spawn(move || {
crate::replication::run_sender::<A>(
crate::replication::Sender {
listener: repl_listener,
repl_consumer_1,
repl_consumer_2,
replica_slots: repl_slots,
journal_path,
authorized_keys: repl_auth_keys,
evict_flags,
active_flags,
metrics: repl_metrics,
handler_cores,
batch_size,
heartbeat_secs,
busy_spin,
fence_state: sender_fence,
ack_policy: sender_ack_policy,
},
&s_repl,
&ready_flag,
&connected_counter,
);
})
.map_err(|e| format!("spawn replication sender thread: {e}"))?;
Some(repl_accept_handle)
} else {
if !config.standalone && config.replica_of.is_none() {
info!("running in standalone mode (no replication)");
}
None
};
let event_publisher_handle = spawn_event_publisher::<A>(
event_publisher_consumer,
event_publisher,
config,
&cores,
&authorized_keys,
&shutdown,
busy_spin,
)?;
let shadow_handle = spawn_shadow_stage::<A>(
shadow_consumer,
shadow_exchange,
chain_hash_lock,
config,
&cores,
&shutdown,
busy_spin,
fence_state.epoch(),
)?;
let pipeline_healthy = Arc::new(AtomicBool::new(true));
let health_handle = spawn_health_endpoint(
config,
&active_connections,
&events_processed,
&cursors,
input_cursor,
&pipeline_healthy,
&replicas_connected,
&fence_state,
&replication_metrics,
&replica_active,
&replication_ring_progress,
&journal_utilization,
&matching_utilization,
&response_utilization,
&shutdown,
&raft_status,
)?;
if enable_replication && needs_seeding {
info!("waiting for replica to connect before seeding...");
while !replica_ready.load(Ordering::Acquire) {
if shutdown.load(Ordering::Relaxed) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
if needs_seeding {
use melin_app::unix_epoch_nanos;
use melin_journal::JournalEvent;
use melin_transport_core::trace::mono_trace_ns;
let seed_start = std::time::Instant::now();
let seed_events = factory.seed_events();
let seed_count = seed_events.len();
let mut last_published_seq = 0u64;
for event in seed_events {
last_published_seq = input_producer.publish(InputSlot {
connection_id: 0,
key_hash: 0,
request_seq: 0,
sequence: 0,
timestamp_ns: unix_epoch_nanos(),
event: JournalEvent::App(event),
publish_ts: mono_trace_ns(),
recv_ts: mono_trace_ns(),
});
}
let publish_elapsed = seed_start.elapsed();
let drain_start = std::time::Instant::now();
if seed_count > 0 {
let last_seed_seq = last_published_seq + 1;
info!(
last_seed_seq,
journal = journal_cursor
.get()
.load(std::sync::atomic::Ordering::Relaxed),
matching = matching_cursor
.get()
.load(std::sync::atomic::Ordering::Relaxed),
"seed drain: waiting for pipeline cursors"
);
while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
&& (journal_cursor
.get()
.load(std::sync::atomic::Ordering::Acquire)
< last_seed_seq
|| matching_cursor
.get()
.load(std::sync::atomic::Ordering::Acquire)
< last_seed_seq)
{
std::hint::spin_loop();
}
info!("seed drain: pipeline cursors reached target");
if let Some(ref ring_progress) = replication_ring_progress {
for i in 0..ring_progress.producer_cursors.len() {
if !ring_progress.active_flags[i].load(std::sync::atomic::Ordering::Relaxed) {
continue;
}
let target = ring_progress.producer_cursors[i].load();
while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
&& ring_progress.consumer_cursors[i]
.get()
.load(std::sync::atomic::Ordering::Acquire)
< target
{
std::hint::spin_loop();
}
}
}
info!("seed drain: replication rings drained");
}
let drain_elapsed = drain_start.elapsed();
info!(
seed_events = seed_count,
publish_ms = publish_elapsed.as_millis(),
drain_ms = drain_elapsed.as_millis(),
total_ms = seed_start.elapsed().as_millis(),
"seeded application state through pipeline"
);
}
if let Some(requested_epoch) = promotion {
use melin_app::unix_epoch_nanos;
use melin_journal::JournalEvent;
use melin_transport_core::trace::mono_trace_ns;
let new_epoch = fence_state.epoch().saturating_add(1).max(requested_epoch);
if requested_epoch > crate::promotion::PromotionRequest::MANUAL
&& new_epoch != requested_epoch
{
warn!(
new_epoch,
requested_epoch,
"promotion epoch does not match its election term — a concurrent \
promotion advanced the fencing epoch mid-drain; auto-promotion \
refusals may report term/epoch misalignment until a newer election"
);
}
info!(
new_epoch,
requested_epoch, "promotion: injecting epoch bump"
);
input_producer.publish(InputSlot {
connection_id: 0,
key_hash: 0,
request_seq: 0,
sequence: 0,
timestamp_ns: unix_epoch_nanos(),
event: JournalEvent::EpochBump { epoch: new_epoch },
publish_ts: mono_trace_ns(),
recv_ts: mono_trace_ns(),
});
while fence_state.epoch() < new_epoch {
if shutdown.load(Ordering::Relaxed) {
break;
}
std::hint::spin_loop();
}
}
let reader_shutdown = Arc::new(AtomicBool::new(false));
let mut reader_handle = crate::reader::spawn_reader::<A, _>(
input_producer,
decoder,
control_tx.clone(),
config.cores.reader,
connection_timeout,
config.tick_interval(),
Arc::clone(&reader_shutdown),
);
listener.set_nonblocking(true);
info!(addr = %config.bind, "listening");
let next_connection_id = AtomicU64::new(1);
loop {
if shutdown.load(Ordering::Relaxed) {
info!("shutdown signal received");
break;
}
let event_pub_died = event_publisher_handle
.as_ref()
.is_some_and(|h| h.is_finished());
let shadow_died = shadow_handle.as_ref().is_some_and(|h| h.is_finished());
if (journal_handle.is_finished()
|| matching_handle.is_finished()
|| response_handle.is_finished()
|| event_pub_died
|| shadow_died)
&& !shutdown.load(Ordering::Relaxed)
{
error!("pipeline thread died, initiating shutdown");
pipeline_healthy.store(false, Ordering::Relaxed);
break;
}
let (mut std_read, mut std_write, addr) = match listener.accept() {
Ok(conn) => conn,
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock {
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
}
error!(error = %e, "accept error");
continue;
}
};
if config.max_connections > 0
&& active_connections.load(Ordering::Relaxed) >= config.max_connections
{
warn!(addr = %addr, "connection rejected: max_connections reached");
drop(std_read);
drop(std_write);
continue;
}
let connection_id = ConnectionId(next_connection_id.fetch_add(1, Ordering::Relaxed));
debug!(connection_id = connection_id.0, addr = %addr, "new connection");
if let Err(e) = set_read_timeout(&std_read, Some(std::time::Duration::from_secs(5))) {
debug!(connection_id = connection_id.0, error = %e, "failed to set auth timeout");
}
let (permission, public_key_bytes) = match authenticate_connection(
connection_id,
addr,
&mut std_read,
&mut std_write,
&authorized_keys,
) {
Ok(pair) => pair,
Err(e) => {
debug!(connection_id = connection_id.0, addr = %addr, error = %e, "auth failed, dropping");
continue;
}
};
let key_hash = {
let mut hasher = rustc_hash::FxHasher::default();
public_key_bytes.hash(&mut hasher);
hasher.finish()
};
active_connections.fetch_add(1, Ordering::Relaxed);
if let Err(e) = set_read_timeout(&std_read, None) {
debug!(connection_id = connection_id.0, error = %e, "failed to clear auth timeout");
}
if let Err(e) = set_busy_poll(&std_read, BUSY_POLL_US) {
debug!(
connection_id = connection_id.0,
error = %e,
"failed to set SO_BUSY_POLL on client socket"
);
}
if let Err(e) = set_write_timeout(&std_write, Some(std::time::Duration::from_secs(5))) {
debug!(connection_id = connection_id.0, error = %e, "failed to set write timeout");
}
let fd = std_write.as_raw_fd();
let boxed_writer: Box<dyn std::io::Write + Send> = Box::new(std_write);
let control_event = ControlEvent::Connected {
connection_id: connection_id.0,
fd,
writer: BlockingFrameWriter::new(boxed_writer),
};
if control_tx.send(control_event).is_err() {
info!("response thread gone, shutting down");
break;
}
reader_handle.register(crate::reader::ReaderRegistration {
connection_id,
reader: std_read,
addr,
permission,
key_hash,
});
}
info!("shutdown: stopping reader thread");
reader_handle.shutdown();
reader_handle.join();
shutdown_pipeline_stages(
PipelineHandles {
journal: journal_handle,
matching: matching_handle,
response: response_handle,
replication: replication_handle,
event_publisher: event_publisher_handle,
shadow: shadow_handle,
health: health_handle,
},
Vec::new(),
&pipeline_healthy,
&shutdown,
)
}
#[cfg(feature = "dpdk")]
fn run_dpdk<A>(
config: ServerConfig,
factory: Arc<dyn AppFactory<App = A>>,
decoder: RequestDecoderArc<A>,
encoder: ResponseEncoderArc<A>,
event_publisher: Option<EventPublisherFn<A>>,
shutdown: Arc<AtomicBool>,
) -> Result<(), Box<dyn std::error::Error>>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
{
let dpdk_config = dpdk_config_from(&config)?;
run_dpdk_impl::<A>(
config,
factory,
decoder,
encoder,
event_publisher,
dpdk_config,
shutdown,
)
}
#[cfg(feature = "dpdk")]
fn dpdk_config_from(cfg: &ServerConfig) -> Result<melin_dpdk::DpdkConfig, String> {
let parse_ip = |value: &str, flag: &str| -> Result<std::net::Ipv4Addr, String> {
value
.parse()
.map_err(|e| format!("invalid {flag} address '{value}': {e}"))
};
let parse_mac = |value: &str, flag: &str| -> Result<[u8; 6], String> {
melin_dpdk::try_parse_mac(value).map_err(|e| format!("invalid {flag}: {e}"))
};
Ok(melin_dpdk::DpdkConfig {
eal_args: cfg
.dpdk_eal_args
.split_whitespace()
.map(String::from)
.collect(),
port_ids: cfg.dpdk_ports.clone(),
ip_addr: parse_ip(&cfg.dpdk_ip, "--dpdk-ip")?,
prefix_len: cfg.dpdk_prefix_len,
gateway: cfg
.dpdk_gateway
.as_deref()
.map(|s| parse_ip(s, "--dpdk-gateway"))
.transpose()?,
listen_port: cfg.bind.port(),
mtu: cfg.dpdk_mtu,
vlan_id: cfg.dpdk_vlan,
peer_ip: cfg
.dpdk_peer_ip
.as_deref()
.map(|s| parse_ip(s, "--dpdk-peer-ip"))
.transpose()?,
gateway_mac: cfg
.dpdk_gateway_mac
.as_deref()
.map(|s| parse_mac(s, "--dpdk-gateway-mac"))
.transpose()?,
peer_mac: cfg
.dpdk_peer_mac
.as_deref()
.map(|s| parse_mac(s, "--dpdk-peer-mac"))
.transpose()?,
num_queues: 1,
})
}
#[cfg(feature = "dpdk")]
fn run_dpdk_impl<A>(
config: ServerConfig,
factory: Arc<dyn AppFactory<App = A>>,
decoder: RequestDecoderArc<A>,
encoder: ResponseEncoderArc<A>,
event_publisher: Option<EventPublisherFn<A>>,
dpdk_config: melin_dpdk::DpdkConfig,
shutdown: Arc<AtomicBool>,
) -> Result<(), Box<dyn std::error::Error>>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
{
warn_if_chain_disabled();
let ack_policy_atomic = Arc::new(AtomicU8::new(config.ack_policy.as_u8()));
let shared = melin_dpdk::DpdkShared::init(&dpdk_config)?;
let num_dpdk_threads = shared.num_queues as usize;
if let Some(primary_addr) = config.replica_of {
info!(primary = %primary_addr, "starting in replica mode (DPDK)");
let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
info!(
keys = authorized_keys.len(),
path = %config.authorized_keys.display(),
"loaded authorized keys (DPDK replica mode)"
);
let replication_key_path = config.replication_key.as_ref().ok_or_else(|| {
std::io::Error::other("--replication-key is required in replica mode (--replica-of)")
})?;
let signing_key = {
let seed = std::fs::read(replication_key_path).map_err(|e| {
std::io::Error::other(format!(
"failed to read replication key {}: {e}",
replication_key_path.display()
))
})?;
if seed.len() != 32 {
return Err(format!(
"replication key must be 32 bytes, got {} ({})",
seed.len(),
replication_key_path.display()
)
.into());
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&seed);
ed25519_dalek::SigningKey::from_bytes(&bytes)
};
let control = crate::replication::ReplicaControlPlane::new();
let promotion_request = control.promote.clone();
let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
let _admin_handle = config
.admin_bind
.map(|addr| {
crate::admin::spawn(
addr,
Some(promotion_request.clone()),
rotate_flag.clone(),
Some(Arc::clone(&ack_policy_atomic)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
})
.transpose()?;
let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(0));
let journal_tip = control.journal_tip.clone();
let mut raft = match crate::raft::build_raft_config(&config)? {
None => crate::raft::RaftDriverGuard::disabled(&shutdown),
Some(cfg) => crate::raft::spawn_raft_driver(
cfg,
config.raft_auto_promote,
&signing_key,
&authorized_keys,
&fence_state,
journal_tip.clone(),
Arc::clone(&control.tip_ready),
{
let promote = control.promote.clone();
Arc::new(move || promote.is_requested())
},
&shutdown,
)?,
};
if config.raft_auto_promote
&& let Some(wiring) = raft.promotion_wiring()
{
raft.arm_promotion(crate::raft_promotion::spawn_auto_promotion(
wiring.status,
control.clone(),
Arc::clone(&fence_state),
Arc::clone(&ack_policy_atomic),
wiring.peer_tips,
wiring.peer_ids,
wiring.elect_requested,
wiring.elect_enabled,
Arc::clone(&shutdown),
));
}
let raft_status = raft.status();
let mut replica_health = crate::raft::spawn_replica_health(
&config,
&fence_state,
raft_status.as_ref(),
Arc::clone(&control.pipeline_healthy),
)?;
let mut repl_transport = melin_dpdk::DpdkTransport::from_shared_with_port(
&shared,
&dpdk_config,
0,
39999, )?;
repl_transport.send_gratuitous_arp();
let primary_ipv4 = match primary_addr.ip() {
std::net::IpAddr::V4(ip) => ip,
std::net::IpAddr::V6(_) => {
return Err("DPDK replication requires IPv4".into());
}
};
match crate::replication::run_receiver_dpdk::<A>(
repl_transport,
primary_ipv4,
primary_addr.port(),
&signing_key,
&config.journal,
&shutdown,
&control,
config.snapshot_interval_ms,
config.shadow_snapshot_path(),
config.cores,
config.group_commit_delay(),
config.replication_pipeline_depth,
!config.yield_idle,
Arc::clone(&factory),
Arc::clone(&fence_state),
)? {
None => return Ok(()),
Some((mut exchange, writer)) => {
info!("replica promoted (DPDK) — transitioning to primary");
replica_health.stop();
<A as Application>::prefault(&mut exchange);
if let Some(ref flag) = rotate_flag {
flag.store(false, Ordering::Release);
}
warn!("DPDK primary promotion not yet implemented — falling back to kernel TCP");
let listener = melin_wire_protocol::tcp::BlockingTcpListener::bind(config.bind)?;
let repl_listener = config
.replication_bind
.map(bind_replication_listener)
.transpose()?;
return run_as_primary::<A, _>(
exchange,
writer,
listener,
repl_listener,
&config,
&*factory,
decoder,
encoder,
event_publisher,
Arc::clone(&shutdown),
authorized_keys,
false,
rotate_flag,
ack_policy_atomic,
fence_state,
promotion_request.pending(), raft_status,
journal_tip,
);
}
}
}
let num_client_queues = num_dpdk_threads;
let mut transports = Vec::with_capacity(num_client_queues);
for q in 0..num_client_queues {
let mut transport =
melin_dpdk::DpdkTransport::from_shared(&shared, &dpdk_config, q as u16)?;
if q == 0 {
transport.send_gratuitous_arp();
}
transports.push(transport);
}
let authorized_keys = Arc::new(AuthorizedKeys::load(&config.authorized_keys)?);
info!(
keys = authorized_keys.len(),
path = %config.authorized_keys.display(),
"loaded authorized keys"
);
let (mut exchange, writer, needs_seeding, recovered_epoch) =
init_engine::<A, BufferedWriter<A::Event>>(&config, &*factory)?;
<A as Application>::prefault(&mut exchange);
let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(
recovered_epoch,
));
let journal_tip = melin_transport_core::AdvertisedJournalTip::new(
melin_transport_core::WireSeq::new(writer.next_sequence().saturating_sub(1)),
);
let raft = match crate::raft::build_raft_config(&config)? {
None => crate::raft::RaftDriverGuard::disabled(&shutdown),
Some(cfg) => {
let signing_key = load_replication_key(&config)?;
crate::raft::spawn_raft_driver(
cfg,
config.raft_auto_promote,
&signing_key,
&authorized_keys,
&fence_state,
journal_tip.clone(),
Arc::new(AtomicBool::new(true)),
Arc::new(|| true),
&shutdown,
)?
}
};
let raft_status = raft.status();
let enable_shadow = config.snapshot_interval_ms > 0;
let shadow_exchange = if enable_shadow {
Some(<A as Application>::clone_via_snapshot(&exchange)?)
} else {
None
};
let active_connections = Arc::new(AtomicU64::new(0));
let enable_replication = config.replication_bind.is_some();
if enable_replication && config.standalone {
return Err("--replication-bind and --standalone are mutually exclusive".into());
}
if config.standalone && config.ack_policy != crate::ack_policy::AckPolicy::Disk {
return Err(format!(
"--standalone requires --ack-policy disk; got `{}` (this policy needs at least one connected replica)",
config.ack_policy,
)
.into());
}
let enable_event_publisher = event_publisher.is_some() && config.event_bind.is_some();
let enable_shadow = config.snapshot_interval_ms > 0;
let Pipeline {
input_producer,
journal_stage,
matching_stage,
mut output_consumers,
events_processed,
input_cursor,
replication_consumers,
replicas_connected,
shadow_consumer,
chain_hash_lock,
replication_ring_progress,
cursors,
} = build_pipeline_with_replication(
exchange,
writer,
config.group_commit_delay(),
Arc::clone(&active_connections),
enable_replication,
config.max_journal_batch,
config.replication_ring_size,
!config.yield_idle,
enable_event_publisher,
enable_shadow,
Arc::clone(&fence_state),
);
let journal_cursor = cursors.journal_ring_arc();
let matching_cursor = cursors.matching_ring_arc();
let heartbeat_interval = config.heartbeat_interval();
let mut input_producer = input_producer;
let tick_cadence = config.tick_interval();
let (control_tx, control_rx) = std::sync::mpsc::channel();
let mut tx_producers = Vec::with_capacity(num_dpdk_threads);
let mut tx_consumers = Vec::with_capacity(num_dpdk_threads);
for _ in 0..num_dpdk_threads {
let (tx_out, tx_rx) = melin_pipeline::spsc::channel::<crate::dpdk_response::TxFrame>(4096);
tx_producers.push(tx_out);
tx_consumers.push(tx_rx);
}
let cores = config.cores;
let mut journal_stage = journal_stage;
let rotate_flag = config.admin_bind.map(|_| Arc::new(AtomicBool::new(false)));
let _admin_handle = config
.admin_bind
.map(|addr| {
crate::admin::spawn(
addr,
None,
rotate_flag.clone(),
Some(Arc::clone(&ack_policy_atomic)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
})
.transpose()?;
let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
journal_stage.set_rotation(max_journal_bytes, rotate_flag.clone());
journal_stage.set_preparer_core(config.cores.journal_prep);
journal_stage.set_disk_core(config.cores.journal_disk);
journal_stage.set_advertised_tip_publisher(journal_tip.clone());
if config.max_journal_mib > 0 {
info!(
max_journal_mib = config.max_journal_mib,
"runtime journal rotation enabled (size threshold, DPDK)"
);
}
let journal_utilization = journal_stage.utilization();
let matching_utilization = matching_stage.utilization();
let response_utilization = Arc::new(melin_transport_core::pipeline::StageUtilization::new());
let s1 = Arc::clone(&shutdown);
let journal_handle = std::thread::Builder::new()
.name("journal".into())
.spawn(move || {
melin_app::affinity::pin_thread("journal", cores.journal);
journal_stage.run(&s1)
})
.map_err(|e| format!("spawn journal thread: {e}"))?;
let s2 = Arc::clone(&shutdown);
let matching_handle = std::thread::Builder::new()
.name("matching".into())
.spawn(move || {
melin_app::affinity::pin_thread("matching", cores.matching);
matching_stage.run(&s2)
})
.map_err(|e| format!("spawn matching thread: {e}"))?;
let replication_metrics = build_replication_metrics(
replication_consumers.is_some(),
&ack_policy_atomic,
config.ack_policy,
);
let replica_active: Option<[Arc<AtomicBool>; 2]> =
replication_ring_progress.as_ref().map(|rp| {
[
Arc::clone(&rp.active_flags[0]),
Arc::clone(&rp.active_flags[1]),
]
});
let output_consumer = output_consumers.remove(0);
let journal_persisted_wire_seq_response = cursors.durable_wire_seq();
let replication_metrics_response = replication_metrics.as_ref().map(Arc::clone);
let replica_active_response = replica_active.clone();
let ack_policy_response = Arc::clone(&ack_policy_atomic);
let active_connections_response = Arc::clone(&active_connections);
let s3 = Arc::clone(&shutdown);
let response_utilization_thread = Arc::clone(&response_utilization);
let busy_spin = !config.yield_idle;
let response_handle = std::thread::Builder::new()
.name("response".into())
.spawn(move || {
melin_app::affinity::pin_thread("response", cores.response);
crate::dpdk_response::run::<A>(
output_consumer,
control_rx,
journal_persisted_wire_seq_response,
ack_policy_response,
replication_metrics_response,
replica_active_response,
&s3,
heartbeat_interval,
active_connections_response,
tx_producers,
response_utilization_thread,
busy_spin,
encoder,
);
})
.map_err(|e| format!("spawn response thread: {e}"))?;
let shadow_handle = spawn_shadow_stage::<A>(
shadow_consumer,
shadow_exchange,
chain_hash_lock,
&config,
&cores,
&shutdown,
busy_spin,
fence_state.epoch(),
)?;
let replica_ready = Arc::new(AtomicBool::new(false));
let (repl_driver, repl_listen_port) = if let Some((repl_consumer_1, repl_consumer_2)) =
replication_consumers
{
let repl_bind = config
.replication_bind
.ok_or("replication_bind must be set when replication is enabled")?;
let repl_port = repl_bind.port();
let repl_slots = cursors.replica_slot_cursors();
let ready_flag = Arc::clone(&replica_ready);
let batch_size = config.replication_batch_size;
let heartbeat_secs = config.replication_heartbeat_secs;
let repl_metrics = replication_metrics
.clone()
.ok_or("replication_metrics must be Some when replication is enabled")?;
let connected_counter = replicas_connected
.clone()
.ok_or("replicas_connected must be Some when replication is enabled")?;
let dpdk_active_flags: [Arc<AtomicBool>; 2] = replication_ring_progress
.as_ref()
.map(|rp| {
[
Arc::clone(&rp.active_flags[0]),
Arc::clone(&rp.active_flags[1]),
]
})
.unwrap_or_else(|| {
[
Arc::new(AtomicBool::new(false)),
Arc::new(AtomicBool::new(false)),
]
});
let dpdk_evict_flags: [Arc<AtomicBool>; 2] = replication_ring_progress
.as_ref()
.map(|rp| {
[
Arc::clone(&rp.evict_flags[0]),
Arc::clone(&rp.evict_flags[1]),
]
})
.unwrap_or_else(|| {
[
Arc::new(AtomicBool::new(false)),
Arc::new(AtomicBool::new(false)),
]
});
let journal_path = config.journal.clone();
const REPL_TX_BUF: usize = 512 * 1024;
const REPL_TX_QUEUE: usize = 512 * 1024;
const REPL_RX_BUF: usize = 64 * 1024;
transports[0]
.add_listener_with_buffers(
repl_port,
REPL_RX_BUF,
REPL_TX_BUF,
REPL_TX_QUEUE,
crate::replication::REPL_DISPATCH_BURST,
)
.map_err(|e| format!("add replication listener: {e}"))?;
let driver = crate::replication::DpdkReplicationDriver::new(
[repl_consumer_1, repl_consumer_2],
repl_slots,
journal_path,
ready_flag,
connected_counter,
dpdk_evict_flags,
dpdk_active_flags,
repl_metrics,
batch_size,
heartbeat_secs,
Arc::clone(&fence_state),
Arc::clone(&ack_policy_atomic),
Arc::clone(&authorized_keys),
)?;
info!(addr = %repl_bind, "DPDK replication sender started (single-queue, in client poll thread)");
(Some(driver), repl_port)
} else {
if !config.standalone && config.replica_of.is_none() {
info!("running in standalone mode (no replication)");
}
(None, 0)
};
let replication_handle: Option<std::thread::JoinHandle<()>> = None;
let _ = (&enable_replication, &replica_ready); if needs_seeding {
use melin_app::unix_epoch_nanos;
use melin_journal::JournalEvent;
use melin_transport_core::trace::mono_trace_ns;
let seed_events = factory.seed_events();
let seed_count = seed_events.len();
let mut last_published_seq = 0u64;
for event in seed_events {
last_published_seq = input_producer.publish(InputSlot {
connection_id: 0,
key_hash: 0,
request_seq: 0,
sequence: 0,
timestamp_ns: unix_epoch_nanos(),
event: JournalEvent::App(event),
publish_ts: mono_trace_ns(),
recv_ts: mono_trace_ns(),
});
}
if seed_count > 0 {
let last_seed_seq = last_published_seq + 1;
while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
&& (journal_cursor
.get()
.load(std::sync::atomic::Ordering::Acquire)
< last_seed_seq
|| matching_cursor
.get()
.load(std::sync::atomic::Ordering::Acquire)
< last_seed_seq)
{
std::hint::spin_loop();
}
if let Some(ref ring_progress) = replication_ring_progress {
for i in 0..ring_progress.producer_cursors.len() {
if !ring_progress.active_flags[i].load(std::sync::atomic::Ordering::Relaxed) {
continue;
}
let target = ring_progress.producer_cursors[i].load();
while !shutdown.load(std::sync::atomic::Ordering::Relaxed)
&& ring_progress.consumer_cursors[i]
.get()
.load(std::sync::atomic::Ordering::Acquire)
< target
{
std::hint::spin_loop();
}
}
}
}
info!(
accounts = config.accounts,
instruments = config.instruments,
"seeded test data through pipeline"
);
}
let pipeline_healthy = Arc::new(AtomicBool::new(true));
let health_handle = spawn_health_endpoint(
&config,
&active_connections,
&events_processed,
&cursors,
input_cursor,
&pipeline_healthy,
&replicas_connected,
&fence_state,
&replication_metrics,
&replica_active,
&replication_ring_progress,
&journal_utilization,
&matching_utilization,
&response_utilization,
&shutdown,
&raft_status,
)?;
info!(
ip = %dpdk_config.ip_addr,
port = dpdk_config.listen_port,
num_dpdk_threads,
"DPDK transport listening"
);
let connection_timeout = config.connection_timeout();
let max_conns = config.max_connections;
let reader_core = config.cores.reader;
assert_eq!(
transports.len(),
1,
"expected exactly one client DPDK transport"
);
let transport_0 = transports.pop().expect("one client transport");
let tx_rx_0 = tx_consumers.remove(0);
melin_app::affinity::pin_thread("dpdk-poll-0", reader_core);
crate::dpdk_transport::run_dpdk_poll::<A>(
transport_0,
input_producer,
decoder,
control_tx,
tx_rx_0,
&shutdown,
authorized_keys,
connection_timeout,
tick_cadence,
max_conns,
Arc::clone(&active_connections),
0,
repl_driver,
repl_listen_port,
);
let dpdk_extras: Vec<(String, std::thread::Result<()>)> = Vec::new();
shutdown_pipeline_stages(
PipelineHandles {
journal: journal_handle,
matching: matching_handle,
response: response_handle,
replication: replication_handle,
event_publisher: None,
shadow: shadow_handle,
health: health_handle,
},
dpdk_extras,
&pipeline_healthy,
&shutdown,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BootstrapSource {
SnapshotAndJournal,
SnapshotOnly,
JournalOnly,
Fresh,
}
fn choose_bootstrap(
snapshot_exists: bool,
live_exists: bool,
archives_exist: bool,
) -> BootstrapSource {
let lineage_exists = live_exists || archives_exist;
match (snapshot_exists, lineage_exists) {
(true, true) => BootstrapSource::SnapshotAndJournal,
(true, false) => BootstrapSource::SnapshotOnly,
(false, true) => BootstrapSource::JournalOnly,
(false, false) => BootstrapSource::Fresh,
}
}
pub(crate) fn init_engine<A, W>(
config: &ServerConfig,
factory: &dyn AppFactory<App = A>,
) -> Result<(A, W, bool, u64), Box<dyn std::error::Error>>
where
A: Application,
W: JournalWrite<A::Event>,
{
let derived_snap = config.journal.with_extension("snapshot");
let snap_path = config.snapshot.as_deref().or_else(|| {
if derived_snap.exists() {
Some(derived_snap.as_path())
} else {
None
}
});
let journal_exists = config.journal.exists();
let archives_exist = !melin_journal::segment::list_archives(&config.journal)?.is_empty();
let snapshot_exists = snap_path.is_some_and(|p| p.exists());
let mut engine: JournaledApp<A, W> =
match choose_bootstrap(snapshot_exists, journal_exists, archives_exist) {
BootstrapSource::SnapshotAndJournal => {
let snap_path = snap_path.expect("snapshot_exists implies snap_path");
info!(snapshot = %snap_path.display(), "recovering from snapshot + journal");
JournaledApp::<A, W>::recover_from_snapshot(snap_path, &config.journal)?
}
BootstrapSource::SnapshotOnly => {
let snap_path = snap_path.expect("snapshot_exists implies snap_path");
info!(
snapshot = %snap_path.display(),
"recovering from snapshot only (no journal segments on disk)"
);
let (app, snap_sequence, snap_chain_hash, snap_epoch) =
melin_transport_core::snapshot::load::<A>(snap_path)?;
let writer =
W::create_continuing(&config.journal, snap_sequence + 1, snap_chain_hash)?;
JournaledApp::<A, W>::from_parts(app, writer, snap_epoch)
}
BootstrapSource::JournalOnly => {
info!("recovering from journal");
let mut app = factory.empty();
factory.prefault(&mut app);
JournaledApp::<A, W>::recover(app, &config.journal)?
}
BootstrapSource::Fresh => {
info!("creating new journal");
let mut app = factory.empty();
factory.prefault(&mut app);
JournaledApp::<A, W>::create(app, &config.journal)?
}
};
let needs_seeding = !journal_exists && !archives_exist;
factory.apply_operator_policy(engine.app_mut());
if config.max_journal_mib > 0 {
let threshold = config.max_journal_mib * 1024 * 1024;
let current_size = engine.journal_size();
if current_size > threshold {
info!(
current_mib = current_size / (1024 * 1024),
threshold_mib = config.max_journal_mib,
"journal exceeds threshold, rotating segment"
);
engine.rotate_segment()?;
info!("journal segment rotated successfully");
}
}
let recovered_epoch = engine.recovered_epoch();
let (app, writer) = engine.into_parts();
Ok((app, writer, needs_seeding, recovered_epoch))
}
fn build_replication_metrics(
has_replication: bool,
ack_policy_atomic: &AtomicU8,
config_policy: crate::ack_policy::AckPolicy,
) -> Option<Arc<crate::replication::ReplicationMetrics>> {
let metrics = if has_replication {
Some(Arc::new(crate::replication::ReplicationMetrics::default()))
} else {
None
};
let active_policy =
crate::ack_policy::AckPolicy::from_u8(ack_policy_atomic.load(Ordering::Relaxed))
.unwrap_or(config_policy);
info!(
ack_policy = %active_policy,
clauses = %active_policy.to_policy(),
"ack policy active"
);
metrics
}
fn spawn_shadow_stage<A: Application + Send + 'static>(
shadow_consumer: Option<Consumer<InputSlot<A::Event>>>,
shadow_exchange: Option<A>,
chain_hash_lock: Option<
melin_pipeline::seqlock::SeqLockReader<melin_transport_core::pipeline::FsyncState>,
>,
config: &ServerConfig,
cores: &PipelineCores,
shutdown: &Arc<AtomicBool>,
busy_spin: bool,
initial_epoch: u64,
) -> Result<Option<std::thread::JoinHandle<()>>, Box<dyn std::error::Error>>
where
A::Event: Send + Sync + 'static,
{
let Some(shadow_cons) = shadow_consumer else {
return Ok(None);
};
let snap_path = config.shadow_snapshot_path();
let interval = std::time::Duration::from_millis(config.snapshot_interval_ms);
let chain_hash =
chain_hash_lock.ok_or("chain hash lock must be Some when shadow is enabled")?;
let shadow_ex = shadow_exchange.ok_or("shadow exchange must be Some when shadow is enabled")?;
let s_shadow = Arc::clone(shutdown);
let shadow_core = cores.shadow;
let shadow_initial_epoch = initial_epoch;
let handle = std::thread::Builder::new()
.name("shadow".into())
.spawn(move || {
melin_app::affinity::pin_thread("shadow", shadow_core);
melin_transport_core::shadow::run(
shadow_cons,
shadow_ex,
snap_path,
interval,
chain_hash,
&s_shadow,
busy_spin,
shadow_initial_epoch,
);
})
.map_err(|e| format!("spawn shadow thread: {e}"))?;
info!(
interval_ms = config.snapshot_interval_ms,
path = %config.shadow_snapshot_path().display(),
"shadow snapshot stage started"
);
Ok(Some(handle))
}
#[allow(clippy::too_many_arguments)]
fn spawn_health_endpoint(
config: &ServerConfig,
active_connections: &Arc<AtomicU64>,
events_processed: &Arc<AtomicU64>,
cursors: &melin_transport_core::PipelineCursors,
input_cursor: Box<dyn melin_pipeline::ring::QueueCursor>,
pipeline_healthy: &Arc<AtomicBool>,
replicas_connected: &Option<Arc<std::sync::atomic::AtomicU32>>,
fence_state: &Arc<melin_transport_core::fence::FenceState>,
replication_metrics: &Option<Arc<crate::replication::ReplicationMetrics>>,
replica_active: &Option<[Arc<AtomicBool>; 2]>,
replication_ring_progress: &Option<melin_transport_core::pipeline::ReplicationRingProgress>,
journal_utilization: &Arc<melin_transport_core::pipeline::StageUtilization>,
matching_utilization: &Arc<melin_transport_core::pipeline::StageUtilization>,
response_utilization: &Arc<melin_transport_core::pipeline::StageUtilization>,
shutdown: &Arc<AtomicBool>,
raft_status: &Option<Arc<melin_transport_core::health::RaftStatus>>,
) -> Result<Option<std::thread::JoinHandle<()>>, Box<dyn std::error::Error>> {
let Some(health_addr) = config.health_bind else {
return Ok(None);
};
let (repl_ring_producers, repl_ring_consumers) = replication_ring_progress
.as_ref()
.map(|rp| {
(
Some([
Arc::clone(&rp.producer_cursors[0]),
Arc::clone(&rp.producer_cursors[1]),
]),
Some([
Arc::clone(&rp.consumer_cursors[0]),
Arc::clone(&rp.consumer_cursors[1]),
]),
)
})
.unwrap_or((None, None));
Ok(Some(melin_transport_core::health::spawn(
health_addr,
melin_transport_core::health::HealthState {
active_connections: Arc::clone(active_connections),
events_processed: Arc::clone(events_processed),
cursors: cursors.clone(),
input_cursor,
pipeline_healthy: Arc::clone(pipeline_healthy),
replicas_connected: replicas_connected.clone(),
fence_state: Some(Arc::clone(fence_state)),
replication_metrics: replication_metrics.clone(),
replica_active: replica_active.clone(),
replication_ring_producer_cursors: repl_ring_producers,
replication_ring_consumer_cursors: repl_ring_consumers,
journal_utilization: Arc::clone(journal_utilization),
matching_utilization: Arc::clone(matching_utilization),
response_utilization: Arc::clone(response_utilization),
raft: raft_status.clone(),
},
Arc::clone(shutdown),
)?))
}
fn spawn_event_publisher<A: Application>(
consumer: Option<Consumer<OutputSlot<A>>>,
run_fn: Option<EventPublisherFn<A>>,
config: &ServerConfig,
cores: &PipelineCores,
authorized_keys: &Arc<AuthorizedKeys>,
shutdown: &Arc<AtomicBool>,
busy_spin: bool,
) -> Result<Option<std::thread::JoinHandle<()>>, Box<dyn std::error::Error>>
where
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
{
let (Some(event_consumer), Some(run_fn)) = (consumer, run_fn) else {
return Ok(None);
};
let event_bind = config
.event_bind
.ok_or("event_bind must be set when event publisher is enabled")?;
let s_event = Arc::clone(shutdown);
let event_keys = Arc::clone(authorized_keys);
let event_core = cores.event_publisher;
let event_handle = std::thread::Builder::new()
.name("event-publisher".into())
.spawn(move || {
melin_app::affinity::pin_thread("event-publisher", event_core);
run_fn(event_consumer, event_bind, event_keys, &s_event, busy_spin);
})
.map_err(|e| format!("spawn event publisher thread: {e}"))?;
info!(addr = %event_bind, "event publisher started");
Ok(Some(event_handle))
}
fn authenticate_connection<R: std::io::Read, W: std::io::Write>(
connection_id: ConnectionId,
addr: SocketAddr,
reader: &mut R,
writer: &mut W,
authorized_keys: &AuthorizedKeys,
) -> Result<(Permission, [u8; 32]), Box<dyn std::error::Error>> {
use std::io;
use ed25519_dalek::{Verifier, VerifyingKey};
use melin_wire_protocol::control::TransportResponse;
use melin_wire_protocol::control_codec;
let mut nonce = [0u8; 32];
getrandom::fill(&mut nonce).map_err(|e| io::Error::other(format!("getrandom failed: {e}")))?;
let mut buf = [0u8; 128];
let written =
control_codec::encode_transport_response(&TransportResponse::Challenge { nonce }, &mut buf)
.map_err(|e| io::Error::other(format!("encode Challenge: {e}")))?;
writer.write_all(&buf[..written])?;
writer.flush()?;
let mut len_buf = [0u8; 4];
reader
.read_exact(&mut len_buf)
.map_err(|e| io::Error::other(format!("read auth frame length: {e}")))?;
let frame_len = u32::from_le_bytes(len_buf) as usize;
if frame_len > 256 {
send_auth_failed(writer);
return Err(io::Error::other(format!("auth frame too large: {frame_len}")).into());
}
let mut frame_buf = [0u8; 256];
reader
.read_exact(&mut frame_buf[..frame_len])
.map_err(|e| io::Error::other(format!("read auth frame payload: {e}")))?;
let (_seq, cr) = match control_codec::decode_challenge_response(&frame_buf[..frame_len]) {
Ok(pair) => pair,
Err(e) => {
send_auth_failed(writer);
return Err(io::Error::other(format!("decode ChallengeResponse: {e}")).into());
}
};
let (signature_bytes, public_key_bytes) = (cr.signature, cr.public_key);
let permission = match authorized_keys.lookup(&public_key_bytes) {
Some(perm) => perm,
None => {
send_auth_failed(writer);
return Err("unknown public key".into());
}
};
let verifying_key = VerifyingKey::from_bytes(&public_key_bytes).map_err(|e| {
send_auth_failed(writer);
io::Error::other(format!("invalid public key: {e}"))
})?;
let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes);
verifying_key.verify(&nonce, &signature).map_err(|e| {
send_auth_failed(writer);
io::Error::other(format!("signature verification failed: {e}"))
})?;
let written =
control_codec::encode_transport_response(&TransportResponse::ServerReady, &mut buf)
.map_err(|e| io::Error::other(format!("encode ServerReady: {e}")))?;
writer.write_all(&buf[..written])?;
writer.flush()?;
debug!(
connection_id = connection_id.0,
addr = %addr,
permission = ?permission,
"authenticated"
);
Ok((permission, public_key_bytes))
}
fn set_read_timeout<F: std::os::unix::io::AsRawFd>(
fd: &F,
timeout: Option<std::time::Duration>,
) -> std::io::Result<()> {
let tv = match timeout {
Some(d) => libc::timeval {
tv_sec: d.as_secs() as libc::time_t,
tv_usec: d.subsec_micros() as libc::suseconds_t,
},
None => libc::timeval {
tv_sec: 0,
tv_usec: 0,
},
};
let ret = unsafe {
libc::setsockopt(
fd.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_RCVTIMEO,
&tv as *const libc::timeval as *const libc::c_void,
std::mem::size_of::<libc::timeval>() as libc::socklen_t,
)
};
if ret < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
pub(crate) fn set_busy_poll<F: std::os::unix::io::AsRawFd>(
fd: &F,
micros: i32,
) -> std::io::Result<()> {
let val: libc::c_int = micros;
let ret = unsafe {
libc::setsockopt(
fd.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_BUSY_POLL,
&val as *const libc::c_int as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
)
};
if ret < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
pub(crate) const BUSY_POLL_US: i32 = 50;
fn set_write_timeout<F: std::os::unix::io::AsRawFd>(
fd: &F,
timeout: Option<std::time::Duration>,
) -> std::io::Result<()> {
let tv = match timeout {
Some(d) => libc::timeval {
tv_sec: d.as_secs() as libc::time_t,
tv_usec: d.subsec_micros() as libc::suseconds_t,
},
None => libc::timeval {
tv_sec: 0,
tv_usec: 0,
},
};
let ret = unsafe {
libc::setsockopt(
fd.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_SNDTIMEO,
&tv as *const libc::timeval as *const libc::c_void,
std::mem::size_of::<libc::timeval>() as libc::socklen_t,
)
};
if ret < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
fn send_auth_failed(writer: &mut impl std::io::Write) {
use melin_wire_protocol::control::TransportResponse;
use melin_wire_protocol::control_codec;
let mut buf = [0u8; 8];
if let Ok(written) =
control_codec::encode_transport_response(&TransportResponse::AuthFailed, &mut buf)
{
let _ = writer.write_all(&buf[..written]);
let _ = writer.flush();
}
}
#[cfg(test)]
mod tests {
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use ed25519_dalek::{Signer, SigningKey};
use melin_app::auth::AuthorizedKeys;
use melin_app::auth::Permission;
use melin_wire_protocol::control::ConnectionId;
use melin_wire_protocol::control_codec::{
TAG_AUTH_FAILED, TAG_CHALLENGE, TAG_CHALLENGE_RESPONSE, TAG_RESPONSE_HEARTBEAT,
TAG_SERVER_READY,
};
use super::authenticate_connection;
use super::{BootstrapSource, choose_bootstrap};
#[test]
fn parse_cores_accepts_nine_through_eleven_entries() {
let nine = super::parse_cores("1,2,3,4,5,6,7,8,9").expect("9 entries must parse");
assert_eq!(nine.journal_prep, 0, "omitted journal-prep = unpinned");
assert_eq!(nine.journal_disk, 0, "omitted journal-disk = unpinned");
assert_eq!(nine.repl_handler_1, 9);
let ten = super::parse_cores("1,2,3,4,5,6,7,8,9,10").expect("10 entries must parse");
assert_eq!(ten.journal_prep, 10);
assert_eq!(ten.journal_disk, 0, "omitted journal-disk = unpinned");
let eleven = super::parse_cores("1,2,3,4,5,6,7,8,9,10,11").expect("11 entries must parse");
assert_eq!(eleven.journal_prep, 10);
assert_eq!(eleven.journal_disk, 11);
assert!(super::parse_cores("1,2,3").is_err());
assert!(super::parse_cores("1,2,3,4,5,6,7,8,9,10,11,12").is_err());
}
#[test]
fn parse_cores_skips_the_retired_fifth_entry_without_shifting() {
let cores = super::parse_cores("1,2,3,4,99,6,7,8,9,10,11").expect("11 entries must parse");
assert_eq!(cores.journal, 1);
assert_eq!(cores.matching, 2);
assert_eq!(cores.response, 3);
assert_eq!(cores.reader, 4);
assert_eq!(
cores.event_publisher, 6,
"sixth entry stays event-publisher"
);
assert_eq!(cores.shadow, 7);
assert_eq!(cores.repl_handler_0, 8);
assert_eq!(cores.repl_handler_1, 9);
assert_eq!(cores.journal_prep, 10);
assert_eq!(cores.journal_disk, 11);
}
#[test]
fn parse_cores_still_rejects_a_malformed_fifth_entry() {
assert!(super::parse_cores("1,2,3,4,x,6,7,8,9").is_err());
}
#[test]
fn bootstrap_decision_routes_archives_through_recovery() {
use BootstrapSource::*;
let matrix = [
((false, false, false), Fresh),
((false, false, true), JournalOnly), ((false, true, false), JournalOnly),
((false, true, true), JournalOnly),
((true, false, false), SnapshotOnly),
((true, false, true), SnapshotAndJournal), ((true, true, false), SnapshotAndJournal),
((true, true, true), SnapshotAndJournal),
];
for ((snap, live, arch), expected) in matrix {
assert_eq!(
choose_bootstrap(snap, live, arch),
expected,
"snapshot={snap} live={live} archives={arch}"
);
}
}
fn test_key() -> SigningKey {
SigningKey::from_bytes(&[0xAA; 32])
}
fn keys_with_test_key(perm: &str) -> AuthorizedKeys {
let pub_bytes = test_key().verifying_key().to_bytes();
let pub_b64 = base64_encode(&pub_bytes);
AuthorizedKeys::parse(&format!("{perm} {pub_b64} test\n")).unwrap()
}
fn base64_encode(bytes: &[u8]) -> String {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::new();
for chunk in bytes.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[(n >> 18 & 0x3F) as usize] as char);
out.push(ALPHABET[(n >> 12 & 0x3F) as usize] as char);
if chunk.len() > 1 {
out.push(ALPHABET[(n >> 6 & 0x3F) as usize] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(ALPHABET[(n & 0x3F) as usize] as char);
} else {
out.push('=');
}
}
out
}
fn run_server_auth(
mut stream: UnixStream,
keys: AuthorizedKeys,
) -> std::thread::JoinHandle<Result<Permission, String>> {
std::thread::spawn(move || {
let mut writer = stream.try_clone().unwrap();
authenticate_connection(
ConnectionId(1),
"127.0.0.1:0".parse().unwrap(),
&mut stream,
&mut writer,
&keys,
)
.map(|(perm, _pk)| perm)
.map_err(|e| e.to_string())
})
}
fn read_challenge(stream: &mut UnixStream) -> [u8; 32] {
let mut len_buf = [0u8; 4];
stream.read_exact(&mut len_buf).unwrap();
let len = u32::from_le_bytes(len_buf) as usize;
let mut payload = vec![0u8; len];
stream.read_exact(&mut payload).unwrap();
assert_eq!(payload[0], TAG_CHALLENGE, "expected Challenge");
payload[1..33].try_into().unwrap()
}
fn write_challenge_response(
stream: &mut UnixStream,
signature: [u8; 64],
public_key: [u8; 32],
) {
let mut frame = Vec::with_capacity(105);
frame.extend_from_slice(&0u64.to_le_bytes()); frame.push(TAG_CHALLENGE_RESPONSE);
frame.extend_from_slice(&signature);
frame.extend_from_slice(&public_key);
stream
.write_all(&(frame.len() as u32).to_le_bytes())
.unwrap();
stream.write_all(&frame).unwrap();
stream.flush().unwrap();
}
fn client_sign_challenge(stream: &mut UnixStream, key: &SigningKey) {
let nonce = read_challenge(stream);
let sig = key.sign(&nonce);
write_challenge_response(stream, sig.to_bytes(), key.verifying_key().to_bytes());
}
fn client_sign_challenge_bad(stream: &mut UnixStream, key: &SigningKey) {
let nonce = read_challenge(stream);
let mut sig_bytes = key.sign(&nonce).to_bytes();
sig_bytes[0] ^= 0xFF;
write_challenge_response(stream, sig_bytes, key.verifying_key().to_bytes());
}
fn read_response_tag(stream: &mut UnixStream) -> u8 {
let mut len_buf = [0u8; 4];
stream.read_exact(&mut len_buf).unwrap();
let len = u32::from_le_bytes(len_buf) as usize;
let mut buf = vec![0u8; len];
stream.read_exact(&mut buf).unwrap();
buf[0]
}
#[test]
fn auth_success_returns_permission() {
let keys = keys_with_test_key("trader");
let key = test_key();
let (s1, mut s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
client_sign_challenge(&mut s2, &key);
let resp = read_response_tag(&mut s2);
assert_eq!(resp, TAG_SERVER_READY);
let result = handle.join().unwrap();
assert_eq!(result.unwrap(), Permission::Trader);
}
#[test]
fn auth_admin_permission() {
let keys = keys_with_test_key("operator");
let key = test_key();
let (s1, mut s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
client_sign_challenge(&mut s2, &key);
let resp = read_response_tag(&mut s2);
assert_eq!(resp, TAG_SERVER_READY);
assert_eq!(handle.join().unwrap().unwrap(), Permission::Operator);
}
#[test]
fn auth_unknown_key_sends_auth_failed() {
let keys = AuthorizedKeys::parse("").unwrap();
let key = test_key();
let (s1, mut s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
client_sign_challenge(&mut s2, &key);
let resp = read_response_tag(&mut s2);
assert_eq!(resp, TAG_AUTH_FAILED);
assert!(handle.join().unwrap().is_err());
}
#[test]
fn auth_bad_signature_sends_auth_failed() {
let keys = keys_with_test_key("operator");
let key = test_key();
let (s1, mut s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
client_sign_challenge_bad(&mut s2, &key);
let resp = read_response_tag(&mut s2);
assert_eq!(resp, TAG_AUTH_FAILED);
assert!(handle.join().unwrap().is_err());
}
#[test]
fn auth_wrong_message_type_sends_auth_failed() {
let keys = keys_with_test_key("trader");
let (s1, mut s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
read_challenge(&mut s2);
let mut frame = vec![0u8; 105];
frame[8] = TAG_RESPONSE_HEARTBEAT; s2.write_all(&(frame.len() as u32).to_le_bytes()).unwrap();
s2.write_all(&frame).unwrap();
s2.flush().unwrap();
let resp = read_response_tag(&mut s2);
assert_eq!(resp, TAG_AUTH_FAILED);
assert!(handle.join().unwrap().is_err());
}
#[test]
fn auth_client_disconnects_is_error() {
let keys = keys_with_test_key("trader");
let (s1, s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
drop(s2);
assert!(handle.join().unwrap().is_err());
}
#[test]
fn auth_different_key_than_authorized_is_rejected() {
let keys = keys_with_test_key("trader");
let wrong_key = SigningKey::from_bytes(&[0xCC; 32]);
let (s1, mut s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
client_sign_challenge(&mut s2, &wrong_key);
let resp = read_response_tag(&mut s2);
assert_eq!(resp, TAG_AUTH_FAILED);
assert!(handle.join().unwrap().is_err());
}
#[test]
fn auth_oversized_frame_sends_auth_failed() {
let keys = keys_with_test_key("trader");
let (s1, mut s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
read_challenge(&mut s2);
let fake_len: u32 = 1000;
s2.write_all(&fake_len.to_le_bytes()).unwrap();
s2.flush().unwrap();
let resp = read_response_tag(&mut s2);
assert_eq!(resp, TAG_AUTH_FAILED);
assert!(handle.join().unwrap().is_err());
}
#[test]
fn auth_zero_length_frame_sends_auth_failed() {
let keys = keys_with_test_key("trader");
let (s1, mut s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
read_challenge(&mut s2);
let zero_len: u32 = 0;
s2.write_all(&zero_len.to_le_bytes()).unwrap();
s2.flush().unwrap();
let resp = read_response_tag(&mut s2);
assert_eq!(resp, TAG_AUTH_FAILED);
assert!(handle.join().unwrap().is_err());
}
#[test]
fn auth_readonly_permission() {
let keys = keys_with_test_key("readonly");
let key = test_key();
let (s1, mut s2) = UnixStream::pair().unwrap();
let handle = run_server_auth(s1, keys);
client_sign_challenge(&mut s2, &key);
let resp = read_response_tag(&mut s2);
assert_eq!(resp, TAG_SERVER_READY);
let perm = handle.join().unwrap().unwrap();
assert_eq!(perm, Permission::ReadOnly);
assert!(!perm.can_trade());
}
}
#[cfg(all(test, feature = "dpdk"))]
mod dpdk_config_tests {
use super::{ServerConfig, dpdk_config_from};
#[test]
fn a_supplied_peer_mac_reaches_the_transport_config() {
let cfg = ServerConfig {
dpdk_peer_mac: Some("0c:42:a1:5b:2e:80".into()),
..Default::default()
};
let dpdk = dpdk_config_from(&cfg).expect("valid config");
assert_eq!(dpdk.peer_mac, Some([0x0c, 0x42, 0xa1, 0x5b, 0x2e, 0x80]));
}
#[test]
fn an_absent_peer_mac_leaves_the_transport_to_derive_one() {
let cfg = ServerConfig::default();
let dpdk = dpdk_config_from(&cfg).expect("valid config");
assert_eq!(dpdk.peer_mac, None);
}
#[test]
fn a_malformed_peer_mac_fails_config_translation() {
let cfg = ServerConfig {
dpdk_peer_mac: Some("0c:42:a1:5b:2e".into()),
..Default::default()
};
let err = dpdk_config_from(&cfg).expect_err("a 5-octet MAC must be rejected");
assert!(err.contains("--dpdk-peer-mac"), "{err}");
}
#[test]
fn a_malformed_gateway_mac_fails_config_translation() {
let cfg = ServerConfig {
dpdk_gateway_mac: Some("not-a-mac".into()),
..Default::default()
};
let err = dpdk_config_from(&cfg).expect_err("a non-MAC must be rejected");
assert!(err.contains("--dpdk-gateway-mac"), "{err}");
}
#[test]
fn a_malformed_ip_names_the_flag_it_came_from() {
let cfg = ServerConfig {
dpdk_peer_ip: Some("10.0.0".into()),
..Default::default()
};
let err = dpdk_config_from(&cfg).expect_err("a truncated IPv4 must be rejected");
assert!(err.contains("--dpdk-peer-ip"), "{err}");
}
#[test]
fn a_valid_configuration_translates() {
let cfg = ServerConfig {
dpdk_gateway: Some("10.0.0.254".into()),
dpdk_gateway_mac: Some("0c:42:a1:5b:2e:80".into()),
..Default::default()
};
let dpdk = dpdk_config_from(&cfg).expect("valid config");
assert_eq!(dpdk.gateway_mac, Some([0x0c, 0x42, 0xa1, 0x5b, 0x2e, 0x80]));
}
}