use super::{invalid, validate_identifier, CanonicalHeadV1};
use crate::sync::{types::is_valid_sync_batch_id, SyncResult};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
pub const MAX_DURABILITY_RECEIPT_BYTES: usize = 16 * 1024;
pub const MAX_DURABILITY_RECEIPTS: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DurabilityTargetV1 {
pub batch_id: String,
pub head: CanonicalHeadV1,
pub writer_epoch: u64,
pub writer_core_id: String,
}
impl DurabilityTargetV1 {
pub fn validate(&self) -> SyncResult<()> {
self.head.stream.validate()?;
validate_identifier(&self.writer_core_id)?;
if !is_valid_sync_batch_id(&self.batch_id)
|| self.writer_epoch == 0
|| self.head.sequence == 0
{
return Err(invalid("invalid durability target"));
}
for hash in [&self.head.batch_hash, &self.head.history_hash] {
if !hash.as_ref().is_some_and(|hash| {
hash.len() == 64
&& hash
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}) {
return Err(invalid("invalid durability head digest"));
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum DurabilityPolicyV1 {
Local {
core_id: String,
},
Master,
MasterAndReplica {
replica_core_id: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DurabilityReceiptV1 {
pub schema_version: u16,
pub target: DurabilityTargetV1,
pub persisted_by_core_id: String,
}
impl DurabilityReceiptV1 {
pub fn validate(&self) -> SyncResult<()> {
if self.schema_version != 1 {
return Err(invalid("NO MORE SUPPORTED PLEASE UPDATE"));
}
self.target.validate()?;
validate_identifier(&self.persisted_by_core_id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DurabilityStatusV1 {
pub policy: DurabilityPolicyV1,
pub target: DurabilityTargetV1,
pub confirmed: bool,
pub persisted_core_ids: Vec<String>,
pub missing_core_ids: Vec<String>,
}
pub fn evaluate_durability_v1(
target: &DurabilityTargetV1,
policy: &DurabilityPolicyV1,
receipts: &[DurabilityReceiptV1],
) -> SyncResult<DurabilityStatusV1> {
target.validate()?;
if receipts.len() > MAX_DURABILITY_RECEIPTS {
return Err(invalid("too many durability receipts"));
}
let required = required_cores(target, policy)?;
let mut observed = BTreeSet::new();
for receipt in receipts {
receipt.validate()?;
if &receipt.target != target {
return Err(invalid("durability receipt target mismatch"));
}
observed.insert(receipt.persisted_by_core_id.as_str());
}
let (persisted, missing): (Vec<_>, Vec<_>) = required
.into_iter()
.partition(|core| observed.contains(core.as_str()));
Ok(DurabilityStatusV1 {
policy: policy.clone(),
target: target.clone(),
confirmed: missing.is_empty(),
persisted_core_ids: persisted,
missing_core_ids: missing,
})
}
fn required_cores(
target: &DurabilityTargetV1,
policy: &DurabilityPolicyV1,
) -> SyncResult<BTreeSet<String>> {
let mut cores = BTreeSet::new();
match policy {
DurabilityPolicyV1::Local { core_id } => {
validate_identifier(core_id)?;
cores.insert(core_id.clone());
}
DurabilityPolicyV1::Master => {
cores.insert(target.writer_core_id.clone());
}
DurabilityPolicyV1::MasterAndReplica { replica_core_id } => {
validate_identifier(replica_core_id)?;
if replica_core_id == &target.writer_core_id {
return Err(invalid("durability replica must differ from writer"));
}
cores.insert(target.writer_core_id.clone());
cores.insert(replica_core_id.clone());
}
}
Ok(cores)
}
pub fn decode_durability_receipt_v1(bytes: &[u8]) -> SyncResult<DurabilityReceiptV1> {
if bytes.len() > MAX_DURABILITY_RECEIPT_BYTES {
return Err(invalid("durability receipt size exceeded"));
}
#[derive(Deserialize)]
struct Version {
#[serde(default)]
schema_version: u16,
}
let version: Version =
serde_json::from_slice(bytes).map_err(|_| invalid("invalid durability receipt JSON"))?;
if version.schema_version != 1 {
return Err(invalid("NO MORE SUPPORTED PLEASE UPDATE"));
}
let receipt: DurabilityReceiptV1 =
serde_json::from_slice(bytes).map_err(|_| invalid("invalid durability receipt JSON"))?;
receipt.validate()?;
Ok(receipt)
}