use std::collections::HashMap;
use std::sync::Arc;
use ractor::{ActorRef, RpcReplyPort};
use super::scan::{ScanMatch, ScanSpec};
use crate::wire::{batch_get_result_encoded_len, RepeatedFieldSizer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum OpPriority {
High,
#[default]
Default,
Background,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionedValue {
pub value: Vec<u8>,
pub timestamp_ms: u64,
pub node_id: String,
pub expires_at_ms: Option<u64>,
pub is_tombstone: bool,
}
impl VersionedValue {
pub fn new(value: Vec<u8>, timestamp_ms: u64, node_id: String) -> Self {
Self {
value,
timestamp_ms,
node_id,
expires_at_ms: None,
is_tombstone: false,
}
}
pub fn tombstone(timestamp_ms: u64, node_id: String) -> Self {
Self {
value: Vec::new(),
timestamp_ms,
node_id,
expires_at_ms: None,
is_tombstone: true,
}
}
pub fn expiry_tombstone(&self) -> Self {
Self {
value: Vec::new(),
timestamp_ms: self.timestamp_ms,
node_id: self.node_id.clone(),
expires_at_ms: self.expires_at_ms,
is_tombstone: true,
}
}
pub fn wins_over(&self, other: &VersionedValue) -> bool {
(self.timestamp_ms, &self.node_id, self.is_tombstone)
> (other.timestamp_ms, &other.node_id, other.is_tombstone)
}
pub fn is_expired(&self, now_ms: u64) -> bool {
!self.is_tombstone && matches!(self.expires_at_ms, Some(exp) if now_ms >= exp)
}
}
#[derive(Debug, Clone)]
pub struct ReplicatedOp {
pub key: String,
pub value: VersionedValue,
pub seq: u64,
pub boot_id: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotSummary {
pub key: String,
pub timestamp_ms: u64,
pub node_id: String,
pub is_tombstone: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SummaryPage {
pub summaries: Vec<SnapshotSummary>,
pub revision: u64,
pub snapshot_time_ms: u64,
pub done: bool,
pub restart: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionedPage {
pub entries: Vec<(String, VersionedValue)>,
pub next_index: usize,
pub missing: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BatchReadEntry {
pub key: String,
pub result: Result<Option<VersionedValue>, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BatchReadPage {
pub entries: Vec<BatchReadEntry>,
}
#[doc(hidden)]
pub struct RetainedBatchRead {
pub(crate) keys: Vec<String>,
pub(crate) response_base_payload_len: usize,
pub(crate) response_soft_limit: usize,
pub(crate) reply: RpcReplyPort<BatchReadPage>,
pub(crate) priority: OpPriority,
pub(crate) retention: Arc<dyn Send + Sync>,
}
impl RetainedBatchRead {
pub(crate) fn new(
keys: Vec<String>,
response_base_payload_len: usize,
response_soft_limit: usize,
reply: RpcReplyPort<BatchReadPage>,
priority: OpPriority,
retention: Arc<dyn Send + Sync>,
) -> Self {
Self {
keys,
response_base_payload_len,
response_soft_limit,
reply,
priority,
retention,
}
}
}
#[doc(hidden)]
pub struct BatchReadContinuation {
keys: Vec<String>,
next_index: usize,
pub(super) results: Vec<Result<Option<VersionedValue>, String>>,
response_sizing: RepeatedFieldSizer,
response_soft_limit: usize,
pub(super) priority: OpPriority,
pub(super) reply: RpcReplyPort<BatchReadPage>,
pub(super) dispatcher: ActorRef<KvMessage>,
_retention: Option<Arc<dyn Send + Sync>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApplyOutcome {
Applied,
AlreadyPresent,
Superseded {
timestamp_ms: u64,
node_id: String,
is_tombstone: bool,
},
Expired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApplyDurability {
MemoryApplied,
StoreCommitted,
StableCommitted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApplyError {
Backpressure,
InvalidInput(String),
Storage(String),
}
impl std::fmt::Display for ApplyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Backpressure => write!(f, "fragment worker pool is at capacity"),
Self::InvalidInput(error) => write!(f, "invalid mutation: {error}"),
Self::Storage(error) => write!(f, "persistence failed: {error}"),
}
}
}
impl std::error::Error for ApplyError {}
#[non_exhaustive]
pub enum KvMessage {
ApplyOp {
op: ReplicatedOp,
priority: OpPriority,
is_replication: bool,
reply: Option<RpcReplyPort<Result<ApplyOutcome, ApplyError>>>,
},
Flush {
reply: RpcReplyPort<Result<ApplyDurability, ApplyError>>,
},
FlushStable {
reply: RpcReplyPort<Result<ApplyDurability, ApplyError>>,
},
Get {
key: String,
reply: RpcReplyPort<Result<Option<VersionedValue>, String>>,
priority: OpPriority,
},
Scan {
spec: ScanSpec,
reply: RpcReplyPort<Result<Vec<ScanMatch>, String>>,
},
#[doc(hidden)]
ScanRetained {
spec: ScanSpec,
retention: Arc<crate::query::QueryRetention>,
reply: RpcReplyPort<Result<Vec<ScanMatch>, String>>,
},
QueryPage {
work: crate::query::QueryPageWork,
reply: RpcReplyPort<crate::proto::QueryPageResponse>,
},
GetBatch {
keys: Vec<String>,
response_base_payload_len: usize,
response_soft_limit: usize,
reply: RpcReplyPort<BatchReadPage>,
priority: OpPriority,
},
#[doc(hidden)]
ContinueBatchRead(BatchReadContinuation),
#[doc(hidden)]
GetBatchRetained(RetainedBatchRead),
GetAll {
reply: RpcReplyPort<HashMap<String, VersionedValue>>,
},
GetSnapshot {
reply: RpcReplyPort<(HashMap<String, VersionedValue>, u64)>,
},
GetSummaryPage {
after_key: Option<String>,
expected_revision: Option<u64>,
snapshot_time_ms: Option<u64>,
limit: usize,
reply: RpcReplyPort<SummaryPage>,
},
GetVersioned {
key: String,
snapshot_time_ms: Option<u64>,
reply: RpcReplyPort<Option<VersionedValue>>,
},
GetVersionedPage {
keys: Arc<Vec<String>>,
start_index: usize,
snapshot_time_ms: Option<u64>,
reply: RpcReplyPort<VersionedPage>,
},
GetRevision { reply: RpcReplyPort<u64> },
GetShedCounts {
reply: RpcReplyPort<(u64, u64, u64)>,
},
}
impl BatchReadContinuation {
pub(super) fn new(
keys: Vec<String>,
response_base_payload_len: usize,
response_soft_limit: usize,
priority: OpPriority,
reply: RpcReplyPort<BatchReadPage>,
dispatcher: ActorRef<KvMessage>,
) -> Self {
let result_capacity = keys.len().min(256);
Self {
keys,
next_index: 0,
results: Vec::with_capacity(result_capacity),
response_sizing: RepeatedFieldSizer::new(17, 2, response_base_payload_len),
response_soft_limit,
priority,
reply,
dispatcher,
_retention: None,
}
}
pub(super) fn with_retention(mut self, retention: Arc<dyn Send + Sync>) -> Self {
self._retention = Some(retention);
self
}
pub(super) fn current_key(&self) -> Option<&str> {
self.keys.get(self.next_index).map(String::as_str)
}
pub(super) fn push(&mut self, result: Result<Option<VersionedValue>, String>) {
let key = self
.current_key()
.expect("batch continuation must point at an input key");
let result_payload_len = match &result {
Ok(value) => batch_get_result_encoded_len(key, value.as_ref(), false),
Err(_) => batch_get_result_encoded_len(key, None, true),
};
self.response_sizing.push(result_payload_len);
self.results.push(result);
self.next_index += 1;
}
pub(super) fn is_complete(&self) -> bool {
self.next_index == self.keys.len()
|| (!self.results.is_empty()
&& self.response_sizing.encoded_len() > self.response_soft_limit)
}
pub(super) fn finish(mut self) {
self.keys.truncate(self.results.len());
let entries = self
.keys
.into_iter()
.zip(self.results)
.map(|(key, result)| BatchReadEntry { key, result })
.collect();
let _ = self.reply.send(BatchReadPage { entries });
}
pub(super) fn return_to_dispatcher(self) {
if self.reply.is_closed() {
return;
}
if self.is_complete() {
self.finish();
return;
}
let dispatcher = self.dispatcher.clone();
if let Err(ractor::MessagingErr::SendErr(KvMessage::ContinueBatchRead(work))) =
dispatcher.send_message(KvMessage::ContinueBatchRead(self))
{
work.finish();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lww_orders_by_timestamp_then_node_id() {
let a = VersionedValue::new(vec![1], 100, "node-a".to_string());
let b = VersionedValue::new(vec![2], 200, "node-a".to_string());
let c = VersionedValue::new(vec![3], 200, "node-b".to_string());
assert!(b.wins_over(&a));
assert!(!a.wins_over(&b));
assert!(c.wins_over(&b));
assert!(!b.wins_over(&c));
assert!(!a.wins_over(&a));
}
#[test]
fn tombstone_wins_exact_tie() {
let ts = 1_000u64;
let live = VersionedValue::new(b"v".to_vec(), ts, "node-a".to_string());
let tomb = VersionedValue::tombstone(ts, "node-a".to_string());
assert!(tomb.wins_over(&live));
assert!(!live.wins_over(&tomb));
let newer = VersionedValue::new(b"v2".to_vec(), ts + 1, "node-a".to_string());
assert!(newer.wins_over(&tomb));
assert!(!tomb.wins_over(&newer));
}
}