use liminal::durability::settle::{append_all_settled, append_settled, cas_settled, flush_settled};
use std::{collections::VecDeque, sync::Arc};
use liminal::durability::{DurabilityError, DurableStore};
use liminal_protocol::wire::{
AttachAttemptToken, AttachSecret, BindingEpoch, ConnectionIncarnation, CredentialAttachRequest,
DeliverySeq, DetachAttemptToken, DetachRequest, EnrollmentRequest, EnrollmentToken, Generation,
LeaveAttemptToken, LeaveRequest, ParticipantAck, ParticipantId, RecordAdmission,
RecordAdmissionAttemptToken, TransactionOrder,
};
use serde::{Deserialize, Serialize};
use super::facts::{Digest, FactsError};
pub(super) use super::log_error::FencedAttachProofRefusal;
use super::operation_batch::OperationBatch;
use super::routed_stream::{RoutedStreamError, RoutedStreamRefusal};
use super::write_issuance::WriteIssuance;
pub(super) const STREAM_PREFIX: &str = "liminal:participant-production:";
pub(super) const GENERATION_STREAM_PREFIX: &str = "liminal:participant-production-generation:";
pub(super) const GENERATION_KEY_PREFIX: &str = "liminal:participant-production-log-generation:";
pub(super) const READ_BATCH_SIZE: usize = 64;
pub(super) const SCHEMA_VERSION: u8 = 4;
pub(super) const SCHEMA_VERSION_V3: u8 = 3;
pub(super) const SCHEMA_VERSION_V2: u8 = 2;
#[derive(Debug, thiserror::Error)]
pub(super) enum GenerationSwitchFailure {
#[error("the generation compare-and-swap was refused: {0}")]
NotSwapped(DurabilityError),
#[error("the generation was swapped but its flush failed: {0}")]
SwappedUnconfirmed(DurabilityError),
}
#[derive(Debug, thiserror::Error)]
pub enum OperationLogError {
#[error(transparent)]
Durability(#[from] DurabilityError),
#[error("routed stream refused: {0}")]
RoutedStream(#[source] RoutedStreamRefusal),
#[error(transparent)]
Bridge(#[from] liminal::durability::bridge::BridgeError),
#[error("participant production log serialization failed: {0}")]
Serialization(#[from] serde_json::Error),
#[error("unsupported participant production log schema version {0}")]
SchemaVersion(u8),
#[error(
"participant production log row at sequence {sequence} carries the wrong retained-charge audit form for schema version {schema_version}"
)]
RetainedChargeForm { sequence: u64, schema_version: u8 },
#[error(
"participant production log schema regressed at sequence {sequence}: previous {previous}, actual {actual}"
)]
SchemaVersionTransition {
sequence: u64,
previous: u8,
actual: u8,
},
#[error("participant production log expected sequence {expected}, found {actual}")]
Sequence {
expected: u64,
actual: u64,
},
#[error("participant production log append expected {expected}, got {actual}")]
AssignedSequence {
expected: u64,
actual: u64,
},
#[error("participant production log entry carries zero generation")]
ZeroGeneration,
#[error("durable participant row at sequence {sequence} was refused during restore")]
CorruptRow {
sequence: u64,
},
#[error("prune receipt {key} refused its proof: {source}")]
PruneReceipt {
key: String,
#[source]
source: super::prune::PruneProofError,
},
#[error("v2 Attached row at sequence {sequence} has no durable fenced proof")]
V2AttachedFencedProofUnavailable { sequence: u64 },
#[error("v2 Attached row at sequence {sequence} has contradictory mode evidence")]
V2AttachedModeMismatch { sequence: u64 },
#[error("v3 fenced Attached proof at sequence {sequence} was refused: {reason}")]
FencedAttachProof {
sequence: u64,
reason: FencedAttachProofRefusal,
},
#[error("participant production retained-charge count exceeds u64")]
RetainedChargeCount,
#[error("reconstructed fate operation drifted from durable sequence {sequence}")]
FateReplayDrift { sequence: u64 },
#[error("participant production log row timestamp: {0}")]
Clock(#[from] FactsError),
#[error("participant conversation registry snapshot refused: {0}")]
RegistrySnapshot(#[source] super::registry_snapshot::RegistrySnapshotError),
}
impl From<RoutedStreamError> for OperationLogError {
fn from(error: RoutedStreamError) -> Self {
match error {
RoutedStreamError::Store(original) => Self::Durability(original),
RoutedStreamError::Refusal(refusal) => Self::RoutedStream(refusal),
}
}
}
impl From<super::operation_batch::CheckpointStageError> for OperationLogError {
fn from(error: super::operation_batch::CheckpointStageError) -> Self {
use super::operation_batch::CheckpointStageError;
match error {
CheckpointStageError::Store(original) => Self::Durability(original),
CheckpointStageError::Checkpoint(original) => {
Self::RoutedStream(RoutedStreamRefusal::Checkpoint(Box::new(original)))
}
CheckpointStageError::Locator(original) => {
Self::RoutedStream(RoutedStreamRefusal::CheckpointLocator(original))
}
CheckpointStageError::BatchRefused => {
Self::RoutedStream(RoutedStreamRefusal::BatchRefused)
}
CheckpointStageError::RouteMismatch => {
Self::RoutedStream(RoutedStreamRefusal::CheckpointRouteMismatch)
}
CheckpointStageError::PositionRegression => {
Self::RoutedStream(RoutedStreamRefusal::CheckpointPositionRegression)
}
CheckpointStageError::BodyMismatch => {
Self::RoutedStream(RoutedStreamRefusal::CheckpointBodyMismatch)
}
}
}
}
#[derive(Debug)]
pub(super) struct OperationLog {
store: Arc<dyn DurableStore>,
conversation_id: u64,
pinned_generation: Option<u64>,
issuance: WriteIssuance,
}
impl OperationLog {
pub(super) fn new(store: Arc<dyn DurableStore>, conversation_id: u64) -> Self {
Self {
store,
conversation_id,
pinned_generation: None,
issuance: WriteIssuance::default(),
}
}
pub(super) fn at_generation(
store: Arc<dyn DurableStore>,
conversation_id: u64,
generation: u64,
) -> Self {
Self {
store,
conversation_id,
pinned_generation: Some(generation),
issuance: WriteIssuance::default(),
}
}
pub(super) const fn issuance(&self) -> &WriteIssuance {
&self.issuance
}
pub(super) const fn conversation_id(&self) -> u64 {
self.conversation_id
}
fn generation_key(&self) -> String {
format!("{GENERATION_KEY_PREFIX}{}", self.conversation_id)
}
pub(super) async fn generation(&self) -> Result<u64, OperationLogError> {
Ok(self.read_generation().await?)
}
async fn read_generation(&self) -> Result<u64, DurabilityError> {
if let Some(generation) = self.pinned_generation {
return Ok(generation);
}
Ok(self
.store
.read_value(&self.generation_key())
.await?
.unwrap_or(0))
}
pub(super) async fn stream_key(&self) -> Result<String, DurabilityError> {
let generation = self.read_generation().await?;
Ok(if generation == 0 {
format!("{STREAM_PREFIX}{}", self.conversation_id)
} else {
format!(
"{GENERATION_STREAM_PREFIX}{generation}:{}",
self.conversation_id
)
})
}
pub(super) async fn read_raw_page(
&self,
from_sequence: u64,
) -> Result<Vec<liminal::durability::StoredEntry>, OperationLogError> {
let key = self.stream_key().await?;
Ok(self
.store
.read_from(&key, from_sequence, READ_BATCH_SIZE)
.await?)
}
pub(super) async fn append_encoded(
&self,
payloads: Vec<Vec<u8>>,
expected_sequence: u64,
) -> Result<u64, OperationLogError> {
let key = self.stream_key().await?;
self.issuance.mark_submitted();
let next =
append_all_settled(self.store.as_ref(), &key, payloads, expected_sequence).await?;
flush_settled(self.store.as_ref()).await?;
Ok(next)
}
pub(super) async fn switch_generation(
&self,
from: u64,
to: u64,
) -> Result<(), GenerationSwitchFailure> {
let key = self.generation_key();
self.issuance.mark_submitted();
cas_settled(self.store.as_ref(), &key, from, to)
.await
.map_err(GenerationSwitchFailure::NotSwapped)?;
flush_settled(self.store.as_ref())
.await
.map_err(GenerationSwitchFailure::SwappedUnconfirmed)?;
Ok(())
}
pub(super) fn store(&self) -> Arc<dyn DurableStore> {
Arc::clone(&self.store)
}
pub(super) async fn read_at(
&self,
sequence: u64,
) -> Result<Option<DecodedOperation>, OperationLogError> {
self.read_committed_at(
&super::restore_checkpoint_locator::checkpoint_conversation_route(self.conversation_id),
sequence,
)
.await
}
async fn read_flat_at(
&self,
sequence: u64,
) -> Result<Option<DecodedOperation>, OperationLogError> {
let key = self.stream_key().await?;
let Some(entry) = self.store.read_at(&key, sequence).await? else {
return Ok(None);
};
if entry.sequence != sequence {
return Err(OperationLogError::Sequence {
expected: sequence,
actual: entry.sequence,
});
}
Self::decode_at(&entry)
}
pub(super) async fn read_routed_at(
&self,
route: &str,
sequence: u64,
) -> Result<Option<DecodedOperation>, OperationLogError> {
let key = self.stream_key().await?;
let head = super::routed_stream::read_head(self.store.as_ref(), route, &key)
.await?
.ok_or_else(|| {
OperationLogError::RoutedStream(RoutedStreamRefusal::MissingHead {
stream: key.as_bytes().to_vec(),
})
})?;
let mut entries =
super::routed_stream::read_page(self.store.as_ref(), route, &key, &head, sequence, 1)
.await?;
entries
.pop()
.as_ref()
.map(Self::decode_at)
.transpose()
.map(Option::flatten)
}
pub(super) async fn read_committed_at(
&self,
route: &str,
sequence: u64,
) -> Result<Option<DecodedOperation>, OperationLogError> {
let native_head = self.read_stream_next_seq().await?.unwrap_or(0);
if sequence < native_head {
return self.read_flat_at(sequence).await;
}
match self.read_routed_at(route, sequence).await {
Err(OperationLogError::RoutedStream(RoutedStreamRefusal::MissingHead { .. })) => {
Ok(None)
}
read => read,
}
}
pub(super) async fn read_staged_page(
&self,
batch: &mut OperationBatch,
sequence: u64,
phase: OperationSchemaPhase,
) -> Result<DecodedOperationPage, OperationLogError> {
let stream = self.stream_key().await?;
let rows = batch
.read_stream_page(self.store.as_ref(), &stream, sequence, READ_BATCH_SIZE)
.await?
.ok_or(OperationLogError::RoutedStream(
super::routed_stream::RoutedStreamRefusal::BatchRefused,
))?;
Self::decode_page(rows, phase)
}
pub(super) async fn read_staged_at(
&self,
batch: &OperationBatch,
sequence: u64,
) -> Result<Option<DecodedOperation>, OperationLogError> {
if let Some(operation) = self.read_at(sequence).await? {
return Ok(Some(operation));
}
let stream = self.stream_key().await?;
batch
.read_source(self.store.as_ref(), &stream, sequence)
.await?
.as_ref()
.map(Self::decode_at)
.transpose()
.map(Option::flatten)
}
fn decode_at(
entry: &liminal::durability::StoredEntry,
) -> Result<Option<DecodedOperation>, OperationLogError> {
let sequence = entry.sequence;
let version: StoredEntryVersion = serde_json::from_slice(&entry.payload)?;
let operation = match version.schema_version {
SCHEMA_VERSION_V2 => {
let stored: StoredEntryV2 = serde_json::from_slice(&entry.payload)?;
DecodedStoredOperation::V2(stored.operation)
}
SCHEMA_VERSION_V3 | SCHEMA_VERSION => {
let stored: StoredEntryV3 = serde_json::from_slice(&entry.payload)?;
stored.operation.validate_durable(sequence)?;
DecodedStoredOperation::V3(stored.operation)
}
actual => return Err(OperationLogError::SchemaVersion(actual)),
};
operation.validate_retained_charge_form(version.schema_version, sequence)?;
Ok(Some(DecodedOperation {
sequence,
schema_version: version.schema_version,
operation,
}))
}
pub(super) async fn read_stream_next_seq(&self) -> Result<Option<u64>, OperationLogError> {
let key = self.stream_key().await?;
Ok(self.store.read_stream_next_seq(&key).await?)
}
pub(super) async fn read_bounded_page(
&self,
from_sequence: u64,
phase: OperationSchemaPhase,
limit: usize,
) -> Result<DecodedOperationPage, OperationLogError> {
if limit == 0 || limit > READ_BATCH_SIZE {
return Err(DurabilityError::ConfigError(
"operation page limit is outside its domain".to_owned(),
)
.into());
}
let key = self.stream_key().await?;
let entries = self.store.read_from(&key, from_sequence, limit).await?;
Self::decode_page(entries, phase)
}
pub(super) async fn read_routed_page(
&self,
route: &str,
from_sequence: u64,
phase: OperationSchemaPhase,
) -> Result<DecodedOperationPage, OperationLogError> {
let key = self.stream_key().await?;
let head = super::routed_stream::read_head(self.store.as_ref(), route, &key)
.await?
.ok_or_else(|| {
OperationLogError::RoutedStream(RoutedStreamRefusal::MissingHead {
stream: key.as_bytes().to_vec(),
})
})?;
let entries = super::routed_stream::read_page(
self.store.as_ref(),
route,
&key,
&head,
from_sequence,
READ_BATCH_SIZE,
)
.await?;
Self::decode_page(entries, phase)
}
pub(super) async fn read_rebuild_page(
&self,
route: &str,
from_sequence: u64,
phase: OperationSchemaPhase,
) -> Result<Option<DecodedOperationPage>, OperationLogError> {
let key = self.stream_key().await?;
let Some(head) = super::routed_stream::read_head(self.store.as_ref(), route, &key).await?
else {
return Ok(None);
};
let entries = super::routed_stream::read_page(
self.store.as_ref(),
route,
&key,
&head,
from_sequence,
READ_BATCH_SIZE,
)
.await?;
Self::decode_page(entries, phase).map(Some)
}
pub(super) async fn stage_bootstrap_head(
&self,
batch: &mut OperationBatch,
next_sequence: u64,
) -> Result<(), OperationLogError> {
let result: Result<(), OperationLogError> = async {
let key = self.stream_key().await?;
batch
.stage_bootstrap_stream(self.store.as_ref(), &key, next_sequence)
.await?;
Ok(())
}
.await;
if result.is_err() {
batch.refuse();
}
result
}
fn decode_page(
entries: Vec<liminal::durability::StoredEntry>,
mut phase: OperationSchemaPhase,
) -> Result<DecodedOperationPage, OperationLogError> {
let mut rows = Vec::with_capacity(entries.len());
let mut canonical_rows = VecDeque::with_capacity(entries.len());
for entry in entries {
let version: StoredEntryVersion = serde_json::from_slice(&entry.payload)?;
let operation = match version.schema_version {
SCHEMA_VERSION_V2 if phase == OperationSchemaPhase::V2Prefix => {
let stored: StoredEntryV2 = serde_json::from_slice(&entry.payload)?;
DecodedStoredOperation::V2(stored.operation)
}
SCHEMA_VERSION_V2 => {
return Err(OperationLogError::SchemaVersionTransition {
sequence: entry.sequence,
previous: phase.schema_version(),
actual: SCHEMA_VERSION_V2,
});
}
SCHEMA_VERSION_V3 if phase == OperationSchemaPhase::V4Suffix => {
return Err(OperationLogError::SchemaVersionTransition {
sequence: entry.sequence,
previous: SCHEMA_VERSION,
actual: SCHEMA_VERSION_V3,
});
}
SCHEMA_VERSION_V3 | SCHEMA_VERSION => {
phase = if version.schema_version == SCHEMA_VERSION {
OperationSchemaPhase::V4Suffix
} else {
OperationSchemaPhase::V3Suffix
};
let stored: StoredEntryV3 = serde_json::from_slice(&entry.payload)?;
stored.operation.validate_durable(entry.sequence)?;
DecodedStoredOperation::V3(stored.operation)
}
actual => return Err(OperationLogError::SchemaVersion(actual)),
};
operation.validate_retained_charge_form(version.schema_version, entry.sequence)?;
canonical_rows.push_back((entry.sequence, entry.payload));
rows.push(DecodedOperation {
sequence: entry.sequence,
schema_version: version.schema_version,
operation,
});
}
Ok(DecodedOperationPage {
rows,
canonical_rows,
next_phase: phase,
})
}
#[cfg(test)]
pub(super) async fn read_v2_page(
&self,
from_sequence: u64,
) -> Result<Vec<(u64, StoredOperationV2)>, OperationLogError> {
let key = self.stream_key().await?;
let entries = self
.store
.read_from(&key, from_sequence, READ_BATCH_SIZE)
.await?;
let mut decoded = Vec::with_capacity(entries.len());
for entry in entries {
let version: StoredEntryVersion = serde_json::from_slice(&entry.payload)?;
if version.schema_version != SCHEMA_VERSION_V2 {
return Err(OperationLogError::SchemaVersion(version.schema_version));
}
let stored: StoredEntryV2 = serde_json::from_slice(&entry.payload)?;
decoded.push((entry.sequence, stored.operation));
}
Ok(decoded)
}
pub(super) async fn stage_into(
&self,
batch: &mut OperationBatch,
operation: &StoredOperation,
expected_sequence: u64,
timestamp: u64,
) -> Result<(), OperationLogError> {
let result: Result<(), OperationLogError> = async {
let payload = encode_current(operation, expected_sequence)?;
let key = self.stream_key().await?;
batch
.stage_source_stream(
self.store.as_ref(),
&key,
super::restore_checkpoint_locator::CheckpointSource::Base,
liminal::durability::StoredEntry {
payload,
sequence: expected_sequence,
timestamp,
},
)
.await?;
Ok(())
}
.await;
if result.is_err() {
batch.refuse();
}
result
}
#[cfg(test)]
pub(super) async fn append(
&self,
operation: &StoredOperation,
expected_sequence: u64,
) -> Result<(), OperationLogError> {
let mut batch = OperationBatch::new(
&super::restore_checkpoint_locator::checkpoint_conversation_route(self.conversation_id),
);
self.stage_into(
&mut batch,
operation,
expected_sequence,
super::facts::now_unix_nanos()?,
)
.await?;
batch
.commit(self.store.as_ref(), || self.issuance.mark_submitted())
.await?;
Ok(())
}
pub(super) async fn append_flat(
&self,
operation: &StoredOperation,
expected_sequence: u64,
) -> Result<(), OperationLogError> {
let payload = encode_current(operation, expected_sequence)?;
let key = self.stream_key().await?;
self.issuance.mark_submitted();
let assigned =
append_settled(self.store.as_ref(), &key, payload, expected_sequence).await?;
if assigned != expected_sequence {
return Err(OperationLogError::AssignedSequence {
expected: expected_sequence,
actual: assigned,
});
}
flush_settled(self.store.as_ref()).await?;
Ok(())
}
}
#[cfg(not(test))]
const fn append_schema_version() -> u8 {
SCHEMA_VERSION
}
#[cfg(test)]
fn append_schema_version() -> u8 {
if legacy_rows::armed() {
SCHEMA_VERSION_V3
} else {
SCHEMA_VERSION
}
}
pub(super) fn encode_current(
operation: &StoredOperation,
sequence: u64,
) -> Result<Vec<u8>, OperationLogError> {
encode_versioned(operation, sequence, append_schema_version())
}
pub(super) fn encode_versioned(
operation: &StoredOperation,
sequence: u64,
schema_version: u8,
) -> Result<Vec<u8>, OperationLogError> {
operation.validate_durable(sequence)?;
DecodedStoredOperation::V3(operation.clone())
.validate_retained_charge_form(schema_version, sequence)?;
Ok(serde_json::to_vec(&StoredEntryV3 {
schema_version,
operation: operation.clone(),
})?)
}
pub(super) fn encode_v2(operation: &StoredOperationV2) -> Result<Vec<u8>, OperationLogError> {
Ok(serde_json::to_vec(&StoredEntryV2 {
schema_version: SCHEMA_VERSION_V2,
operation: operation.clone(),
})?)
}
pub(super) fn record_retained_charges(
prior: &[StoredRetainedCharge],
resulting: &[StoredRetainedCharge],
) -> Result<
(
Option<Vec<StoredRetainedCharge>>,
Option<StoredRetainedChargeChange>,
),
OperationLogError,
> {
if append_schema_version() == SCHEMA_VERSION {
Ok((
None,
Some(StoredRetainedChargeChange::between(prior, resulting)?),
))
} else {
Ok((Some(resulting.to_vec()), None))
}
}
#[cfg(test)]
pub(super) mod legacy_rows {
use std::cell::Cell;
thread_local! {
static ARMED: Cell<bool> = const { Cell::new(false) };
}
pub(in super::super) struct LegacyRows(());
impl Drop for LegacyRows {
fn drop(&mut self) {
ARMED.with(|armed| armed.set(false));
}
}
pub(in super::super) fn arm() -> LegacyRows {
ARMED.with(|armed| armed.set(true));
LegacyRows(())
}
pub(in super::super) fn armed() -> bool {
ARMED.with(Cell::get)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum OperationSchemaPhase {
#[default]
V2Prefix,
V3Suffix,
V4Suffix,
}
impl OperationSchemaPhase {
const fn schema_version(self) -> u8 {
match self {
Self::V2Prefix => SCHEMA_VERSION_V2,
Self::V3Suffix => SCHEMA_VERSION_V3,
Self::V4Suffix => SCHEMA_VERSION,
}
}
}
#[derive(Clone, Debug)]
pub(super) struct DecodedOperation {
pub(super) sequence: u64,
pub(super) schema_version: u8,
pub(super) operation: DecodedStoredOperation,
}
#[derive(Clone, Debug)]
pub(super) struct DecodedOperationPage {
pub(super) rows: Vec<DecodedOperation>,
pub(super) canonical_rows: VecDeque<(u64, Vec<u8>)>,
pub(super) next_phase: OperationSchemaPhase,
}
#[derive(Clone, Debug)]
pub(super) enum DecodedStoredOperation {
V2(StoredOperationV2),
V3(StoredOperationV3),
}
impl DecodedStoredOperation {
fn validate_retained_charge_form(
&self,
schema_version: u8,
sequence: u64,
) -> Result<(), OperationLogError> {
let audit = match self {
Self::V2(StoredOperationV2::MarkerDrained { row })
| Self::V3(StoredOperationV3::MarkerDrained { row }) => {
row.retained_charge_audit(sequence)?
}
Self::V2(StoredOperationV2::RecordAdmission { row })
| Self::V3(StoredOperationV3::RecordAdmission { row }) => {
row.retained_charge_audit(sequence)?
}
Self::V2(_) | Self::V3(_) => return Ok(()),
};
let expects_change = schema_version == SCHEMA_VERSION;
if expects_change == matches!(audit, RetainedChargeAudit::Change(_)) {
Ok(())
} else {
Err(OperationLogError::RetainedChargeForm {
sequence,
schema_version,
})
}
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
struct StoredEntryVersion {
schema_version: u8,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct StoredEntryV2 {
schema_version: u8,
operation: StoredOperationV2,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct StoredEntryV3 {
schema_version: u8,
operation: StoredOperationV3,
}
pub(super) use super::log_v2::*;
pub(super) use super::log_v3::*;
pub(super) type StoredOperation = StoredOperationV3;
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
pub(super) struct StoredResourceVector {
pub(super) entries: u64,
pub(super) bytes: u64,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
pub(super) struct StoredRetainedCharge {
pub(super) delivery_seq: DeliverySeq,
pub(super) transaction_order: TransactionOrder,
pub(super) candidate_phase: u8,
pub(super) participant_id: ParticipantId,
pub(super) charge: StoredResourceVector,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredRetainedChargeChange {
pub(super) removed: Vec<u64>,
pub(super) added: Vec<(u64, StoredRetainedCharge)>,
pub(super) resulting_count: u64,
}
impl StoredRetainedChargeChange {
pub(super) fn between(
prior: &[StoredRetainedCharge],
resulting: &[StoredRetainedCharge],
) -> Result<Self, OperationLogError> {
let position =
|index: usize| u64::try_from(index).map_err(|_| OperationLogError::RetainedChargeCount);
let mut removed = Vec::new();
let mut added = Vec::new();
let (mut prior_index, mut resulting_index) = (0_usize, 0_usize);
loop {
match (prior.get(prior_index), resulting.get(resulting_index)) {
(None, None) => break,
(Some(before), Some(after)) if before == after => {
prior_index += 1;
resulting_index += 1;
}
(Some(before), Some(after)) if before.delivery_seq <= after.delivery_seq => {
removed.push(position(prior_index)?);
prior_index += 1;
}
(Some(_), None) => {
removed.push(position(prior_index)?);
prior_index += 1;
}
(_, Some(after)) => {
added.push((position(resulting_index)?, *after));
resulting_index += 1;
}
}
}
Ok(Self {
removed,
added,
resulting_count: position(resulting.len())?,
})
}
#[cfg(test)]
pub(super) fn apply(&self, prior: &[StoredRetainedCharge]) -> Vec<StoredRetainedCharge> {
let mut kept = prior
.iter()
.enumerate()
.filter(|(index, _)| {
!self
.removed
.iter()
.any(|removed| u64::try_from(*index).is_ok_and(|index| index == *removed))
})
.map(|(_, charge)| *charge);
let mut added = self.added.iter().peekable();
let mut rebuilt = Vec::new();
loop {
let at = u64::try_from(rebuilt.len()).unwrap_or(u64::MAX);
match added.peek() {
Some((position, charge)) if *position == at => {
rebuilt.push(*charge);
added.next();
}
_ => match kept.next() {
Some(charge) => rebuilt.push(charge),
None => match added.next() {
Some((_, charge)) => rebuilt.push(*charge),
None => return rebuilt,
},
},
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub(super) enum RetainedChargeAudit<'row> {
Complete(&'row [StoredRetainedCharge]),
Change(&'row StoredRetainedChargeChange),
}
impl RetainedChargeAudit<'_> {
pub(super) fn matches(
self,
prior: &[StoredRetainedCharge],
resulting: &[StoredRetainedCharge],
) -> Result<bool, OperationLogError> {
Ok(match self {
Self::Complete(stored) => stored == resulting,
Self::Change(stored) => {
*stored == StoredRetainedChargeChange::between(prior, resulting)?
}
})
}
}
fn retained_charge_audit<'row>(
complete: Option<&'row Vec<StoredRetainedCharge>>,
change: Option<&'row StoredRetainedChargeChange>,
sequence: u64,
) -> Result<RetainedChargeAudit<'row>, OperationLogError> {
match (complete, change) {
(Some(complete), None) => Ok(RetainedChargeAudit::Complete(complete)),
(None, Some(change)) => Ok(RetainedChargeAudit::Change(change)),
(Some(_), Some(_)) | (None, None) => Err(OperationLogError::CorruptRow { sequence }),
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredMarkerDrain {
pub(super) marker: Vec<u8>,
pub(super) retained_charge: StoredRetainedCharge,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) resulting_retained_charges: Option<Vec<StoredRetainedCharge>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) retained_charge_change: Option<StoredRetainedChargeChange>,
pub(super) successor: Vec<u8>,
}
impl StoredMarkerDrain {
pub(super) fn retained_charge_audit(
&self,
sequence: u64,
) -> Result<RetainedChargeAudit<'_>, OperationLogError> {
retained_charge_audit(
self.resulting_retained_charges.as_ref(),
self.retained_charge_change.as_ref(),
sequence,
)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredRecordAdmissionRequest {
pub(super) conversation_id: u64,
pub(super) participant_id: ParticipantId,
pub(super) capability_generation: u64,
pub(super) token: [u8; 16],
pub(super) payload: Vec<u8>,
}
impl From<&RecordAdmission> for StoredRecordAdmissionRequest {
fn from(request: &RecordAdmission) -> Self {
Self {
conversation_id: request.conversation_id,
participant_id: request.participant_id,
capability_generation: request.capability_generation.get(),
token: request.record_admission_attempt_token.into_bytes(),
payload: request.payload.clone(),
}
}
}
impl StoredRecordAdmissionRequest {
pub(super) fn into_request(self) -> Result<RecordAdmission, OperationLogError> {
Ok(RecordAdmission {
conversation_id: self.conversation_id,
participant_id: self.participant_id,
capability_generation: stored_generation(self.capability_generation)?,
record_admission_attempt_token: RecordAdmissionAttemptToken::new(self.token),
payload: self.payload,
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredRecordAdmission {
pub(super) request: StoredRecordAdmissionRequest,
pub(super) receiving_epoch: StoredBindingEpoch,
pub(super) transaction_order: TransactionOrder,
pub(super) delivery_seq: DeliverySeq,
pub(super) encoded_record_charge: StoredResourceVector,
pub(super) resulting_connection_count: u64,
pub(super) newly_tracked: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) resulting_retained_charges: Option<Vec<StoredRetainedCharge>>,
pub(super) resulting_closure_accounting: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) retained_charge_change: Option<StoredRetainedChargeChange>,
}
impl StoredRecordAdmission {
pub(super) fn retained_charge_audit(
&self,
sequence: u64,
) -> Result<RetainedChargeAudit<'_>, OperationLogError> {
retained_charge_audit(
self.resulting_retained_charges.as_ref(),
self.retained_charge_change.as_ref(),
sequence,
)
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredLeaveRequest {
pub(super) conversation_id: u64,
pub(super) participant_id: ParticipantId,
pub(super) capability_generation: u64,
pub(super) attach_secret: [u8; 32],
pub(super) token: [u8; 16],
}
impl From<&LeaveRequest> for StoredLeaveRequest {
fn from(request: &LeaveRequest) -> Self {
Self {
conversation_id: request.conversation_id,
participant_id: request.participant_id,
capability_generation: request.capability_generation.get(),
attach_secret: request.attach_secret.into_bytes(),
token: request.leave_attempt_token.into_bytes(),
}
}
}
impl StoredLeaveRequest {
pub(super) fn into_request(self) -> Result<LeaveRequest, OperationLogError> {
Ok(LeaveRequest {
conversation_id: self.conversation_id,
participant_id: self.participant_id,
capability_generation: stored_generation(self.capability_generation)?,
attach_secret: AttachSecret::new(self.attach_secret),
leave_attempt_token: LeaveAttemptToken::new(self.token),
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredLeave {
pub(super) request: StoredLeaveRequest,
pub(super) request_verifier: Digest,
pub(super) receiving_epoch: StoredBindingEpoch,
pub(super) left_transaction_order: TransactionOrder,
pub(super) left_delivery_seq: DeliverySeq,
pub(super) ended_binding_epoch: Option<StoredBindingEpoch>,
pub(super) prior_terminal_delivery_seq: Option<DeliverySeq>,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredEnrollmentRequest {
pub(super) conversation_id: u64,
pub(super) token: [u8; 16],
}
impl From<&EnrollmentRequest> for StoredEnrollmentRequest {
fn from(request: &EnrollmentRequest) -> Self {
Self {
conversation_id: request.conversation_id,
token: request.enrollment_token.into_bytes(),
}
}
}
impl StoredEnrollmentRequest {
pub(super) const fn to_request(self) -> EnrollmentRequest {
EnrollmentRequest {
conversation_id: self.conversation_id,
enrollment_token: EnrollmentToken::new(self.token),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredEnrollmentAllocation {
pub(super) participant_id: ParticipantId,
pub(super) identity_limit: u64,
pub(super) attach_secret: [u8; 32],
pub(super) origin_epoch: StoredBindingEpoch,
pub(super) attached_order: TransactionOrder,
pub(super) attached_seq: DeliverySeq,
pub(super) receipt_expires_at: StoredU128,
pub(super) provenance_expires_at: StoredU128,
pub(super) enrollment_fingerprint: Digest,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredAttachRequest {
pub(super) conversation_id: u64,
pub(super) participant_id: ParticipantId,
pub(super) capability_generation: u64,
pub(super) attach_secret: [u8; 32],
pub(super) token: [u8; 16],
pub(super) accept_marker_delivery_seq: Option<DeliverySeq>,
}
impl From<&CredentialAttachRequest> for StoredAttachRequest {
fn from(request: &CredentialAttachRequest) -> Self {
Self {
conversation_id: request.conversation_id,
participant_id: request.participant_id,
capability_generation: request.capability_generation.get(),
attach_secret: request.attach_secret.into_bytes(),
token: request.attach_attempt_token.into_bytes(),
accept_marker_delivery_seq: request.accept_marker_delivery_seq,
}
}
}
impl StoredAttachRequest {
pub(super) fn to_request(self) -> Result<CredentialAttachRequest, OperationLogError> {
Ok(CredentialAttachRequest {
conversation_id: self.conversation_id,
participant_id: self.participant_id,
capability_generation: stored_generation(self.capability_generation)?,
attach_secret: AttachSecret::new(self.attach_secret),
attach_attempt_token: AttachAttemptToken::new(self.token),
accept_marker_delivery_seq: self.accept_marker_delivery_seq,
})
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredDetachRequest {
pub(super) conversation_id: u64,
pub(super) participant_id: ParticipantId,
pub(super) capability_generation: u64,
pub(super) token: [u8; 16],
}
impl From<&DetachRequest> for StoredDetachRequest {
fn from(request: &DetachRequest) -> Self {
Self {
conversation_id: request.conversation_id,
participant_id: request.participant_id,
capability_generation: request.capability_generation.get(),
token: request.detach_attempt_token.into_bytes(),
}
}
}
impl StoredDetachRequest {
pub(super) fn to_request(self) -> Result<DetachRequest, OperationLogError> {
Ok(DetachRequest {
conversation_id: self.conversation_id,
participant_id: self.participant_id,
capability_generation: stored_generation(self.capability_generation)?,
detach_attempt_token: DetachAttemptToken::new(self.token),
})
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredAck {
pub(super) conversation_id: u64,
pub(super) participant_id: ParticipantId,
pub(super) capability_generation: u64,
pub(super) through_seq: DeliverySeq,
}
impl From<&ParticipantAck> for StoredAck {
fn from(request: &ParticipantAck) -> Self {
Self {
conversation_id: request.conversation_id,
participant_id: request.participant_id,
capability_generation: request.capability_generation.get(),
through_seq: request.through_seq,
}
}
}
impl StoredAck {
pub(super) fn to_request(self) -> Result<ParticipantAck, OperationLogError> {
Ok(ParticipantAck {
conversation_id: self.conversation_id,
participant_id: self.participant_id,
capability_generation: stored_generation(self.capability_generation)?,
through_seq: self.through_seq,
})
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredBindingEpoch {
pub(super) server_incarnation: u64,
pub(super) connection_ordinal: u64,
pub(super) capability_generation: u64,
}
impl From<BindingEpoch> for StoredBindingEpoch {
fn from(epoch: BindingEpoch) -> Self {
Self {
server_incarnation: epoch.connection_incarnation.server_incarnation,
connection_ordinal: epoch.connection_incarnation.connection_ordinal,
capability_generation: epoch.capability_generation.get(),
}
}
}
impl StoredBindingEpoch {
pub(super) fn to_epoch(self) -> Result<BindingEpoch, OperationLogError> {
Ok(BindingEpoch::new(
ConnectionIncarnation::new(self.server_incarnation, self.connection_ordinal),
stored_generation(self.capability_generation)?,
))
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredU128(pub(super) [u8; 16]);
impl StoredU128 {
pub(super) const fn get(self) -> u128 {
u128::from_be_bytes(self.0)
}
}
impl From<u128> for StoredU128 {
fn from(value: u128) -> Self {
Self(value.to_be_bytes())
}
}
fn stored_generation(value: u64) -> Result<Generation, OperationLogError> {
Generation::new(value).ok_or(OperationLogError::ZeroGeneration)
}