use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::cluster::{
ClusterEpoch, ClusterGeneration, ClusterMember, ClusterNodeId, PartitionId,
RendezvousClusterOwnership,
};
pub trait ClusterReplicationStrategy: Send + Sync {
fn name(&self) -> &'static str;
fn replicas_for_key(
&self,
key: &str,
members: &[ClusterMember],
replication_factor: usize,
) -> Option<Replicas>;
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Replicas {
pub primary: ClusterNodeId,
pub backups: Vec<ClusterNodeId>,
}
impl Replicas {
pub fn new(primary: impl Into<ClusterNodeId>, backups: Vec<ClusterNodeId>) -> Self {
let primary = primary.into();
let mut seen = BTreeSet::new();
let backups = backups
.into_iter()
.filter(|backup| backup != &primary)
.filter(|backup| seen.insert(backup.clone()))
.collect();
Self { primary, backups }
}
pub fn all_nodes(&self) -> Vec<ClusterNodeId> {
let mut nodes = Vec::with_capacity(1 + self.backups.len());
nodes.push(self.primary.clone());
nodes.extend(self.backups.iter().cloned());
nodes
}
pub fn copy_count(&self) -> usize {
1 + self.backups.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EffectiveReplicationMap {
pub natural: Replicas,
pub reading: Vec<ClusterNodeId>,
pub pending: Option<Replicas>,
}
impl EffectiveReplicationMap {
pub fn new(natural: Replicas) -> Self {
let reading = natural.all_nodes();
Self {
natural,
reading,
pending: None,
}
}
pub fn with_pending(natural: Replicas, pending: Replicas) -> Self {
let mut reading = natural.all_nodes();
for node in pending.all_nodes() {
if !reading.contains(&node) {
reading.push(node);
}
}
Self {
natural,
reading,
pending: Some(pending),
}
}
pub fn is_readable_from(&self, node_id: &ClusterNodeId) -> bool {
self.reading.iter().any(|node| node == node_id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReplicationConfig {
pub replication_factor: usize,
pub read_quorum: usize,
pub write_quorum: usize,
pub sync_backups: usize,
pub async_backups: usize,
pub max_replicated_entry_bytes: usize,
pub replicate_values: bool,
}
impl ReplicationConfig {
pub const fn local_first() -> Self {
Self {
replication_factor: 1,
read_quorum: 1,
write_quorum: 1,
sync_backups: 0,
async_backups: 0,
max_replicated_entry_bytes: 0,
replicate_values: false,
}
}
pub fn validate(self) -> Result<(), ReplicationConfigError> {
if self.replication_factor == 0 {
return Err(ReplicationConfigError::ReplicationFactorZero);
}
if self.read_quorum == 0 || self.write_quorum == 0 {
return Err(ReplicationConfigError::QuorumZero);
}
if self.read_quorum > self.replication_factor || self.write_quorum > self.replication_factor
{
return Err(ReplicationConfigError::QuorumExceedsReplicationFactor);
}
let requested_backups = self.sync_backups.saturating_add(self.async_backups);
if requested_backups > self.replication_factor.saturating_sub(1) {
return Err(ReplicationConfigError::BackupCountExceedsReplicationFactor);
}
if self.replicate_values && self.max_replicated_entry_bytes == 0 {
return Err(ReplicationConfigError::MissingReplicatedEntryByteCap);
}
Ok(())
}
}
impl Default for ReplicationConfig {
fn default() -> Self {
Self::local_first()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReplicationConfigError {
ReplicationFactorZero,
QuorumZero,
QuorumExceedsReplicationFactor,
BackupCountExceedsReplicationFactor,
MissingReplicatedEntryByteCap,
}
impl fmt::Display for ReplicationConfigError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::ReplicationFactorZero => "replication_factor must be at least 1",
Self::QuorumZero => "read_quorum and write_quorum must be at least 1",
Self::QuorumExceedsReplicationFactor => {
"read_quorum/write_quorum cannot exceed replication_factor"
}
Self::BackupCountExceedsReplicationFactor => {
"sync_backups + async_backups cannot exceed replication_factor - 1"
}
Self::MissingReplicatedEntryByteCap => {
"replicate_values(true) requires max_replicated_entry_bytes"
}
};
formatter.write_str(message)
}
}
impl std::error::Error for ReplicationConfigError {}
impl ClusterReplicationStrategy for RendezvousClusterOwnership {
fn name(&self) -> &'static str {
"rendezvous"
}
fn replicas_for_key(
&self,
key: &str,
members: &[ClusterMember],
replication_factor: usize,
) -> Option<Replicas> {
let mut ranked = members
.iter()
.filter(|member| member.is_member())
.map(|member| {
(
grid_rendezvous_score(key, &member.node_id),
member.node_id.clone(),
)
})
.collect::<Vec<_>>();
ranked.sort_by(|(left_score, left_node), (right_score, right_node)| {
right_score
.cmp(left_score)
.then_with(|| right_node.cmp(left_node))
});
ranked.dedup_by(|(_, left_node), (_, right_node)| left_node == right_node);
let mut nodes = ranked
.into_iter()
.map(|(_, node)| node)
.take(replication_factor.max(1))
.collect::<Vec<_>>();
if nodes.is_empty() {
return None;
}
let primary = nodes.remove(0);
Some(Replicas::new(primary, nodes))
}
}
fn grid_rendezvous_score(key: &str, node_id: &ClusterNodeId) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET;
for byte in key.bytes().chain([0xff]).chain(node_id.as_str().bytes()) {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RebalanceTask {
MovePartition {
partition: PartitionId,
from: ClusterNodeId,
to: ClusterNodeId,
},
ReReplicate {
partition: PartitionId,
target: ClusterNodeId,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RebalancePlan {
pub epoch: ClusterEpoch,
pub tasks: Vec<RebalanceTask>,
}
impl RebalancePlan {
pub fn new(epoch: ClusterEpoch, mut tasks: Vec<RebalanceTask>) -> Self {
tasks.sort_by_key(rebalance_task_sort_key);
tasks.dedup();
Self { epoch, tasks }
}
pub fn is_complete(&self, acks: &[RebalanceTaskAck]) -> bool {
self.tasks.iter().all(|task| {
acks.iter()
.any(|ack| ack.epoch == self.epoch && ack.task == *task)
})
}
pub fn pending_task_count(&self, acks: &[RebalanceTaskAck]) -> usize {
self.tasks
.iter()
.filter(|task| {
!acks
.iter()
.any(|ack| ack.epoch == self.epoch && ack.task == **task)
})
.count()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RebalanceTaskAck {
pub epoch: ClusterEpoch,
pub task: RebalanceTask,
}
pub fn diff_effective_maps(
partition: PartitionId,
old: &EffectiveReplicationMap,
new: &EffectiveReplicationMap,
) -> Vec<RebalanceTask> {
let mut tasks = Vec::new();
if old.natural.primary != new.natural.primary {
tasks.push(RebalanceTask::MovePartition {
partition,
from: old.natural.primary.clone(),
to: new.natural.primary.clone(),
});
}
let old_backups = old.natural.backups.iter().collect::<BTreeSet<_>>();
for target in &new.natural.backups {
if !old_backups.contains(target) {
tasks.push(RebalanceTask::ReReplicate {
partition,
target: target.clone(),
});
}
}
tasks.sort_by_key(rebalance_task_sort_key);
tasks
}
fn rebalance_task_sort_key(task: &RebalanceTask) -> (u32, u8, String, String) {
match task {
RebalanceTask::MovePartition {
partition,
from,
to,
} => (
partition.value(),
0,
from.as_str().to_owned(),
to.as_str().to_owned(),
),
RebalanceTask::ReReplicate { partition, target } => (
partition.value(),
1,
target.as_str().to_owned(),
String::new(),
),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReplicatedSlot<V> {
Value {
value: V,
version: u64,
},
Tombstone {
version: u64,
gc_eligible_after: Option<ClusterEpoch>,
},
}
impl<V> ReplicatedSlot<V> {
pub fn version(&self) -> u64 {
match self {
Self::Value { version, .. } | Self::Tombstone { version, .. } => *version,
}
}
pub fn is_tombstone(&self) -> bool {
matches!(self, Self::Tombstone { .. })
}
pub fn merge(self, other: Self) -> Self {
match self.version().cmp(&other.version()) {
std::cmp::Ordering::Greater => self,
std::cmp::Ordering::Less => other,
std::cmp::Ordering::Equal if self.is_tombstone() => self,
std::cmp::Ordering::Equal => other,
}
}
}
pub fn replicated_slot_version(generation: ClusterGeneration, message_id: u64) -> u64 {
generation.value().min(0xffff_ffff) << 32 | (message_id & 0xffff_ffff)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct TombstoneBudget {
pub max_tombstones: usize,
pub max_tombstone_bytes: u64,
}
impl TombstoneBudget {
pub fn new(max_tombstones: usize, max_tombstone_bytes: u64) -> Self {
Self {
max_tombstones: max_tombstones.max(1),
max_tombstone_bytes: max_tombstone_bytes.max(1),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TombstoneAdmission {
Stored,
EvictedEligible {
freed: usize,
},
RepairDebt,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct TombstoneRecord {
key: String,
version: u64,
approx_bytes: u64,
gc_eligible_after: Option<ClusterEpoch>,
}
#[derive(Debug, Clone)]
pub struct TombstoneTracker {
budget: TombstoneBudget,
records: Vec<TombstoneRecord>,
repair_debt: bool,
}
impl TombstoneTracker {
pub fn new(budget: TombstoneBudget) -> Self {
Self {
budget,
records: Vec::new(),
repair_debt: false,
}
}
pub fn admit(
&mut self,
key: impl Into<String>,
version: u64,
approx_bytes: u64,
gc_eligible_after: Option<ClusterEpoch>,
) -> TombstoneAdmission {
let key = key.into();
self.records.retain(|record| record.key != key);
self.records.push(TombstoneRecord {
key,
version,
approx_bytes: approx_bytes.max(1),
gc_eligible_after,
});
self.records.sort_by_key(|record| record.version);
self.enforce_budget()
}
pub fn confirm_repair(&mut self, key: &str, epoch: ClusterEpoch) {
if let Some(record) = self.records.iter_mut().find(|record| record.key == key) {
record.gc_eligible_after = Some(epoch);
}
}
pub fn repair_debt(&self) -> bool {
self.repair_debt
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn contains_key(&self, key: &str) -> bool {
self.records.iter().any(|record| record.key == key)
}
fn total_bytes(&self) -> u64 {
self.records
.iter()
.map(|record| record.approx_bytes)
.sum::<u64>()
}
fn over_budget(&self) -> bool {
self.records.len() > self.budget.max_tombstones
|| self.total_bytes() > self.budget.max_tombstone_bytes
}
fn enforce_budget(&mut self) -> TombstoneAdmission {
let mut freed = 0;
while self.over_budget() {
let Some(index) = self
.records
.iter()
.position(|record| record.gc_eligible_after.is_some())
else {
self.repair_debt = true;
return TombstoneAdmission::RepairDebt;
};
self.records.remove(index);
freed += 1;
}
self.repair_debt = false;
if freed == 0 {
TombstoneAdmission::Stored
} else {
TombstoneAdmission::EvictedEligible { freed }
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Replication {
Eligible,
LocalOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReplicatedValueSecurityPosture {
Disabled,
Encrypted,
PlaintextAcknowledged,
PlaintextUnacknowledged,
}
impl Default for ReplicatedValueSecurityPosture {
fn default() -> Self {
Self::Disabled
}
}
impl ReplicatedValueSecurityPosture {
pub fn highlight(self) -> Option<&'static str> {
match self {
Self::PlaintextUnacknowledged => Some("REPLICATED VALUES PLAINTEXT"),
Self::Disabled | Self::Encrypted | Self::PlaintextAcknowledged => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplicationCryptoError {
message: String,
}
impl ReplicationCryptoError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for ReplicationCryptoError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for ReplicationCryptoError {}
pub trait ReplicationKeyProvider: Send + Sync {
fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, ReplicationCryptoError>;
fn open(&self, sealed: &[u8]) -> Result<Vec<u8>, ReplicationCryptoError>;
}
pub trait RedactReplicatedValue: Send + Sync {
fn redact(&self, plaintext: &[u8]) -> Vec<u8>;
}
#[derive(Clone)]
pub struct ReplicationPayload {
pub bytes: Vec<u8>,
pub posture: ReplicatedValueSecurityPosture,
}
pub fn prepare_replicated_payload(
value: &[u8],
eligibility: Replication,
plaintext_acknowledged: bool,
key_provider: Option<&dyn ReplicationKeyProvider>,
redactor: Option<&dyn RedactReplicatedValue>,
) -> Result<Option<ReplicationPayload>, ReplicationCryptoError> {
if eligibility == Replication::LocalOnly {
return Ok(None);
}
let redacted = redactor
.map(|redactor| redactor.redact(value))
.unwrap_or_else(|| value.to_vec());
if let Some(provider) = key_provider {
return Ok(Some(ReplicationPayload {
bytes: provider.seal(&redacted)?,
posture: ReplicatedValueSecurityPosture::Encrypted,
}));
}
let posture = if plaintext_acknowledged {
ReplicatedValueSecurityPosture::PlaintextAcknowledged
} else {
ReplicatedValueSecurityPosture::PlaintextUnacknowledged
};
Ok(Some(ReplicationPayload {
bytes: redacted,
posture,
}))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepairingTask {
pub interval: Duration,
}
impl RepairingTask {
pub fn new(interval: Duration) -> Self {
Self {
interval: if interval.is_zero() {
Duration::from_secs(1)
} else {
interval
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PromotionPhase {
Before,
Commit,
Finalize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BackupPromotion {
pub partition: PartitionId,
pub departing_primary: ClusterNodeId,
pub new_primary: ClusterNodeId,
pub phase: PromotionPhase,
}
pub fn select_backup_promotion(
partition: PartitionId,
replicas: &Replicas,
) -> Option<BackupPromotion> {
replicas
.backups
.first()
.cloned()
.map(|new_primary| BackupPromotion {
partition,
departing_primary: replicas.primary.clone(),
new_primary,
phase: PromotionPhase::Before,
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PartitionReplicaVersions {
versions: BTreeMap<(PartitionId, ClusterNodeId), u64>,
}
impl PartitionReplicaVersions {
pub fn set_version(
&mut self,
partition: PartitionId,
node: impl Into<ClusterNodeId>,
version: u64,
) {
self.versions.insert((partition, node.into()), version);
}
pub fn version(&self, partition: PartitionId, node: &ClusterNodeId) -> Option<u64> {
self.versions.get(&(partition, node.clone())).copied()
}
pub fn lagging_replicas(
&self,
partition: PartitionId,
primary: &ClusterNodeId,
backups: &[ClusterNodeId],
) -> Vec<ClusterNodeId> {
let primary_version = self.version(partition, primary).unwrap_or_default();
backups
.iter()
.filter(|backup| self.version(partition, backup).unwrap_or_default() < primary_version)
.cloned()
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AntiEntropyTask {
pub interval: Duration,
}
impl AntiEntropyTask {
pub fn new(interval: Duration) -> Self {
Self {
interval: if interval.is_zero() {
Duration::from_secs(1)
} else {
interval
},
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct HotCacheDirectory {
holders: BTreeMap<String, BTreeSet<ClusterNodeId>>,
}
impl HotCacheDirectory {
pub fn record_holder(&mut self, key: impl Into<String>, holder: impl Into<ClusterNodeId>) {
self.holders
.entry(key.into())
.or_default()
.insert(holder.into());
}
pub fn invalidate(&mut self, key: &str) -> Vec<ClusterNodeId> {
self.holders
.remove(key)
.map(|holders| holders.into_iter().collect())
.unwrap_or_default()
}
pub fn holders(&self, key: &str) -> Vec<ClusterNodeId> {
self.holders
.get(key)
.map(|holders| holders.iter().cloned().collect())
.unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ClusterGridCounters {
pub replication_success_total: u64,
pub replication_failure_total: u64,
pub bytes_replicated_total: u64,
pub replication_backpressure_total: u64,
pub replication_oversized_rejected_total: u64,
pub replication_decrypt_failure_total: u64,
pub under_replicated_keys: u64,
pub failover_total: u64,
pub repair_task_total: u64,
pub repair_failure_total: u64,
pub rebalance_plan_total: u64,
pub rebalance_task_ack_total: u64,
pub topology_fence_rejected_total: u64,
pub tombstone_repair_debt: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ClusterMetricDescriptor {
pub name: &'static str,
pub labels: &'static [&'static str],
}
pub fn cluster_grid_metric_descriptors() -> &'static [ClusterMetricDescriptor] {
const DESCRIPTORS: &[ClusterMetricDescriptor] = &[
ClusterMetricDescriptor {
name: "hydracache_replication_success_total",
labels: &["role", "outcome"],
},
ClusterMetricDescriptor {
name: "hydracache_replication_failure_total",
labels: &["role", "outcome"],
},
ClusterMetricDescriptor {
name: "hydracache_bytes_replicated_total",
labels: &["role"],
},
ClusterMetricDescriptor {
name: "hydracache_replication_backpressure_total",
labels: &["role"],
},
ClusterMetricDescriptor {
name: "hydracache_replication_oversized_rejected_total",
labels: &["role"],
},
ClusterMetricDescriptor {
name: "hydracache_under_replicated_keys",
labels: &[],
},
ClusterMetricDescriptor {
name: "hydracache_topology_fence_rejected_total",
labels: &["role"],
},
ClusterMetricDescriptor {
name: "hydracache_tombstone_repair_debt",
labels: &[],
},
];
DESCRIPTORS
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClusterGridDiagnostics {
pub partition_replica_versions: BTreeMap<PartitionId, Vec<(ClusterNodeId, u64)>>,
pub counters: ClusterGridCounters,
pub replicated_value_security: ReplicatedValueSecurityPosture,
}
pub type SharedReplicationKeyProvider = Arc<dyn ReplicationKeyProvider>;