mod codec;
use std::sync::Arc;
use liminal::durability::{DurabilityError, DurableStore};
use liminal_protocol::wire::{
BindingEpoch, MarkerAck, ParticipantDelivery, ParticipantId, ParticipantRecord,
};
pub(super) use codec::{decode_row, encode_row};
pub(super) const OUTBOX_STREAM_PREFIX: &str = "liminal:participant-production-unit2:";
pub(super) const UNIT2_OUTBOX_RESTORE_BATCH_ROWS: usize = 64;
pub(super) const OUTBOX_SCHEMA_VERSION: u8 = 1;
#[derive(Debug, thiserror::Error)]
pub(super) enum OutboxLogError {
#[error(transparent)]
Durability(#[from] DurabilityError),
#[error("Unit 2 extension row is missing its schema version")]
MissingSchemaVersion,
#[error("unsupported Unit 2 extension schema version {0}")]
SchemaVersion(u8),
#[error("mixed Unit 2 extension schema versions: expected {expected}, found {actual}")]
MixedSchemaVersions {
expected: u8,
actual: u8,
},
#[error("Unit 2 extension row ended before {field}")]
UnexpectedEnd {
field: &'static str,
},
#[error("unknown Unit 2 extension {domain} tag {value}")]
UnknownTag {
domain: &'static str,
value: u8,
},
#[error("invalid Unit 2 extension {field} selector {value}")]
InvalidSelector {
field: &'static str,
value: u8,
},
#[error("Unit 2 extension row carries zero capability generation")]
ZeroGeneration,
#[error("Unit 2 extension {field} length {length} exceeds u32")]
LengthOverflow {
field: &'static str,
length: usize,
},
#[error("Unit 2 extension participant push is not canonically encodable: {0:?}")]
PushCodec(liminal_protocol::wire::CodecError),
#[error("Unit 2 extension row has {remaining} trailing bytes")]
TrailingBytes {
remaining: usize,
},
#[error("Unit 2 extension stream expected sequence {expected}, found {actual}")]
Sequence {
expected: u64,
actual: u64,
},
#[error("Unit 2 extension append expected {expected}, got {actual}")]
AssignedSequence {
expected: u64,
actual: u64,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ProducedSourceKind {
Enrolled,
Attached,
Detached,
MarkerDrained,
RecordAdmission,
Left,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ProjectedRecord {
pub(super) delivery_seq: u64,
pub(super) body: ParticipantRecord,
pub(super) recipients: Vec<ParticipantId>,
pub(super) sender: Option<ParticipantId>,
pub(super) encoded_push_bytes: u64,
}
impl ProjectedRecord {
pub(super) fn try_new(
conversation_id: u64,
delivery_seq: u64,
body: ParticipantRecord,
recipients: Vec<ParticipantId>,
sender: Option<ParticipantId>,
) -> Result<Self, OutboxLogError> {
let encoded_push_bytes = codec::canonical_push_bytes(conversation_id, delivery_seq, &body)?;
Ok(Self {
delivery_seq,
body,
recipients,
sender,
encoded_push_bytes,
})
}
pub(super) const fn delivery_seq(&self) -> u64 {
self.delivery_seq
}
pub(super) const fn body(&self) -> &ParticipantRecord {
&self.body
}
pub(super) fn recipients(&self) -> &[ParticipantId] {
&self.recipients
}
pub(super) const fn sender(&self) -> Option<ParticipantId> {
self.sender
}
pub(super) const fn encoded_push_bytes(&self) -> u64 {
self.encoded_push_bytes
}
pub(super) fn into_delivery(self, conversation_id: u64) -> ParticipantDelivery {
ParticipantDelivery {
conversation_id,
delivery_seq: self.delivery_seq,
record: self.body,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ProducedBatch {
pub(super) source_log_sequence: u64,
pub(super) source_kind: ProducedSourceKind,
pub(super) ordered_records: Vec<ProjectedRecord>,
}
impl ProducedBatch {
pub(super) const fn source_log_sequence(&self) -> u64 {
self.source_log_sequence
}
pub(super) const fn source_kind(&self) -> ProducedSourceKind {
self.source_kind
}
pub(super) fn ordered_records(&self) -> &[ProjectedRecord] {
&self.ordered_records
}
pub(super) const fn new(
source_log_sequence: u64,
source_kind: ProducedSourceKind,
ordered_records: Vec<ProjectedRecord>,
) -> Self {
Self {
source_log_sequence,
source_kind,
ordered_records,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct StoredMarkerAckCommitted {
pub(super) request: MarkerAck,
pub(super) receiving_binding_epoch: BindingEpoch,
pub(super) offered_marker_delivery_seq: u64,
pub(super) delivered_binding_epoch: BindingEpoch,
pub(super) from_cursor: u64,
pub(super) resulting_cursor: u64,
pub(super) base_log_head: u64,
pub(super) extension_sequence: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum OutboxRow {
Produced(ProducedBatch),
AckAdvanced {
source_log_sequence: u64,
participant_id: ParticipantId,
through_seq: u64,
},
MarkerAckCommitted(StoredMarkerAckCommitted),
}
impl OutboxRow {
pub(super) const fn base_log_head(&self) -> Option<u64> {
match self {
Self::Produced(batch) => batch.source_log_sequence.checked_add(1),
Self::AckAdvanced {
source_log_sequence,
..
} => source_log_sequence.checked_add(1),
Self::MarkerAckCommitted(row) => Some(row.base_log_head),
}
}
}
#[derive(Debug)]
pub(super) struct OutboxLog {
store: Arc<dyn DurableStore>,
stream_key: String,
}
impl OutboxLog {
pub(super) fn new(store: Arc<dyn DurableStore>, conversation_id: u64) -> Self {
Self {
store,
stream_key: format!("{OUTBOX_STREAM_PREFIX}{conversation_id}"),
}
}
pub(super) async fn append(
&self,
row: &OutboxRow,
expected_sequence: u64,
) -> Result<(), OutboxLogError> {
let payload = encode_row(row)?;
let assigned = self
.store
.append(&self.stream_key, payload, expected_sequence)
.await?;
if assigned != expected_sequence {
return Err(OutboxLogError::AssignedSequence {
expected: expected_sequence,
actual: assigned,
});
}
self.store.flush().await?;
Ok(())
}
pub(super) async fn read_all(&self) -> Result<Vec<(u64, OutboxRow)>, OutboxLogError> {
let mut rows = Vec::new();
let mut sequence = 0_u64;
let mut established_version = None;
loop {
let entries = self
.store
.read_from(&self.stream_key, sequence, UNIT2_OUTBOX_RESTORE_BATCH_ROWS)
.await?;
if entries.is_empty() {
break;
}
let page_len = entries.len();
for entry in entries {
if entry.sequence != sequence {
return Err(OutboxLogError::Sequence {
expected: sequence,
actual: entry.sequence,
});
}
let version = entry
.payload
.first()
.copied()
.ok_or(OutboxLogError::MissingSchemaVersion)?;
if let Some(expected) = established_version {
if version != expected {
return Err(OutboxLogError::MixedSchemaVersions {
expected,
actual: version,
});
}
} else {
established_version = Some(version);
}
if version != OUTBOX_SCHEMA_VERSION {
return Err(OutboxLogError::SchemaVersion(version));
}
rows.push((sequence, decode_row(&entry.payload)?));
sequence = sequence.checked_add(1).ok_or(OutboxLogError::Sequence {
expected: u64::MAX,
actual: entry.sequence,
})?;
}
if page_len < UNIT2_OUTBOX_RESTORE_BATCH_ROWS {
break;
}
}
Ok(rows)
}
}