use std::fmt;
use serde::{Deserialize, Serialize};
use crate::grid::session_context::{PartitionKey, SessionId, SessionWatermark, VersionStamp};
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct SessionSequence(u64);
impl SessionSequence {
pub const fn new(value: u64) -> Self {
Self(value)
}
pub const fn value(self) -> u64 {
self.0
}
pub const fn next(self) -> Self {
Self(self.0.saturating_add(1))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionWriteStamp {
pub session_id: SessionId,
pub sequence: SessionSequence,
pub key: PartitionKey,
pub stamp: VersionStamp,
}
impl SessionWriteStamp {
pub fn new(
session_id: impl Into<SessionId>,
sequence: SessionSequence,
key: PartitionKey,
stamp: VersionStamp,
) -> Self {
Self {
session_id: session_id.into(),
sequence,
key,
stamp,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MonotonicReadDecision {
ServeLocal,
PreventStale {
required: VersionStamp,
candidate: VersionStamp,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MonotonicWriteDecision {
Apply,
PreventReorder {
accepted: SessionSequence,
incoming: SessionSequence,
},
PreventStaleStamp {
accepted: VersionStamp,
incoming: VersionStamp,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MonotonicReadViolation {
pub key: PartitionKey,
pub required: VersionStamp,
pub candidate: VersionStamp,
}
impl fmt::Display for MonotonicReadViolation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"monotonic read violation for partition {}: candidate {:?} below required {:?}",
self.key.partition.value(),
self.candidate,
self.required
)
}
}
impl std::error::Error for MonotonicReadViolation {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MonotonicWriteViolation {
pub accepted: Box<SessionWriteStamp>,
pub incoming: Box<SessionWriteStamp>,
pub decision: MonotonicWriteDecision,
}
impl fmt::Display for MonotonicWriteViolation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"monotonic write violation for session {}: decision {:?}",
self.incoming.session_id.as_str(),
self.decision
)
}
}
impl std::error::Error for MonotonicWriteViolation {}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionMonotonicMetrics {
pub monotonic_read_violations_prevented_total: u64,
pub monotonic_write_reorders_prevented_total: u64,
}
pub fn resolve_monotonic_read(
watermark: &SessionWatermark,
key: &PartitionKey,
candidate: VersionStamp,
) -> MonotonicReadDecision {
let Some(required) = watermark.highest_seen(key) else {
return MonotonicReadDecision::ServeLocal;
};
if candidate >= required {
MonotonicReadDecision::ServeLocal
} else {
MonotonicReadDecision::PreventStale {
required,
candidate,
}
}
}
pub fn apply_monotonic_read(
watermark: &mut SessionWatermark,
key: PartitionKey,
candidate: VersionStamp,
) -> Result<(), MonotonicReadViolation> {
match resolve_monotonic_read(watermark, &key, candidate) {
MonotonicReadDecision::ServeLocal => {
watermark.observe(key, candidate);
Ok(())
}
MonotonicReadDecision::PreventStale {
required,
candidate,
} => Err(MonotonicReadViolation {
key,
required,
candidate,
}),
}
}
pub fn resolve_monotonic_write(
accepted: Option<&SessionWriteStamp>,
incoming: &SessionWriteStamp,
) -> MonotonicWriteDecision {
let Some(accepted) = accepted else {
return MonotonicWriteDecision::Apply;
};
if accepted.session_id != incoming.session_id || accepted.key != incoming.key {
return MonotonicWriteDecision::Apply;
}
if incoming.sequence <= accepted.sequence {
return MonotonicWriteDecision::PreventReorder {
accepted: accepted.sequence,
incoming: incoming.sequence,
};
}
if incoming.stamp < accepted.stamp {
return MonotonicWriteDecision::PreventStaleStamp {
accepted: accepted.stamp,
incoming: incoming.stamp,
};
}
MonotonicWriteDecision::Apply
}
pub fn apply_monotonic_write(
accepted: Option<&SessionWriteStamp>,
incoming: SessionWriteStamp,
) -> Result<SessionWriteStamp, MonotonicWriteViolation> {
match resolve_monotonic_write(accepted, &incoming) {
MonotonicWriteDecision::Apply => Ok(incoming),
decision => Err(MonotonicWriteViolation {
accepted: Box::new(
accepted
.expect("non-apply monotonic decisions require an accepted write")
.clone(),
),
incoming: Box::new(incoming),
decision,
}),
}
}