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::tree::{Hash, TreeError};
use crate::wal::WalError;
use super::native::{self, ShardNativeHandler};
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>,
},
}
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:?}"
),
}
}
}
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 { .. } => 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(super) type ScanReply = SyncSender<Result<Vec<StreamSeq>, 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>>,
},
Put {
key: Vec<u8>,
value: Vec<u8>,
reply: SyncSender<Result<(), ShardError>>,
},
Delete {
key: Vec<u8>,
reply: SyncSender<Result<(), ShardError>>,
},
Commit {
reply: SyncSender<Result<Hash, ShardError>>,
},
Range {
from: Vec<u8>,
to: Vec<u8>,
reply: SyncSender<Result<Vec<RangeItem>, ShardError>>,
},
Append {
key: Vec<u8>,
entries: Vec<Vec<u8>>,
expected_seq: u64,
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>>,
},
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)?
}
pub fn put(&self, key: Vec<u8>, value: Vec<u8>, timeout: Duration) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Put { key, value, reply })?;
recv(&response, self.pid, timeout)?
}
pub fn delete(&self, key: Vec<u8>, timeout: Duration) -> Result<(), ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Delete { key, 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(
&self,
key: Vec<u8>,
entries: Vec<Vec<u8>>,
expected_seq: u64,
timeout: Duration,
) -> Result<u64, ShardError> {
let (reply, response) = mpsc::sync_channel(1);
self.enqueue(ShardCommandKind::Append {
key,
entries,
expected_seq,
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 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)?
}
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 },
})
}