use std::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;
pub(super) const STREAM_PREFIX: &str = "liminal:participant-production:";
pub(super) const READ_BATCH_SIZE: usize = 64;
pub(super) const SCHEMA_VERSION: u8 = 2;
#[derive(Debug, thiserror::Error)]
pub enum OperationLogError {
#[error(transparent)]
Durability(#[from] DurabilityError),
#[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 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,
},
}
#[derive(Debug)]
pub(super) struct OperationLog {
store: Arc<dyn DurableStore>,
stream_key: String,
}
impl OperationLog {
pub(super) fn new(store: Arc<dyn DurableStore>, conversation_id: u64) -> Self {
Self {
store,
stream_key: format!("{STREAM_PREFIX}{conversation_id}"),
}
}
pub(super) async fn read_page(
&self,
from_sequence: u64,
) -> Result<Vec<(u64, StoredOperation)>, OperationLogError> {
let entries = self
.store
.read_from(&self.stream_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 {
return Err(OperationLogError::SchemaVersion(version.schema_version));
}
let stored: StoredEntry = serde_json::from_slice(&entry.payload)?;
decoded.push((entry.sequence, stored.operation));
}
Ok(decoded)
}
pub(super) async fn append(
&self,
operation: &StoredOperation,
expected_sequence: u64,
) -> Result<(), OperationLogError> {
let payload = serde_json::to_vec(&StoredEntry {
schema_version: SCHEMA_VERSION,
operation: operation.clone(),
})?;
let assigned = self
.store
.append(&self.stream_key, payload, expected_sequence)
.await?;
if assigned != expected_sequence {
return Err(OperationLogError::AssignedSequence {
expected: expected_sequence,
actual: assigned,
});
}
self.store.flush().await?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
struct StoredEntryVersion {
schema_version: u8,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct StoredEntry {
schema_version: u8,
operation: StoredOperation,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case", tag = "operation")]
pub(super) enum StoredOperation {
Genesis {
event: Vec<u8>,
},
Enrolled {
request: StoredEnrollmentRequest,
allocation: StoredEnrollmentAllocation,
event: Vec<u8>,
},
Attached {
request: StoredAttachRequest,
secret_verified: bool,
allocation: StoredAttachAllocation,
event: Vec<u8>,
},
Detached {
request: StoredDetachRequest,
verifier: Digest,
receiving_epoch: StoredBindingEpoch,
terminal_order: TransactionOrder,
terminal_seq: DeliverySeq,
event: Vec<u8>,
},
ZeroDebtAck {
request: StoredAck,
receiving_epoch: StoredBindingEpoch,
contiguously_available_through: DeliverySeq,
},
MarkerDrained {
row: StoredMarkerDrain,
},
RecordAdmission {
row: StoredRecordAdmission,
},
Left {
row: StoredLeave,
},
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub(super) struct StoredResourceVector {
pub(super) entries: u64,
pub(super) bytes: u64,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, 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 StoredMarkerDrain {
pub(super) marker: Vec<u8>,
pub(super) retained_charge: StoredRetainedCharge,
pub(super) resulting_retained_charges: Vec<StoredRetainedCharge>,
pub(super) successor: Vec<u8>,
}
#[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,
pub(super) resulting_retained_charges: Vec<StoredRetainedCharge>,
pub(super) resulting_closure_accounting: Vec<u8>,
}
#[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, 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, 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, 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, Serialize)]
pub(super) struct StoredAttachAllocation {
pub(super) binding_epoch: StoredBindingEpoch,
pub(super) attach_secret: [u8; 32],
pub(super) attached_order: TransactionOrder,
pub(super) attached_seq: DeliverySeq,
pub(super) receipt_expires_at: StoredU128,
pub(super) provenance_expires_at: StoredU128,
pub(super) admitted_now_ms: u64,
pub(super) superseded_terminal_seq: Option<DeliverySeq>,
}
#[derive(Clone, Copy, Debug, Deserialize, 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, 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, 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)
}