use std::cmp::Ordering;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::MutexGuard;
use std::sync::mpsc::SyncSender;
use beamr::process::ExitReason;
use beamr::term::Term;
use beamr::{NativeContext, NativeHandler, NativeOutcome};
use crate::shard::actor::ShardActor;
use crate::store::DiskStore;
use crate::tree::Cursor;
use crate::ttl::filter::{Visibility, visible_value};
use crate::wal::{DurableWal, FsyncPolicy, Mutation, WalRecovery};
use super::expiry_index::{ArmError, DeadlineScheduler, Generation};
use super::handle::{
CommandQueue, RangeItem, ShardCommand, ShardCommandKind, ShardError, StreamSeq,
};
use super::{GroupOutcome, GroupWrite};
type GroupReply = SyncSender<Result<(), ShardError>>;
pub struct ShardNativeHandler {
state: Option<ShardState>,
startup_error: Option<ShardError>,
commands: CommandQueue,
}
struct ShardState {
actor: ShardActor,
store: DiskStore,
}
impl ShardNativeHandler {
pub(super) fn make_factory(
store_dir: PathBuf,
wal_path: PathBuf,
commands: CommandQueue,
) -> beamr::native::native_process::NativeHandlerFactory {
Box::new(move || Box::new(Self::build(&store_dir, &wal_path, Arc::clone(&commands))))
}
fn build(store_dir: &Path, wal_path: &Path, commands: CommandQueue) -> Self {
match Self::boot(store_dir, wal_path) {
Ok(state) => Self {
state: Some(state),
startup_error: None,
commands,
},
Err(error) => Self {
state: None,
startup_error: Some(error),
commands,
},
}
}
fn boot(store_dir: &Path, wal_path: &Path) -> Result<ShardState, ShardError> {
let store = DiskStore::new(store_dir)?;
let recovered = WalRecovery::recover_path(wal_path, &store)?;
let wal = DurableWal::new(wal_path, FsyncPolicy::CommitOnly)?;
let actor = ShardActor::from_recovered(wal, recovered, &store)?;
Ok(ShardState { actor, store })
}
fn pop_and_execute(
state: &mut ShardState,
commands: &CommandQueue,
ctx: &mut NativeContext<'_>,
) -> Option<NativeOutcome> {
match pop_next_unit(commands)? {
DrainUnit::Group(group) => {
state.run_group(group, ctx);
None
}
DrainUnit::Single(command) => state.execute(command, ctx),
}
}
}
enum DrainUnit {
Group(Vec<(GroupWrite, GroupReply)>),
Single(ShardCommand),
}
fn pop_next_unit(commands: &CommandQueue) -> Option<DrainUnit> {
let mut queue = lock_queue(commands);
if !queue
.front()
.is_some_and(|command| is_groupable(&command.kind))
{
return queue.pop_front().map(DrainUnit::Single);
}
let mut group = Vec::new();
while queue
.front()
.is_some_and(|command| is_groupable(&command.kind))
{
if let Some(command) = queue.pop_front()
&& let Some(entry) = into_group_write(command.kind)
{
group.push(entry);
}
}
Some(DrainUnit::Group(group))
}
const fn is_groupable(kind: &ShardCommandKind) -> bool {
matches!(
kind,
ShardCommandKind::Cas { .. }
| ShardCommandKind::ApplyDurable { .. }
| ShardCommandKind::ApplyDurableTombstone { .. }
)
}
fn into_group_write(kind: ShardCommandKind) -> Option<(GroupWrite, GroupReply)> {
match kind {
ShardCommandKind::Cas {
key,
expected,
new,
reply,
} => Some((GroupWrite::Cas { key, expected, new }, reply)),
ShardCommandKind::ApplyDurable {
key,
expected,
value,
ttl,
stamp,
reply,
} => Some((
GroupWrite::ApplyValue {
key,
expected,
value,
ttl,
stamp,
},
reply,
)),
ShardCommandKind::ApplyDurableTombstone {
key,
expected,
stamp,
reply,
} => Some((
GroupWrite::ApplyTombstone {
key,
expected,
stamp,
},
reply,
)),
_ => None,
}
}
struct NativeDeadlineScheduler<'context, 'process> {
context: &'context mut NativeContext<'process>,
}
impl DeadlineScheduler for NativeDeadlineScheduler<'_, '_> {
fn schedule(
&mut self,
delay: std::time::Duration,
generation: Generation,
) -> Result<(), ArmError> {
let value =
i64::try_from(generation.value()).map_err(|_error| ArmError::TokenUnrepresentable)?;
let token = Term::try_small_int(value).ok_or(ArmError::TokenUnrepresentable)?;
self.context
.schedule(delay, token)
.map(drop)
.ok_or(ArmError::SchedulerUnavailable)
}
}
#[cfg(test)]
pub(super) fn schedule_probe(
context: &mut NativeContext<'_>,
delay: std::time::Duration,
token: u64,
) -> Result<(), ArmError> {
let generation = Generation::from_value(token).ok_or(ArmError::TokenUnrepresentable)?;
NativeDeadlineScheduler { context }.schedule(delay, generation)
}
impl ShardState {
fn arm_pending(&mut self, ctx: &mut NativeContext<'_>) {
let mut scheduler = NativeDeadlineScheduler { context: ctx };
if let Err(error) = self.actor.arm_expiry(&mut scheduler) {
log::error!("shard TTL deadline arm refused: {error}");
}
}
fn receive_deadline(&mut self, generation: Generation, ctx: &mut NativeContext<'_>) {
let Some(due) = self.actor.begin_expiry_deadline(generation) else {
return;
};
for (position, (_deadline, key)) in due.iter().enumerate() {
self.actor.inspect_expiry_key();
let (reply, response) = std::sync::mpsc::sync_channel(1);
let command = ShardCommand {
id: 0,
kind: ShardCommandKind::DeleteIfExpired {
key: key.clone(),
reply,
},
};
let _outcome = self.execute(command, ctx);
match response.recv() {
Ok(Ok(_removed)) => {}
Ok(Err(error)) => {
log::error!("shard TTL deadline delete failed: {error}");
self.actor.restore_detached(&due[position..]);
break;
}
Err(error) => {
log::error!("shard TTL deadline delete reply failed: {error}");
self.actor.restore_detached(&due[position..]);
break;
}
}
}
self.actor.finish_expiry_deadline();
self.arm_pending(ctx);
}
fn run_group(&mut self, group: Vec<(GroupWrite, GroupReply)>, ctx: &mut NativeContext<'_>) {
let mut writes = Vec::with_capacity(group.len());
let mut replies = Vec::with_capacity(group.len());
for (write, reply) in group {
writes.push(write);
replies.push(reply);
}
let outcomes = self.actor.apply_group(writes, &mut self.store);
self.arm_pending(ctx);
for (reply, outcome) in replies.into_iter().zip(outcomes) {
let result = match outcome {
GroupOutcome::Committed => Ok(()),
GroupOutcome::Rejected(error) | GroupOutcome::CommitFailed(error) => Err(error),
};
drop(reply.send(result));
}
}
fn execute(
&mut self,
command: ShardCommand,
ctx: &mut NativeContext<'_>,
) -> Option<NativeOutcome> {
match command.kind {
ShardCommandKind::Get { key, reply } => {
let result = self.actor.get(&key, &self.store).map_err(ShardError::from);
drop(reply.send(result));
None
}
ShardCommandKind::GetRaw { key, reply } => {
let result = self
.actor
.get_raw(&key, &self.store)
.map_err(ShardError::from);
drop(reply.send(result));
None
}
ShardCommandKind::Put {
key,
value,
ttl,
reply,
} => {
let result = self
.actor
.put_with_ttl(key, value, ttl, &self.store)
.map_err(ShardError::from);
self.arm_pending(ctx);
drop(reply.send(result));
None
}
ShardCommandKind::Delete { key, stamp, reply } => {
let result = self
.actor
.delete(key, stamp, &self.store)
.map_err(ShardError::from);
self.arm_pending(ctx);
drop(reply.send(result));
None
}
ShardCommandKind::DeleteIfExpired { key, reply } => {
let result = self
.actor
.delete_if_expired(&key, &self.store)
.map_err(ShardError::from);
self.arm_pending(ctx);
drop(reply.send(result));
None
}
ShardCommandKind::Commit { reply } => {
let result = self.actor.commit(&mut self.store).map_err(ShardError::from);
drop(reply.send(result));
None
}
range @ (ShardCommandKind::Range { .. } | ShardCommandKind::HasLiveInRange(..)) => {
self.execute_range(range)
}
ShardCommandKind::Append {
key,
entries,
expected_seq,
ttl,
reply,
} => {
let result = self
.actor
.append(&key, entries, expected_seq, ttl, &mut self.store)
.map_err(ShardError::from);
self.arm_pending(ctx);
drop(reply.send(result));
None
}
ShardCommandKind::ReadValue { key, reply } => {
let result = self
.actor
.read_value(&key, &self.store)
.map_err(ShardError::from);
drop(reply.send(result));
None
}
ShardCommandKind::ReadPromiseState { reply } => {
drop(reply.send(Ok(self.actor.promise_state())));
None
}
extra @ (ShardCommandKind::Cas { .. }
| ShardCommandKind::ApplyDurable { .. }
| ShardCommandKind::ApplyDurableTombstone { .. }
| ShardCommandKind::ApplyDurableBatch { .. }
| ShardCommandKind::RecordPromise { .. }
| ShardCommandKind::RecordOwnerEpoch { .. }
| ShardCommandKind::ReserveMinted { .. }
| ShardCommandKind::ExportReachable { .. }
| ShardCommandKind::MergeAdopt { .. }) => self.execute_extra(extra, ctx),
ShardCommandKind::ScanSequences { reply } => {
drop(reply.send(self.scan_sequences()));
None
}
ShardCommandKind::Shutdown { reply } => {
self.actor.invalidate_expiry_on_shutdown();
drop(reply.send(Ok(())));
Some(NativeOutcome::Stop(ExitReason::Normal))
}
}
}
fn execute_extra(
&mut self,
command: ShardCommandKind,
ctx: &mut NativeContext<'_>,
) -> Option<NativeOutcome> {
match command {
ShardCommandKind::Cas {
key,
expected,
new,
reply,
} => {
let result = self
.actor
.cas(&key, expected, new, &mut self.store)
.map_err(ShardError::from);
self.arm_pending(ctx);
drop(reply.send(result));
}
ShardCommandKind::ApplyDurable {
key,
expected,
value,
ttl,
stamp,
reply,
} => {
let result = self
.actor
.apply_durable(&key, expected, value, ttl, stamp, &mut self.store)
.map_err(ShardError::from);
self.arm_pending(ctx);
drop(reply.send(result));
}
ShardCommandKind::ApplyDurableTombstone {
key,
expected,
stamp,
reply,
} => {
let result = self
.actor
.apply_durable_tombstone(&key, expected, stamp, &mut self.store)
.map_err(ShardError::from);
self.arm_pending(ctx);
drop(reply.send(result));
}
ShardCommandKind::ApplyDurableBatch {
items,
stamp,
reply,
} => {
let result = self
.actor
.apply_durable_batch(items, stamp, &mut self.store)
.map_err(ShardError::from);
self.arm_pending(ctx);
drop(reply.send(result));
}
ShardCommandKind::RecordPromise { ballot, reply } => {
let result = self.actor.record_promise(ballot).map_err(ShardError::from);
drop(reply.send(result));
}
ShardCommandKind::RecordOwnerEpoch { ballot, reply } => {
let result = self
.actor
.record_owner_epoch(ballot)
.map_err(ShardError::from);
drop(reply.send(result));
}
ShardCommandKind::ReserveMinted { counter, reply } => {
let result = self.actor.reserve_minted(counter).map_err(ShardError::from);
drop(reply.send(result));
}
ShardCommandKind::ExportReachable { shard_id, reply } => {
let result = self
.actor
.export_reachable(shard_id, &self.store)
.map_err(ShardError::from);
drop(reply.send(result));
}
ShardCommandKind::MergeAdopt { promisers, reply } => {
let result = self
.actor
.merge_adopt(&promisers, &mut self.store)
.map_err(ShardError::from);
self.arm_pending(ctx);
drop(reply.send(result));
}
_ => {}
}
None
}
fn scan_sequences(&self) -> Result<Vec<StreamSeq>, ShardError> {
self.actor.scan_sequences()
}
fn execute_range(&self, command: ShardCommandKind) -> Option<NativeOutcome> {
match command {
ShardCommandKind::Range { from, to, reply } => {
drop(reply.send(self.collect_range(&from, &to)));
}
ShardCommandKind::HasLiveInRange(from, to, reply) => {
let result = super::liveness::has_live_in_range(
&self.store,
self.actor.committed_root(),
self.actor.buffer(),
from.as_slice(),
to.as_slice(),
);
drop(reply.send(result));
}
_ => {}
}
None
}
fn collect_range(&self, from: &[u8], to: &[u8]) -> Result<Vec<RangeItem>, ShardError> {
let tree_entries = match self.actor.committed_root() {
Some(root) => Cursor::new(&self.store, root)
.range(from, to)
.collect::<Result<Vec<_>, _>>()
.map_err(ShardError::from)?,
None => Vec::new(),
};
let buffer_entries: Vec<&Mutation> = self
.actor
.buffer()
.iter()
.filter(|mutation| in_range(mutation, from, to))
.collect();
merge_range(&tree_entries, &buffer_entries)
}
}
impl NativeHandler for ShardNativeHandler {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
let Some(state) = self.state.as_mut() else {
return self.fail_startup();
};
state.arm_pending(ctx);
while let Some(message) = ctx.recv() {
if let Some(raw_generation) = message.as_small_int()
&& let Ok(value) = u64::try_from(raw_generation)
&& let Some(generation) = Generation::from_value(value)
{
state.receive_deadline(generation, ctx);
} else if let Some(outcome) = Self::pop_and_execute(state, &self.commands, ctx) {
return outcome;
}
}
NativeOutcome::Wait
}
}
impl ShardNativeHandler {
fn fail_startup(&self) -> NativeOutcome {
let message = self
.startup_error
.as_ref()
.map_or_else(|| "shard startup failed".to_owned(), ToString::to_string);
while let Some(command) = pop_command(&self.commands) {
super::startup::reply_startup_error(command, &message);
}
NativeOutcome::Stop(ExitReason::Error)
}
}
pub(super) fn lock_queue(commands: &CommandQueue) -> MutexGuard<'_, VecDeque<ShardCommand>> {
commands
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn pop_command(commands: &CommandQueue) -> Option<ShardCommand> {
lock_queue(commands).pop_front()
}
fn in_range(mutation: &Mutation, from: &[u8], to: &[u8]) -> bool {
let key = mutation.key();
from <= key && key < to
}
fn merge_range(
tree_entries: &[(Vec<u8>, Vec<u8>)],
buffer_entries: &[&Mutation],
) -> Result<Vec<RangeItem>, ShardError> {
let capacity = tree_entries
.len()
.saturating_add(buffer_entries.len())
.saturating_add(1);
let mut items = Vec::with_capacity(capacity);
let mut tree_index = 0;
let mut buffer_index = 0;
loop {
match (
tree_entries.get(tree_index),
buffer_entries.get(buffer_index),
) {
(Some((tree_key, tree_value)), Some(buffer_mutation)) => {
match tree_key.as_slice().cmp(buffer_mutation.key()) {
Ordering::Less => {
push_entry(&mut items, tree_key, tree_value)?;
tree_index = tree_index.saturating_add(1);
}
Ordering::Equal => {
push_mutation(&mut items, buffer_mutation)?;
tree_index = tree_index.saturating_add(1);
buffer_index = buffer_index.saturating_add(1);
}
Ordering::Greater => {
push_mutation(&mut items, buffer_mutation)?;
buffer_index = buffer_index.saturating_add(1);
}
}
}
(Some((tree_key, tree_value)), None) => {
push_entry(&mut items, tree_key, tree_value)?;
tree_index = tree_index.saturating_add(1);
}
(None, Some(buffer_mutation)) => {
push_mutation(&mut items, buffer_mutation)?;
buffer_index = buffer_index.saturating_add(1);
}
(None, None) => break,
}
}
items.push(RangeItem::Done);
Ok(items)
}
fn push_entry(items: &mut Vec<RangeItem>, key: &[u8], value: &[u8]) -> Result<(), ShardError> {
match visible_value(value)
.map_err(|error| ShardError::Wal(crate::wal::WalError::TreeError(error.to_string())))?
{
Visibility::Live(value) => items.push(RangeItem::Entry {
key: key.to_vec(),
value,
}),
Visibility::Expired => {}
}
Ok(())
}
fn push_mutation(items: &mut Vec<RangeItem>, mutation: &Mutation) -> Result<(), ShardError> {
if let Mutation::Put { key, value } = mutation {
push_entry(items, key, value)?;
}
Ok(())
}
#[cfg(test)]
const fn assert_disk_store_send() {
const fn require_send<T: Send>() {}
require_send::<DiskStore>();
require_send::<ShardNativeHandler>();
let _ = require_send::<DiskStore>;
}
#[cfg(test)]
const _: () = assert_disk_store_send();
#[cfg(test)]
mod boot_failure_tests {
use super::ShardNativeHandler;
use crate::shard::actor::handle::{
CommandQueue, RangeItem, ShardCommand, ShardCommandKind, ShardError,
};
use beamr::NativeOutcome;
use beamr::process::ExitReason;
use std::collections::VecDeque;
use std::error::Error;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
fn boot_failed_handler(
commands: CommandQueue,
) -> Result<(ShardNativeHandler, tempfile::TempDir), Box<dyn Error>> {
let dir = tempfile::tempdir()?;
let store_path = dir.path().join("store-is-a-file");
std::fs::write(&store_path, b"not a directory")?;
let wal_path = dir.path().join("shard.wal");
let handler = ShardNativeHandler::build(&store_path, &wal_path, commands);
Ok((handler, dir))
}
#[test]
fn fail_startup_drains_every_command_kind_with_spawn_then_stops() -> Result<(), Box<dyn Error>>
{
let commands: CommandQueue = Arc::new(Mutex::new(VecDeque::new()));
let (get_tx, get_rx) = mpsc::sync_channel(1);
let (put_tx, put_rx) = mpsc::sync_channel(1);
let (del_tx, del_rx) = mpsc::sync_channel(1);
let (commit_tx, commit_rx) = mpsc::sync_channel(1);
let (range_tx, range_rx) = mpsc::sync_channel::<Result<Vec<RangeItem>, ShardError>>(1);
{
let mut queue = commands.lock().map_err(|_| "queue poisoned")?;
queue.push_back(ShardCommand {
id: 1,
kind: ShardCommandKind::Get {
key: b"k".to_vec(),
reply: get_tx,
},
});
queue.push_back(ShardCommand {
id: 2,
kind: ShardCommandKind::Put {
key: b"k".to_vec(),
value: b"v".to_vec(),
ttl: None,
reply: put_tx,
},
});
queue.push_back(ShardCommand {
id: 3,
kind: ShardCommandKind::Delete {
key: b"k".to_vec(),
stamp: crate::sync::ballot::Stamp::bottom(),
reply: del_tx,
},
});
queue.push_back(ShardCommand {
id: 4,
kind: ShardCommandKind::Commit { reply: commit_tx },
});
queue.push_back(ShardCommand {
id: 5,
kind: ShardCommandKind::Range {
from: b"a".to_vec(),
to: b"z".to_vec(),
reply: range_tx,
},
});
}
let (handler, _dir) = boot_failed_handler(Arc::clone(&commands))?;
assert!(handler.state.is_none(), "boot must have failed");
assert!(handler.startup_error.is_some(), "startup_error must be set");
let outcome = handler.fail_startup();
assert!(
matches!(get_rx.try_recv(), Ok(Err(ShardError::Spawn(_)))),
"Get arm"
);
assert!(
matches!(put_rx.try_recv(), Ok(Err(ShardError::Spawn(_)))),
"Put arm"
);
assert!(
matches!(del_rx.try_recv(), Ok(Err(ShardError::Spawn(_)))),
"Delete arm"
);
assert!(
matches!(commit_rx.try_recv(), Ok(Err(ShardError::Spawn(_)))),
"Commit arm"
);
assert!(
matches!(range_rx.try_recv(), Ok(Err(ShardError::Spawn(_)))),
"Range arm"
);
assert!(
matches!(outcome, NativeOutcome::Stop(ExitReason::Error)),
"sentinel stops"
);
assert!(
commands.lock().map_err(|_| "queue poisoned")?.is_empty(),
"queue fully drained"
);
Ok(())
}
}
#[cfg(test)]
mod group_drain_tests {
use super::{DrainUnit, lock_queue, pop_next_unit};
use crate::shard::actor::handle::{CommandQueue, ShardCommand, ShardCommandKind};
use crate::sync::ballot::{Ballot, Stamp};
use crate::sync::topology::SyncNodeId;
use std::collections::VecDeque;
use std::error::Error;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
fn stamp() -> Stamp {
Stamp::new(Ballot::new(1, SyncNodeId::new("owner")), 0)
}
fn apply_command(id: u64, key: &[u8]) -> ShardCommand {
let (reply, _rx) = mpsc::sync_channel(1);
ShardCommand {
id,
kind: ShardCommandKind::ApplyDurable {
key: key.to_vec(),
expected: None,
value: b"v".to_vec(),
ttl: None,
stamp: stamp(),
reply,
},
}
}
fn cas_command(id: u64, key: &[u8]) -> ShardCommand {
let (reply, _rx) = mpsc::sync_channel(1);
ShardCommand {
id,
kind: ShardCommandKind::Cas {
key: key.to_vec(),
expected: None,
new: 1,
reply,
},
}
}
fn promise_command(id: u64) -> ShardCommand {
let (reply, _rx) = mpsc::sync_channel(1);
ShardCommand {
id,
kind: ShardCommandKind::RecordPromise {
ballot: Ballot::new(2, SyncNodeId::new("owner")),
reply,
},
}
}
#[test]
fn drain_groups_consecutive_writes_and_stops_at_non_groupable() -> Result<(), Box<dyn Error>> {
let commands: CommandQueue = Arc::new(Mutex::new(VecDeque::new()));
{
let mut queue = lock_queue(&commands);
queue.push_back(apply_command(1, b"a")); queue.push_back(cas_command(2, b"b")); queue.push_back(promise_command(3)); queue.push_back(apply_command(4, b"c")); }
match pop_next_unit(&commands).ok_or("expected a first unit")? {
DrainUnit::Group(group) => assert_eq!(group.len(), 2, "coalesce the leading run"),
DrainUnit::Single(_) => return Err("expected a group, got a single".into()),
}
match pop_next_unit(&commands).ok_or("expected a second unit")? {
DrainUnit::Single(command) => assert!(
matches!(command.kind, ShardCommandKind::RecordPromise { .. }),
"RecordPromise must be handled as a single, never grouped"
),
DrainUnit::Group(_) => return Err("RecordPromise must not be grouped".into()),
}
match pop_next_unit(&commands).ok_or("expected a third unit")? {
DrainUnit::Group(group) => assert_eq!(group.len(), 1, "trailing write groups alone"),
DrainUnit::Single(_) => return Err("expected a trailing group".into()),
}
assert!(pop_next_unit(&commands).is_none(), "queue fully drained");
Ok(())
}
#[test]
fn non_groupable_front_is_single_even_with_groupable_behind() -> Result<(), Box<dyn Error>> {
let commands: CommandQueue = Arc::new(Mutex::new(VecDeque::new()));
{
let mut queue = lock_queue(&commands);
queue.push_back(promise_command(1)); queue.push_back(apply_command(2, b"a")); }
match pop_next_unit(&commands).ok_or("expected a unit")? {
DrainUnit::Single(command) => assert!(matches!(
command.kind,
ShardCommandKind::RecordPromise { .. }
)),
DrainUnit::Group(_) => return Err("front non-groupable must be single".into()),
}
match pop_next_unit(&commands).ok_or("expected a unit")? {
DrainUnit::Group(group) => assert_eq!(group.len(), 1),
DrainUnit::Single(_) => return Err("trailing write should group".into()),
}
Ok(())
}
}