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_journal::SectorWriter;
use melin_transport_core::journaled_app::JournaledApp;
use melin_transport_core::pipeline::{
InputSlot, JournalStage, JournalStageRun, OutputSlot as GenericOutputSlot,
Pipeline as GenericPipeline, build_pipeline_with_replication,
};
type Pipeline<A, W> = GenericPipeline<A, W>;
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 = 8;
#[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,6,7,8,9,10", value_parser = parse_cores)]
pub cores: PipelineCores,
#[arg(long, default_value_t = 4)]
pub reader_cores: usize,
#[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 = melin_journal::JournalWriterMode::default(),
value_parser = melin_journal::JournalWriterMode::parse,
)]
pub journal_writer: melin_journal::JournalWriterMode,
#[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::durability_policy::DurabilityMode::Hybrid)]
pub durability_mode: crate::durability_policy::DurabilityMode,
#[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, 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,
}
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,
repl_sender: 6,
event_publisher: 7,
shadow: 8,
repl_handler_0: 9,
repl_handler_1: 10,
},
reader_cores: 4,
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,
journal_writer: melin_journal::JournalWriterMode::default(),
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,
durability_mode: crate::durability_policy::DurabilityMode::Hybrid,
yield_idle: false,
dpdk_eal_args: String::new(),
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,
}
}
}
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 repl_sender: usize,
pub event_publisher: usize,
pub shadow: usize,
pub repl_handler_0: usize,
pub repl_handler_1: 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,
repl_sender: 4,
event_publisher: 5,
shadow: 6,
repl_handler_0: 0,
repl_handler_1: 0,
};
Ok((cores, 7))
}
}
fn parse_cores(s: &str) -> Result<PipelineCores, String> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 8 {
return Err(format!(
"expected 8 comma-separated core IDs (journal,matching,response,repl-sender,event-publisher,shadow,repl-handler-0,repl-handler-1), got {}",
parts.len()
));
}
let parse = |p: &str| {
p.parse::<usize>()
.map_err(|_| format!("invalid core ID: {p}"))
};
Ok(PipelineCores {
journal: parse(parts[0])?,
matching: parse(parts[1])?,
response: parse(parts[2])?,
repl_sender: parse(parts[3])?,
event_publisher: parse(parts[4])?,
shadow: parse(parts[5])?,
repl_handler_0: parse(parts[6])?,
repl_handler_1: parse(parts[7])?,
})
}
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,
{
match config.journal_writer {
melin_journal::JournalWriterMode::Buffered => run_impl::<A, L, BufferedWriter<A::Event>>(
listener,
config,
factory,
decoder,
encoder,
event_publisher,
shutdown,
),
melin_journal::JournalWriterMode::Sector => run_impl::<A, L, SectorWriter<A::Event>>(
listener,
config,
factory,
decoder,
encoder,
event_publisher,
shutdown,
),
}
}
fn run_impl<A, L, W>(
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,
W: JournalWrite<A::Event> + Send + 'static,
JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
{
let durability_mode_atomic = Arc::new(AtomicU8::new(config.durability_mode.as_u8()));
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 promote_flag = Arc::new(AtomicBool::new(false));
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(Arc::clone(&promote_flag)),
rotate_flag.clone(),
Some(Arc::clone(&durability_mode_atomic)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
});
let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
let rotation = match (max_journal_bytes, rotate_flag.as_ref()) {
(0, None) => None,
(b, f) => Some((
b,
f.cloned()
.unwrap_or_else(|| Arc::new(AtomicBool::new(false))),
)),
};
match crate::replication::run_receiver::<A, W>(
primary_addr,
&config.journal,
&signing_key,
&shutdown,
&promote_flag,
config.snapshot_interval_ms,
config.shadow_snapshot_path(),
config.cores,
config.reader_cores,
config.group_commit_delay(),
config.replication_pipeline_depth,
!config.yield_idle,
rotation,
Arc::clone(&factory),
)? {
None => return Ok(()), Some((mut exchange, writer)) => {
info!("replica promoted — transitioning to primary");
<A as Application>::prefault(&mut exchange);
return run_as_primary::<A, L, W>(
exchange,
writer,
listener,
&config,
&*factory,
decoder,
encoder,
event_publisher,
shutdown,
authorized_keys,
false, rotate_flag,
durability_mode_atomic,
);
}
}
}
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(&durability_mode_atomic)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
});
let (mut exchange, writer, needs_seeding) = init_engine::<A, W>(&config, &*factory)?;
<A as Application>::prefault(&mut exchange);
run_as_primary::<A, L, W>(
exchange,
writer,
listener,
&config,
&*factory,
decoder,
encoder,
event_publisher,
shutdown,
authorized_keys,
needs_seeding,
rotate_flag,
durability_mode_atomic,
)
}
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, W>(
exchange: A,
writer: W,
mut listener: L,
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>>,
durability_mode_atomic: Arc<AtomicU8>,
) -> 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,
W: JournalWrite<A::Event> + Send + 'static,
JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
{
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.durability_mode != crate::durability_policy::DurabilityMode::Local
{
return Err(format!(
"--standalone requires --durability-mode local; got `{}` (this mode needs at least one connected replica)",
config.durability_mode,
)
.into());
}
let genesis_entry = if enable_replication {
writer
.read_genesis_entry()
.map_err(|e| format!("failed to read genesis entry: {e}"))?
} else {
Vec::new()
};
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,
journal_cursor,
matching_cursor,
events_processed,
input_cursor,
replication_consumers,
replication_cursor,
replicas_connected,
shadow_consumer,
chain_hash_lock,
replication_ring_progress,
last_seq,
} = 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,
);
let fastest_replica_cursor = Arc::new(AtomicU64::new(u64::MAX));
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());
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(),
&durability_mode_atomic,
config.durability_mode,
);
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 = Arc::clone(&last_seq);
let replication_metrics_response = replication_metrics.as_ref().map(Arc::clone);
let replica_active_response = replica_active.clone();
let durability_mode_response = Arc::clone(&durability_mode_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_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,
durability_mode: durability_mode_response,
replication_metrics: replication_metrics_response,
replica_active: replica_active_response,
heartbeat_interval,
busy_spin,
utilization: response_utilization_thread,
encoder,
},
&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 repl_bind = config
.replication_bind
.ok_or("replication_bind must be set when replication is enabled")?;
let s_repl = Arc::clone(&shutdown);
let repl_cursor = Arc::clone(&replication_cursor);
let fastest_repl_cursor = Arc::clone(&fastest_replica_cursor);
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 repl_sender_handle = std::thread::Builder::new()
.name("repl-sender".into())
.spawn(move || {
melin_app::affinity::pin_thread("repl-sender", cores.repl_sender);
crate::replication::run_sender::<A>(
crate::replication::Sender {
bind_addr: repl_bind,
repl_consumer_1,
repl_consumer_2,
replication_cursor: repl_cursor,
fastest_replica_cursor: fastest_repl_cursor,
genesis_entry,
journal_path,
authorized_keys: repl_auth_keys,
evict_flags,
active_flags,
metrics: repl_metrics,
handler_cores,
batch_size,
heartbeat_secs,
busy_spin,
},
&s_repl,
&ready_flag,
&connected_counter,
);
})
.map_err(|e| format!("spawn replication sender thread: {e}"))?;
info!(addr = %repl_bind, "replication listener started");
Some(repl_sender_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,
)?;
let pipeline_healthy = Arc::new(AtomicBool::new(true));
let health_handle = spawn_health_endpoint(
config,
&active_connections,
&events_processed,
&journal_cursor,
&matching_cursor,
input_cursor,
&replication_cursor,
&pipeline_healthy,
&replicas_connected,
&replication_metrics,
&replication_ring_progress,
&fastest_replica_cursor,
&journal_utilization,
&matching_utilization,
&response_utilization,
&shutdown,
)?;
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();
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"
);
}
let reader_shutdown = Arc::new(AtomicBool::new(false));
let mut reader_handle = crate::reader::spawn_reader::<A, _>(
input_producer,
decoder,
control_tx.clone(),
config.reader_cores,
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);
match config.journal_writer {
melin_journal::JournalWriterMode::Buffered => run_dpdk_impl::<A, BufferedWriter<A::Event>>(
config,
factory,
decoder,
encoder,
event_publisher,
dpdk_config,
shutdown,
),
melin_journal::JournalWriterMode::Sector => run_dpdk_impl::<A, SectorWriter<A::Event>>(
config,
factory,
decoder,
encoder,
event_publisher,
dpdk_config,
shutdown,
),
}
}
#[cfg(feature = "dpdk")]
fn dpdk_config_from(cfg: &ServerConfig) -> melin_dpdk::DpdkConfig {
melin_dpdk::DpdkConfig {
eal_args: cfg
.dpdk_eal_args
.split_whitespace()
.map(String::from)
.collect(),
port_ids: cfg.dpdk_ports.clone(),
ip_addr: cfg.dpdk_ip.parse().expect("invalid --dpdk-ip address"),
prefix_len: cfg.dpdk_prefix_len,
gateway: cfg
.dpdk_gateway
.as_deref()
.map(|s| s.parse().expect("invalid --dpdk-gateway address")),
listen_port: cfg.bind.port(),
mtu: cfg.dpdk_mtu,
vlan_id: cfg.dpdk_vlan,
num_queues: 1,
}
}
#[cfg(feature = "dpdk")]
fn run_dpdk_impl<A, W>(
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,
W: JournalWrite<A::Event> + Send + 'static,
JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
{
let durability_mode_atomic = Arc::new(AtomicU8::new(config.durability_mode.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 promote_flag = Arc::new(AtomicBool::new(false));
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(Arc::clone(&promote_flag)),
rotate_flag.clone(),
Some(Arc::clone(&durability_mode_atomic)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
});
let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
let rotation = match (max_journal_bytes, rotate_flag.as_ref()) {
(0, None) => None,
(b, f) => Some((
b,
f.cloned()
.unwrap_or_else(|| Arc::new(AtomicBool::new(false))),
)),
};
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, W>(
repl_transport,
primary_ipv4,
primary_addr.port(),
&config.journal,
&shutdown,
&promote_flag,
config.snapshot_interval_ms,
config.shadow_snapshot_path(),
config.cores,
config.reader_cores,
config.group_commit_delay(),
config.replication_pipeline_depth,
!config.yield_idle,
rotation,
Arc::clone(&factory),
)? {
None => return Ok(()), Some((mut exchange, writer)) => {
info!("replica promoted (DPDK) — transitioning to primary");
<A as Application>::prefault(&mut exchange);
warn!("DPDK primary promotion not yet implemented — falling back to kernel TCP");
let listener = melin_wire_protocol::tcp::BlockingTcpListener::bind(config.bind)?;
return run_as_primary::<A, _, W>(
exchange,
writer,
listener,
&config,
&*factory,
decoder,
encoder,
event_publisher,
shutdown,
authorized_keys,
false,
rotate_flag,
durability_mode_atomic,
);
}
}
}
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) = init_engine::<A, W>(&config, &*factory)?;
<A as Application>::prefault(&mut exchange);
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.durability_mode != crate::durability_policy::DurabilityMode::Local
{
return Err(format!(
"--standalone requires --durability-mode local; got `{}` (this mode needs at least one connected replica)",
config.durability_mode,
)
.into());
}
let genesis_entry = if enable_replication {
writer
.read_genesis_entry()
.map_err(|e| format!("failed to read genesis entry: {e}"))?
} else {
Vec::new()
};
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,
journal_cursor,
matching_cursor,
events_processed,
input_cursor,
replication_consumers,
replication_cursor,
replicas_connected,
shadow_consumer,
chain_hash_lock,
replication_ring_progress,
last_seq,
} = 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,
);
let heartbeat_interval = config.heartbeat_interval();
let mut input_producer = input_producer;
let tick_cadence = config.tick_interval();
let fastest_replica_cursor = Arc::new(AtomicU64::new(u64::MAX));
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(&durability_mode_atomic)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
});
let max_journal_bytes = config.max_journal_mib.saturating_mul(1024 * 1024);
journal_stage.set_rotation(max_journal_bytes, rotate_flag.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(),
&durability_mode_atomic,
config.durability_mode,
);
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 = Arc::clone(&last_seq);
let replication_metrics_response = replication_metrics.as_ref().map(Arc::clone);
let replica_active_response = replica_active.clone();
let durability_mode_response = Arc::clone(&durability_mode_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,
durability_mode_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,
)?;
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_cursor = Arc::clone(&replication_cursor);
let fastest_repl_cursor = Arc::clone(&fastest_replica_cursor);
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)
.map_err(|e| format!("add replication listener: {e}"))?;
let driver = crate::replication::DpdkReplicationDriver::new(
[repl_consumer_1, repl_consumer_2],
repl_cursor,
fastest_repl_cursor,
genesis_entry,
journal_path,
ready_flag,
connected_counter,
dpdk_evict_flags,
dpdk_active_flags,
repl_metrics,
batch_size,
heartbeat_secs,
);
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 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 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,
&journal_cursor,
&matching_cursor,
input_cursor,
&replication_cursor,
&pipeline_healthy,
&replicas_connected,
&replication_metrics,
&replication_ring_progress,
&fastest_replica_cursor,
&journal_utilization,
&matching_utilization,
&response_utilization,
&shutdown,
)?;
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_cores = config.reader_cores;
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_cores);
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,
)
}
pub(crate) fn init_engine<A, W>(
config: &ServerConfig,
factory: &dyn AppFactory<App = A>,
) -> Result<(A, W, bool), 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 writer_mode = config.journal_writer;
let mut engine: JournaledApp<A, W> = if let Some(snap_path) = snap_path
&& snap_path.exists()
&& journal_exists
{
info!(snapshot = %snap_path.display(), writer_mode = %writer_mode, "recovering from snapshot + journal");
JournaledApp::<A, W>::recover_from_snapshot(snap_path, &config.journal)?
} else if let Some(snap_path) = snap_path
&& snap_path.exists()
&& !journal_exists
{
info!(
snapshot = %snap_path.display(),
writer_mode = %writer_mode,
"recovering from snapshot only (journal missing, post-rotation crash?)"
);
let (app, snap_sequence, snap_chain_hash) =
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)
} else if journal_exists {
info!(writer_mode = %writer_mode, "recovering from journal");
let mut app = factory.empty();
factory.prefault(&mut app);
JournaledApp::<A, W>::recover(app, &config.journal)?
} else {
info!(writer_mode = %writer_mode, "creating new journal");
let mut app = factory.empty();
factory.prefault(&mut app);
JournaledApp::<A, W>::create(app, &config.journal)?
};
let needs_seeding = !journal_exists;
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 (app, writer) = engine.into_parts();
Ok((app, writer, needs_seeding))
}
fn build_replication_metrics(
has_replication: bool,
durability_mode_atomic: &AtomicU8,
config_mode: crate::durability_policy::DurabilityMode,
) -> Option<Arc<crate::replication::ReplicationMetrics>> {
let metrics = if has_replication {
Some(Arc::new(crate::replication::ReplicationMetrics::default()))
} else {
None
};
let active_mode = crate::durability_policy::DurabilityMode::from_u8(
durability_mode_atomic.load(Ordering::Relaxed),
)
.unwrap_or(config_mode);
info!(
mode = %active_mode,
policy = %active_mode.to_policy(),
"durability mode 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<
Arc<melin_pipeline::seqlock::SeqLock<melin_transport_core::pipeline::FsyncState>>,
>,
config: &ServerConfig,
cores: &PipelineCores,
shutdown: &Arc<AtomicBool>,
busy_spin: bool,
) -> 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 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,
);
})
.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>,
journal_cursor: &Arc<melin_pipeline::padding::Sequence>,
matching_cursor: &Arc<melin_pipeline::padding::Sequence>,
input_cursor: Box<dyn melin_pipeline::ring::QueueCursor>,
replication_cursor: &Arc<AtomicU64>,
pipeline_healthy: &Arc<AtomicBool>,
replicas_connected: &Option<Arc<std::sync::atomic::AtomicU32>>,
replication_metrics: &Option<Arc<crate::replication::ReplicationMetrics>>,
replication_ring_progress: &Option<melin_transport_core::pipeline::ReplicationRingProgress>,
fastest_replica_cursor: &Arc<AtomicU64>,
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>,
) -> 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));
let fastest_repl_cursor_health = replication_metrics
.as_ref()
.map(|_| Arc::clone(fastest_replica_cursor));
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),
journal_cursor: Arc::clone(journal_cursor),
matching_cursor: Arc::clone(matching_cursor),
input_cursor,
replication_cursor: Arc::clone(replication_cursor),
pipeline_healthy: Arc::clone(pipeline_healthy),
replicas_connected: replicas_connected.clone(),
replication_metrics: replication_metrics.clone(),
replication_ring_producer_cursors: repl_ring_producers,
replication_ring_consumer_cursors: repl_ring_consumers,
fastest_replica_cursor: fastest_repl_cursor_health,
journal_utilization: Arc::clone(journal_utilization),
matching_utilization: Arc::clone(matching_utilization),
response_utilization: Arc::clone(response_utilization),
},
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_protocol::codec;
use melin_protocol::message::{ConnectionId, Request, ResponseKind};
use super::authenticate_connection;
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 client_sign_challenge(stream: &mut UnixStream, key: &SigningKey) {
let mut len_buf = [0u8; 4];
let mut payload = [0u8; 128];
stream.read_exact(&mut len_buf).unwrap();
let len = u32::from_le_bytes(len_buf) as usize;
stream.read_exact(&mut payload[..len]).unwrap();
let resp = codec::decode_response(&payload[..len]).unwrap();
let nonce = match resp {
ResponseKind::Challenge { nonce } => nonce,
other => panic!("expected Challenge, got {other:?}"),
};
let sig = key.sign(&nonce);
let request = Request::ChallengeResponse {
signature: sig.to_bytes(),
public_key: key.verifying_key().to_bytes(),
};
let mut buf = [0u8; 256];
let written = codec::encode_request(&request, 0, &mut buf).unwrap();
stream.write_all(&buf[..written]).unwrap();
stream.flush().unwrap();
}
fn client_sign_challenge_bad(stream: &mut UnixStream, key: &SigningKey) {
let mut len_buf = [0u8; 4];
let mut payload = [0u8; 128];
stream.read_exact(&mut len_buf).unwrap();
let len = u32::from_le_bytes(len_buf) as usize;
stream.read_exact(&mut payload[..len]).unwrap();
let resp = codec::decode_response(&payload[..len]).unwrap();
let nonce = match resp {
ResponseKind::Challenge { nonce } => nonce,
other => panic!("expected Challenge, got {other:?}"),
};
let mut sig_bytes = key.sign(&nonce).to_bytes();
sig_bytes[0] ^= 0xFF;
let request = Request::ChallengeResponse {
signature: sig_bytes,
public_key: key.verifying_key().to_bytes(),
};
let mut buf = [0u8; 256];
let written = codec::encode_request(&request, 0, &mut buf).unwrap();
stream.write_all(&buf[..written]).unwrap();
stream.flush().unwrap();
}
fn read_response(stream: &mut UnixStream) -> ResponseKind {
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 = [0u8; 64];
stream.read_exact(&mut buf[..len]).unwrap();
codec::decode_response(&buf[..len]).unwrap()
}
#[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(&mut s2);
assert!(matches!(resp, ResponseKind::ServerReady));
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(&mut s2);
assert!(matches!(resp, ResponseKind::ServerReady));
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(&mut s2);
assert!(matches!(resp, ResponseKind::AuthFailed));
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(&mut s2);
assert!(matches!(resp, ResponseKind::AuthFailed));
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);
let mut len_buf = [0u8; 4];
let mut payload = [0u8; 128];
s2.read_exact(&mut len_buf).unwrap();
let len = u32::from_le_bytes(len_buf) as usize;
s2.read_exact(&mut payload[..len]).unwrap();
let mut buf = [0u8; 16];
let written = codec::encode_request(&Request::Heartbeat, 0, &mut buf).unwrap();
s2.write_all(&buf[..written]).unwrap();
s2.flush().unwrap();
let resp = read_response(&mut s2);
assert!(matches!(resp, ResponseKind::AuthFailed));
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(&mut s2);
assert!(matches!(resp, ResponseKind::AuthFailed));
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);
let mut len_buf = [0u8; 4];
let mut payload = [0u8; 128];
s2.read_exact(&mut len_buf).unwrap();
let len = u32::from_le_bytes(len_buf) as usize;
s2.read_exact(&mut payload[..len]).unwrap();
let fake_len: u32 = 1000;
s2.write_all(&fake_len.to_le_bytes()).unwrap();
s2.flush().unwrap();
let resp = read_response(&mut s2);
assert!(matches!(resp, ResponseKind::AuthFailed));
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);
let mut len_buf = [0u8; 4];
let mut payload = [0u8; 128];
s2.read_exact(&mut len_buf).unwrap();
let len = u32::from_le_bytes(len_buf) as usize;
s2.read_exact(&mut payload[..len]).unwrap();
let zero_len: u32 = 0;
s2.write_all(&zero_len.to_le_bytes()).unwrap();
s2.flush().unwrap();
let resp = read_response(&mut s2);
assert!(matches!(resp, ResponseKind::AuthFailed));
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(&mut s2);
assert!(matches!(resp, ResponseKind::ServerReady));
let perm = handle.join().unwrap().unwrap();
assert_eq!(perm, Permission::ReadOnly);
assert!(!perm.can_trade());
}
}