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 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::StartupEvents;
use crate::reader::RequestDecoderArc;
use crate::response::ResponseEncoderArc;
use melin_app::Application;
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;
use melin_pipeline::wait::WaitStrategy;
use crate::layout::{DEFAULT_CORES, parse_cores};
pub use crate::layout::{PipelineCores, Placement, ReaderThread};
const ORCHESTRATOR_WAIT: WaitStrategy = WaitStrategy::SpinThenYield;
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,
wait: WaitStrategy,
);
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(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
pub enum JournalStagingMode {
#[default]
ZeroFill,
Allocate,
}
impl From<JournalStagingMode> for melin_journal::StagingMode {
fn from(m: JournalStagingMode) -> Self {
match m {
JournalStagingMode::ZeroFill => melin_journal::StagingMode::ZeroFill,
JournalStagingMode::Allocate => melin_journal::StagingMode::Allocate,
}
}
}
#[derive(clap::Parser)]
#[command(
name = "melin-server",
about = "A node of the Melin replicated sequencer"
)]
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 = DEFAULT_CORES, 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 = "authorized_keys")]
pub authorized_keys: PathBuf,
#[arg(long, default_value_t = 256)]
pub max_journal_mib: u64,
#[arg(long, value_enum, default_value_t = JournalStagingMode::ZeroFill)]
pub journal_staging_mode: JournalStagingMode,
#[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 = "", require_equals = 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_seq: Placement::spinning(1),
matching: Placement::spinning(2),
response: Placement::spinning(3),
reader: Placement::spinning(4),
event_publisher: Placement::spinning(6),
shadow: Placement::spinning(7),
repl_handler_0: Placement::spinning(8),
repl_handler_1: Placement::spinning(9),
journal_prep: Placement::yielding(10),
journal_disk: Placement::spinning(11),
},
group_commit_us: 0,
heartbeat_interval_secs: 10,
connection_timeout_secs: 30,
max_connections: 1024,
authorized_keys: PathBuf::from("authorized_keys"),
max_journal_mib: 256,
journal_staging_mode: JournalStagingMode::ZeroFill,
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,
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))
}
}
pub fn resolved_cores(&self, reader: ReaderThread) -> Result<PipelineCores, String> {
self.cores.resolve(reader)
}
}
pub fn run<A>(
config: ServerConfig,
startup: StartupEvents<A::Event>,
sizing: A::Sizing,
decoder: impl RequestDecoder<Event = A::Event> + 'static,
encoder: impl ResponseEncoder<Report = A::Report, Query = A::QueryResponse> + 'static,
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,
{
melin_app::affinity::capture_home_mask();
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::<A>(
config,
startup,
sizing,
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::<A, _>(
listener,
config,
startup,
sizing,
Arc::new(decoder),
Arc::new(encoder),
event_publisher,
shutdown,
)
}
}
pub fn run_with_listener<A>(
listener: impl BlockingTransportListener,
config: ServerConfig,
startup: StartupEvents<A::Event>,
sizing: A::Sizing,
decoder: impl RequestDecoder<Event = A::Event> + 'static,
encoder: impl ResponseEncoder<Report = A::Report, Query = A::QueryResponse> + 'static,
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,
{
melin_app::affinity::capture_home_mask();
run_tcp::<A, _>(
listener,
config,
startup,
sizing,
Arc::new(decoder),
Arc::new(encoder),
event_publisher,
shutdown,
)
}
fn run_tcp<A, L>(
listener: L,
config: ServerConfig,
startup: StartupEvents<A::Event>,
sizing: A::Sizing,
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,
startup,
sizing,
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 log_layout(cores: &PipelineCores) {
info!(cores = %cores, "pipeline layout");
if cores.pins_nothing() {
warn!(
"--cores none: no pipeline thread is pinned. Every thread runs wherever the \
scheduler puts it and shares that core with everything else on the host. A \
development layout: latency measured under it is not representative. Name a \
core per thread in --cores"
);
return;
}
let unpinned = cores.unpinned_mandatory();
if !unpinned.is_empty() {
warn!(
threads = %unpinned.join(", "),
"--cores: mandatory pipeline threads have no core of their own. Every request \
passes through them, and each runs wherever the scheduler puts it, sharing \
that core with whatever else is there. Give them cores"
);
}
}
fn run_impl<A, L>(
listener: L,
config: ServerConfig,
mut startup: StartupEvents<A::Event>,
sizing: A::Sizing,
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 config = ServerConfig {
cores: config.resolved_cores(ReaderThread::IoUring)?,
..config
};
log_layout(&config.cores);
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");
startup.genesis = Vec::new();
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.journal_staging_mode.into(),
config.group_commit_delay(),
config.replication_pipeline_depth,
Arc::clone(&fence_state),
&sizing,
)? {
None => return Ok(()),
Some((mut app, writer)) => {
info!("replica promoted — transitioning to primary");
replica_health.stop();
<A as Application>::prefault(&mut app, &sizing);
if let Some(ref flag) = rotate_flag {
flag.store(false, Ordering::Release);
}
return run_as_primary::<A, L>(
app,
writer,
listener,
repl_listener,
&config,
startup,
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 app, writer, needs_seeding, recovered_epoch) =
init_engine::<A, BufferedWriter<A::Event>>(&config, &sizing)?;
<A as Application>::prefault(&mut app, &sizing);
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>(
app,
writer,
listener,
repl_listener,
&config,
startup,
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-seq", error = %e, "journal stage returned error");
}
check_join("journal-seq", 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>(
app: A,
writer: BufferedWriter<A::Event>,
mut listener: L,
repl_listener: Option<crate::replication::ReplicationListener>,
config: &ServerConfig,
startup: StartupEvents<A::Event>,
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_app = if enable_shadow {
Some(<A as Application>::clone_via_snapshot(&app)?)
} 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(
app,
writer,
config.group_commit_delay(),
Arc::clone(&active_connections),
enable_replication,
config.max_journal_batch,
config.replication_ring_size,
config.cores.stage_waits(),
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 refused_writes = Arc::new(AtomicU64::new(0));
let halt_gate = crate::halt::HaltGate::new(
replicas_connected.clone(),
Arc::clone(&fence_state),
Arc::clone(&refused_writes),
);
let (refusal_tx, refusal_rx) =
crate::halt::refusal_channel::<A::Report>(Arc::clone(&matching_cursor));
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());
config.cores.place_journal_children(&mut journal_stage);
journal_stage.set_staging_mode(config.journal_staging_mode.into());
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 sequencer = journal_stage
.start()
.map_err(|e| format!("start the journal stage: {e}"))?;
let journal_handle = std::thread::Builder::new()
.name("journal-seq".into())
.spawn(move || {
melin_app::affinity::pin_thread("journal-seq", cores.journal_seq.core);
let result = sequencer.run(&s1);
let was_shutdown = shutdown_for_journal.load(Ordering::Relaxed);
match &result {
Ok(_) if was_shutdown => info!("journal-seq thread exited cleanly on shutdown"),
Ok(_) => error!("journal-seq thread returned without shutdown signal"),
Err(e) => error!(error = %e, "journal-seq thread returned Err"),
}
result
})
.map_err(|e| format!("spawn journal-seq 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.core);
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 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.core);
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,
wait: cores.response.wait,
utilization: response_utilization_thread,
encoder,
fence_state: response_fence,
active_connections: active_connections_response,
refusals: refusal_rx,
#[cfg(test)]
pause_after_control_drain: None,
},
&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 handlers = [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,
handlers,
batch_size,
heartbeat_secs,
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,
)?;
let shadow_handle = spawn_shadow_stage::<A>(
shadow_consumer,
shadow_app,
chain_hash_lock,
config,
&cores,
&shutdown,
fence_state.epoch(),
)?;
let pipeline_healthy = Arc::new(AtomicBool::new(true));
let health_handle = spawn_health_endpoint(
config,
&active_connections,
&events_processed,
&refused_writes,
&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 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,
sequence: 0,
timestamp_ns: unix_epoch_nanos(),
event: JournalEvent::EpochBump { epoch: new_epoch },
publish_ts: mono_trace_ns(),
recv_ts: mono_trace_ns(),
});
ORCHESTRATOR_WAIT
.wait_until(|| fence_state.epoch() >= new_epoch || shutdown.load(Ordering::Relaxed));
}
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));
}
}
let genesis = if needs_seeding {
startup.genesis
} else {
Vec::new()
};
journal_startup_events(
genesis,
startup.on_primary,
&mut input_producer,
&journal_cursor,
&matching_cursor,
&replication_ring_progress,
&shutdown,
);
let reader_shutdown = Arc::new(AtomicBool::new(false));
let mut reader_handle = crate::reader::spawn_reader::<A, _>(
input_producer,
decoder,
halt_gate,
refusal_tx,
control_tx.clone(),
config.cores.reader.core,
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 = melin_app::key_hash(&public_key_bytes);
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,
startup: StartupEvents<A::Event>,
sizing: A::Sizing,
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,
startup,
sizing,
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,
mut startup: StartupEvents<A::Event>,
sizing: A::Sizing,
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 config = ServerConfig {
cores: config.resolved_cores(ReaderThread::DpdkPoll)?,
..config
};
log_layout(&config.cores);
if !config.cores.reader.is_pinned() {
warn!(
"--cores: the reader is unpinned, but on DPDK it is the NIC poll thread and \
busy-polls regardless; it will take a full core wherever the scheduler places \
it. Give it a core of its own"
);
}
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)");
startup.genesis = Vec::new();
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.journal_staging_mode.into(),
config.group_commit_delay(),
config.replication_pipeline_depth,
Arc::clone(&fence_state),
&sizing,
)? {
None => return Ok(()),
Some((mut app, writer)) => {
info!("replica promoted (DPDK) — transitioning to primary");
replica_health.stop();
<A as Application>::prefault(&mut app, &sizing);
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, _>(
app,
writer,
listener,
repl_listener,
&config,
startup,
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 app, writer, needs_seeding, recovered_epoch) =
init_engine::<A, BufferedWriter<A::Event>>(&config, &sizing)?;
<A as Application>::prefault(&mut app, &sizing);
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_app = if enable_shadow {
Some(<A as Application>::clone_via_snapshot(&app)?)
} 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(
app,
writer,
config.group_commit_delay(),
Arc::clone(&active_connections),
enable_replication,
config.max_journal_batch,
config.replication_ring_size,
config.cores.stage_waits(),
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 refused_writes = Arc::new(AtomicU64::new(0));
let halt_gate = crate::halt::HaltGate::new(
replicas_connected.clone(),
Arc::clone(&fence_state),
Arc::clone(&refused_writes),
);
let (refusal_tx, refusal_rx) =
crate::halt::refusal_channel::<A::Report>(Arc::clone(&matching_cursor));
let mut tx_producers = Vec::with_capacity(num_dpdk_threads);
let mut tx_consumers = Vec::with_capacity(num_dpdk_threads);
let wait = config.cores.response.wait;
for _ in 0..num_dpdk_threads {
let (tx_out, tx_rx) =
melin_pipeline::spsc::channel::<crate::dpdk_response::TxFrame>(4096, wait);
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());
config.cores.place_journal_children(&mut journal_stage);
journal_stage.set_staging_mode(config.journal_staging_mode.into());
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 sequencer = journal_stage
.start()
.map_err(|e| format!("start the journal stage: {e}"))?;
let journal_handle = std::thread::Builder::new()
.name("journal-seq".into())
.spawn(move || {
melin_app::affinity::pin_thread("journal-seq", cores.journal_seq.core);
sequencer.run(&s1)
})
.map_err(|e| format!("spawn journal-seq 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.core);
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 response_handle = std::thread::Builder::new()
.name("response".into())
.spawn(move || {
melin_app::affinity::pin_thread("response", cores.response.core);
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,
wait,
encoder,
refusal_rx,
);
})
.map_err(|e| format!("spawn response thread: {e}"))?;
let shadow_handle = spawn_shadow_stage::<A>(
shadow_consumer,
shadow_app,
chain_hash_lock,
&config,
&cores,
&shutdown,
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); let genesis = if needs_seeding {
startup.genesis
} else {
Vec::new()
};
journal_startup_events(
genesis,
startup.on_primary,
&mut input_producer,
&journal_cursor,
&matching_cursor,
&replication_ring_progress,
&shutdown,
);
let pipeline_healthy = Arc::new(AtomicBool::new(true));
let health_handle = spawn_health_endpoint(
&config,
&active_connections,
&events_processed,
&refused_writes,
&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.core;
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,
halt_gate,
refusal_tx,
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,
)
}
fn journal_startup_events<E: melin_app::AppEvent>(
genesis: Vec<E>,
on_primary: Vec<E>,
input_producer: &mut melin_pipeline::ring::Producer<InputSlot<E>>,
journal_cursor: &melin_pipeline::padding::Sequence,
matching_cursor: &melin_pipeline::padding::Sequence,
replication_ring_progress: &Option<melin_transport_core::pipeline::ReplicationRingProgress>,
shutdown: &AtomicBool,
) {
use melin_app::unix_epoch_nanos;
use melin_journal::JournalEvent;
use melin_transport_core::trace::mono_trace_ns;
let (genesis_count, on_primary_count) = (genesis.len(), on_primary.len());
if genesis_count + on_primary_count == 0 {
return;
}
let start = std::time::Instant::now();
let mut last_published_seq = 0u64;
for event in genesis.into_iter().chain(on_primary) {
last_published_seq = input_producer.publish(InputSlot {
connection_id: 0,
key_hash: 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 = start.elapsed();
let target = last_published_seq + 1; info!(
target,
journal = journal_cursor.get().load(Ordering::Relaxed),
matching = matching_cursor.get().load(Ordering::Relaxed),
"startup events: waiting for pipeline cursors"
);
ORCHESTRATOR_WAIT.wait_until(|| {
shutdown.load(Ordering::Relaxed)
|| (journal_cursor.get().load(Ordering::Acquire) >= target
&& matching_cursor.get().load(Ordering::Acquire) >= target)
});
if let Some(ring_progress) = replication_ring_progress {
for i in 0..ring_progress.producer_cursors.len() {
if !ring_progress.active_flags[i].load(Ordering::Relaxed) {
continue;
}
let ring_target = ring_progress.producer_cursors[i].load();
ORCHESTRATOR_WAIT.wait_until(|| {
shutdown.load(Ordering::Relaxed)
|| ring_progress.consumer_cursors[i]
.get()
.load(Ordering::Acquire)
>= ring_target
});
}
}
info!(
genesis_events = genesis_count,
on_primary_events = on_primary_count,
publish_ms = publish_elapsed.as_millis(),
total_ms = start.elapsed().as_millis(),
"journaled startup events through pipeline"
);
}
#[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,
sizing: &A::Sizing,
) -> 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 = A::default();
<A as Application>::prefault(&mut app, sizing);
JournaledApp::<A, W>::recover(app, &config.journal)?
}
BootstrapSource::Fresh => {
info!("creating new journal");
JournaledApp::<A, W>::create(A::default(), &config.journal)?
}
};
let needs_seeding = !journal_exists && !archives_exist;
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_app: Option<A>,
chain_hash_lock: Option<
melin_pipeline::seqlock::SeqLockReader<melin_transport_core::pipeline::FsyncState>,
>,
config: &ServerConfig,
cores: &PipelineCores,
shutdown: &Arc<AtomicBool>,
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_app.ok_or("shadow application must be Some when shadow is enabled")?;
let s_shadow = Arc::clone(shutdown);
let shadow = 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,
shadow.wait,
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>,
refused_writes: &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),
refused_writes: Arc::clone(refused_writes),
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>,
) -> 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 publisher = cores.event_publisher;
let event_handle = std::thread::Builder::new()
.name("event-publisher".into())
.spawn(move || {
melin_app::affinity::pin_thread("event-publisher", publisher.core);
run_fn(
event_consumer,
event_bind,
event_keys,
&s_event,
publisher.wait,
);
})
.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 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 cr = match control_codec::decode_challenge_response(&frame_buf[..frame_len]) {
Ok(cr) => cr,
Err(e) => {
send_auth_failed(writer);
return Err(io::Error::other(format!("decode ChallengeResponse: {e}")).into());
}
};
let public_key_bytes = cr.public_key;
let permission = crate::client_auth::verify_client(
authorized_keys,
&nonce,
&public_key_bytes,
&cr.signature,
)
.inspect_err(|_| send_auth_failed(writer))?;
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 dpdk_eal_args_requires_the_joined_form() {
use clap::Parser;
let cfg = super::ServerConfig::try_parse_from([
"melin-server",
"--dpdk-eal-args=-l 0-7 --huge-dir /dev/hugepages",
])
.expect("joined form must parse");
assert_eq!(cfg.dpdk_eal_args, "-l 0-7 --huge-dir /dev/hugepages");
let space_form =
super::ServerConfig::try_parse_from(["melin-server", "--dpdk-eal-args", "-l 0-7"]);
assert!(space_form.is_err(), "space-separated form must be rejected");
let forgotten_value =
super::ServerConfig::try_parse_from(["melin-server", "--dpdk-eal-args", "--no-mlock"]);
assert!(
forgotten_value.is_err(),
"a forgotten value must error, not swallow the next flag"
);
}
#[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(97);
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; 97];
frame[0] = 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());
}
#[test]
fn auth_replication_key_refused_on_client_listener() {
let keys = keys_with_test_key("replication");
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);
let err = handle.join().unwrap().unwrap_err();
assert!(err.contains("client listener"), "unexpected error: {err}");
}
}
#[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]));
}
}