use std::io::BufWriter;
use std::io::Write as _;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use rtrb::{Consumer, Producer, RingBuffer};
use crate::error::{DbError, DbResult};
use crate::shutdown::ShutdownSignal;
use super::engine_access::ArcEngine;
use super::event::FixedReplicationEvent;
use super::protocol::*;
pub const SPSC_CAPACITY: usize = 8192;
const SCAN_CHUNK_BYTES: usize = 64 * 1024;
const PHASE2_READ_TIMEOUT_MS: u64 = 20;
const WRITE_TIMEOUT_SECS: u64 = HEARTBEAT_INTERVAL_SECS * 2;
type PendingSlot = crate::sync::Mutex<(
Option<Producer<FixedReplicationEvent>>,
Option<Consumer<FixedReplicationEvent>>,
)>;
pub struct FixedReplicationServer {
stop: ShutdownSignal,
acceptor_handle: Option<JoinHandle<()>>,
handler_handles: Arc<crate::sync::Mutex<Vec<JoinHandle<()>>>>,
#[allow(dead_code)]
producers_installed: Arc<AtomicBool>,
}
impl FixedReplicationServer {
pub fn start(
bind_addr: SocketAddr,
engine: ArcEngine,
signal: ShutdownSignal,
) -> DbResult<Self> {
let shard_count = engine.shard_count();
let mut pending: Vec<PendingSlot> = Vec::with_capacity(shard_count);
for _ in 0..shard_count {
let (p, c) = RingBuffer::new(SPSC_CAPACITY);
pending.push(crate::sync::Mutex::new((Some(p), Some(c))));
}
let pending: Arc<Vec<PendingSlot>> = Arc::new(pending);
let producers_installed = Arc::new(AtomicBool::new(false));
let handler_handles = Arc::new(crate::sync::Mutex::new(Vec::new()));
let listener = TcpListener::bind(bind_addr).map_err(DbError::from)?;
listener.set_nonblocking(true).ok();
let acceptor_handle = {
let engine = engine.clone();
let pending = pending.clone();
let producers_installed = producers_installed.clone();
let stop = signal.clone();
let hh = handler_handles.clone();
thread::spawn(move || {
acceptor_loop(listener, engine, pending, producers_installed, hh, stop);
})
};
Ok(Self {
stop: signal,
acceptor_handle: Some(acceptor_handle),
handler_handles,
producers_installed,
})
}
pub fn stop(&self) {
self.stop.shutdown();
}
}
impl Drop for FixedReplicationServer {
fn drop(&mut self) {
self.stop.shutdown();
if let Some(h) = self.acceptor_handle.take() {
let _ = h.join();
}
let mut handles = crate::sync::lock(&self.handler_handles);
for h in handles.drain(..) {
let _ = h.join();
}
}
}
fn acceptor_loop(
listener: TcpListener,
engine: ArcEngine,
pending: Arc<Vec<PendingSlot>>,
producers_installed: Arc<AtomicBool>,
handler_handles: Arc<crate::sync::Mutex<Vec<JoinHandle<()>>>>,
stop: ShutdownSignal,
) {
while !stop.is_shutdown() {
match listener.accept() {
Ok((stream, addr)) => {
tracing::info!(%addr, "fixed follower connected");
stream.set_nodelay(true).ok();
stream.set_nonblocking(false).ok();
stream
.set_read_timeout(Some(Duration::from_secs(HEARTBEAT_INTERVAL_SECS * 2)))
.ok();
if !producers_installed.swap(true, Ordering::SeqCst) {
tracing::info!("first fixed follower — installing SPSC producers");
let mut producers = Vec::with_capacity(pending.len());
for slot in pending.iter() {
let mut guard = crate::sync::lock(slot);
producers.push(guard.0.take().expect("producer present on first install"));
}
engine.install_replication_producers(producers);
}
let engine = engine.clone();
let pending = pending.clone();
let stop = stop.clone();
let hh = handler_handles.clone();
let handle = thread::spawn(move || {
if let Err(e) = serve_connection(stream, engine, pending, stop) {
tracing::error!(error = %e, "fixed replication connection error");
}
});
let mut handles = crate::sync::lock(&hh);
handles.retain(|h| !h.is_finished());
handles.push(handle);
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
stop.wait_timeout(Duration::from_millis(50));
}
Err(e) => {
tracing::error!(error = %e, "fixed accept error");
stop.wait_timeout(Duration::from_millis(100));
}
}
}
tracing::info!("fixed replication acceptor stopped");
}
struct ShardConsumerGuard {
pending: Arc<Vec<PendingSlot>>,
shard_id: usize,
consumer: Option<Consumer<FixedReplicationEvent>>,
}
impl ShardConsumerGuard {
fn new(
pending: Arc<Vec<PendingSlot>>,
shard_id: usize,
consumer: Consumer<FixedReplicationEvent>,
) -> Self {
Self {
pending,
shard_id,
consumer: Some(consumer),
}
}
fn take(&mut self) -> Consumer<FixedReplicationEvent> {
self.consumer.take().expect("consumer present")
}
fn put_back(&mut self, consumer: Consumer<FixedReplicationEvent>) {
self.consumer = Some(consumer);
}
}
impl Drop for ShardConsumerGuard {
fn drop(&mut self) {
if let Some(consumer) = self.consumer.take() {
let mut guard = crate::sync::lock(&self.pending[self.shard_id]);
if guard.1.is_none() {
guard.1 = Some(consumer);
}
}
}
}
fn serve_connection(
stream: TcpStream,
engine: ArcEngine,
pending: Arc<Vec<PendingSlot>>,
stop: ShutdownSignal,
) -> DbResult<()> {
let mut reader = stream.try_clone().map_err(DbError::from)?;
let _ = stream.set_write_timeout(Some(Duration::from_secs(WRITE_TIMEOUT_SECS)));
let mut writer = BufWriter::new(stream);
let frame = read_frame(&mut reader).map_err(DbError::from)?;
if frame.msg_type != FixedMessageType::SyncRequest {
return Err(DbError::Replication(format!(
"expected SyncRequest, got {:?}",
frame.msg_type
)));
}
let req = SyncRequest::decode(&frame.payload).map_err(DbError::from)?;
if req.protocol_version != PROTOCOL_VERSION {
let msg = format!(
"protocol version mismatch: leader {PROTOCOL_VERSION}, follower {}",
req.protocol_version
);
write_frame(&mut writer, &encode_error(&msg)).map_err(DbError::from)?;
return Err(DbError::Replication(msg));
}
let shard_id = req.shard_id as usize;
if shard_id >= engine.shard_count() {
write_frame(&mut writer, &encode_error("invalid shard_id")).map_err(DbError::from)?;
return Err(DbError::Replication(format!("invalid shard_id {shard_id}")));
}
let info = ShardInfo {
shard_count: engine.shard_count() as u8,
key_len: engine.key_len() as u16,
value_len: engine.value_len() as u16,
slot_size: engine.slot_size(),
current_slot_count: engine.current_slot_count(shard_id),
shard_prefix_bits: engine.shard_prefix_bits(),
};
write_frame(&mut writer, &info.encode()).map_err(DbError::from)?;
let skip_deleted = (req.flags & FLAG_EMPTY_STATE) != 0;
tracing::info!(
shard_id,
skip_deleted,
protocol_version = req.protocol_version,
"fixed follower handshake complete"
);
let mut consumer = {
let mut guard = crate::sync::lock(&pending[shard_id]);
match guard.1.take() {
Some(c) => c,
None => {
let msg = "shard already streaming";
write_frame(&mut writer, &encode_error(msg)).map_err(DbError::from)?;
return Err(DbError::Replication(msg.to_string()));
}
}
};
while consumer.pop().is_ok() {}
let _ = engine.take_dropped(shard_id);
let mut consumer_guard = ShardConsumerGuard::new(pending.clone(), shard_id, consumer);
let total = phase1_full_scan(
&engine,
shard_id,
&mut writer,
&mut reader,
skip_deleted,
&stop,
)?;
write_frame(
&mut writer,
&CaughtUp {
shard_id: shard_id as u8,
total_scanned: total,
}
.encode(),
)
.map_err(DbError::from)?;
tracing::info!(shard_id, total, "fixed catch-up complete");
let consumer = consumer_guard.take();
let consumer = phase2_streaming(&engine, shard_id, consumer, &mut writer, &mut reader, &stop)?;
consumer_guard.put_back(consumer);
Ok(())
}
fn phase1_full_scan(
engine: &ArcEngine,
shard_id: usize,
writer: &mut BufWriter<TcpStream>,
reader: &mut TcpStream,
skip_deleted: bool,
stop: &ShutdownSignal,
) -> DbResult<u64> {
use crate::fixed::slot::{
SLOT_HEADER_SIZE, STATUS_DELETED, STATUS_FREE, STATUS_OCCUPIED, meta_of, pack_meta,
read_slot, status_of, version_of,
};
let slot_size = engine.slot_size() as usize;
let key_len = engine.key_len();
let value_len = engine.value_len();
let slot_count = engine.current_slot_count(shard_id);
let slots_per_chunk = (SCAN_CHUNK_BYTES / slot_size).max(1);
let mut total_scanned = 0u64;
let mut batch = SlotBatchEncoder::new(shard_id as u8, key_len, value_len);
let mut slot_id = 0u32;
while slot_id < slot_count {
if stop.is_shutdown() {
return Ok(total_scanned);
}
let remaining = slot_count - slot_id;
let this_chunk = remaining.min(slots_per_chunk as u32) as usize;
let chunk = engine.read_shard_chunk(shard_id, slot_id, this_chunk)?;
for i in 0..this_chunk {
let off = i * slot_size;
let slot_buf = &chunk[off..off + slot_size];
let meta = meta_of(slot_buf);
let status = status_of(meta);
let current_slot = slot_id + i as u32;
match status {
STATUS_OCCUPIED => {
if let Some((_meta, key, value)) = read_slot(slot_buf, key_len, value_len) {
batch.add_occupied(current_slot, meta, key, value);
total_scanned += 1;
} else {
let reset_meta = pack_meta(STATUS_FREE, version_of(meta));
batch.add_reset(current_slot, reset_meta);
total_scanned += 1;
}
}
STATUS_DELETED => {
if !skip_deleted {
let key = &slot_buf[SLOT_HEADER_SIZE..SLOT_HEADER_SIZE + key_len];
batch.add_deleted(current_slot, meta, key);
total_scanned += 1;
}
}
STATUS_FREE => {
if version_of(meta) != 0 {
let reset_meta = pack_meta(STATUS_FREE, version_of(meta));
batch.add_reset(current_slot, reset_meta);
total_scanned += 1;
}
}
other => {
return Err(DbError::Replication(format!(
"fixed Phase-1 scan found unknown slot status {other} at shard {shard_id} slot {current_slot}"
)));
}
}
if !batch.is_empty()
&& (batch.len() as usize >= BATCH_MAX_ENTRIES || batch.bytes() >= BATCH_MAX_BYTES)
{
flush_and_wait_ack(writer, reader, batch, engine, shard_id)?;
batch = SlotBatchEncoder::new(shard_id as u8, key_len, value_len);
}
}
slot_id += this_chunk as u32;
}
if !batch.is_empty() {
flush_and_wait_ack(writer, reader, batch, engine, shard_id)?;
}
metrics::counter!(
"armdb.fixed.catchup_slots_scanned",
"shard" => shard_id.to_string()
)
.increment(total_scanned);
Ok(total_scanned)
}
fn flush_and_wait_ack(
writer: &mut BufWriter<TcpStream>,
reader: &mut TcpStream,
batch: SlotBatchEncoder,
engine: &ArcEngine,
shard_id: usize,
) -> DbResult<()> {
const MAX_HEARTBEATS: u32 = 8;
let frame = batch.finish();
write_frame(writer, &frame).map_err(DbError::from)?;
writer.flush().map_err(DbError::from)?;
let mut heartbeats_skipped: u32 = 0;
loop {
let ack_frame = read_frame(reader).map_err(DbError::from)?;
match ack_frame.msg_type {
FixedMessageType::Ack => {
let ack = Ack::decode(&ack_frame.payload).map_err(DbError::from)?;
engine.update_min_replicated_version(shard_id, ack.max_version_seen);
return Ok(());
}
FixedMessageType::Heartbeat => {
heartbeats_skipped += 1;
if heartbeats_skipped > MAX_HEARTBEATS {
return Err(DbError::Replication(format!(
"too many consecutive heartbeats ({heartbeats_skipped}) \
while waiting for Phase-1 Ack"
)));
}
continue;
}
other => {
return Err(DbError::Replication(format!(
"expected Ack during Phase-1 catch-up, got {other:?}"
)));
}
}
}
}
fn phase2_streaming(
engine: &ArcEngine,
shard_id: usize,
mut consumer: Consumer<FixedReplicationEvent>,
writer: &mut BufWriter<TcpStream>,
reader: &mut TcpStream,
stop: &ShutdownSignal,
) -> DbResult<Consumer<FixedReplicationEvent>> {
use crate::fixed::slot::{SLOT_HEADER_SIZE, meta_of};
let key_len = engine.key_len();
let value_len = engine.value_len();
let slot_size = engine.slot_size() as usize;
let mut last_heartbeat = Instant::now();
let hb_interval = Duration::from_secs(HEARTBEAT_INTERVAL_SECS);
reader
.set_read_timeout(Some(Duration::from_millis(PHASE2_READ_TIMEOUT_MS)))
.ok();
let mut frame_reader = FrameReader::new();
loop {
if stop.is_shutdown() {
return Ok(consumer);
}
if engine.take_dropped(shard_id) {
metrics::counter!(
"armdb.fixed.dropped_rescan",
"shard" => shard_id.to_string()
)
.increment(1);
let _ = write_frame(writer, &encode_error("events dropped, rescan required"))
.and_then(|_| writer.flush());
return Ok(consumer);
}
let mut batch = SlotBatchEncoder::new(shard_id as u8, key_len, value_len);
while (batch.len() as usize) < BATCH_MAX_ENTRIES && batch.bytes() < BATCH_MAX_BYTES {
match consumer.pop() {
Ok(FixedReplicationEvent::Write { slot_id, payload }) => {
debug_assert_eq!(payload.len(), slot_size);
let meta = meta_of(&payload);
let key = &payload[SLOT_HEADER_SIZE..SLOT_HEADER_SIZE + key_len];
let value = &payload
[SLOT_HEADER_SIZE + key_len..SLOT_HEADER_SIZE + key_len + value_len];
batch.add_occupied(slot_id, meta, key, value);
}
Ok(FixedReplicationEvent::Delete { slot_id, meta, key }) => {
batch.add_deleted(slot_id, meta, &key);
}
Err(_) => break,
}
}
if !batch.is_empty() {
let frame_events = batch.len() as u64;
let frame = batch.finish();
if write_frame(writer, &frame)
.and_then(|_| writer.flush())
.is_err()
{
return Ok(consumer);
}
metrics::counter!(
"armdb.fixed.streaming_events_sent",
"shard" => shard_id.to_string()
)
.increment(frame_events);
match frame_reader.read_frame(reader) {
Ok(f) if f.msg_type == FixedMessageType::Ack => {
if let Ok(ack) = Ack::decode(&f.payload) {
engine.update_min_replicated_version(shard_id, ack.max_version_seen);
}
}
Ok(_) => {}
Err(ref e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut => {}
Err(e) => {
tracing::debug!(shard_id, error = %e, "fixed streaming: peer disconnected");
return Ok(consumer);
}
}
} else {
match frame_reader.read_frame(reader) {
Err(ref e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut => {}
Ok(f) if f.msg_type == FixedMessageType::Heartbeat => {
}
Ok(_) => {}
Err(e) => {
tracing::debug!(shard_id, error = %e, "fixed streaming idle: peer disconnected");
return Ok(consumer);
}
}
if last_heartbeat.elapsed() >= hb_interval {
if write_frame(writer, &encode_heartbeat())
.and_then(|_| writer.flush())
.is_err()
{
return Ok(consumer);
}
last_heartbeat = Instant::now();
}
thread::sleep(Duration::from_millis(TAIL_POLL_MS));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::TcpListener;
use std::sync::atomic::AtomicBool;
struct MockEngine {
dropped: AtomicBool,
}
impl crate::fixed_replication::FixedEngineAccess for MockEngine {
fn shard_count(&self) -> usize {
1
}
fn key_len(&self) -> usize {
8
}
fn value_len(&self) -> usize {
16
}
fn slot_size(&self) -> u16 {
32
}
fn current_slot_count(&self, _shard_id: usize) -> u32 {
0
}
fn shard_prefix_bits(&self) -> u8 {
0
}
fn read_shard_chunk(
&self,
_shard_id: usize,
_start_slot: u32,
count: usize,
) -> DbResult<Vec<u8>> {
Ok(vec![0u8; count * 32])
}
fn install_replication_producers(&self, _producers: Vec<Producer<FixedReplicationEvent>>) {}
fn update_min_replicated_version(&self, _shard_id: usize, _version: u32) {}
fn take_dropped(&self, _shard_id: usize) -> bool {
self.dropped.swap(false, Ordering::Relaxed)
}
}
#[test]
fn test_f02_phase2_dropped_sends_error_and_returns_consumer() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let mut client = TcpStream::connect(addr).unwrap();
let (server_stream, _) = listener.accept().unwrap();
let engine: ArcEngine = Arc::new(MockEngine {
dropped: AtomicBool::new(true),
});
let (_producer, consumer) = RingBuffer::<FixedReplicationEvent>::new(8);
let mut writer = BufWriter::new(server_stream.try_clone().unwrap());
let mut reader = server_stream;
let stop = ShutdownSignal::new();
let returned =
phase2_streaming(&engine, 0, consumer, &mut writer, &mut reader, &stop).unwrap();
drop(returned);
client
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
let mut fr = FrameReader::new();
let frame = loop {
match fr.read_frame(&mut client) {
Ok(f) => break f,
Err(ref e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut =>
{
continue;
}
Err(e) => panic!("unexpected error reading Error frame: {e}"),
}
};
assert_eq!(frame.msg_type, FixedMessageType::Error);
assert!(
decode_error(&frame.payload).contains("events dropped"),
"unexpected error payload: {:?}",
decode_error(&frame.payload)
);
}
struct LivelockMock {
dropped: AtomicBool,
injector: crate::sync::Mutex<(Producer<FixedReplicationEvent>, bool)>,
}
impl crate::fixed_replication::FixedEngineAccess for LivelockMock {
fn shard_count(&self) -> usize {
1
}
fn key_len(&self) -> usize {
8
}
fn value_len(&self) -> usize {
16
}
fn slot_size(&self) -> u16 {
32
}
fn current_slot_count(&self, _shard_id: usize) -> u32 {
4
}
fn shard_prefix_bits(&self) -> u8 {
0
}
fn read_shard_chunk(
&self,
_shard_id: usize,
_start_slot: u32,
count: usize,
) -> DbResult<Vec<u8>> {
let mut g = crate::sync::lock(&self.injector);
if !g.1 {
g.1 = true;
let ev = FixedReplicationEvent::Delete {
slot_id: 0,
meta: 1,
key: vec![0u8; 8],
};
if g.0.push(ev).is_err() {
self.dropped.store(true, Ordering::Relaxed);
}
}
Ok(vec![0u8; count * 32])
}
fn install_replication_producers(&self, _producers: Vec<Producer<FixedReplicationEvent>>) {}
fn update_min_replicated_version(&self, _shard_id: usize, _version: u32) {}
fn take_dropped(&self, _shard_id: usize) -> bool {
self.dropped.swap(false, Ordering::Relaxed)
}
}
#[test]
fn test_p1_6_claim_drains_overflowed_ring_no_livelock() {
let (mut prod, cons) = RingBuffer::<FixedReplicationEvent>::new(4);
while prod
.push(FixedReplicationEvent::Delete {
slot_id: 0,
meta: 1,
key: vec![0u8; 8],
})
.is_ok()
{}
let pending: Arc<Vec<PendingSlot>> =
Arc::new(vec![crate::sync::Mutex::new((None, Some(cons)))]);
let engine: ArcEngine = Arc::new(LivelockMock {
dropped: AtomicBool::new(false),
injector: crate::sync::Mutex::new((prod, false)),
});
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let mut client = TcpStream::connect(addr).unwrap();
let (server_stream, _) = listener.accept().unwrap();
let stop = ShutdownSignal::new();
let stop_srv = stop.clone();
let handle = thread::spawn(move || {
let _ = serve_connection(server_stream, engine, pending, stop_srv);
});
write_frame(
&mut client,
&SyncRequest {
shard_id: 0,
protocol_version: PROTOCOL_VERSION,
flags: 0,
}
.encode(),
)
.unwrap();
client
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
let mut fr = FrameReader::new();
let mut caught_up = false;
let mut verdict: Option<FixedMessageType> = None;
for _ in 0..16 {
match fr.read_frame(&mut client) {
Ok(f) => {
if caught_up {
verdict = Some(f.msg_type);
break;
}
match f.msg_type {
FixedMessageType::CaughtUp => caught_up = true,
FixedMessageType::Error => {
verdict = Some(FixedMessageType::Error);
break;
}
_ => {}
}
}
Err(ref e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut => {}
Err(e) => panic!("unexpected read error: {e}"),
}
}
stop.shutdown();
drop(client);
let _ = handle.join();
assert!(caught_up, "Phase-1 never completed (no CaughtUp frame)");
assert_ne!(
verdict,
Some(FixedMessageType::Error),
"claim did not drain the overflowed ring: a Phase-1 write re-armed the \
rescan flag and Phase-2 bailed — the livelock this fix prevents"
);
}
}