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, 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::lineage::{Lineage, LineagePolicy};
use melin_transport_core::node_store::{NodeStore, NodeStoreError};
use melin_transport_core::pipeline::{
InputSlot, OutputSlot as GenericOutputSlot, Pipeline as GenericPipeline,
build_pipeline_with_replication,
};
use melin_transport_core::replica_presence::ReplicaPresence;
type Pipeline<A> = GenericPipeline<A>;
use crate::StartupEvents;
use crate::client_auth::MAX_AUTH_FRAME;
use crate::reader::RequestDecoderArc;
use crate::response::ResponseEncoderArc;
use melin_app::auth::{AuthorizedKeys, ClientRole, KeyRole, RoleId};
use melin_app::decoder::{ErasedDecoder, RequestDecoder};
use melin_app::encoder::ResponseEncoder;
use melin_app::{AppEvent, Application};
use melin_pipeline::ring::Consumer;
use melin_pipeline::wait::WaitStrategy;
use crate::layout::{DEFAULT_CORES, parse_cores};
pub use crate::layout::{PipelineCores, Placement, ReaderThread};
pub use melin_dpdk::DpdkArgs;
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;
const fn kib_of(bytes: usize) -> u32 {
(bytes / 1024) as u32
}
#[cfg(feature = "dpdk")]
fn kib(value: u32) -> usize {
value as usize * 1024
}
#[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,
}
}
}
const DEFAULT_DPDK_IP: &str = "10.0.0.1";
#[derive(clap::Parser)]
#[command(
name = "melin-server",
about = "A node of the Melin replicated sequencer",
mut_arg("dpdk_ip", |arg| arg.default_value(DEFAULT_DPDK_IP))
)]
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, 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,
value_parser = clap::value_parser!(u64).range(1..=crate::connection_limit::MAX_SUPPORTED_CONNECTIONS)
)]
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_t = false)]
pub no_degraded_acks: bool,
#[arg(long, default_value_t = crate::durability_gate::DEFAULT_DEGRADED_ACK_GRACE_MS)]
pub degraded_ack_grace_ms: u64,
#[command(flatten)]
pub dpdk: DpdkArgs,
#[arg(
long,
default_value_t = kib_of(melin_dpdk::SocketBuffers::DEFAULT.rx_buf_size),
value_parser = clap::value_parser!(u32).range(1..)
)]
pub dpdk_client_rx_buf_kib: u32,
#[arg(
long,
default_value_t = kib_of(melin_dpdk::SocketBuffers::DEFAULT.tx_buf_size),
value_parser = clap::value_parser!(u32).range(1..)
)]
pub dpdk_client_tx_buf_kib: u32,
#[arg(
long,
default_value_t = kib_of(melin_dpdk::SocketBuffers::DEFAULT.tx_queue_limit),
value_parser = clap::value_parser!(u32).range(1..)
)]
pub dpdk_client_tx_queue_kib: u32,
#[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"),
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,
no_degraded_acks: false,
degraded_ack_grace_ms: crate::durability_gate::DEFAULT_DEGRADED_ACK_GRACE_MS,
dpdk: DpdkArgs {
ip: Some(melin_dpdk::NicConfig::default().ip_addr),
..DpdkArgs::default()
},
dpdk_client_rx_buf_kib: kib_of(melin_dpdk::SocketBuffers::DEFAULT.rx_buf_size),
dpdk_client_tx_buf_kib: kib_of(melin_dpdk::SocketBuffers::DEFAULT.tx_buf_size),
dpdk_client_tx_queue_kib: kib_of(melin_dpdk::SocketBuffers::DEFAULT.tx_queue_limit),
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(crate) fn replication(&self) -> crate::halt::Replication {
match self.replication_bind {
Some(_) => crate::halt::Replication::Enabled,
None => crate::halt::Replication::Disabled,
}
}
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 store(&self) -> Result<NodeStore, NodeStoreError> {
NodeStore::resolve(self.journal.clone(), self.snapshot_path.clone())
}
fn degraded_release(
&self,
replicas_connected: Option<&Arc<ReplicaPresence>>,
) -> Option<crate::halt::DegradedRelease> {
if self.no_degraded_acks {
return None;
}
Some(crate::halt::DegradedRelease {
presence: Arc::clone(replicas_connected?),
grace: std::time::Duration::from_millis(self.degraded_ack_grace_ms),
})
}
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 authorized_keys = load_authorized_keys(&decoder, &config.authorized_keys)?;
let shutdown = Arc::new(AtomicBool::new(false));
crate::process::install_shutdown_handler(&shutdown);
run_selected::<A>(
config,
startup,
sizing,
decoder,
encoder,
event_publisher,
authorized_keys,
shutdown,
)
}
pub fn run_with_shutdown<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>>,
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();
let authorized_keys = load_authorized_keys(&decoder, &config.authorized_keys)?;
run_selected::<A>(
config,
startup,
sizing,
decoder,
encoder,
event_publisher,
authorized_keys,
shutdown,
)
}
#[allow(clippy::too_many_arguments)] fn run_selected<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>>,
authorized_keys: Arc<AuthorizedKeys>,
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,
{
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,
authorized_keys,
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,
authorized_keys,
shutdown,
)
}
}
fn load_authorized_keys<D: RequestDecoder>(
_decoder: &D,
path: &std::path::Path,
) -> Result<Arc<AuthorizedKeys>, Box<dyn std::error::Error>> {
let keys = AuthorizedKeys::load::<D::Role>(path)?;
info!(keys = keys.len(), path = %path.display(), "loaded authorized keys");
Ok(Arc::new(keys))
}
fn check_keys_match_decoder<E: AppEvent>(
decoder: &dyn ErasedDecoder<E>,
authorized_keys: &AuthorizedKeys,
) -> Result<(), Box<dyn std::error::Error>> {
if decoder.matches_keys(authorized_keys) {
Ok(())
} else {
Err("authorized keys were parsed for a role type other than the decoder's".into())
}
}
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();
let authorized_keys = load_authorized_keys(&decoder, &config.authorized_keys)?;
run_tcp::<A, _>(
listener,
config,
startup,
sizing,
Arc::new(decoder),
Arc::new(encoder),
event_publisher,
authorized_keys,
shutdown,
)
}
#[allow(clippy::too_many_arguments)] 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>>,
authorized_keys: Arc<AuthorizedKeys>,
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,
{
check_keys_match_decoder(&*decoder, &authorized_keys)?;
run_impl::<A, L>(
listener,
config,
startup,
sizing,
decoder,
encoder,
event_publisher,
authorized_keys,
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"
);
}
}
#[allow(clippy::too_many_arguments)] 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>>,
authorized_keys: Arc<AuthorizedKeys>,
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 =
crate::policy_control::AckPolicyControl::new(config.ack_policy, config.replication());
validate_primary_config(&config)?;
let store = config.store()?;
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 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)),
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),
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,
&store,
&signing_key,
&shutdown,
&control,
config.snapshot_interval_ms,
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");
check_promotable(&writer)?;
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,
&store,
startup.on_primary,
decoder,
encoder,
event_publisher,
Arc::clone(&shutdown),
authorized_keys,
rotate_flag,
ack_policy,
fence_state,
control.lineage_ack_policy.load(),
promotion_request.pending(), false, raft_status,
journal_tip,
);
}
}
}
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)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
})
.transpose()?;
let InitializedEngine {
mut app,
writer,
recovered_lineage,
began_history,
} = init_engine::<A, BufferedWriter<A::Event>>(
&config,
&store,
&sizing,
std::mem::take(&mut startup.genesis),
)?;
<A as Application>::prefault(&mut app, &sizing);
let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(
recovered_lineage.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,
&store,
startup.on_primary,
decoder,
encoder,
event_publisher,
Arc::clone(&shutdown),
authorized_keys,
rotate_flag,
ack_policy,
fence_state,
recovered_lineage.ack_policy,
None, began_history,
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 validate_primary_config(config: &ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
crate::connection_limit::RingSizing::for_max_connections(config.max_connections)?;
if config.replication_bind.is_some() && 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());
}
Ok(())
}
fn check_promoted_history_holds_genesis(
next_sequence: u64,
genesis_entries: Option<u64>,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some((held, genesis_entries)) = genesis_shortfall(next_sequence, genesis_entries) {
return Err(format!(
"refusing promotion: this replica's history holds {held} entries but the \
lineage's genesis takes {genesis_entries}, so it copied only part of the genesis \
from a primary that stopped before finishing it; restart the original primary, or \
start the cluster again from a new journal"
)
.into());
}
Ok(())
}
pub(crate) fn check_promotable<E, W>(writer: &W) -> Result<(), Box<dyn std::error::Error>>
where
E: melin_app::AppEvent,
W: JournalWrite<E>,
{
check_promoted_history_holds_genesis(
writer.next_sequence(),
writer.read_header_info()?.genesis_entries,
)
}
fn check_recovered_history_holds_genesis(
next_sequence: u64,
genesis_entries: Option<u64>,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some((held, genesis_entries)) = genesis_shortfall(next_sequence, genesis_entries) {
return Err(format!(
"refusing to start: the recovered history holds {held} entries but the lineage's \
genesis takes {genesis_entries}, so it holds only part of the genesis (a replica's \
partial copy started as a primary); start again from a new history (remove the \
journal, its archives and its snapshots), or restart this node as a replica of a \
primary holding the whole history"
)
.into());
}
Ok(())
}
fn genesis_shortfall(next_sequence: u64, genesis_entries: Option<u64>) -> Option<(u64, u64)> {
let genesis_entries = genesis_entries?;
let held = next_sequence.saturating_sub(1);
(held < genesis_entries).then_some((held, genesis_entries))
}
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: Option<std::thread::JoinHandle<Result<W, JournalError>>>,
matching: Option<std::thread::JoinHandle<A>>,
response: Option<std::thread::JoinHandle<()>>,
replication: Option<std::thread::JoinHandle<()>>,
event_publisher: Option<std::thread::JoinHandle<()>>,
shadow: Option<std::thread::JoinHandle<()>>,
health: Option<std::thread::JoinHandle<()>>,
shutdown: Arc<AtomicBool>,
pipeline_healthy: Option<Arc<AtomicBool>>,
}
impl<A: Send + 'static, W: Send + 'static> PipelineHandles<A, W> {
fn new(shutdown: Arc<AtomicBool>) -> Self {
Self {
journal: None,
matching: None,
response: None,
replication: None,
event_publisher: None,
shadow: None,
health: None,
shutdown,
pipeline_healthy: None,
}
}
fn stage_died(&self) -> bool {
fn finished<T>(handle: &Option<std::thread::JoinHandle<T>>) -> bool {
handle.as_ref().is_some_and(|h| h.is_finished())
}
finished(&self.journal)
|| finished(&self.matching)
|| finished(&self.response)
|| finished(&self.event_publisher)
|| finished(&self.shadow)
}
fn stop_and_join(&mut self) -> bool {
if let Some(healthy) = &self.pipeline_healthy {
healthy.store(false, Ordering::Relaxed);
}
self.shutdown.store(true, Ordering::Relaxed);
fn panicked(name: &str, result: std::thread::Result<()>) -> bool {
let Err(panic) = result else {
return false;
};
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");
true
}
let mut failed = false;
if let Some(h) = self.journal.take() {
let journal_result = h.join();
if let Ok(Err(ref e)) = journal_result {
error!(thread = "journal-seq", error = %e, "journal stage returned error");
failed = true;
}
failed |= panicked("journal-seq", journal_result.map(|_| ()));
}
if let Some(h) = self.matching.take() {
failed |= panicked("matching", h.join().map(|_| ()));
}
if let Some(h) = self.response.take() {
failed |= panicked("response", h.join());
}
if let Some(h) = self.replication.take() {
failed |= panicked("replication-sender", h.join());
}
if let Some(h) = self.event_publisher.take() {
failed |= panicked("event-publisher", h.join());
}
if let Some(h) = self.shadow.take() {
failed |= panicked("shadow", h.join());
}
if let Some(h) = self.health.take() {
failed |= panicked("health", h.join());
}
failed
}
}
impl<A: Send + 'static, W: Send + 'static> Drop for PipelineHandles<A, W> {
fn drop(&mut self) {
let started = self.journal.is_some()
|| self.matching.is_some()
|| self.response.is_some()
|| self.replication.is_some()
|| self.event_publisher.is_some()
|| self.shadow.is_some()
|| self.health.is_some();
if started {
info!("startup failed: stopping the stages already running");
self.stop_and_join();
}
}
}
fn shutdown_pipeline_stages<A: Send + 'static, W: Send + 'static>(
mut handles: PipelineHandles<A, W>,
) -> Result<(), Box<dyn std::error::Error>> {
info!("shutdown: draining pipeline");
let failed = handles.stop_and_join();
melin_transport_core::trace::print_report_all();
if 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,
store: &NodeStore,
on_primary: Vec<A::Event>,
decoder: RequestDecoderArc<A>,
encoder: ResponseEncoderArc<A>,
event_publisher: Option<EventPublisherFn<A>>,
shutdown: Arc<AtomicBool>,
authorized_keys: Arc<AuthorizedKeys>,
rotate_flag: Option<Arc<AtomicBool>>,
ack_policy: Arc<crate::policy_control::AckPolicyControl>,
fence_state: Arc<melin_transport_core::fence::FenceState>,
lineage_ack_policy: Option<LineagePolicy>,
promotion: Option<u64>,
began_history: bool,
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() == crate::halt::Replication::Enabled;
validate_primary_config(config)?;
let ring_sizing =
crate::connection_limit::RingSizing::for_max_connections(config.max_connections)?;
let genesis_entries = writer.read_header_info()?.genesis_entries;
let starting_wire_seq = writer.next_sequence();
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 ingress_metrics = Arc::new(melin_transport_core::health::IngressMetrics::default());
let halt_gate = crate::halt::HaltGate::new(
replicas_connected.clone(),
Arc::clone(&fence_state),
Arc::clone(&refused_writes),
);
let (early_tx, early_rx) = crate::early_reply::early_reply_channel::<A::Report>(
crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY,
);
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 ack_policy_claim = ack_policy.claim();
let gate_policy = primary_gate_policy(
lineage_ack_policy,
ack_policy_claim.policy(),
halt_gate.replication(),
&matching_stage,
)?;
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 mut stages = PipelineHandles::new(Arc::clone(&shutdown));
stages.journal = Some(
melin_app::affinity::spawn_placed("journal-seq", cores.journal_seq.core, move || {
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);
stages.matching = Some(
melin_app::affinity::spawn_placed("matching", cores.matching.core, move || {
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());
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 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_ready_tx, response_ready_rx) = std::sync::mpsc::sync_channel(1);
let degraded_release = config.degraded_release(replicas_connected.as_ref());
stages.response = Some(
melin_app::affinity::spawn_placed("response", cores.response.core, move || {
let started = crate::response::run::<A>(
output_consumer,
control_rx,
crate::response::Response::<A> {
journal_persisted_wire_seq: journal_persisted_wire_seq_response,
policy: gate_policy,
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,
early_replies: early_rx,
ring_sizing,
ready: Some(response_ready_tx),
degraded_release,
#[cfg(test)]
pause_after_control_drain: None,
},
&s3,
);
if !started {
return;
}
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}"))?,
);
crate::connection_limit::await_startup("response", &response_ready_rx)?;
let replica_ready = Arc::new(AtomicBool::new(false));
stages.replication = 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 store = store.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 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,
store,
authorized_keys: repl_auth_keys,
evict_flags,
active_flags,
metrics: repl_metrics,
handlers,
batch_size,
heartbeat_secs,
fence_state: sender_fence,
},
&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
};
stages.event_publisher = spawn_event_publisher::<A>(
event_publisher_consumer,
event_publisher,
config,
&cores,
&authorized_keys,
&shutdown,
)?;
stages.shadow = spawn_shadow_stage::<A>(
shadow_consumer,
shadow_app,
chain_hash_lock,
config,
store,
&cores,
&shutdown,
Lineage {
epoch: fence_state.epoch(),
ack_policy: lineage_ack_policy,
},
starting_wire_seq,
genesis_entries,
)?;
let pipeline_healthy = Arc::new(AtomicBool::new(true));
stages.pipeline_healthy = Some(Arc::clone(&pipeline_healthy));
stages.health = spawn_health_endpoint(
config,
&active_connections,
&events_processed,
&refused_writes,
&ingress_metrics,
&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 {
journal_promotion_epoch_bump(
requested_epoch,
&fence_state,
&mut input_producer,
&shutdown,
);
}
let ack_policy_claim =
ack_policy_claim.journal_boot(lineage_ack_policy, &mut input_producer, &halt_gate);
if enable_replication && began_history {
info!("new history: waiting for the first replica before serving clients");
while !replica_ready.load(Ordering::Acquire) && !shutdown.load(Ordering::Relaxed) {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
journal_on_primary_events(
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,
early_tx,
ingress_metrics,
ack_policy_claim,
control_tx.clone(),
config.cores.reader.core,
connection_timeout,
config.tick_interval(),
ring_sizing,
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;
}
if stages.stage_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 crate::connection_limit::connection_cap_reached(
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 (role, 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,
role,
key_hash,
});
}
info!("shutdown: stopping reader thread");
reader_handle.shutdown();
reader_handle.join();
shutdown_pipeline_stages(stages)
}
#[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>>,
authorized_keys: Arc<AuthorizedKeys>,
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,
{
check_keys_match_decoder(&*decoder, &authorized_keys)?;
let dpdk_config = dpdk_config_from(&config)?;
run_dpdk_impl::<A>(
config,
startup,
sizing,
decoder,
encoder,
event_publisher,
authorized_keys,
dpdk_config,
shutdown,
)
}
#[cfg(feature = "dpdk")]
fn dpdk_config_from(
cfg: &ServerConfig,
) -> Result<melin_dpdk::DpdkConfig, melin_dpdk::DpdkArgsError> {
Ok(melin_dpdk::DpdkConfig {
nic: cfg
.dpdk
.nic_config_or(melin_dpdk::NicConfig::default().ip_addr)?,
listen_port: cfg.bind.port(),
client_buffers: melin_dpdk::SocketBuffers {
rx_buf_size: kib(cfg.dpdk_client_rx_buf_kib),
tx_buf_size: kib(cfg.dpdk_client_tx_buf_kib),
tx_queue_limit: kib(cfg.dpdk_client_tx_queue_kib),
},
num_queues: 1,
})
}
#[cfg(feature = "dpdk")]
#[allow(clippy::too_many_arguments)] 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>>,
authorized_keys: Arc<AuthorizedKeys>,
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 =
crate::policy_control::AckPolicyControl::new(config.ack_policy, config.replication());
validate_primary_config(&config)?;
let store = config.store()?;
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 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)),
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),
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 transport =
melin_dpdk::DpdkTransport::from_shared_unlistening(&shared, &dpdk_config, 0)?;
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>(
&mut transport,
primary_ipv4,
primary_addr.port(),
&signing_key,
&store,
&shutdown,
&control,
config.snapshot_interval_ms,
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");
check_promotable(&writer)?;
replica_health.stop();
<A as Application>::prefault(&mut app, &sizing);
if let Some(ref flag) = rotate_flag {
flag.store(false, Ordering::Release);
}
if let Err(e) = melin_app::affinity::clear_affinity() {
warn!(
error = e,
"failed to clear the receiver's affinity before promotion"
);
}
transport
.add_client_listener(&dpdk_config)
.map_err(|e| format!("add the client listener on promotion: {e}"))?;
transport.send_gratuitous_arp();
return run_as_primary_dpdk::<A>(
app,
writer,
transport,
&dpdk_config,
&config,
&store,
startup.on_primary,
decoder,
encoder,
event_publisher,
Arc::clone(&shutdown),
authorized_keys,
rotate_flag,
ack_policy,
fence_state,
control.lineage_ack_policy.load(),
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 transport = match <[_; 1]>::try_from(transports) {
Ok([transport]) => transport,
Err(transports) => {
return Err(format!(
"expected exactly one client DPDK transport, the port gave {}",
transports.len()
)
.into());
}
};
let InitializedEngine {
mut app,
writer,
recovered_lineage,
began_history: _,
} = init_engine::<A, BufferedWriter<A::Event>>(
&config,
&store,
&sizing,
std::mem::take(&mut startup.genesis),
)?;
<A as Application>::prefault(&mut app, &sizing);
let fence_state = Arc::new(melin_transport_core::fence::FenceState::new(
recovered_lineage.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 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)),
Arc::clone(&shutdown),
Arc::clone(&authorized_keys),
)
})
.transpose()?;
run_as_primary_dpdk::<A>(
app,
writer,
transport,
&dpdk_config,
&config,
&store,
startup.on_primary,
decoder,
encoder,
event_publisher,
Arc::clone(&shutdown),
authorized_keys,
rotate_flag,
ack_policy,
fence_state,
recovered_lineage.ack_policy,
None, raft_status,
journal_tip,
)
}
#[cfg(feature = "dpdk")]
#[allow(clippy::too_many_arguments)] fn run_as_primary_dpdk<A>(
app: A,
writer: BufferedWriter<A::Event>,
mut transport: melin_dpdk::DpdkTransport,
dpdk_config: &melin_dpdk::DpdkConfig,
config: &ServerConfig,
store: &NodeStore,
on_primary: Vec<A::Event>,
decoder: RequestDecoderArc<A>,
encoder: ResponseEncoderArc<A>,
event_publisher: Option<EventPublisherFn<A>>,
shutdown: Arc<AtomicBool>,
authorized_keys: Arc<AuthorizedKeys>,
rotate_flag: Option<Arc<AtomicBool>>,
ack_policy: Arc<crate::policy_control::AckPolicyControl>,
fence_state: Arc<melin_transport_core::fence::FenceState>,
lineage_ack_policy: Option<LineagePolicy>,
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,
{
validate_primary_config(config)?;
let genesis_entries = writer.read_header_info()?.genesis_entries;
let starting_wire_seq = writer.next_sequence();
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() == crate::halt::Replication::Enabled;
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 ingress_metrics = Arc::new(melin_transport_core::health::IngressMetrics::default());
let halt_gate = crate::halt::HaltGate::new(
replicas_connected.clone(),
Arc::clone(&fence_state),
Arc::clone(&refused_writes),
);
let (early_tx, early_rx) = crate::early_reply::early_reply_channel::<A::Report>(
crate::early_reply::EARLY_REPLY_QUEUE_CAPACITY,
);
let wait = config.cores.response.wait;
let (tx_out, tx_rx_0) =
melin_pipeline::spsc::channel::<crate::dpdk_response::TxFrame>(4096, wait);
let tx_producers = vec![tx_out];
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, 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 ack_policy_claim = ack_policy.claim();
let gate_policy = primary_gate_policy(
lineage_ack_policy,
ack_policy_claim.policy(),
halt_gate.replication(),
&matching_stage,
)?;
let s1 = Arc::clone(&shutdown);
let sequencer = journal_stage
.start()
.map_err(|e| format!("start the journal stage: {e}"))?;
let mut stages = PipelineHandles::new(Arc::clone(&shutdown));
stages.journal = Some(
melin_app::affinity::spawn_placed("journal-seq", cores.journal_seq.core, move || {
sequencer.run(&s1)
})
.map_err(|e| format!("spawn journal-seq thread: {e}"))?,
);
let s2 = Arc::clone(&shutdown);
stages.matching = Some(
melin_app::affinity::spawn_placed("matching", cores.matching.core, move || {
matching_stage.run(&s2)
})
.map_err(|e| format!("spawn matching thread: {e}"))?,
);
let replication_metrics = build_replication_metrics(replication_consumers.is_some());
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 active_connections_response = Arc::clone(&active_connections);
let s3 = Arc::clone(&shutdown);
let response_utilization_thread = Arc::clone(&response_utilization);
let degraded_release = config.degraded_release(replicas_connected.as_ref());
stages.response = Some(
melin_app::affinity::spawn_placed("response", cores.response.core, move || {
crate::dpdk_response::run::<A>(
output_consumer,
control_rx,
journal_persisted_wire_seq_response,
gate_policy,
replication_metrics_response,
replica_active_response,
&s3,
heartbeat_interval,
active_connections_response,
tx_producers,
response_utilization_thread,
wait,
encoder,
early_rx,
degraded_release,
#[cfg(test)]
None,
);
})
.map_err(|e| format!("spawn response thread: {e}"))?,
);
stages.shadow = spawn_shadow_stage::<A>(
shadow_consumer,
shadow_app,
chain_hash_lock,
config,
store,
&cores,
&shutdown,
Lineage {
epoch: fence_state.epoch(),
ack_policy: lineage_ack_policy,
},
starting_wire_seq,
genesis_entries,
)?;
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)),
]
});
const REPL_TX_BUF: usize = 512 * 1024;
const REPL_TX_QUEUE: usize = 512 * 1024;
const REPL_RX_BUF: usize = 64 * 1024;
transport
.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,
store.clone(),
ready_flag,
connected_counter,
dpdk_evict_flags,
dpdk_active_flags,
repl_metrics,
batch_size,
heartbeat_secs,
Arc::clone(&fence_state),
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)
};
if let Some(requested_epoch) = promotion {
journal_promotion_epoch_bump(
requested_epoch,
&fence_state,
&mut input_producer,
&shutdown,
);
}
let ack_policy_claim =
ack_policy_claim.journal_boot(lineage_ack_policy, &mut input_producer, &halt_gate);
journal_on_primary_events(
on_primary,
&mut input_producer,
&journal_cursor,
&matching_cursor,
&replication_ring_progress,
&shutdown,
);
let pipeline_healthy = Arc::new(AtomicBool::new(true));
stages.pipeline_healthy = Some(Arc::clone(&pipeline_healthy));
stages.health = spawn_health_endpoint(
config,
&active_connections,
&events_processed,
&refused_writes,
&ingress_metrics,
&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.nic.ip_addr,
port = dpdk_config.listen_port,
num_dpdk_threads = 1,
"DPDK transport listening"
);
let connection_timeout = config.connection_timeout();
let max_conns = config.max_connections;
let reader_core = config.cores.reader.core;
melin_app::affinity::pin_thread("dpdk-poll-0", reader_core);
let ack_policy = ack_policy_claim.attach(&mut input_producer, &halt_gate, None);
crate::dpdk_transport::run_dpdk_poll::<A>(
transport,
input_producer,
decoder,
halt_gate,
early_tx,
ingress_metrics,
ack_policy,
control_tx,
tx_rx_0,
&shutdown,
authorized_keys,
connection_timeout,
tick_cadence,
max_conns,
Arc::clone(&active_connections),
0,
repl_driver,
repl_listen_port,
);
shutdown_pipeline_stages(stages)
}
fn journal_promotion_epoch_bump<E: melin_app::AppEvent>(
requested_epoch: u64,
fence_state: &melin_transport_core::fence::FenceState,
input_producer: &mut melin_pipeline::ring::Producer<InputSlot<E>>,
shutdown: &AtomicBool,
) {
use melin_app::unix_epoch_nanos;
use melin_journal::JournalEvent;
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::node_event(
JournalEvent::EpochBump { epoch: new_epoch },
unix_epoch_nanos(),
));
ORCHESTRATOR_WAIT
.wait_until(|| fence_state.epoch() >= new_epoch || shutdown.load(Ordering::Relaxed));
}
fn journal_on_primary_events<E: melin_app::AppEvent>(
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;
let on_primary_count = on_primary.len();
if on_primary_count == 0 {
return;
}
let start = std::time::Instant::now();
let mut last_published_seq = 0u64;
for event in on_primary {
last_published_seq = input_producer.publish(InputSlot::node_event(
JournalEvent::App(event),
unix_epoch_nanos(),
));
}
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!(
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,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GenesisTarget {
None,
EmptyJournal,
}
fn genesis_target<E: melin_app::AppEvent>(
archives_exist: bool,
next_sequence: u64,
recorded: Option<u64>,
genesis: &[E],
) -> GenesisTarget {
if !archives_exist
&& next_sequence == 1
&& recorded.is_none()
&& genesis.iter().any(|e| !e.is_query())
{
GenesisTarget::EmptyJournal
} else {
GenesisTarget::None
}
}
pub(crate) struct InitializedEngine<A, W> {
pub(crate) app: A,
pub(crate) writer: W,
pub(crate) recovered_lineage: melin_transport_core::lineage::Lineage,
pub(crate) began_history: bool,
}
pub(crate) fn init_engine<A, W>(
config: &ServerConfig,
store: &NodeStore,
sizing: &A::Sizing,
genesis: Vec<A::Event>,
) -> Result<InitializedEngine<A, W>, Box<dyn std::error::Error>>
where
A: Application,
W: JournalWrite<A::Event>,
{
use melin_transport_core::journaled_app::write_genesis_journal;
let journal = store.journal();
let recover_journal = || -> Result<JournaledApp<A, W>, Box<dyn std::error::Error>> {
let mut app = A::default();
<A as Application>::prefault(&mut app, sizing);
Ok(JournaledApp::<A, W>::recover(app, journal)?)
};
let snap_path = store.snapshot();
let journal_exists = journal.try_exists()?;
let archives_exist = !melin_journal::segment::list_archives(journal)?.is_empty();
let snapshot_exists = snap_path.try_exists()?;
melin_transport_core::journaled_app::discard_genesis_staging(journal)?;
let mut began_history = false;
let mut engine: JournaledApp<A, W> =
match choose_bootstrap(snapshot_exists, journal_exists, archives_exist) {
BootstrapSource::SnapshotAndJournal => {
info!(snapshot = %snap_path.display(), "recovering from snapshot + journal");
JournaledApp::<A, W>::recover_from_snapshot(snap_path, journal)?
}
BootstrapSource::SnapshotOnly => {
info!(
snapshot = %snap_path.display(),
"recovering from snapshot only (no journal segments on disk)"
);
let (app, snap) = melin_transport_core::snapshot::load::<A>(snap_path)?;
let writer = W::create_continuing(
journal,
snap.sequence + 1,
snap.chain_hash,
snap.genesis_entries,
)?;
JournaledApp::<A, W>::from_parts(app, writer, snap.lineage)
}
BootstrapSource::JournalOnly => {
info!("recovering from journal");
let engine = recover_journal()?;
match genesis_target(
archives_exist,
engine.next_sequence(),
engine.genesis_entries()?,
&genesis,
) {
GenesisTarget::None => engine,
GenesisTarget::EmptyJournal => {
drop(engine);
let anchor = melin_journal::segment::read_header_info(journal)?.anchor_hash;
warn!(
journal = %journal.display(),
"the journal holds no entry and records no genesis \
length: journaling genesis (an earlier release's \
first boot stopped before journaling it)"
);
write_genesis_journal::<A::Event, W>(journal, Some(anchor), genesis)?;
began_history = true;
recover_journal()?
}
}
}
BootstrapSource::Fresh => {
info!("creating new journal");
write_genesis_journal::<A::Event, W>(journal, None, genesis)?;
began_history = true;
recover_journal()?
}
};
check_recovered_history_holds_genesis(engine.next_sequence(), engine.genesis_entries()?)?;
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_lineage = engine.recovered_lineage();
let (app, writer) = engine.into_parts();
Ok(InitializedEngine {
app,
writer,
recovered_lineage,
began_history,
})
}
fn build_replication_metrics(
has_replication: bool,
) -> Option<Arc<crate::replication::ReplicationMetrics>> {
has_replication.then(|| Arc::new(crate::replication::ReplicationMetrics::default()))
}
fn primary_gate_policy<A: Application>(
lineage_ack_policy: Option<LineagePolicy>,
requested: crate::ack_policy::AckPolicy,
replication: crate::halt::Replication,
matching_stage: &melin_transport_core::pipeline::MatchingStage<A>,
) -> Result<crate::durability_gate::GatePolicy, String> {
let start =
crate::durability_gate::initial_gate_policy(lineage_ack_policy, requested, replication)?;
matching_stage
.utilization()
.ack_policy_journaled
.store(lineage_ack_policy.map(|p| p.policy()));
info!(
ack_policy = %start.in_force,
clauses = %start.in_force.to_policy(),
pending = ?start.pending.map(|p| p.policy.as_str()),
requested = %requested,
from_lineage = lineage_ack_policy.is_some(),
"ack policy active"
);
Ok(crate::durability_gate::GatePolicy {
start,
forced: matching_stage.forced_policy(),
})
}
#[allow(clippy::too_many_arguments)] 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,
store: &NodeStore,
cores: &PipelineCores,
shutdown: &Arc<AtomicBool>,
initial_lineage: Lineage,
starting_wire_seq: u64,
genesis_entries: Option<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 = store.snapshot().to_path_buf();
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 handle = melin_app::affinity::spawn_placed("shadow", shadow.core, move || {
melin_transport_core::shadow::run(
shadow_cons,
shadow_ex,
snap_path,
interval,
chain_hash,
&s_shadow,
shadow.wait,
initial_lineage,
starting_wire_seq,
genesis_entries,
);
})
.map_err(|e| format!("spawn shadow thread: {e}"))?;
info!(
interval_ms = config.snapshot_interval_ms,
path = %store.snapshot().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>,
ingress_metrics: &Arc<melin_transport_core::health::IngressMetrics>,
cursors: &melin_transport_core::PipelineCursors,
input_cursor: Box<dyn melin_pipeline::ring::QueueCursor>,
pipeline_healthy: &Arc<AtomicBool>,
replicas_connected: &Option<Arc<ReplicaPresence>>,
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),
ingress: Arc::clone(ingress_metrics),
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 =
melin_app::affinity::spawn_placed("event-publisher", publisher.core, move || {
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<(ClientRole<RoleId>, [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 > MAX_AUTH_FRAME {
send_auth_failed(writer);
return Err(io::Error::other(format!("auth frame too large: {frame_len}")).into());
}
let mut frame_buf = [0u8; MAX_AUTH_FRAME];
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 role = 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,
role = authorized_keys.token(KeyRole::Client(role)),
"authenticated"
);
Ok((role, 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, ClientRole, KeyRole, NoRoles};
use melin_app::decoder::{Decoded, RequestDecoder};
use melin_wire_protocol::control::ConnectionId;
use melin_wire_protocol::control_codec::{
TAG_AUTH_FAILED, TAG_CHALLENGE_RESPONSE, TAG_RESPONSE_HEARTBEAT, TAG_SERVER_READY,
};
use super::authenticate_connection;
use super::{BootstrapSource, choose_bootstrap};
#[test]
fn dropped_pipeline_handles_stop_and_join_the_threads_started() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
let shutdown = Arc::new(AtomicBool::new(false));
let healthy = Arc::new(AtomicBool::new(true));
let exited = Arc::new(AtomicUsize::new(0));
let stage = |value: u32| {
let shutdown = Arc::clone(&shutdown);
let exited = Arc::clone(&exited);
move || {
while !shutdown.load(Ordering::Acquire) {
std::thread::yield_now();
}
exited.fetch_add(1, Ordering::AcqRel);
value
}
};
let mut stages = super::PipelineHandles::<u32, u32>::new(Arc::clone(&shutdown));
let journal = stage(1);
stages.journal = Some(std::thread::spawn(move || Ok(journal())));
stages.matching = Some(std::thread::spawn(stage(2)));
stages.pipeline_healthy = Some(Arc::clone(&healthy));
drop(stages);
assert!(shutdown.load(Ordering::Acquire), "the stop flag is raised");
assert!(!healthy.load(Ordering::Acquire), "and health lowered");
assert_eq!(
exited.load(Ordering::Acquire),
2,
"every thread started has exited by the time the drop returns"
);
}
#[test]
fn the_dpdk_flags_are_the_shared_ones_with_the_node_default_address() {
use clap::Parser;
use std::net::Ipv4Addr;
let parsed = super::ServerConfig::try_parse_from(["melin-server"]).expect("parses");
let address: Ipv4Addr = super::DEFAULT_DPDK_IP.parse().expect("an IPv4 address");
assert_eq!(address, melin_dpdk::NicConfig::default().ip_addr);
assert_eq!(parsed.dpdk.ip, Some(address));
assert_eq!(parsed.dpdk, super::ServerConfig::default().dpdk);
assert_eq!(
super::DpdkArgs {
ip: None,
..parsed.dpdk
},
super::DpdkArgs::default(),
"only the address default is the node's own"
);
let given = super::ServerConfig::try_parse_from(["melin-server", "--dpdk-ip", "10.0.0.7"])
.expect("parses");
assert_eq!(given.dpdk.ip, Some(Ipv4Addr::new(10, 0, 0, 7)));
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 the_dpdk_client_buffer_flags_default_to_the_transport_sizes() {
use clap::Parser;
let sizes = melin_dpdk::SocketBuffers::DEFAULT;
for (flag_kib, bytes) in [
(super::kib_of(sizes.rx_buf_size), sizes.rx_buf_size),
(super::kib_of(sizes.tx_buf_size), sizes.tx_buf_size),
(super::kib_of(sizes.tx_queue_limit), sizes.tx_queue_limit),
] {
assert_eq!(
flag_kib as usize * 1024,
bytes,
"a default not in whole KiB"
);
}
let parsed = super::ServerConfig::try_parse_from(["melin-server"]).expect("parses");
let built = super::ServerConfig::default();
for config in [&parsed, &built] {
assert_eq!(
(
config.dpdk_client_rx_buf_kib,
config.dpdk_client_tx_buf_kib,
config.dpdk_client_tx_queue_kib,
),
(
super::kib_of(sizes.rx_buf_size),
super::kib_of(sizes.tx_buf_size),
super::kib_of(sizes.tx_queue_limit),
)
);
}
let given = super::ServerConfig::try_parse_from([
"melin-server",
"--dpdk-client-rx-buf-kib",
"256",
"--dpdk-client-tx-buf-kib",
"512",
"--dpdk-client-tx-queue-kib",
"32768",
])
.expect("parses");
assert_eq!(given.dpdk_client_rx_buf_kib, 256);
assert_eq!(given.dpdk_client_tx_buf_kib, 512);
assert_eq!(given.dpdk_client_tx_queue_kib, 32768);
for flag in [
"--dpdk-client-rx-buf-kib",
"--dpdk-client-tx-buf-kib",
"--dpdk-client-tx-queue-kib",
] {
assert!(
super::ServerConfig::try_parse_from(["melin-server", flag, "0"]).is_err(),
"{flag} 0 must be refused"
);
}
}
#[test]
fn the_snapshot_path_flag_is_the_store_s_snapshot() {
use clap::Parser;
let dir = tempfile::tempdir().expect("tempdir");
let journal = dir.path().join("node.journal");
let snapshot = dir.path().join("elsewhere.snap");
let journal_arg = journal.to_str().expect("utf-8 path");
let snapshot_arg = snapshot.to_str().expect("utf-8 path");
let Err(removed) = super::ServerConfig::try_parse_from([
"melin-server",
"--journal",
journal_arg,
"--snapshot",
snapshot_arg,
]) else {
panic!("--snapshot is no longer a flag");
};
assert_eq!(removed.kind(), clap::error::ErrorKind::UnknownArgument);
let given = super::ServerConfig::try_parse_from([
"melin-server",
"--journal",
journal_arg,
"--snapshot-path",
snapshot_arg,
])
.expect("parses");
let store = given.store().expect("store");
assert_eq!(store.snapshot(), snapshot);
assert_eq!(store.journal(), journal);
let derived =
super::ServerConfig::try_parse_from(["melin-server", "--journal", journal_arg])
.expect("parses")
.store()
.expect("store");
assert_eq!(derived.snapshot(), dir.path().join("node.snapshot"));
let missing = super::ServerConfig {
journal: journal.clone(),
snapshot_path: Some(dir.path().join("missing").join("x.snapshot")),
..super::ServerConfig::default()
};
assert!(matches!(
missing.store(),
Err(super::NodeStoreError::SnapshotDirMissing { .. })
));
}
#[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(role: &str) -> AuthorizedKeys {
let pub_bytes = test_key().verifying_key().to_bytes();
let pub_b64 = base64_encode(&pub_bytes);
crate::test_roles::desk_keys(role, &pub_b64)
}
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<&'static str, 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(|(role, _pk)| keys.token(KeyRole::Client(role)))
.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();
melin_wire_protocol::control_codec::decode_challenge(&payload).expect("a Challenge")
}
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_the_application_role() {
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(), "trader");
}
#[test]
fn auth_returns_the_operator_role() {
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(), "operator");
}
#[test]
fn auth_unknown_key_sends_auth_failed() {
let keys = AuthorizedKeys::parse::<NoRoles>("").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_role() {
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);
assert_eq!(handle.join().unwrap().unwrap(), "readonly");
}
struct OperatorOnlyDecoder;
impl RequestDecoder for OperatorOnlyDecoder {
type Event = counter_server::CounterEvent;
type Role = NoRoles;
fn decode(&self, _body: &[u8], _role: ClientRole<NoRoles>) -> Decoded<Self::Event> {
Decoded::Filter
}
}
#[test]
fn keys_parsed_for_another_role_type_are_refused() {
let decoder = OperatorOnlyDecoder;
let other_roles = keys_with_test_key("operator");
let err = super::check_keys_match_decoder(&decoder, &other_roles).unwrap_err();
assert_eq!(
err.to_string(),
"authorized keys were parsed for a role type other than the decoder's"
);
let own_roles = AuthorizedKeys::parse::<NoRoles>("").unwrap();
super::check_keys_match_decoder(&decoder, &own_roles).unwrap();
}
#[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("may not connect as a client"),
"unexpected error: {err}"
);
}
}
#[cfg(test)]
mod genesis_tests {
use std::path::Path;
use counter_server::{Counter, CounterEvent};
use melin_journal::{BufferedWriter, JournalEvent, JournalReader, JournalWrite};
use melin_transport_core::journaled_app::genesis_staging_path;
use super::{
GenesisTarget, ServerConfig, check_promotable, check_recovered_history_holds_genesis,
genesis_target, init_engine, validate_primary_config,
};
type Writer = BufferedWriter<CounterEvent>;
pub(super) fn config(dir: &Path) -> ServerConfig {
ServerConfig {
journal: dir.join("counter.journal"),
..ServerConfig::default()
}
}
fn increments(amounts: &[u64]) -> Vec<CounterEvent> {
amounts
.iter()
.map(|&amount| CounterEvent::Increment { amount })
.collect()
}
fn value(app: &Counter) -> u64 {
use melin_app::Application;
let mut buf = Vec::new();
app.snapshot(&mut buf).expect("snapshot to a vec");
u64::from_le_bytes(buf[..8].try_into().expect("8-byte value"))
}
fn boot(cfg: &ServerConfig, genesis: &[u64]) -> (u64, u64) {
let engine = init_engine::<Counter, Writer>(
cfg,
&cfg.store().expect("store"),
&(),
increments(genesis),
)
.expect("init_engine");
(value(&engine.app), engine.writer.next_sequence())
}
fn began_history(cfg: &ServerConfig, genesis: &[u64]) -> bool {
init_engine::<Counter, Writer>(cfg, &cfg.store().expect("store"), &(), increments(genesis))
.expect("init_engine")
.began_history
}
#[test]
fn only_a_boot_that_begins_the_history_reports_it() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
assert!(began_history(&cfg, &[]));
assert!(!began_history(&cfg, &[]));
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
assert!(began_history(&cfg, &[1_000]));
assert!(!began_history(&cfg, &[1_000]));
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
drop(Writer::create_continuing(&cfg.journal, 1, [0x15; 32], None).expect("v15 journal"));
assert!(began_history(&cfg, &[1_000]));
assert!(!began_history(&cfg, &[1_000]));
}
fn journaled_amounts(path: &Path) -> Vec<u64> {
let mut reader = JournalReader::<CounterEvent>::open(path).expect("open journal");
let mut out = Vec::new();
while let Some(entry) = reader.next_entry().expect("read entry") {
if let JournalEvent::App(CounterEvent::Increment { amount }) = entry.event {
out.push(amount);
}
}
out
}
fn anchor(path: &Path) -> [u8; 32] {
melin_journal::segment::read_header_info(path)
.expect("journal header")
.anchor_hash
}
#[test]
fn a_new_journal_is_created_with_its_genesis_on_disk() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
assert_eq!(boot(&cfg, &[1_000, 2_000]), (3_000, 3));
assert_eq!(journaled_amounts(&cfg.journal), vec![1_000, 2_000]);
assert!(!genesis_staging_path(&cfg.journal).exists());
assert_eq!(boot(&cfg, &[1_000, 2_000]), (3_000, 3));
assert_eq!(journaled_amounts(&cfg.journal), vec![1_000, 2_000]);
}
#[test]
fn a_snapshot_only_boot_journals_no_genesis() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
let engine = init_engine::<Counter, Writer>(
&cfg,
&cfg.store().expect("store"),
&(),
increments(&[1_000_000]),
)
.expect("boot");
save_snapshot(&engine, cfg.store().expect("store").snapshot());
drop(engine);
std::fs::remove_file(&cfg.journal).expect("move the journal aside");
let (value, next_sequence) = boot(&cfg, &[1_000_000]);
assert_eq!(value, 1_000_000, "genesis applied once, from the snapshot");
assert_eq!(
next_sequence, 2,
"the new segment continues the snapshot, empty"
);
assert!(journaled_amounts(&cfg.journal).is_empty());
assert_eq!(genesis_entries(&cfg.journal), Some(1));
}
pub(super) fn save_snapshot(engine: &super::InitializedEngine<Counter, Writer>, path: &Path) {
melin_transport_core::snapshot::save::<Counter>(
&engine.app,
melin_transport_core::WireSeq::new(engine.writer.next_sequence() - 1),
engine.writer.chain_hash().unwrap_or([0u8; 32]),
engine.recovered_lineage,
engine
.writer
.read_header_info()
.expect("journal header")
.genesis_entries,
path,
)
.expect("save snapshot");
}
fn genesis_entries(path: &Path) -> Option<u64> {
melin_journal::segment::read_header_info(path)
.expect("journal header")
.genesis_entries
}
#[test]
fn a_crash_mid_genesis_leaves_no_journal_and_the_next_boot_journals_it_whole() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
let staging = genesis_staging_path(&cfg.journal);
let mut partial = Writer::create(&staging).expect("staging journal");
partial
.append(&JournalEvent::App(CounterEvent::Increment {
amount: 1_000,
}))
.expect("first genesis event");
drop(partial);
assert!(!cfg.journal.exists());
assert_eq!(boot(&cfg, &[1_000, 2_000, 4_000]), (7_000, 4));
assert_eq!(journaled_amounts(&cfg.journal), vec![1_000, 2_000, 4_000]);
assert!(!staging.exists(), "the partial journal is discarded");
assert_eq!(genesis_entries(&cfg.journal), Some(3));
}
#[test]
fn an_empty_journal_with_a_recorded_length_is_not_rewritten() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
drop(Writer::create(&cfg.journal).expect("empty journal"));
assert_eq!(genesis_entries(&cfg.journal), Some(0));
let before = anchor(&cfg.journal);
assert_eq!(boot(&cfg, &[1_000_000]), (0, 1));
assert!(journaled_amounts(&cfg.journal).is_empty());
assert_eq!(anchor(&cfg.journal), before);
assert_eq!(genesis_entries(&cfg.journal), Some(0));
std::fs::remove_file(&cfg.journal).expect("move the journal aside");
drop(
Writer::create_continuing(&cfg.journal, 1, [0x2C; 32], Some(2))
.expect("a replica's empty copy"),
);
for configured in [&[][..], &[1_000, 2_000], &[1_000, 2_000, 4_000]] {
let refused = init_engine::<Counter, Writer>(
&cfg,
&cfg.store().expect("store"),
&(),
increments(configured),
)
.err()
.expect("an empty copy of a genesis is refused");
assert!(
refused.to_string().contains("refusing to start"),
"{configured:?}: {refused}"
);
}
assert!(journaled_amounts(&cfg.journal).is_empty());
assert_eq!(anchor(&cfg.journal), [0x2C; 32]);
assert_eq!(genesis_entries(&cfg.journal), Some(2));
}
#[test]
fn an_empty_old_format_journal_gets_its_genesis_and_its_length() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
drop(Writer::create_continuing(&cfg.journal, 1, [0x15; 32], None).expect("v15 journal"));
assert_eq!(genesis_entries(&cfg.journal), None);
assert_eq!(boot(&cfg, &[1_000, 2_000]), (3_000, 3));
assert_eq!(anchor(&cfg.journal), [0x15; 32]);
assert_eq!(genesis_entries(&cfg.journal), Some(2));
}
#[test]
fn a_stale_staging_file_is_swept_on_any_boot() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
assert_eq!(boot(&cfg, &[1_000]), (1_000, 2));
let staging = genesis_staging_path(&cfg.journal);
drop(Writer::create(&staging).expect("orphaned staging file"));
assert_eq!(boot(&cfg, &[1_000]), (1_000, 2));
assert!(!staging.exists());
assert_eq!(journaled_amounts(&cfg.journal), vec![1_000]);
}
#[test]
fn an_empty_journal_without_genesis_is_left_alone() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
drop(Writer::create(&cfg.journal).expect("empty journal"));
let before = anchor(&cfg.journal);
assert_eq!(boot(&cfg, &[]), (0, 1));
assert_eq!(anchor(&cfg.journal), before);
assert!(!genesis_staging_path(&cfg.journal).exists());
}
#[test]
fn a_journal_with_history_never_gets_genesis() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
let mut writer = Writer::create(&cfg.journal).expect("journal");
writer
.append(&JournalEvent::App(CounterEvent::Increment { amount: 5 }))
.expect("one entry");
drop(writer);
assert_eq!(boot(&cfg, &[1_000_000]), (5, 2));
assert_eq!(journaled_amounts(&cfg.journal), vec![5]);
}
fn partial_replica_copy(path: &Path) {
let mut writer =
Writer::create_continuing(path, 1, [0x2B; 32], Some(3)).expect("replica journal");
for amount in [1_000, 2_000] {
writer
.append(&JournalEvent::App(CounterEvent::Increment { amount }))
.expect("genesis prefix");
}
}
#[test]
fn a_genesis_prefix_is_refused_at_boot() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
partial_replica_copy(&cfg.journal);
for configured in [&[][..], &[1_000, 2_000, 4_000], &[1, 2, 3, 4, 5]] {
let refused = init_engine::<Counter, Writer>(
&cfg,
&cfg.store().expect("store"),
&(),
increments(configured),
)
.err()
.expect("a genesis prefix is refused");
assert!(
refused.to_string().contains("refusing to start"),
"{configured:?}: {refused}"
);
}
assert_eq!(journaled_amounts(&cfg.journal), vec![1_000, 2_000]);
melin_transport_core::JournaledApp::<Counter, Writer>::recover(
Counter::default(),
&cfg.journal,
)
.expect("recover the partial copy")
.save_snapshot(cfg.store().expect("store").snapshot())
.expect("save snapshot");
init_engine::<Counter, Writer>(&cfg, &cfg.store().expect("store"), &(), Vec::new())
.err()
.expect("snapshot + journal holding a prefix is refused");
std::fs::remove_file(&cfg.journal).expect("move the journal aside");
let refused =
init_engine::<Counter, Writer>(&cfg, &cfg.store().expect("store"), &(), Vec::new())
.err()
.expect("a snapshot holding a prefix is refused");
assert!(
refused.to_string().contains("refusing to start"),
"{refused}"
);
}
#[test]
fn a_grown_genesis_configuration_does_not_refuse_a_short_history() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
assert_eq!(boot(&cfg, &[1_000]), (1_000, 2));
assert_eq!(genesis_entries(&cfg.journal), Some(1));
assert_eq!(boot(&cfg, &[1_000, 2_000, 4_000, 8_000]), (1_000, 2));
assert_eq!(journaled_amounts(&cfg.journal), vec![1_000]);
assert_eq!(genesis_entries(&cfg.journal), Some(1));
}
#[test]
fn an_old_format_journal_boots_without_the_check() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = config(dir.path());
let mut writer =
Writer::create_continuing(&cfg.journal, 1, [0x15; 32], None).expect("v15 journal");
writer
.append(&JournalEvent::App(CounterEvent::Increment { amount: 7 }))
.expect("one entry");
drop(writer);
let engine = init_engine::<Counter, Writer>(
&cfg,
&cfg.store().expect("store"),
&(),
increments(&[1, 2, 3]),
)
.expect("an unknown genesis length is not checked");
assert_eq!(value(&engine.app), 7);
assert_eq!(engine.writer.next_sequence(), 2);
assert_eq!(genesis_entries(&cfg.journal), None, "still a v15 lineage");
check_promotable(&engine.writer).expect("promotion is not checked either");
save_snapshot(&engine, cfg.store().expect("store").snapshot());
drop(engine);
std::fs::remove_file(&cfg.journal).expect("move the journal aside");
assert_eq!(boot(&cfg, &[1, 2, 3]), (7, 2));
assert_eq!(genesis_entries(&cfg.journal), None);
}
#[test]
fn genesis_target_starts_only_an_empty_history() {
let genesis = increments(&[1]);
let none: Vec<CounterEvent> = Vec::new();
let queries = vec![CounterEvent::GetValue];
type Case<'a> = (bool, u64, Option<u64>, &'a [CounterEvent], GenesisTarget);
let cases: [Case<'_>; 8] = [
(false, 1, None, &genesis, GenesisTarget::EmptyJournal),
(false, 2, None, &genesis, GenesisTarget::None),
(true, 1, None, &genesis, GenesisTarget::None),
(true, 5, None, &genesis, GenesisTarget::None),
(false, 1, None, &none, GenesisTarget::None),
(false, 1, None, &queries, GenesisTarget::None),
(false, 1, Some(0), &genesis, GenesisTarget::None),
(false, 1, Some(3), &genesis, GenesisTarget::None),
];
for (archives, next_sequence, recorded, genesis, expected) in cases {
assert_eq!(
genesis_target(archives, next_sequence, recorded, genesis),
expected,
"archives={archives} next_sequence={next_sequence} recorded={recorded:?} \
genesis={genesis:?}"
);
}
}
#[test]
fn standalone_needs_the_disk_policy() {
let refused = ServerConfig {
standalone: true,
..ServerConfig::default()
};
let err = validate_primary_config(&refused).expect_err("default policy with --standalone");
assert!(err.to_string().contains("--ack-policy disk"), "{err}");
let accepted = ServerConfig {
standalone: true,
ack_policy: crate::ack_policy::AckPolicy::Disk,
..ServerConfig::default()
};
validate_primary_config(&accepted).expect("standalone under disk");
}
#[test]
fn max_connections_must_be_a_supported_cap() {
use crate::connection_limit::MAX_SUPPORTED_CONNECTIONS;
use clap::Parser as _;
for refused in [0, MAX_SUPPORTED_CONNECTIONS + 1] {
let config = ServerConfig {
max_connections: refused,
..ServerConfig::default()
};
let err = validate_primary_config(&config).expect_err("an unsupported cap");
assert!(err.to_string().contains("--max-connections"), "{err}");
assert!(
ServerConfig::try_parse_from([
"melin-server",
"--max-connections",
&refused.to_string()
])
.is_err(),
"the CLI accepted --max-connections {refused}"
);
}
for accepted in [1, 1024, MAX_SUPPORTED_CONNECTIONS] {
let config = ServerConfig {
max_connections: accepted,
..ServerConfig::default()
};
validate_primary_config(&config).expect("a supported cap");
let parsed = ServerConfig::try_parse_from([
"melin-server",
"--max-connections",
&accepted.to_string(),
])
.expect("the CLI accepts a supported cap");
assert_eq!(parsed.max_connections, accepted);
}
assert_eq!(
ServerConfig::try_parse_from(["melin-server"])
.expect("defaults parse")
.max_connections,
ServerConfig::default().max_connections,
"the CLI default and `Default` agree"
);
}
#[test]
fn promotion_needs_the_whole_genesis() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("replica.journal");
let genesis = increments(&[1_000, 2_000, 4_000]);
let mut writer =
Writer::create_continuing(&path, 1, [0x2B; 32], Some(3)).expect("replica journal");
let refused = check_promotable(&writer).expect_err("an empty history");
assert!(
refused.to_string().contains("refusing promotion"),
"{refused}"
);
for event in &genesis[..2] {
writer
.append(&JournalEvent::App(*event))
.expect("genesis prefix");
}
check_promotable(&writer).expect_err("a genesis prefix");
writer
.append(&JournalEvent::App(genesis[2]))
.expect("last genesis event");
check_promotable(&writer).expect("the whole genesis");
writer
.append(&JournalEvent::App(CounterEvent::Increment { amount: 5 }))
.expect("client history");
check_promotable(&writer).expect("genesis and history after it");
let none = dir.path().join("none.journal");
check_promotable(&Writer::create(&none).expect("journal")).expect("nothing to hold");
let unknown = dir.path().join("unknown.journal");
check_promotable(
&Writer::create_continuing(&unknown, 1, [0x15; 32], None).expect("journal"),
)
.expect("unknown is not checked");
let continued = dir.path().join("continued.journal");
check_promotable(
&Writer::create_continuing(&continued, 10, [0x2B; 32], Some(3)).expect("journal"),
)
.expect("snapshot covers genesis");
}
#[test]
fn the_boot_check_needs_the_whole_genesis() {
check_recovered_history_holds_genesis(1, Some(3)).expect_err("empty");
check_recovered_history_holds_genesis(3, Some(3)).expect_err("a prefix");
check_recovered_history_holds_genesis(4, Some(3)).expect("the whole genesis");
check_recovered_history_holds_genesis(100, Some(3)).expect("and history after it");
check_recovered_history_holds_genesis(1, Some(0)).expect("no genesis");
check_recovered_history_holds_genesis(1, None).expect("unknown: not checked");
check_recovered_history_holds_genesis(3, None).expect("unknown: not checked");
}
}
#[cfg(test)]
mod primary_gate_policy_tests {
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use counter_server::{Counter, CounterEvent};
use melin_pipeline::ring::DisruptorBuilder;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::lineage::ForcedPolicy;
use melin_transport_core::pipeline::{InputSlot, MatchingStage, OutputSlot};
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use super::primary_gate_policy;
use crate::ack_policy::AckPolicy;
use crate::halt::Replication::Enabled;
fn matching_stage() -> MatchingStage<Counter> {
let (_, mut inputs) = DisruptorBuilder::<InputSlot<CounterEvent>>::new(8)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
let (output, _) = DisruptorBuilder::<
OutputSlot<
<Counter as melin_app::Application>::Report,
<Counter as melin_app::Application>::QueryResponse,
>,
>::new(8)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
MatchingStage::new(
Counter::default(),
inputs.pop().expect("one consumer"),
output,
Arc::new(AtomicU64::new(0)),
DurableWireSeqCursor::detached(WireSeq::new(0)),
Arc::new(AtomicU64::new(0)),
Arc::new(FenceState::new(0)),
WaitStrategy::SpinThenYield,
1,
)
}
#[test]
fn the_lineage_policy_wins_and_seeds_the_journaled_gauge() {
let stage = matching_stage();
let lineage = melin_transport_core::lineage::LineagePolicy {
change: melin_journal::AckPolicyChange {
policy: AckPolicy::TwoDisks.to_byte(),
unwitnessed: false,
},
wire_seq: WireSeq::new(7),
previous: Some(AckPolicy::Ram.to_byte()),
};
let policy = primary_gate_policy(Some(lineage), AckPolicy::Disk, Enabled, &stage)
.expect("a known policy");
assert_eq!(
policy.start,
crate::durability_gate::GateStart {
in_force: AckPolicy::Ram,
pending: Some(crate::durability_gate::PendingChange {
wire_seq: WireSeq::new(7),
policy: AckPolicy::TwoDisks,
unwitnessed: false,
}),
}
);
assert_eq!(
stage.utilization().ack_policy_journaled.load(),
Some(AckPolicy::TwoDisks.to_byte())
);
assert_eq!(policy.forced.load(), None::<ForcedPolicy>);
}
#[test]
fn a_boot_from_a_snapshot_alone_starts_the_gate_with_its_change_pending() {
use melin_journal::{AckPolicyChange, JournalEvent};
use melin_transport_core::lineage::{Lineage, LineagePolicy};
type Writer = melin_journal::BufferedWriter<CounterEvent>;
let boot = |cfg: &super::ServerConfig| {
super::init_engine::<Counter, Writer>(
cfg,
&cfg.store().expect("store"),
&(),
Vec::new(),
)
.expect("boot")
};
let change = |policy: AckPolicy, unwitnessed| {
JournalEvent::AckPolicyChange(AckPolicyChange {
policy: policy.to_byte(),
unwitnessed,
})
};
let dir = tempfile::tempdir().expect("tempdir");
let cfg = super::genesis_tests::config(dir.path());
let mut engine = boot(&cfg);
for event in [
change(AckPolicy::DiskAndRam, false),
JournalEvent::App(CounterEvent::Increment { amount: 1 }),
change(AckPolicy::Disk, true),
JournalEvent::App(CounterEvent::Increment { amount: 2 }),
] {
let seq = engine.writer.allocate_sequence();
engine
.writer
.encode_event(seq, 0, &event, 0)
.expect("encode");
}
engine.writer.flush_batch_sync().expect("flush");
drop(engine);
let expected = Lineage {
epoch: 0,
ack_policy: Some(LineagePolicy {
change: AckPolicyChange {
policy: AckPolicy::Disk.to_byte(),
unwitnessed: true,
},
wire_seq: WireSeq::new(3),
previous: Some(AckPolicy::DiskAndRam.to_byte()),
}),
};
let engine = boot(&cfg);
assert_eq!(
engine.recovered_lineage, expected,
"recovered from the journal"
);
super::genesis_tests::save_snapshot(&engine, cfg.store().expect("store").snapshot());
drop(engine);
std::fs::remove_file(&cfg.journal).expect("prune the journal");
assert!(
melin_journal::segment::list_archives(&cfg.journal)
.expect("list archives")
.is_empty()
);
let pending = crate::durability_gate::GateStart {
in_force: AckPolicy::DiskAndRam,
pending: Some(crate::durability_gate::PendingChange {
wire_seq: WireSeq::new(3),
policy: AckPolicy::Disk,
unwitnessed: true,
}),
};
for (layout, segment) in [
("the snapshot alone", false),
("the snapshot and an empty segment", true),
] {
assert_eq!(cfg.journal.exists(), segment, "{layout}");
let engine = boot(&cfg);
assert_eq!(engine.writer.next_sequence(), 5, "{layout}");
assert_eq!(engine.recovered_lineage, expected, "{layout}");
let policy = primary_gate_policy(
engine.recovered_lineage.ack_policy,
AckPolicy::Disk,
Enabled,
&matching_stage(),
)
.expect("a known policy");
assert_eq!(policy.start, pending, "{layout}");
}
}
#[test]
fn a_promotion_seeds_its_gate_from_the_replicas_record() {
let streamed = |policy: AckPolicy, unwitnessed| melin_journal::AckPolicyChange {
policy: policy.to_byte(),
unwitnessed,
};
let record = melin_transport_core::lineage::SharedLineagePolicy::default();
record.observe(WireSeq::new(1), streamed(AckPolicy::DiskAndRam, false));
record.observe(WireSeq::new(6), streamed(AckPolicy::Disk, false));
let start = |record: &melin_transport_core::lineage::SharedLineagePolicy| {
primary_gate_policy(record.load(), AckPolicy::Disk, Enabled, &matching_stage())
.expect("a known policy")
.start
};
let pending = |unwitnessed| crate::durability_gate::GateStart {
in_force: AckPolicy::DiskAndRam,
pending: Some(crate::durability_gate::PendingChange {
wire_seq: WireSeq::new(6),
policy: AckPolicy::Disk,
unwitnessed,
}),
};
assert_eq!(start(&record), pending(false));
record.observe(WireSeq::new(9), streamed(AckPolicy::Disk, true));
assert_eq!(start(&record), pending(true));
}
}
#[cfg(all(test, feature = "dpdk"))]
mod dpdk_config_tests {
use super::{DpdkArgs, ServerConfig, dpdk_config_from};
use std::net::Ipv4Addr;
#[test]
fn the_node_listens_on_its_bind_port_with_one_queue() {
let cfg = ServerConfig {
bind: "127.0.0.1:9999".parse().expect("an address"),
..Default::default()
};
let dpdk = dpdk_config_from(&cfg).expect("valid config");
assert_eq!(dpdk.listen_port, 9999);
assert_eq!(dpdk.num_queues, 1);
assert_eq!(dpdk.nic.ip_addr, Ipv4Addr::new(10, 0, 0, 1));
assert_eq!(dpdk.nic.peer_mac, None);
}
#[test]
fn the_client_buffers_default_to_the_transport_s() {
let dpdk = dpdk_config_from(&ServerConfig::default()).expect("valid config");
assert_eq!(dpdk.client_buffers, melin_dpdk::SocketBuffers::default());
}
#[test]
fn the_client_buffer_flags_are_in_kib() {
let cfg = ServerConfig {
dpdk_client_rx_buf_kib: 256,
dpdk_client_tx_buf_kib: 512,
dpdk_client_tx_queue_kib: 32768,
..Default::default()
};
let dpdk = dpdk_config_from(&cfg).expect("valid config");
assert_eq!(
dpdk.client_buffers,
melin_dpdk::SocketBuffers {
rx_buf_size: 256 * 1024,
tx_buf_size: 512 * 1024,
tx_queue_limit: 32768 * 1024,
}
);
}
#[test]
fn the_address_defaults_to_the_node_s() {
let cfg = ServerConfig {
dpdk: DpdkArgs::default(),
..Default::default()
};
assert_eq!(cfg.dpdk.ip, None);
let dpdk = dpdk_config_from(&cfg).expect("valid config");
assert_eq!(dpdk.nic.ip_addr, Ipv4Addr::new(10, 0, 0, 1));
}
#[test]
fn a_supplied_peer_mac_reaches_the_transport_config() {
let mut cfg = ServerConfig::default();
cfg.dpdk.peer_mac = Some("0c:42:a1:5b:2e:80".into());
let dpdk = dpdk_config_from(&cfg).expect("valid config");
assert_eq!(
dpdk.nic.peer_mac,
Some([0x0c, 0x42, 0xa1, 0x5b, 0x2e, 0x80])
);
}
#[test]
fn a_malformed_peer_mac_fails_config_translation() {
let mut cfg = ServerConfig::default();
cfg.dpdk.peer_mac = Some("0c:42:a1:5b:2e".into());
let err = dpdk_config_from(&cfg)
.expect_err("a 5-octet MAC must be rejected")
.to_string();
assert!(err.starts_with("invalid --dpdk-peer-mac: "), "{err}");
}
}