use loonfs_api::wire::control::{HeadState, WalSegmentPointer};
use loonfs_api::wire::wal::{WalCommitDelta, WalCommitPayload, WalSegmentEnvelope};
use loonfs_api::{ChangeSeq, CommitId, NamespaceId, WalSegmentId, WriterEpoch};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PreparedWalSegment {
pub object_key: String,
pub segment_id: WalSegmentId,
pub envelope: WalSegmentEnvelope,
pub encoded_bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
pub enum WalBuildError {
#[error("WAL segment contains no records")]
EmptySegment,
#[error("WAL build namespace mismatch: request `{request}`, plan `{plan}`")]
NamespaceMismatch {
request: NamespaceId,
plan: NamespaceId,
},
#[error("WAL segment namespace mismatch: record `{record}`, segment `{segment}`")]
SegmentNamespaceMismatch {
record: NamespaceId,
segment: NamespaceId,
},
#[error("WAL build base head seq mismatch: request `{request}`, plan `{plan}`")]
BaseHeadSeqMismatch { request: ChangeSeq, plan: ChangeSeq },
#[error("non-contiguous WAL seq: expected `{expected}`, actual `{actual}`")]
NonContiguousSeq {
expected: ChangeSeq,
actual: ChangeSeq,
},
#[error("WAL codec error: {0}")]
Codec(String),
#[error("sequence counter overflow")]
SeqOverflow,
}
#[derive(Debug, Clone)]
pub(crate) struct WalChainLoadRequest<'a> {
pub(crate) namespace_id: &'a NamespaceId,
pub(crate) chain_base_seq: ChangeSeq,
pub(crate) head_seq: ChangeSeq,
pub(crate) visible_tip: Option<WalSegmentPointer>,
pub(crate) stop_after_seq: Option<ChangeSeq>,
pub(crate) recent_segments: &'a [WalSegmentPointer],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ValidatedWalSegment {
object_key: String,
envelope: WalSegmentEnvelope,
}
#[derive(Debug, Clone)]
pub(crate) struct DecodedWalRecord<'a> {
pub(crate) namespace_id: &'a NamespaceId,
pub(crate) seq: ChangeSeq,
pub(crate) writer_epoch: WriterEpoch,
pub(crate) commit_id: &'a CommitId,
pub(crate) committed_at_ms: u64,
pub(crate) semantic_commit_fingerprint: &'a str,
pub(crate) message: Option<&'a str>,
pub(crate) deltas: Cow<'a, [WalCommitDelta]>,
}
impl ValidatedWalSegment {
pub(crate) fn new(object_key: String, envelope: WalSegmentEnvelope) -> Self {
Self {
object_key,
envelope,
}
}
pub(crate) fn object_key(&self) -> &str {
&self.object_key
}
pub(crate) fn envelope(&self) -> &WalSegmentEnvelope {
&self.envelope
}
pub(crate) fn records(&self) -> &[WalCommitPayload] {
&self.envelope.payload.records
}
pub(crate) fn pointer(&self) -> WalSegmentPointer {
self.envelope.pointer(self.object_key.clone())
}
pub(crate) fn decoded_records(&self) -> impl Iterator<Item = DecodedWalRecord<'_>> {
let namespace_id = &self.envelope.payload.namespace_id;
let writer_epoch = self.envelope.payload.writer_epoch;
self.envelope
.payload
.records
.iter()
.map(move |record| DecodedWalRecord {
namespace_id,
seq: record.seq,
writer_epoch,
commit_id: &record.commit_id,
committed_at_ms: record.committed_at_ms,
semantic_commit_fingerprint: &record.semantic_commit_fingerprint,
message: record.message.as_deref(),
deltas: Cow::Borrowed(&record.deltas),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ValidatedWalChain {
segments: Vec<ValidatedWalSegment>,
}
impl ValidatedWalChain {
pub(crate) fn new(segments: Vec<ValidatedWalSegment>) -> Self {
Self { segments }
}
pub(crate) fn empty() -> Self {
Self {
segments: Vec::new(),
}
}
pub(crate) fn segments(&self) -> &[ValidatedWalSegment] {
&self.segments
}
pub(crate) fn decoded_records(&self) -> impl Iterator<Item = DecodedWalRecord<'_>> {
self.segments
.iter()
.flat_map(ValidatedWalSegment::decoded_records)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
pub enum WalChainLoadError {
#[error("invalid WAL chain seq range: base `{chain_base_seq}` is after head `{head_seq}`")]
InvalidSeqRange {
chain_base_seq: ChangeSeq,
head_seq: ChangeSeq,
},
#[error("missing visible WAL tip for namespace `{namespace_id}` at seq `{seq}`")]
MissingVisibleTip {
namespace_id: NamespaceId,
seq: ChangeSeq,
},
#[error("visible WAL tip ends at `{actual}`, expected head seq `{expected}`")]
TipEndSeqMismatch {
expected: ChangeSeq,
actual: ChangeSeq,
},
#[error("failed to read WAL object `{object_key}`: {message}")]
ReadWal { object_key: String, message: String },
#[error("missing WAL object `{object_key}`")]
MissingWalObject { object_key: String },
#[error("WAL pointer does not match segment payload for `{object_key}`")]
PointerMismatch { object_key: String },
#[error(
"WAL chain does not reach expected head seq: expected `{expected}`, actual `{actual}`"
)]
HeadSeqMismatch {
expected: ChangeSeq,
actual: ChangeSeq,
},
#[error("WAL chain suffix does not cover requested cursor `{after_seq}`")]
CursorNotCovered { after_seq: ChangeSeq },
#[error("wal replay validation failed: {0}")]
Replay(#[from] WalReplayError),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ReplayedWalTail {
pub resulting_head: HeadState,
pub resulting_metadata_state: crate::metadata::MetadataState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
pub enum WalReplayError {
#[error("WAL codec error: {0}")]
Codec(String),
#[error("WAL object key mismatch: expected `{expected}`, actual `{actual}`")]
ObjectKeyMismatch { expected: String, actual: String },
#[error("WAL segment namespace mismatch: expected `{expected}`, actual `{actual}`")]
NamespaceMismatch {
expected: NamespaceId,
actual: NamespaceId,
},
#[error("WAL segment base head seq mismatch: expected `{expected}`, actual `{actual}`")]
BaseHeadSeqMismatch {
expected: ChangeSeq,
actual: ChangeSeq,
},
#[error("non-contiguous WAL seq: expected `{expected}`, actual `{actual}`")]
NonContiguousSeq {
expected: ChangeSeq,
actual: ChangeSeq,
},
#[error(
"WAL segment writer epoch mismatch: expected at most `{expected_max}`, actual `{actual}`"
)]
WriterEpochMismatch {
expected_max: WriterEpoch,
actual: WriterEpoch,
},
#[error("WAL segment contains no records")]
EmptySegment,
#[error("WAL segment summary does not match its records")]
SegmentSummaryMismatch,
#[error(
"WAL segment `{object_key}` is missing its previous visible segment link before seq `{required_seq}`"
)]
BrokenChainLink {
object_key: String,
required_seq: ChangeSeq,
},
#[error("sequence counter overflow")]
SeqOverflow,
}