use std::collections::VecDeque;
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{self, RecvTimeoutError, SyncSender};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use beamr::atom::{Atom, AtomTable};
use beamr::scheduler::Scheduler;
use crate::store::StoreError;
use crate::sync::ballot::{Ballot, Stamp};
use crate::tree::{Hash, TreeError};
use crate::wal::WalError;
use super::native::{self, ShardNativeHandler};
use super::{PromiseState, RecordPromiseOutcome};
const WAKE_ATOM_NAME: &str = "haematite_shard_wake";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RangeItem {
Entry { key: Vec<u8>, value: Vec<u8> },
Done,
}
#[derive(Debug)]
pub enum ShardError {
ActorUnavailable { pid: u64 },
ReplyDisconnected { pid: u64 },
ReplyTimeout { pid: u64 },
Wal(WalError),
Tree(TreeError),
Store(StoreError),
Spawn(String),
SequenceConflict { expected: u64, actual: u64 },
CasMismatch {
expected: Option<u64>,
actual: Option<u64>,
},
CasHashMismatch {
expected: Option<Hash>,
actual: Option<Hash>,
},
Fenced { promised: Ballot, attempted: Ballot },
}
impl fmt::Display for ShardError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ActorUnavailable { pid } => {
write!(formatter, "shard actor {pid} is unavailable")
}
Self::ReplyDisconnected { pid } => {
write!(formatter, "shard actor {pid} reply channel disconnected")
}
Self::ReplyTimeout { pid } => {
write!(formatter, "timed out waiting for shard actor {pid}")
}
Self::Wal(error) => write!(formatter, "shard WAL error: {error}"),
Self::Tree(error) => write!(formatter, "shard tree error: {error}"),
Self::Store(error) => write!(formatter, "shard store error: {error}"),
Self::Spawn(message) => write!(formatter, "shard spawn failed: {message}"),
Self::SequenceConflict { expected, actual } => write!(
formatter,
"sequence conflict on append: expected {expected}, actual {actual}"
),
Self::CasMismatch { expected, actual } => write!(
formatter,
"cas mismatch: expected {expected:?}, actual {actual:?}"
),
Self::CasHashMismatch { expected, actual } => write!(
formatter,
"cas hash mismatch: expected {expected:?}, actual {actual:?}"
),
Self::Fenced {
promised,
attempted,
} => write!(
formatter,
"epoch fenced: attempted {attempted:?} < promised {promised:?}"
),
}
}
}
impl std::error::Error for ShardError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Wal(error) => Some(error),
Self::Tree(error) => Some(error),
Self::Store(error) => Some(error),
Self::ActorUnavailable { .. }
| Self::ReplyDisconnected { .. }
| Self::ReplyTimeout { .. }
| Self::Spawn(_)
| Self::SequenceConflict { .. }
| Self::CasMismatch { .. }
| Self::CasHashMismatch { .. }
| Self::Fenced { .. } => None,
}
}
}
impl From<WalError> for ShardError {
fn from(error: WalError) -> Self {
Self::Wal(error)
}
}
impl From<TreeError> for ShardError {
fn from(error: TreeError) -> Self {
Self::Tree(error)
}
}
impl From<StoreError> for ShardError {
fn from(error: StoreError) -> Self {
Self::Store(error)
}
}
pub(super) type StreamSeq = (Vec<u8>, u64);
pub type ShardSyncExport = (Option<Hash>, Vec<crate::sync::NodeTransfer>);
pub type BatchItem = (Vec<u8>, Option<Hash>, Vec<u8>, Option<Duration>);
pub type PromiserContribution = (Option<Hash>, Vec<crate::sync::NodeTransfer>);
pub(super) type ScanReply = SyncSender<Result<Vec<StreamSeq>, ShardError>>;
pub(super) type BoolReply = SyncSender<Result<bool, ShardError>>;
pub(super) struct ShardCommand {
pub(super) id: u64,
pub(super) kind: ShardCommandKind,
}
pub(super) enum ShardCommandKind {
Get {
key: Vec<u8>,
reply: SyncSender<Result<Option<Vec<u8>>, ShardError>>,
},
#[doc(hidden)]
GetRaw {
key: Vec<u8>,
reply: SyncSender<Result<Option<Vec<u8>>, ShardError>>,
},
Put {
key: Vec<u8>,
value: Vec<u8>,
ttl: Option<Duration>,
reply: SyncSender<Result<(), ShardError>>,
},
Delete {
key: Vec<u8>,
stamp: Stamp,
reply: SyncSender<Result<(), ShardError>>,
},
DeleteIfExpired {
key: Vec<u8>,
reply: BoolReply,
},
Commit {
reply: SyncSender<Result<Hash, ShardError>>,
},
Range {
from: Vec<u8>,
to: Vec<u8>,
reply: SyncSender<Result<Vec<RangeItem>, ShardError>>,
},
HasLiveInRange(Vec<u8>, Vec<u8>, BoolReply),
Append {
key: Vec<u8>,
entries: Vec<Vec<u8>>,
expected_seq: u64,
ttl: Option<Duration>,
reply: SyncSender<Result<u64, ShardError>>,
},
ReadValue {
key: Vec<u8>,
reply: SyncSender<Result<Option<u64>, ShardError>>,
},
Cas {
key: Vec<u8>,
expected: Option<u64>,
new: u64,
reply: SyncSender<Result<(), ShardError>>,
},
ApplyDurable {
key: Vec<u8>,
expected: Option<Hash>,
value: Vec<u8>,
ttl: Option<Duration>,
stamp: Stamp,
reply: SyncSender<Result<(), ShardError>>,
},
ApplyDurableTombstone {
key: Vec<u8>,
expected: Option<Hash>,
stamp: Stamp,
reply: SyncSender<Result<(), ShardError>>,
},
ApplyDurableBatch {
items: Vec<BatchItem>,
stamp: Stamp,
reply: SyncSender<Result<(), ShardError>>,
},
RecordPromise {
ballot: Ballot,
reply: SyncSender<Result<RecordPromiseOutcome, ShardError>>,
},
RecordOwnerEpoch {
ballot: Ballot,
reply: SyncSender<Result<(), ShardError>>,
},
ReserveMinted {
counter: u64,
reply: SyncSender<Result<u64, ShardError>>,
},
ReadPromiseState {
reply: SyncSender<Result<PromiseState, ShardError>>,
},
ExportReachable {
shard_id: crate::branch::ShardId,
reply: SyncSender<Result<ShardSyncExport, ShardError>>,
},
MergeAdopt {
promisers: Vec<PromiserContribution>,
reply: SyncSender<Result<Option<Hash>, ShardError>>,
},
ScanSequences {
reply: ScanReply,
},
Shutdown {
reply: SyncSender<Result<(), ShardError>>,
},
}
pub(super) type CommandQueue = Arc<Mutex<VecDeque<ShardCommand>>>;
#[derive(Clone)]
pub struct ShardHandle {
pid: u64,
scheduler: Arc<Scheduler>,
commands: CommandQueue,
wake_atom: Atom,
next_command_id: Arc<AtomicU64>,
}
impl fmt::Debug for ShardHandle {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ShardHandle")
.field("pid", &self.pid)
.finish_non_exhaustive()
}
}
impl ShardHandle {
pub fn spawn(
scheduler: Arc<Scheduler>,
store_dir: impl Into<std::path::PathBuf>,
wal_path: impl Into<std::path::PathBuf>,
) -> Result<Self, ShardError> {
let commands: CommandQueue = Arc::new(Mutex::new(VecDeque::new()));
let factory = ShardNativeHandler::make_factory(
store_dir.into(),
wal_path.into(),
Arc::clone(&commands),
);
let pid = scheduler
.spawn_native(factory)
.map_err(|error| ShardError::Spawn(format!("{error:?}")))?;
let atoms = AtomTable::with_common_atoms();
let wake_atom = atoms.intern(WAKE_ATOM_NAME);
Ok(Self {
pid,
scheduler,
commands,
wake_atom,
next_command_id: Arc::new(AtomicU64::new(1)),
})
}
#[must_use]
pub const fn pid(&self) -> u64 {
self.pid
}
pub fn get(&self, key: Vec<u8>, timeout: Duration) -> Result<Option<Vec<u8>>, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Get { key, reply })?;
recv(&response, self.pid, timeout)?
}
#[doc(hidden)]
pub fn get_raw(&self, key: Vec<u8>, timeout: Duration) -> Result<Option<Vec<u8>>, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::GetRaw { key, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn put_with_ttl(
&self,
key: Vec<u8>,
value: Vec<u8>,
ttl: Option<Duration>,
timeout: Duration,
) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Put {
key,
value,
ttl,
reply,
})?;
recv(&response, self.pid, timeout)?
}
pub fn delete(&self, key: Vec<u8>, stamp: Stamp, timeout: Duration) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Delete { key, stamp, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn apply_durable_tombstone(
&self,
key: Vec<u8>,
expected: Option<Hash>,
stamp: Stamp,
timeout: Duration,
) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::ApplyDurableTombstone {
key,
expected,
stamp,
reply,
})?;
recv(&response, self.pid, timeout)?
}
pub fn commit(&self, timeout: Duration) -> Result<Hash, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Commit { reply })?;
recv(&response, self.pid, timeout)?
}
pub fn range(
&self,
from: Vec<u8>,
to: Vec<u8>,
timeout: Duration,
) -> Result<Vec<RangeItem>, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Range { from, to, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn append_with_ttl(
&self,
key: Vec<u8>,
entries: Vec<Vec<u8>>,
expected_seq: u64,
ttl: Option<Duration>,
timeout: Duration,
) -> Result<u64, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Append {
key,
entries,
expected_seq,
ttl,
reply,
})?;
recv(&response, self.pid, timeout)?
}
pub fn read_value(&self, key: Vec<u8>, timeout: Duration) -> Result<Option<u64>, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::ReadValue { key, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn cas(
&self,
key: Vec<u8>,
expected: Option<u64>,
new: u64,
timeout: Duration,
) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Cas {
key,
expected,
new,
reply,
})?;
recv(&response, self.pid, timeout)?
}
pub fn apply_durable(
&self,
key: Vec<u8>,
expected: Option<Hash>,
value: Vec<u8>,
ttl: Option<Duration>,
stamp: Stamp,
timeout: Duration,
) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::ApplyDurable {
key,
expected,
value,
ttl,
stamp,
reply,
})?;
recv(&response, self.pid, timeout)?
}
pub fn apply_durable_batch(
&self,
items: Vec<BatchItem>,
stamp: Stamp,
timeout: Duration,
) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::ApplyDurableBatch {
items,
stamp,
reply,
})?;
recv(&response, self.pid, timeout)?
}
pub fn record_promise(
&self,
ballot: Ballot,
timeout: Duration,
) -> Result<RecordPromiseOutcome, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::RecordPromise { ballot, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn record_owner_epoch(&self, ballot: Ballot, timeout: Duration) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::RecordOwnerEpoch { ballot, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn reserve_minted(&self, counter: u64, timeout: Duration) -> Result<u64, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::ReserveMinted { counter, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn read_promise_state(&self, timeout: Duration) -> Result<PromiseState, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::ReadPromiseState { reply })?;
recv(&response, self.pid, timeout)?
}
pub fn export_reachable(
&self,
shard_id: crate::branch::ShardId,
timeout: Duration,
) -> Result<ShardSyncExport, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::ExportReachable { shard_id, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn merge_adopt(
&self,
promisers: Vec<PromiserContribution>,
timeout: Duration,
) -> Result<Option<Hash>, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::MergeAdopt { promisers, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn scan_sequences(&self, timeout: Duration) -> Result<Vec<StreamSeq>, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::ScanSequences { reply })?;
recv(&response, self.pid, timeout)?
}
pub(crate) fn shutdown(&self, timeout: Duration) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Shutdown { reply })?;
recv(&response, self.pid, timeout)?
}
pub(super) fn enqueue(&self, kind: ShardCommandKind) -> Result<(), ShardError> {
let id = self.next_command_id.fetch_add(1, Ordering::Relaxed);
native::lock_queue(&self.commands).push_back(ShardCommand { id, kind });
if self
.scheduler
.enqueue_atom_message(self.pid, self.wake_atom)
{
Ok(())
} else {
self.remove_command(id);
Err(ShardError::ActorUnavailable { pid: self.pid })
}
}
fn remove_command(&self, id: u64) {
native::lock_queue(&self.commands).retain(|command| command.id != id);
}
}
fn recv<T>(response: &mpsc::Receiver<T>, pid: u64, timeout: Duration) -> Result<T, ShardError> {
response.recv_timeout(timeout).map_err(|error| match error {
RecvTimeoutError::Timeout => ShardError::ReplyTimeout { pid },
RecvTimeoutError::Disconnected => ShardError::ReplyDisconnected { pid },
})
}