use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use melin_app::auth::AuthorizedKeys;
use melin_raft::driver::{RaftConfig, RaftHandles, RaftPeer};
use tracing::info;
use crate::server::ServerConfig;
fn parse_ed25519_pubkey_b64(b64: &str) -> Result<[u8; 32], String> {
use base64::Engine as _;
let bytes = base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(|e| format!("invalid base64: {e}"))?;
let arr: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| format!("expected 32 bytes, got {}", bytes.len()))?;
Ok(arr)
}
fn parse_raft_peer(entry: &str) -> Result<RaftPeer, String> {
let (id_str, rest) = entry
.split_once('@')
.ok_or_else(|| format!("--raft-peer '{entry}': expected id@host:port#pubkey"))?;
let (addr, pubkey_b64) = rest
.split_once('#')
.ok_or_else(|| format!("--raft-peer '{entry}': missing '#base64-pubkey'"))?;
let id: u64 = id_str
.parse()
.map_err(|e| format!("--raft-peer '{entry}': bad node id: {e}"))?;
if addr.parse::<std::net::SocketAddr>().is_err() {
return Err(format!("--raft-peer '{entry}': '{addr}' is not host:port"));
}
let pubkey =
parse_ed25519_pubkey_b64(pubkey_b64).map_err(|e| format!("--raft-peer '{entry}': {e}"))?;
Ok(RaftPeer {
id,
addr: addr.to_owned(),
pubkey,
})
}
pub(crate) fn build_raft_config(config: &ServerConfig) -> Result<Option<RaftConfig>, String> {
let Some(bind) = config.raft_bind else {
if config.raft_node_id.is_some()
|| !config.raft_peer.is_empty()
|| config.raft_dir.is_some()
|| config.raft_auto_promote
{
return Err(
"raft flags (--raft-node-id/--raft-peer/--raft-dir) require --raft-bind".to_owned(),
);
}
return Ok(None);
};
let node_id = config
.raft_node_id
.ok_or_else(|| "--raft-bind requires --raft-node-id".to_owned())?;
if config.replication_key.is_none() {
return Err(
"--raft-bind requires --replication-key (peer links authenticate with it)".to_owned(),
);
}
let peers = config
.raft_peer
.iter()
.map(|e| parse_raft_peer(e))
.collect::<Result<Vec<_>, _>>()?;
if !peers.iter().any(|p| p.id == node_id) {
return Err(format!(
"--raft-peer list must include this node (id {node_id}) with its dialable address"
));
}
if config.raft_auto_promote && peers.len() < 3 {
return Err(format!(
"--raft-auto-promote requires at least 3 configured voters, got {} — a two-node \
cluster cannot elect a leader after losing either node, so automatic failover \
would never fire while inviting misconfiguration; run manual PROMOTE instead",
peers.len()
));
}
let dir = config
.raft_dir
.clone()
.unwrap_or_else(|| config.journal.with_extension("raft"));
Ok(Some(RaftConfig {
node_id,
bind,
dir,
peers,
}))
}
pub(crate) struct RaftDriverGuard {
handles: Option<RaftHandles>,
peer_ids: Vec<u64>,
promotion: Option<std::thread::JoinHandle<()>>,
shutdown: Arc<AtomicBool>,
}
pub(crate) struct PromotionWiring {
pub(crate) status: Arc<melin_transport_core::health::RaftStatus>,
pub(crate) peer_tips: Arc<melin_raft::recency::PeerTips>,
pub(crate) peer_ids: Vec<u64>,
pub(crate) elect_requested: Arc<AtomicBool>,
pub(crate) elect_enabled: Arc<AtomicBool>,
}
impl RaftDriverGuard {
pub(crate) fn disabled(shutdown: &Arc<AtomicBool>) -> Self {
Self {
handles: None,
peer_ids: Vec::new(),
promotion: None,
shutdown: Arc::clone(shutdown),
}
}
pub(crate) fn status(&self) -> Option<Arc<melin_transport_core::health::RaftStatus>> {
self.handles.as_ref().map(|h| Arc::clone(&h.status))
}
pub(crate) fn promotion_wiring(&self) -> Option<PromotionWiring> {
self.handles.as_ref().map(|h| PromotionWiring {
status: Arc::clone(&h.status),
peer_tips: Arc::clone(&h.peer_tips),
peer_ids: self.peer_ids.clone(),
elect_requested: Arc::clone(&h.elect_requested),
elect_enabled: Arc::clone(&h.elect_enabled),
})
}
pub(crate) fn arm_promotion(&mut self, handle: std::thread::JoinHandle<()>) {
debug_assert!(
self.promotion.is_none(),
"arm_promotion called twice — the first thread would leak"
);
self.promotion = Some(handle);
}
}
impl Drop for RaftDriverGuard {
fn drop(&mut self) {
if self.handles.is_none() && self.promotion.is_none() {
return;
}
self.shutdown.store(true, Ordering::Relaxed);
if let Some(h) = self.handles.take() {
let _ = h.join.join();
}
if let Some(h) = self.promotion.take() {
let _ = h.join();
}
}
}
#[allow(clippy::too_many_arguments)] pub(crate) fn spawn_raft_driver(
raft_config: RaftConfig,
auto_promote: bool,
signing_key: &ed25519_dalek::SigningKey,
authorized_keys: &Arc<AuthorizedKeys>,
fence_state: &Arc<melin_transport_core::fence::FenceState>,
journal_tip: melin_transport_core::AdvertisedJournalTip,
tip_ready: Arc<AtomicBool>,
serving_claim: Arc<dyn Fn() -> bool + Send + Sync>,
shutdown: &Arc<AtomicBool>,
) -> Result<RaftDriverGuard, Box<dyn std::error::Error>> {
info!(
node_id = raft_config.node_id,
bind = %raft_config.bind,
voters = raft_config.peers.len(),
dir = %raft_config.dir.display(),
"starting control-plane raft driver"
);
let tip = Arc::new(melin_raft::recency::TipSource {
fence: Arc::clone(fence_state),
seq: journal_tip,
ready: tip_ready,
});
let supersession = auto_promote.then(|| melin_raft::rpc_server::SupersessionPolicy {
fence: Arc::clone(fence_state),
shutdown: Arc::clone(shutdown),
serving: serving_claim,
});
let peer_ids: Vec<u64> = raft_config
.peers
.iter()
.map(|p| p.id)
.filter(|&id| id != raft_config.node_id)
.collect();
let handles = melin_raft::driver::spawn(
raft_config,
Arc::new(signing_key.clone()),
Arc::clone(authorized_keys),
tip,
supersession,
Arc::clone(shutdown),
)?;
Ok(RaftDriverGuard {
handles: Some(handles),
peer_ids,
promotion: None,
shutdown: Arc::clone(shutdown),
})
}
pub(crate) struct ReplicaHealthGuard {
inner: Option<(std::thread::JoinHandle<()>, Arc<AtomicBool>)>,
}
impl ReplicaHealthGuard {
pub(crate) fn stop(&mut self) {
if let Some((handle, stop)) = self.inner.take() {
stop.store(true, Ordering::Release);
let _ = handle.join();
}
}
}
impl Drop for ReplicaHealthGuard {
fn drop(&mut self) {
self.stop();
}
}
pub(crate) fn spawn_replica_health(
config: &ServerConfig,
fence_state: &Arc<melin_transport_core::fence::FenceState>,
raft_status: Option<&Arc<melin_transport_core::health::RaftStatus>>,
pipeline_healthy: Arc<AtomicBool>,
) -> Result<ReplicaHealthGuard, Box<dyn std::error::Error>> {
let Some(addr) = config.health_bind else {
return Ok(ReplicaHealthGuard { inner: None });
};
let stop = Arc::new(AtomicBool::new(false));
let handle = melin_transport_core::health::spawn(
addr,
melin_transport_core::health::HealthState::for_replica(
Arc::clone(fence_state),
raft_status.map(Arc::clone),
pipeline_healthy,
),
Arc::clone(&stop),
)?;
info!(
addr = %addr,
election = raft_status.is_some(),
"replica health endpoint started"
);
Ok(ReplicaHealthGuard {
inner: Some((handle, stop)),
})
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine as _;
fn b64_key(seed: u8) -> String {
let key = ed25519_dalek::SigningKey::from_bytes(&[seed; 32]);
base64::engine::general_purpose::STANDARD.encode(key.verifying_key().to_bytes())
}
fn base_config() -> ServerConfig {
ServerConfig::default()
}
#[test]
fn no_raft_flags_is_none() {
assert!(build_raft_config(&base_config()).unwrap().is_none());
}
#[test]
fn partial_flags_are_refused() {
let mut c = base_config();
c.raft_node_id = Some(1);
assert!(build_raft_config(&c).unwrap_err().contains("--raft-bind"));
let mut c = base_config();
c.raft_peer = vec![format!("1@127.0.0.1:7001#{}", b64_key(1))];
assert!(build_raft_config(&c).unwrap_err().contains("--raft-bind"));
}
#[test]
fn raft_bind_requires_node_id_and_key() {
let mut c = base_config();
c.raft_bind = Some("127.0.0.1:7001".parse().unwrap());
assert!(
build_raft_config(&c)
.unwrap_err()
.contains("--raft-node-id")
);
c.raft_node_id = Some(1);
assert!(
build_raft_config(&c)
.unwrap_err()
.contains("--replication-key")
);
}
#[test]
fn peer_list_must_include_self() {
let mut c = base_config();
c.raft_bind = Some("127.0.0.1:7001".parse().unwrap());
c.raft_node_id = Some(1);
c.replication_key = Some("key".into());
c.raft_peer = vec![format!("2@127.0.0.1:7002#{}", b64_key(2))];
assert!(
build_raft_config(&c)
.unwrap_err()
.contains("must include this node")
);
}
#[test]
fn auto_promote_requires_three_voters() {
let mut c = base_config();
c.raft_bind = Some("127.0.0.1:7001".parse().unwrap());
c.raft_node_id = Some(1);
c.replication_key = Some("key".into());
c.raft_auto_promote = true;
c.raft_peer = vec![
format!("1@10.0.0.1:7001#{}", b64_key(1)),
format!("2@10.0.0.2:7001#{}", b64_key(2)),
];
let err = build_raft_config(&c).unwrap_err();
assert!(err.contains("at least 3"), "{err}");
c.raft_peer.push(format!("3@10.0.0.3:7001#{}", b64_key(3)));
assert!(build_raft_config(&c).unwrap().is_some());
let mut c = base_config();
c.raft_auto_promote = true;
assert!(build_raft_config(&c).unwrap_err().contains("--raft-bind"));
}
#[test]
fn valid_config_parses_and_defaults_dir() {
let mut c = base_config();
c.journal = "/data/melin.journal".into();
c.raft_bind = Some("0.0.0.0:7001".parse().unwrap());
c.raft_node_id = Some(1);
c.replication_key = Some("key".into());
c.raft_peer = vec![
format!("1@10.0.0.1:7001#{}", b64_key(1)),
format!("2@10.0.0.2:7001#{}", b64_key(2)),
format!("3@10.0.0.3:7001#{}", b64_key(3)),
];
let rc = build_raft_config(&c).unwrap().unwrap();
assert_eq!(rc.node_id, 1);
assert_eq!(rc.peers.len(), 3);
assert_eq!(rc.dir, std::path::PathBuf::from("/data/melin.raft"));
}
#[test]
fn malformed_peer_entries_are_refused() {
for bad in [
"no-separators",
"1@127.0.0.1:7001", "x@127.0.0.1:7001#AAAA", "1@not-an-addr#AAAA", "1@127.0.0.1:7001#!!", ] {
assert!(parse_raft_peer(bad).is_err(), "should refuse: {bad}");
}
let ok = format!("1@127.0.0.1:7001#{}", b64_key(1));
assert!(parse_raft_peer(&ok).is_ok());
}
}