use liminal::durability::settle::cas_settled;
use std::num::NonZeroU64;
use std::sync::Arc;
use liminal::durability::DurableStore;
use liminal_protocol::lifecycle::ConnectionIncarnationAllocatorRestore;
use super::error::IncarnationStreamError;
pub(super) const GENESIS_STREAM_KEY: &str = "liminal/participant/incarnation/v2";
pub(super) const EPOCH_ANCHOR_KEY: &str = "liminal/participant/incarnation/v2/anchor";
const EPOCH_STREAM_KEY_PREFIX: &str = "liminal/participant/incarnation/v2/incarnation/";
pub(super) const EPOCH_STARTUP_SEQUENCE: u64 = 0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum EpochSource {
Genesis,
Epoch(NonZeroU64),
}
impl EpochSource {
pub(super) const fn from_anchor(anchor: u64) -> Self {
match NonZeroU64::new(anchor) {
Some(server_incarnation) => Self::Epoch(server_incarnation),
None => Self::Genesis,
}
}
pub(super) fn stream_key(self) -> String {
match self {
Self::Genesis => GENESIS_STREAM_KEY.to_owned(),
Self::Epoch(server_incarnation) => epoch_stream_key(server_incarnation.get()),
}
}
pub(super) const fn anchor_value(self) -> u64 {
match self {
Self::Genesis => 0,
Self::Epoch(server_incarnation) => server_incarnation.get(),
}
}
pub(super) const fn resume_seed(self) -> ConnectionIncarnationAllocatorRestore {
let server_incarnation = match self {
Self::Genesis => 0,
Self::Epoch(named) => named.get() - 1,
};
ConnectionIncarnationAllocatorRestore {
server_incarnation,
last_examined_connection_ordinal: None,
connection_ordinal_exhausted: false,
}
}
}
pub(super) fn epoch_stream_key(server_incarnation: u64) -> String {
format!("{EPOCH_STREAM_KEY_PREFIX}{server_incarnation}")
}
pub(super) async fn read_anchor(
store: &Arc<dyn DurableStore>,
) -> Result<u64, IncarnationStreamError> {
Ok(store.read_value(EPOCH_ANCHOR_KEY).await?.unwrap_or(0))
}
pub(super) async fn epoch_exists(
store: &Arc<dyn DurableStore>,
server_incarnation: u64,
) -> Result<bool, IncarnationStreamError> {
Ok(store
.read_at(
&epoch_stream_key(server_incarnation),
EPOCH_STARTUP_SEQUENCE,
)
.await?
.is_some())
}
pub(super) async fn advance_anchor(
store: &Arc<dyn DurableStore>,
from: u64,
to: u64,
) -> Result<(), IncarnationStreamError> {
cas_settled(store.as_ref(), EPOCH_ANCHOR_KEY, from, to).await?;
Ok(())
}
#[derive(Debug)]
#[must_use = "an unsettled swap leaves the anchor for the next boot to roll forward"]
pub(in crate::server) struct AnchorSwap {
store: Arc<dyn DurableStore>,
from: u64,
to: u64,
}
impl AnchorSwap {
pub(super) const fn new(store: Arc<dyn DurableStore>, from: u64, to: u64) -> Self {
Self { store, from, to }
}
pub(in crate::server) const fn from(&self) -> u64 {
self.from
}
pub(in crate::server) const fn to(&self) -> u64 {
self.to
}
pub(in crate::server) async fn settle(self) -> Result<(), IncarnationStreamError> {
let settling = std::time::Instant::now();
advance_anchor(&self.store, self.from, self.to).await?;
tracing::info!(
from = self.from,
to = self.to,
anchor_ms = u64::try_from(settling.elapsed().as_millis()).unwrap_or(u64::MAX),
"incarnation-stream anchor settled"
);
Ok(())
}
}
impl super::StartedIncarnationStream {
pub(in crate::server) const fn take_anchor_swap(&mut self) -> Option<AnchorSwap> {
self.anchor_swap.take()
}
#[cfg(test)]
pub(in crate::server) async fn settle_anchor(&mut self) -> Result<(), IncarnationStreamError> {
match self.take_anchor_swap() {
Some(swap) => swap.settle().await,
None => Ok(()),
}
}
}
pub(super) async fn roll_anchor_forward(
store: &Arc<dyn DurableStore>,
anchor: u64,
) -> Result<u64, IncarnationStreamError> {
let mut anchored = anchor;
while anchored > 0 {
let Some(successor) = anchored.checked_add(1) else {
return Ok(anchored);
};
if !epoch_exists(store, successor).await? {
return Ok(anchored);
}
advance_anchor(store, anchored, successor).await?;
anchored = successor;
}
Ok(anchored)
}
#[cfg(test)]
#[path = "epoch_tests.rs"]
mod tests;