use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::cluster::{ClusterEpoch, PartitionId};
use crate::grid::{EffectiveReplicationMap, ReplicatedSlot, ReplicationConfig};
pub type ValueVersion = u64;
pub type SealedBytes = Vec<u8>;
pub const REPLICATED_VALUE_RECORD_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReplicatedValueRecord {
pub partition: PartitionId,
pub version: ValueVersion,
pub epoch: ClusterEpoch,
pub state: ReplicatedSlot<SealedBytes>,
}
impl ReplicatedValueRecord {
pub fn value(
partition: PartitionId,
version: ValueVersion,
epoch: ClusterEpoch,
sealed: impl Into<SealedBytes>,
) -> Self {
Self {
partition,
version,
epoch,
state: ReplicatedSlot::Value {
value: sealed.into(),
version,
},
}
}
pub fn tombstone(
partition: PartitionId,
version: ValueVersion,
epoch: ClusterEpoch,
gc_eligible_after: Option<ClusterEpoch>,
) -> Self {
Self {
partition,
version,
epoch,
state: ReplicatedSlot::Tombstone {
version,
gc_eligible_after,
},
}
}
pub fn is_tombstone(&self) -> bool {
self.state.is_tombstone()
}
pub fn approx_bytes(&self) -> u64 {
match &self.state {
ReplicatedSlot::Value { value, .. } => value.len().max(1) as u64,
ReplicatedSlot::Tombstone { .. } => 1,
}
}
pub fn merge(self, other: Self) -> Self {
match (self.version, self.epoch).cmp(&(other.version, other.epoch)) {
std::cmp::Ordering::Greater => self,
std::cmp::Ordering::Less => other,
std::cmp::Ordering::Equal if self.is_tombstone() => self,
std::cmp::Ordering::Equal => other,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueStoreError {
message: String,
}
impl ValueStoreError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for ValueStoreError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for ValueStoreError {}
pub trait ReplicatedValueStore: Send + Sync {
fn upsert(
&mut self,
key: impl Into<String>,
rec: ReplicatedValueRecord,
) -> Result<(), ValueStoreError>;
fn get(&self, key: &str) -> Result<Option<ReplicatedValueRecord>, ValueStoreError>;
fn tombstone(
&mut self,
key: impl Into<String>,
partition: PartitionId,
version: ValueVersion,
epoch: ClusterEpoch,
) -> Result<(), ValueStoreError>;
fn scan_owned(
&self,
map: &EffectiveReplicationMap,
) -> Result<Vec<(String, ReplicatedValueRecord)>, ValueStoreError>;
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InMemoryReplicatedValueStore {
records: BTreeMap<String, ReplicatedValueRecord>,
max_total_bytes: u64,
rejected_total: u64,
}
impl InMemoryReplicatedValueStore {
pub fn with_budget(max_total_bytes: u64) -> Self {
Self {
records: BTreeMap::new(),
max_total_bytes: max_total_bytes.max(1),
rejected_total: 0,
}
}
pub fn total_bytes(&self) -> u64 {
self.records
.values()
.map(ReplicatedValueRecord::approx_bytes)
.sum()
}
pub fn rejected_total(&self) -> u64 {
self.rejected_total
}
pub fn snapshot(&self) -> BTreeMap<String, ReplicatedValueRecord> {
self.records.clone()
}
pub fn reopen_from_snapshot(
max_total_bytes: u64,
records: BTreeMap<String, ReplicatedValueRecord>,
) -> Self {
Self {
records,
max_total_bytes: max_total_bytes.max(1),
rejected_total: 0,
}
}
fn would_fit(&self, key: &str, rec: &ReplicatedValueRecord) -> bool {
let existing = self
.records
.get(key)
.map(ReplicatedValueRecord::approx_bytes)
.unwrap_or_default();
self.total_bytes()
.saturating_sub(existing)
.saturating_add(rec.approx_bytes())
<= self.max_total_bytes
}
}
impl Default for InMemoryReplicatedValueStore {
fn default() -> Self {
Self::with_budget(u64::MAX)
}
}
impl ReplicatedValueStore for InMemoryReplicatedValueStore {
fn upsert(
&mut self,
key: impl Into<String>,
rec: ReplicatedValueRecord,
) -> Result<(), ValueStoreError> {
let key = key.into();
if !self.would_fit(&key, &rec) {
self.rejected_total = self.rejected_total.saturating_add(1);
return Err(ValueStoreError::new(
"replicated value store total byte budget exceeded",
));
}
let merged = self
.records
.remove(&key)
.map(|current| current.merge(rec.clone()))
.unwrap_or(rec);
self.records.insert(key, merged);
Ok(())
}
fn get(&self, key: &str) -> Result<Option<ReplicatedValueRecord>, ValueStoreError> {
Ok(self.records.get(key).cloned())
}
fn tombstone(
&mut self,
key: impl Into<String>,
partition: PartitionId,
version: ValueVersion,
epoch: ClusterEpoch,
) -> Result<(), ValueStoreError> {
self.upsert(
key,
ReplicatedValueRecord::tombstone(partition, version, epoch, None),
)
}
fn scan_owned(
&self,
map: &EffectiveReplicationMap,
) -> Result<Vec<(String, ReplicatedValueRecord)>, ValueStoreError> {
if map.reading.is_empty() {
return Ok(Vec::new());
}
Ok(self
.records
.iter()
.map(|(key, record)| (key.clone(), record.clone()))
.collect())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdaptiveWindow {
in_flight: usize,
max_in_flight: usize,
floor: usize,
ceil: usize,
}
impl AdaptiveWindow {
pub fn new(floor: usize, initial: usize, ceil: usize) -> Self {
let floor = floor.max(1);
let ceil = ceil.max(floor);
let max_in_flight = initial.max(floor).min(ceil);
Self {
in_flight: 0,
max_in_flight,
floor,
ceil,
}
}
pub fn admit(&self) -> bool {
self.in_flight < self.max_in_flight
}
pub fn try_acquire(&mut self) -> bool {
if !self.admit() {
return false;
}
self.in_flight = self.in_flight.saturating_add(1);
true
}
pub fn on_ack(&mut self, rtt_ok: bool) {
self.in_flight = self.in_flight.saturating_sub(1);
if rtt_ok {
self.max_in_flight = self.max_in_flight.saturating_add(1).min(self.ceil);
} else {
self.max_in_flight = (self.max_in_flight / 2).max(self.floor);
}
}
pub fn in_flight(&self) -> usize {
self.in_flight
}
pub fn max_in_flight(&self) -> usize {
self.max_in_flight
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuorumPosture {
Strong,
DegradedSessionRyow,
}
impl ReplicationConfig {
pub fn is_strong_ryow(&self) -> bool {
self.read_quorum.saturating_add(self.write_quorum) > self.replication_factor
}
pub fn quorum_posture(&self) -> QuorumPosture {
if self.is_strong_ryow() {
QuorumPosture::Strong
} else {
QuorumPosture::DegradedSessionRyow
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct WriteWatermark {
pub partition: PartitionId,
pub version: ValueVersion,
pub epoch: ClusterEpoch,
}
impl WriteWatermark {
pub fn new(partition: PartitionId, version: ValueVersion, epoch: ClusterEpoch) -> Self {
Self {
partition,
version,
epoch,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReadConsistency {
QuorumReadYourWrites,
Eventual,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuorumReadDecision {
pub record: Option<ReplicatedValueRecord>,
pub requires_primary_fallback: bool,
}
pub fn quorum_read_your_writes(
watermark: WriteWatermark,
replicas: impl IntoIterator<Item = ReplicatedValueRecord>,
read_quorum: usize,
) -> QuorumReadDecision {
let mut candidates = replicas
.into_iter()
.filter(|record| record.partition == watermark.partition)
.filter(|record| (record.version, record.epoch) >= (watermark.version, watermark.epoch))
.collect::<Vec<_>>();
candidates.sort_by_key(|record| (record.version, record.epoch));
if candidates.len() < read_quorum.max(1) {
return QuorumReadDecision {
record: None,
requires_primary_fallback: true,
};
}
QuorumReadDecision {
record: candidates.pop(),
requires_primary_fallback: false,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SplitBrainReport {
pub winner_epoch: ClusterEpoch,
pub loser_epoch: ClusterEpoch,
pub merged_entries: u64,
pub discarded_entries: u64,
pub unresolved_conflicts: u64,
}
pub trait MergePolicy: Send + Sync {
fn merge(
&self,
winner: Option<&ReplicatedValueRecord>,
loser: &ReplicatedValueRecord,
) -> Option<ReplicatedValueRecord>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct HigherVersionWins;
impl MergePolicy for HigherVersionWins {
fn merge(
&self,
winner: Option<&ReplicatedValueRecord>,
loser: &ReplicatedValueRecord,
) -> Option<ReplicatedValueRecord> {
Some(
winner
.cloned()
.map(|winner| winner.merge(loser.clone()))
.unwrap_or_else(|| loser.clone()),
)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PutIfAbsent;
impl MergePolicy for PutIfAbsent {
fn merge(
&self,
winner: Option<&ReplicatedValueRecord>,
loser: &ReplicatedValueRecord,
) -> Option<ReplicatedValueRecord> {
winner.cloned().or_else(|| Some(loser.clone()))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DiscardLoser;
impl MergePolicy for DiscardLoser {
fn merge(
&self,
_winner: Option<&ReplicatedValueRecord>,
_loser: &ReplicatedValueRecord,
) -> Option<ReplicatedValueRecord> {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClusterMergeOutcome {
pub records: BTreeMap<String, ReplicatedValueRecord>,
pub report: SplitBrainReport,
}
pub fn merge_split_brain_records(
winner_epoch: ClusterEpoch,
loser_epoch: ClusterEpoch,
mut winner: BTreeMap<String, ReplicatedValueRecord>,
loser: BTreeMap<String, ReplicatedValueRecord>,
policy: &dyn MergePolicy,
) -> ClusterMergeOutcome {
let mut report = SplitBrainReport {
winner_epoch,
loser_epoch,
..SplitBrainReport::default()
};
for (key, loser_record) in loser {
match policy.merge(winner.get(&key), &loser_record) {
Some(record) => {
if winner.get(&key) != Some(&record) {
report.merged_entries = report.merged_entries.saturating_add(1);
}
winner.insert(key, record);
}
None => {
report.discarded_entries = report.discarded_entries.saturating_add(1);
}
}
}
ClusterMergeOutcome {
records: winner,
report,
}
}
pub fn split_brain_winner(left: ClusterEpoch, right: ClusterEpoch) -> (ClusterEpoch, ClusterEpoch) {
if left >= right {
(left, right)
} else {
(right, left)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PromotionFreezeWindow {
pub observed_ms: u64,
pub bound_ms: u64,
}
impl PromotionFreezeWindow {
pub fn is_bounded(self) -> bool {
self.observed_ms <= self.bound_ms
}
}