use crate::server::signals::ServerSignals;
use std::sync::OnceLock;
use std::cell::{Cell, RefCell};
use std::collections::{BTreeMap, HashMap};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Instant;
use liminal::durability::bridge::block_on;
use liminal::durability::{DurabilityError, DurableStore};
use liminal_protocol::lifecycle::{CapacityCounter, ObserverRecoveryAggregate};
use liminal_protocol::wire::ConversationId;
use crate::config::types::ParticipantConfig;
use crate::health::reissue::{
OperatorCredentialReissueError, OperatorCredentialReissueOutcome,
OperatorCredentialReissueRequest, OperatorCredentialReissuer,
};
use crate::health::unloadable::{UnloadableConversation, UnloadableConversationRecord};
use crate::server::participant::{
ObserverPublicationTarget, ParticipantConnectionContext, ParticipantConnectionConversations,
ParticipantSemanticError, ParticipantServiceFatal, dispatch_impact::DispatchImpactAccumulator,
};
use super::barrier::{OperationFacts, ReceiptCapacityLimits};
use super::capacity::ServerCapacity;
#[cfg(test)]
use super::dispatch_work::{ObligationDispatchWorkCounters, ObligationDispatchWorkSnapshot};
use super::facts;
use super::log::{OperationLog, OperationLogError, StoredOperation};
use super::operation_batch::OperationBatch;
use super::outbox::{ConversationOutboxError, ConversationOutboxLimits};
use super::outbox_log::{OutboxLog, OutboxLogError};
use super::outbox_projection::owes_extension_row;
use super::registry::ConversationRegistry;
use super::restore_checkpoint_locator::checkpoint_conversation_route;
use super::routed_stream::RoutedStreamRefusal;
use super::state::{ConversationAuthority, DurableAppend, LifecycleReceipt, StateError};
use super::write_issuance::WriteIssuance;
#[derive(Debug)]
pub(super) struct ObserverArmTarget {
pub(super) refused_epoch: u64,
pub(super) connection_incarnation: liminal_protocol::wire::ConnectionIncarnation,
pub(super) target: ObserverPublicationTarget,
}
#[derive(Debug)]
pub(super) struct ObserverOwner {
pub(super) aggregate: ObserverRecoveryAggregate,
pub(super) head: u64,
pub(super) arm_targets: BTreeMap<ConversationId, ObserverArmTarget>,
}
#[derive(Debug)]
struct OwedReceipts {
starting_log_sequence: u64,
receipts: Vec<LifecycleReceipt>,
}
#[derive(Debug)]
pub struct ProductionParticipantHandler {
signals: OnceLock<ServerSignals>,
pub(super) store: Arc<dyn DurableStore>,
pub(super) config: ParticipantConfig,
outbox_limits: ConversationOutboxLimits,
conversations: Mutex<HashMap<ConversationId, Arc<Mutex<Option<ConversationAuthority>>>>>,
service_fatal: Mutex<Option<ParticipantServiceFatal>>,
pub(super) observer: Mutex<Option<ObserverOwner>>,
pub(super) observer_head_uncertain: AtomicBool,
pub(super) capacity: ServerCapacity,
pub(super) registry: ConversationRegistry,
unloadable: UnloadableConversationRecord,
owed_receipts: Mutex<HashMap<ConversationId, OwedReceipts>>,
#[cfg(test)]
pub(super) obligation_dispatch_work: ObligationDispatchWorkCounters,
now_override: AtomicU64,
}
impl ProductionParticipantHandler {
pub(in crate::server) fn install_signals(
&self,
signals: ServerSignals,
) -> Result<(), ParticipantSemanticError> {
self.signals
.set(signals)
.map_err(|_| ParticipantSemanticError::Internal {
message: "participant lifecycle signals were already installed".to_owned(),
})
}
pub fn new(
store: Arc<dyn DurableStore>,
config: ParticipantConfig,
) -> Result<Self, ParticipantSemanticError> {
let outbox_limits = ConversationOutboxLimits::try_new(
config.max_retained_record_rows,
config.identity_slots,
)
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant outbox limit configuration failed: {error}"),
})?;
let registry = ConversationRegistry::new(Arc::clone(&store));
let handler = Self {
signals: OnceLock::new(),
store,
config,
outbox_limits,
conversations: Mutex::new(HashMap::new()),
service_fatal: Mutex::new(None),
observer: Mutex::new(None),
observer_head_uncertain: AtomicBool::new(false),
capacity: ServerCapacity::default(),
registry,
unloadable: UnloadableConversationRecord::default(),
owed_receipts: Mutex::new(HashMap::new()),
#[cfg(test)]
obligation_dispatch_work: ObligationDispatchWorkCounters::default(),
now_override: AtomicU64::new(0),
};
if handler.config.pruning.is_none() {
tracing::info!(
"pruning is off: [participant.pruning] keep_acknowledged_behind_checkpoint is not configured, so no conversation row is ever cut"
);
}
handler.restore_all_conversations()?;
Ok(handler)
}
fn now_ms(&self) -> Result<u64, facts::FactsError> {
match self.now_override.load(Ordering::SeqCst) {
0 => facts::now_unix_millis(),
fixed => Ok(fixed),
}
}
#[cfg(test)]
pub(super) fn pin_clock_ms(&self, now_ms: u64) {
self.now_override.store(now_ms, Ordering::SeqCst);
}
#[cfg(test)]
pub(crate) fn obligation_dispatch_work_snapshot(&self) -> ObligationDispatchWorkSnapshot {
self.obligation_dispatch_work.snapshot()
}
pub(super) fn current_service_fatal(
&self,
) -> Result<Option<ParticipantServiceFatal>, ParticipantSemanticError> {
let fatal = self
.service_fatal
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "participant service fatal latch is poisoned".to_owned(),
})?
.clone();
Ok(fatal)
}
pub(super) fn ensure_service_live(&self) -> Result<(), ParticipantSemanticError> {
if let Some(fatal) = self.current_service_fatal()? {
return Err(ParticipantSemanticError::ServiceFatal(fatal));
}
Ok(())
}
pub(super) fn latch_connection_fate_fatal(
&self,
open_sequence: u64,
conversation_id: ConversationId,
) -> Result<ParticipantServiceFatal, ParticipantSemanticError> {
let mut fatal =
self.service_fatal
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "participant service fatal latch is poisoned".to_owned(),
})?;
let selected = fatal
.get_or_insert_with(|| ParticipantServiceFatal::ConnectionFateIntentIncomplete {
open_sequence,
conversation_id,
})
.clone();
drop(fatal);
Ok(selected)
}
fn restore_all_conversations(&self) -> Result<(), ParticipantSemanticError> {
let started = Instant::now();
#[cfg(test)]
let conversation_ids = crate::boot_history_harness::during_stage("registry", || {
self.registry.restore().map_err(|error| log_error(&error))
})?;
#[cfg(not(test))]
let conversation_ids = self.registry.restore().map_err(|error| log_error(&error))?;
let total = conversation_ids.len();
tracing::info!(
step = "participant restore",
conversations = total,
"participant restore started"
);
let mut unloadable = 0_usize;
for (index, conversation_id) in conversation_ids.into_iter().enumerate() {
let conversation_started = Instant::now();
tracing::info!(
step = "participant restore",
conversation_id,
done = index,
total,
elapsed_ms = elapsed_ms(started),
"participant conversation restore starting"
);
#[cfg(test)]
let outcome = crate::boot_history_harness::during_stage("authority", || {
self.restore_one_conversation(conversation_id)
});
#[cfg(not(test))]
let outcome = self.restore_one_conversation(conversation_id);
let done = index.saturating_add(1);
match outcome {
Ok(rows) => tracing::info!(
step = "participant restore",
conversation_id,
done,
total,
log_rows = rows,
conversation_elapsed_ms = elapsed_ms(conversation_started),
elapsed_ms = elapsed_ms(started),
"participant conversation restored"
),
Err(error) => {
unloadable = unloadable.saturating_add(1);
let refusal = self.record_unloadable(conversation_id, &error);
tracing::warn!(
step = "participant restore",
conversation_id,
done,
total,
elapsed_ms = elapsed_ms(started),
refusal = %refusal,
"participant conversation refused at restore"
);
}
}
}
tracing::info!(
step = "participant restore",
conversations = total,
unloadable,
elapsed_ms = elapsed_ms(started),
"participant restore finished"
);
Ok(())
}
fn restore_one_conversation(
&self,
conversation_id: ConversationId,
) -> Result<u64, ParticipantSemanticError> {
let mut rows = 0_u64;
{
let cell = self.cell(conversation_id)?;
let mut owner = cell
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: format!(
"participant conversation {conversation_id} owner lock is poisoned"
),
})?;
if owner.is_none() {
let log = OperationLog::new(Arc::clone(&self.store), conversation_id);
let outbox_log = OutboxLog::new(Arc::clone(&self.store), conversation_id);
let mut replayed = block_on(super::restore_checkpoint_rebuild::restore_at_boot(
self.store.as_ref(),
&log,
&outbox_log,
&self.config,
self.outbox_limits,
))
.map_err(|error| bridge_error(&error))?
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant checkpoint restore failed: {error}"),
})?;
self.repair_loaded_fates(conversation_id, &mut replayed, &log, &outbox_log)?;
let witnesses = replayed.observer_progress_witnesses();
if !replayed.tokens.is_empty() || replayed.is_closed() {
self.reconcile_observer_progress(
conversation_id,
witnesses,
replayed.observer_progress_pruned_through(),
replayed.observer_progress,
)?;
replayed
.prune_durable_observer_witnesses()
.map_err(|error| state_error(&error))?;
} else if !witnesses.is_empty() {
return Err(ParticipantSemanticError::Internal {
message: format!(
"unenrolled conversation {conversation_id} projected observer progress"
),
});
}
let now = self
.now_ms()
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant clock read failed: {error}"),
})?;
replayed.prune_expired_provenance(u128::from(now));
let contribution = replayed
.capacity_contribution(u128::from(now))
.map_err(|error| state_error(&error))?;
self.capacity
.fold_conversation(conversation_id, contribution)
.map_err(|error| state_error(&error))?;
let durably_empty = replayed.next_log_sequence == 0;
if durably_empty {
drop(owner);
self.evict_uncommitted(conversation_id, &cell)?;
return Ok(0);
}
let before_drain = replayed.next_log_sequence;
let verdict =
self.drain_restored_candidate_lane(conversation_id, &mut replayed, &log);
let drained = verdict.drained_any();
verdict.observe()?;
if drained {
self.commit_boot_drain(conversation_id, &mut replayed, &log, before_drain)?;
}
rows = replayed.next_log_sequence;
*owner = Some(replayed);
}
drop(owner);
}
Ok(rows)
}
fn commit_boot_drain(
&self,
conversation_id: ConversationId,
replayed: &mut ConversationAuthority,
log: &OperationLog,
before_drain: u64,
) -> Result<(), ParticipantSemanticError> {
let outbox_log = OutboxLog::new(Arc::clone(&self.store), conversation_id);
let batch = RefCell::new(Some(OperationBatch::new(&checkpoint_conversation_route(
conversation_id,
))));
#[cfg(test)]
let flat_legacy = super::log::legacy_rows::armed();
#[cfg(not(test))]
let flat_legacy = false;
let appender = LogAppender::new(
log,
&self.registry,
&outbox_log,
self.config.pruning,
(!flat_legacy).then_some(&batch),
0,
);
self.complete_live_commit(conversation_id, replayed, &appender)?;
let due = OperationBatch::checkpoint_due(
before_drain,
replayed.next_log_sequence,
replayed.is_closed(),
)
.map_err(|error| log_error(&OperationLogError::from(error)))?;
if due {
appender
.commit_with_checkpoint(replayed, before_drain, &self.config)
.map_err(|error| state_error(&error))?;
} else if appender.has_staged().map_err(|error| log_error(&error))? {
appender
.commit_staged()
.map_err(|error| log_error(&error))?;
}
Ok(())
}
fn record_unloadable(
&self,
conversation_id: ConversationId,
error: &ParticipantSemanticError,
) -> ParticipantSemanticError {
let reason = error.to_string();
let class = error.class();
tracing::error!(
conversation_id,
class,
reason = %reason,
"CONTAINMENT: participant conversation is unloadable and is refused on its own; every \
other conversation keeps being served"
);
if !self.unloadable.record(UnloadableConversation {
conversation_id,
class,
reason: reason.clone(),
}) {
tracing::error!(
conversation_id,
"the unloadable-conversation record is poisoned; this refusal is reported but not \
retained"
);
}
ParticipantSemanticError::ConversationUnloadable {
conversation_id,
reason,
}
}
fn clear_unloadable(&self, conversation_id: ConversationId) {
if !self.unloadable.retire(conversation_id) {
tracing::error!(
conversation_id,
"the unloadable-conversation record is poisoned; a conversation that recovered may \
still be reported as unloadable"
);
}
}
pub(crate) fn unloadable_record(&self) -> UnloadableConversationRecord {
self.unloadable.clone()
}
#[cfg(test)]
pub(super) fn unloadable_conversations(&self) -> BTreeMap<ConversationId, String> {
self.unloadable
.snapshot()
.into_iter()
.map(|entry| (entry.conversation_id, entry.reason))
.collect()
}
pub(super) fn registered_conversation_ids(
&self,
) -> Result<Vec<ConversationId>, ParticipantSemanticError> {
self.registry.restore().map_err(|error| log_error(&error))
}
pub(super) fn existing_cell(
&self,
conversation_id: ConversationId,
) -> Result<Option<Arc<Mutex<Option<ConversationAuthority>>>>, ParticipantSemanticError> {
let conversations =
self.conversations
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "participant conversation registry lock is poisoned".to_owned(),
})?;
Ok(conversations.get(&conversation_id).map(Arc::clone))
}
pub(super) fn cell(
&self,
conversation_id: ConversationId,
) -> Result<Arc<Mutex<Option<ConversationAuthority>>>, ParticipantSemanticError> {
let mut conversations =
self.conversations
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "participant conversation registry lock is poisoned".to_owned(),
})?;
let cell = Arc::clone(
conversations
.entry(conversation_id)
.or_insert_with(|| Arc::new(Mutex::new(None))),
);
drop(conversations);
Ok(cell)
}
pub(super) fn with_conversation_impact<T>(
&self,
conversation_id: ConversationId,
impact: &mut DispatchImpactAccumulator,
operation: impl FnOnce(
&mut ConversationAuthority,
&dyn DurableAppend,
&mut DispatchImpactAccumulator,
) -> Result<T, StateError>,
) -> Result<T, ParticipantSemanticError> {
self.with_conversation_reconciliation(
conversation_id,
true,
Some(impact),
|authority, appender, impact| {
impact.map_or_else(
|| Err(StateError::invariant("impact owner is unavailable")),
|impact| operation(authority, appender, impact),
)
},
)
}
pub(super) fn with_conversation_fate_source<T>(
&self,
conversation_id: ConversationId,
impact: Option<&mut DispatchImpactAccumulator>,
operation: impl FnOnce(
&mut ConversationAuthority,
&dyn DurableAppend,
Option<&mut DispatchImpactAccumulator>,
) -> Result<T, StateError>,
) -> Result<T, ParticipantSemanticError> {
self.with_conversation_reconciliation(conversation_id, true, impact, operation)
}
fn operation_timestamp(&self) -> Result<u64, ParticipantSemanticError> {
self.now_ms()
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant clock read failed: {error}"),
})?
.checked_mul(1_000_000)
.ok_or_else(|| ParticipantSemanticError::Internal {
message: "participant timestamp exceeds nanoseconds".to_owned(),
})
}
fn with_conversation_reconciliation<T>(
&self,
conversation_id: ConversationId,
reconcile_appended_source: bool,
mut impact: Option<&mut DispatchImpactAccumulator>,
operation: impl FnOnce(
&mut ConversationAuthority,
&dyn DurableAppend,
Option<&mut DispatchImpactAccumulator>,
) -> Result<T, StateError>,
) -> Result<T, ParticipantSemanticError> {
let cell = self.cell(conversation_id)?;
let mut owner = lock_owner(&cell, conversation_id)?;
let log = OperationLog::new(Arc::clone(&self.store), conversation_id);
let outbox_log = OutboxLog::new(Arc::clone(&self.store), conversation_id);
let mut owed = Vec::new();
if owner.is_none() {
let replayed = self
.replay_and_repair(conversation_id, &log)
.map_err(|error| self.record_unloadable(conversation_id, &error))?;
self.clear_unloadable(conversation_id);
owed.extend(self.settle_owed_receipts(conversation_id, replayed.next_log_sequence)?);
*owner = Some(replayed);
}
let Some(authority) = owner.as_mut() else {
return Err(ParticipantSemanticError::Internal {
message: format!("participant conversation {conversation_id} owner is absent"),
});
};
let timestamp = self.operation_timestamp()?;
let batch = RefCell::new(Some(OperationBatch::new(&checkpoint_conversation_route(
conversation_id,
))));
#[cfg(test)]
let flat_legacy = super::log::legacy_rows::armed();
#[cfg(not(test))]
let flat_legacy = false;
let appender = LogAppender::new(
&log,
&self.registry,
&outbox_log,
self.config.pruning,
(!flat_legacy).then_some(&batch),
timestamp,
);
let starting_log_sequence = authority.next_log_sequence;
let operation_result =
operation(authority, &appender, impact.as_deref_mut()).and_then(|value| {
appender.finish_operation(
authority,
starting_log_sequence,
reconcile_appended_source,
&self.config,
)?;
Ok(value)
});
let mut receipts = owed;
let (result, durably_empty) = match operation_result {
Ok(value)
if reconcile_appended_source
&& authority.next_log_sequence > starting_log_sequence =>
{
match self.complete_appended_source(conversation_id, authority, &appender) {
Ok(()) => {
install_staged(impact.as_deref_mut());
let durably_empty = authority.next_log_sequence == 0;
(Ok(value), durably_empty)
}
Err(error) => {
install_staged(impact.as_deref_mut());
receipts.extend(std::mem::take(&mut authority.lifecycle_receipts));
*owner = None;
(
Err(ParticipantSemanticError::WriteOutcomeUnknown {
message: format!("committed participant follow-up failed: {error}"),
}),
false,
)
}
}
}
Ok(value) => {
install_staged(impact.as_deref_mut());
let durably_empty = authority.next_log_sequence == 0;
(Ok(value), durably_empty)
}
Err(error) => {
if let Some(impact) = impact {
impact.discard_staged();
}
let durably_empty =
self.fail_operation(conversation_id, starting_log_sequence, authority, &log)?;
*owner = None;
(
Err(state_error_with_issuance(&error, log.issuance())),
durably_empty,
)
}
};
if let Some(authority) = owner.as_mut() {
receipts.extend(std::mem::take(&mut authority.lifecycle_receipts));
}
self.emit_lifecycle_receipts(conversation_id, receipts, result.as_ref().err())?;
drop(owner);
if durably_empty {
self.evict_uncommitted(conversation_id, &cell)?;
}
result
}
fn fail_operation(
&self,
conversation_id: ConversationId,
starting_log_sequence: u64,
authority: &mut ConversationAuthority,
log: &OperationLog,
) -> Result<bool, ParticipantSemanticError> {
if !log.issuance().was_submitted() {
return Ok(authority.next_log_sequence == 0);
}
self.hold_owed_receipts(
conversation_id,
starting_log_sequence,
std::mem::take(&mut authority.lifecycle_receipts),
)?;
Ok(false)
}
fn hold_owed_receipts(
&self,
conversation_id: ConversationId,
starting_log_sequence: u64,
receipts: Vec<LifecycleReceipt>,
) -> Result<(), ParticipantSemanticError> {
if receipts.is_empty() {
return Ok(());
}
let mut owed = self.lock_owed_receipts()?;
owed.insert(
conversation_id,
OwedReceipts {
starting_log_sequence,
receipts,
},
);
drop(owed);
Ok(())
}
fn settle_owed_receipts(
&self,
conversation_id: ConversationId,
loaded_log_head: u64,
) -> Result<Vec<LifecycleReceipt>, ParticipantSemanticError> {
let mut owed = self.lock_owed_receipts()?;
let held = owed.remove(&conversation_id);
drop(owed);
Ok(held
.filter(|held| loaded_log_head > held.starting_log_sequence)
.map(|held| held.receipts)
.unwrap_or_default())
}
pub(super) fn emit_owed_receipts(
&self,
conversation_id: ConversationId,
loaded_log_head: u64,
) -> Result<(), ParticipantSemanticError> {
let owed = self.settle_owed_receipts(conversation_id, loaded_log_head)?;
if owed.is_empty() {
return Ok(());
}
self.emit_lifecycle_receipts(conversation_id, owed, None)
}
fn lock_owed_receipts(
&self,
) -> Result<MutexGuard<'_, HashMap<ConversationId, OwedReceipts>>, ParticipantSemanticError>
{
self.owed_receipts
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "owed lifecycle receipt lock poisoned".to_owned(),
})
}
fn emit_lifecycle_receipts(
&self,
conversation_id: ConversationId,
receipts: Vec<LifecycleReceipt>,
operation_error: Option<&ParticipantSemanticError>,
) -> Result<(), ParticipantSemanticError> {
if let Some(signals) = self.signals.get() {
if let Err(error) = signals.lifecycle_batch(
conversation_id,
receipts
.into_iter()
.map(|receipt| (receipt.generation, receipt.transition)),
) {
if let Some(operation_error) = operation_error {
tracing::error!(%operation_error, "operation and lifecycle batch both failed");
} else {
tracing::error!(conversation_id, %error, "committed lifecycle batch failed");
return Ok(());
}
return Err(ParticipantSemanticError::WriteOutcomeUnknown {
message: format!("committed lifecycle batch failed: {error}"),
});
}
}
Ok(())
}
fn complete_appended_source(
&self,
conversation_id: ConversationId,
authority: &mut ConversationAuthority,
appender: &LogAppender<'_>,
) -> Result<(), ParticipantSemanticError> {
let owed = appender.outstanding_extension_rows();
if owed != 0 {
return Err(state_error(&StateError::ExtensionRowsOwed {
conversation_id,
owed,
}));
}
self.complete_live_commit(conversation_id, authority, appender)
}
fn complete_live_commit(
&self,
conversation_id: ConversationId,
authority: &mut ConversationAuthority,
appender: &LogAppender<'_>,
) -> Result<(), ParticipantSemanticError> {
authority
.repair_pending_specific_fates(appender)
.map_err(|error| state_error(&error))?;
self.reconcile_live_observer(conversation_id, authority)?;
let now = self
.now_ms()
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant clock read failed: {error}"),
})?;
let now = u128::from(now);
authority.prune_expired_provenance(now);
let contribution = authority
.capacity_contribution(now)
.map_err(|error| state_error(&error))?;
self.capacity
.fold_conversation(conversation_id, contribution)
.map_err(|error| state_error(&error))?;
Ok(())
}
pub(super) fn reconcile_live_observer(
&self,
conversation_id: ConversationId,
authority: &mut ConversationAuthority,
) -> Result<(), ParticipantSemanticError> {
let observer_progress = authority.observer_progress;
let pruned_through = authority.observer_progress_pruned_through();
let witnesses = authority.observer_progress_witnesses();
if !authority.tokens.is_empty() || authority.is_closed() {
self.reconcile_observer_progress(
conversation_id,
witnesses,
pruned_through,
observer_progress,
)?;
authority
.prune_durable_observer_witnesses()
.map_err(|error| state_error(&error))?;
} else if !witnesses.is_empty() {
return Err(ParticipantSemanticError::Internal {
message: format!(
"unenrolled conversation {conversation_id} projected observer progress"
),
});
}
Ok(())
}
fn repair_loaded_fates(
&self,
conversation_id: ConversationId,
replayed: &mut ConversationAuthority,
log: &OperationLog,
outbox_log: &OutboxLog,
) -> Result<(), ParticipantSemanticError> {
let timestamp = self.operation_timestamp()?;
let batch = RefCell::new(Some(OperationBatch::new(&checkpoint_conversation_route(
conversation_id,
))));
#[cfg(test)]
let flat_legacy = super::log::legacy_rows::armed();
#[cfg(not(test))]
let flat_legacy = false;
let appender = LogAppender::new(
log,
&self.registry,
outbox_log,
self.config.pruning,
(!flat_legacy).then_some(&batch),
timestamp,
);
replayed
.repair_pending_specific_fates(&appender)
.map_err(|error| state_error(&error))?;
if appender.has_staged().map_err(|error| log_error(&error))? {
appender
.commit_staged()
.map_err(|error| log_error(&error))?;
}
Ok(())
}
pub(super) fn replay_and_repair(
&self,
conversation_id: ConversationId,
log: &OperationLog,
) -> Result<ConversationAuthority, ParticipantSemanticError> {
let outbox_log = OutboxLog::new(Arc::clone(&self.store), conversation_id);
let mut replayed = block_on(super::restore_checkpoint_rebuild::restore_at_boot(
self.store.as_ref(),
log,
&outbox_log,
&self.config,
self.outbox_limits,
))
.map_err(|error| bridge_error(&error))?
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant checkpoint restore failed: {error}"),
})?;
self.repair_loaded_fates(conversation_id, &mut replayed, log, &outbox_log)?;
let pruned_through = replayed.observer_progress_pruned_through();
let observer_witnesses = replayed.observer_progress_witnesses();
if !replayed.tokens.is_empty() || replayed.is_closed() {
self.reconcile_observer_progress(
conversation_id,
observer_witnesses,
pruned_through,
replayed.observer_progress,
)?;
replayed
.prune_durable_observer_witnesses()
.map_err(|error| state_error(&error))?;
} else if !observer_witnesses.is_empty() {
return Err(ParticipantSemanticError::Internal {
message: format!(
"unenrolled conversation {conversation_id} projected observer progress"
),
});
}
let now = self
.now_ms()
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant clock read failed: {error}"),
})?;
let now = u128::from(now);
replayed.prune_expired_provenance(now);
let contribution = replayed
.capacity_contribution(now)
.map_err(|error| state_error(&error))?;
self.capacity
.fold_conversation(conversation_id, contribution)
.map_err(|error| state_error(&error))?;
Ok(replayed)
}
#[cfg(test)]
pub(super) fn replay_aggregate_reference(
&self,
conversation_id: ConversationId,
log: &OperationLog,
) -> Result<ConversationAuthority, ParticipantSemanticError> {
let outbox_log = OutboxLog::new(Arc::clone(&self.store), conversation_id);
let extension_rows = block_on(outbox_log.read_all())
.map_err(|error| bridge_error(&error))?
.map_err(|error| outbox_log_error(&error))?;
block_on(ConversationAuthority::replay_aggregate_reference(
conversation_id,
log,
&outbox_log,
extension_rows,
&self.config,
self.outbox_limits,
))
.map_err(|error| bridge_error(&error))?
.map_err(|error| state_error(&error))
}
pub(super) fn evict_uncommitted(
&self,
conversation_id: ConversationId,
cell: &Arc<Mutex<Option<ConversationAuthority>>>,
) -> Result<(), ParticipantSemanticError> {
let mut conversations =
self.conversations
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "participant conversation registry lock is poisoned".to_owned(),
})?;
if let Some(existing) = conversations.get(&conversation_id) {
if Arc::ptr_eq(existing, cell) {
conversations.remove(&conversation_id);
}
}
drop(conversations);
Ok(())
}
#[cfg(test)]
pub(super) fn registry_len(&self) -> usize {
self.conversations
.lock()
.map_or(usize::MAX, |conversations| conversations.len())
}
#[cfg(test)]
pub(super) fn discard_owners_for_test(&self) -> Result<(), ParticipantSemanticError> {
self.conversations
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "participant conversation registry lock is poisoned".to_owned(),
})?
.clear();
self.observer
.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: "observer recovery aggregate lock is poisoned".to_owned(),
})?
.take();
Ok(())
}
pub(crate) fn operator_credential_reissue(
&self,
request: OperatorCredentialReissueRequest,
) -> Result<OperatorCredentialReissueOutcome, ParticipantSemanticError> {
self.ensure_service_live()?;
let now_ms = self
.now_ms()
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant clock read failed: {error}"),
})?;
self.with_conversation_fate_source(
request.conversation_id,
None,
|authority, appender, _| {
authority.apply_operator_credential_reissue(request, now_ms, appender)
},
)
}
pub(super) fn operation_facts(
&self,
context: ParticipantConnectionContext,
conversation_id: ConversationId,
conversations: &ParticipantConnectionConversations,
) -> Result<OperationFacts, ParticipantSemanticError> {
let now_ms = self
.now_ms()
.map_err(|error| ParticipantSemanticError::Internal {
message: format!("participant clock read failed: {error}"),
})?;
let connection_capacity = CapacityCounter::try_new(
self.config.max_semantic_conversations_per_connection,
conversations.occupied(),
)
.map_err(|error| ParticipantSemanticError::Internal {
message: format!(
"connection-conversation occupancy disagrees with its signed limit: {error:?}"
),
})?;
Ok(OperationFacts {
receiving_incarnation: context.connection_incarnation(),
now_ms,
identity_slots: self.config.identity_slots,
attach_receipt_ttl_ms: self.config.attach_receipt_ttl_ms,
receipt_provenance_ttl_ms: self.config.receipt_provenance_ttl_ms,
receipt_limits: ReceiptCapacityLimits::from_config(&self.config),
connection_tracking: conversations.tracking(conversation_id),
connection_capacity,
})
}
}
impl OperatorCredentialReissuer for ProductionParticipantHandler {
fn reissue(
&self,
request: OperatorCredentialReissueRequest,
) -> Result<OperatorCredentialReissueOutcome, OperatorCredentialReissueError> {
self.operator_credential_reissue(request)
.map_err(|error| OperatorCredentialReissueError {
message: error.to_string(),
})
}
}
pub(super) struct LogAppender<'a> {
pub(super) log: &'a OperationLog,
pub(super) registry: &'a ConversationRegistry,
pub(super) conversation_id: ConversationId,
pub(super) outbox_log: &'a OutboxLog,
pub(super) pruning: Option<crate::config::types::PruningConfig>,
pub(super) outstanding_extension_rows: Cell<u64>,
pub(super) batch: Option<&'a RefCell<Option<OperationBatch>>>,
pub(super) timestamp: u64,
}
fn lock_owner(
cell: &Mutex<Option<ConversationAuthority>>,
conversation_id: ConversationId,
) -> Result<MutexGuard<'_, Option<ConversationAuthority>>, ParticipantSemanticError> {
cell.lock().map_err(|_| ParticipantSemanticError::Internal {
message: format!("participant conversation {conversation_id} owner lock is poisoned"),
})
}
fn install_staged(impact: Option<&mut DispatchImpactAccumulator>) {
if let Some(impact) = impact {
impact.install_staged();
}
}
fn checkpoint_state_error(error: super::restore_checkpoint::CheckpointError) -> StateError {
OperationLogError::RoutedStream(RoutedStreamRefusal::Checkpoint(Box::new(error))).into()
}
impl<'a> LogAppender<'a> {
pub(super) const fn new(
log: &'a OperationLog,
registry: &'a ConversationRegistry,
outbox_log: &'a OutboxLog,
pruning: Option<crate::config::types::PruningConfig>,
batch: Option<&'a RefCell<Option<OperationBatch>>>,
timestamp: u64,
) -> Self {
Self {
log,
registry,
conversation_id: log.conversation_id(),
outbox_log,
pruning,
outstanding_extension_rows: Cell::new(0),
batch,
timestamp,
}
}
fn finish_operation(
&self,
authority: &mut ConversationAuthority,
starting_log_sequence: u64,
reconcile_appended_source: bool,
config: &ParticipantConfig,
) -> Result<(), StateError> {
if reconcile_appended_source && authority.next_log_sequence > starting_log_sequence {
authority.repair_pending_specific_fates(self)?;
let owed = self.outstanding_extension_rows();
if owed != 0 {
return Err(StateError::ExtensionRowsOwed {
conversation_id: self.conversation_id,
owed,
});
}
}
if self.has_staged()? {
self.commit_with_checkpoint(authority, starting_log_sequence, config)?;
}
Ok(())
}
fn commit_with_checkpoint(
&self,
authority: &mut ConversationAuthority,
starting_log_sequence: u64,
config: &ParticipantConfig,
) -> Result<(), StateError> {
let (metadata, audit) =
self.staged_checkpoint_metadata(authority, starting_log_sequence, config)?;
let metadata =
self.prepare_crossing_checkpoint(authority, starting_log_sequence, metadata, &audit)?;
self.commit_staged()?;
authority
.install_checkpoint_metadata(metadata)
.map_err(checkpoint_state_error)
}
fn prepare_crossing_checkpoint(
&self,
authority: &mut ConversationAuthority,
before: u64,
mut metadata: super::restore_from_checkpoint::CheckpointMetadata,
audit: &super::fenced_attach_terminal::ComposedTerminalValidation,
) -> Result<super::restore_from_checkpoint::CheckpointMetadata, StateError> {
use super::restore_checkpoint_history::CheckpointHistoryRoot;
let mut root =
CheckpointHistoryRoot::decode_canonical(metadata.history_root(), self.conversation_id)
.map_err(|source| {
checkpoint_state_error(super::restore_checkpoint::CheckpointError::LiveState {
source: Box::new(source),
})
})?;
let due =
OperationBatch::checkpoint_due(before, authority.next_log_sequence, authority.closed)
.map_err(OperationLogError::from)?;
if due {
let store = self.log.store();
let history = block_on(
super::restore_checkpoint_history_capture::capture_identity_history(
store.as_ref(),
authority,
root,
),
)?
.map_err(|error| checkpoint_state_error(error.checkpoint()))?;
root = history.root;
metadata = metadata
.with_history_root(root.encode_canonical())
.map_err(checkpoint_state_error)?;
let pending = self.batch.ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
let mut held = pending
.try_borrow_mut()
.map_err(|_| OperationLogError::RoutedStream(RoutedStreamRefusal::BatchRefused))?;
let pending = held.as_mut().ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
history.stage(pending).map_err(OperationLogError::from)?;
}
authority
.retain_checkpoint_metadata(root, audit)
.map_err(checkpoint_state_error)?;
if due {
let extension = authority
.outbox
.as_ref()
.ok_or_else(|| StateError::invariant("checkpoint outbox is absent"))?
.next_extension_sequence();
let positions = authority
.checkpoint_positions(metadata.schema(), authority.next_log_sequence, extension)
.map_err(checkpoint_state_error)?;
self.stage_borrowed_checkpoint(
positions,
metadata.history_root(),
authority,
metadata.prefix_digest(),
)?;
self.stage_prune(authority, extension)?;
}
Ok(metadata)
}
fn stage_prune(
&self,
authority: &ConversationAuthority,
next_extension_sequence: u64,
) -> Result<(), StateError> {
let keep = self
.pruning
.map(|pruning| pruning.keep_acknowledged_behind_checkpoint);
let Some(outbox) = authority.outbox.as_ref() else {
return Ok(());
};
let acknowledged_log_through = outbox.acknowledged_frontier(authority.next_log_sequence);
let acknowledged_outbox_through = outbox.acknowledged_frontier(next_extension_sequence);
let recipients: Vec<super::prune::RecipientAcknowledgement> = outbox
.recipient_frontiers()
.into_iter()
.map(
|(participant_id, acknowledged_through)| super::prune::RecipientAcknowledgement {
participant_id,
acknowledged_through,
},
)
.collect();
let Some(plan) = super::prune::plan(
keep,
self.conversation_id,
super::prune::StreamBounds {
checkpoint_position: authority.next_log_sequence,
acknowledged_through: acknowledged_log_through,
},
super::prune::StreamBounds {
checkpoint_position: next_extension_sequence,
acknowledged_through: acknowledged_outbox_through,
},
&recipients,
self.timestamp,
) else {
return Ok(());
};
let Some(plan) = block_on(super::prune::unwritten(
self.log.store().as_ref(),
self.conversation_id,
plan,
))
.map_err(OperationLogError::from)?
.map_err(OperationLogError::from)?
else {
return Ok(());
};
let pending = self.batch.ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
let mut held = pending
.try_borrow_mut()
.map_err(|_| OperationLogError::RoutedStream(RoutedStreamRefusal::BatchRefused))?;
let pending = held.as_mut().ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
pending.stage_prune(plan)?;
Ok(())
}
fn staged_checkpoint_metadata(
&self,
authority: &ConversationAuthority,
starting_log_sequence: u64,
config: &ParticipantConfig,
) -> Result<
(
super::restore_from_checkpoint::CheckpointMetadata,
super::fenced_attach_terminal::ComposedTerminalValidation,
),
StateError,
> {
use super::fenced_attach_terminal::{AuditSourceError, ComposedTerminalValidation};
use super::log::OperationSchemaPhase;
use super::restore_checkpoint::CheckpointSchema;
use super::restore_checkpoint_history::CheckpointHistoryRoot;
use super::restore_checkpoint_locator::checkpoint_prefix_seed;
use super::restore_from_checkpoint::CheckpointMetadata;
let (root, prefix, mut phase, mut audit) = if starting_log_sequence == 0 {
(
CheckpointHistoryRoot::empty(self.conversation_id).encode_canonical(),
checkpoint_prefix_seed(),
OperationSchemaPhase::V2Prefix,
ComposedTerminalValidation::for_identity_slots(config.identity_slots)?,
)
} else {
let metadata = authority.checkpoint_metadata()?;
let phase = match metadata.schema() {
CheckpointSchema::V2Prefix => OperationSchemaPhase::V2Prefix,
CheckpointSchema::V3Suffix => OperationSchemaPhase::V3Suffix,
CheckpointSchema::V4Suffix => OperationSchemaPhase::V4Suffix,
};
let bytes = authority
.checkpoint_audit_source
.as_deref()
.ok_or_else(|| StateError::invariant("saved terminal audit is absent"))?;
let audit = ComposedTerminalValidation::decode_checkpoint(
bytes,
config.identity_slots,
starting_log_sequence - 1,
)
.map_err(checkpoint_state_error)?;
(
*metadata.history_root(),
metadata.prefix_digest(),
phase,
audit,
)
};
let pending = self.batch.ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
let mut held = pending
.try_borrow_mut()
.map_err(|_| OperationLogError::RoutedStream(RoutedStreamRefusal::BatchRefused))?;
let pending = held.as_mut().ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
let mut sequence = starting_log_sequence;
block_on(async {
while sequence < authority.next_log_sequence {
let page = self.log.read_staged_page(pending, sequence, phase).await?;
if page.rows.is_empty() {
return Err(StateError::invariant("staged operation has a missing row"));
}
phase = page.next_phase;
for row in page.rows {
if row.sequence != sequence || row.sequence >= authority.next_log_sequence {
return Err(StateError::invariant("staged operation sequence differs"));
}
let source = |position| self.log.read_staged_at(pending, position);
match audit
.validate_from(&source, row.sequence, &row.operation)
.await
{
Ok(()) => {}
Err(AuditSourceError::Semantic(error)) => return Err(error),
Err(AuditSourceError::Source(error)) => return Err(error.into()),
}
sequence = sequence.checked_add(1).ok_or_else(|| {
StateError::invariant("staged operation sequence overflow")
})?;
}
}
Ok::<(), StateError>(())
})??;
let prefix = pending
.checkpoint_prefix(prefix)
.map_err(OperationLogError::from)?;
let schema = match phase {
OperationSchemaPhase::V2Prefix => CheckpointSchema::V2Prefix,
OperationSchemaPhase::V3Suffix => CheckpointSchema::V3Suffix,
OperationSchemaPhase::V4Suffix => CheckpointSchema::V4Suffix,
};
let extension = authority
.outbox
.as_ref()
.ok_or_else(|| StateError::invariant("checkpoint outbox is absent"))?
.next_extension_sequence();
let positions = authority
.checkpoint_positions(schema, authority.next_log_sequence, extension)
.map_err(checkpoint_state_error)?;
let metadata = CheckpointMetadata::validated(positions, root, prefix)
.map_err(checkpoint_state_error)?;
Ok((metadata, audit))
}
fn stage_borrowed_checkpoint<
L: super::restore_checkpoint::CheckpointLiveState,
const N: usize,
>(
&self,
positions: super::restore_checkpoint::CheckpointPositions,
history_root: &[u8; N],
live: &L,
prefix_digest: [u8; 32],
) -> Result<(), StateError> {
let batch = self.batch.ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
let mut pending = batch
.try_borrow_mut()
.map_err(|_| OperationLogError::RoutedStream(RoutedStreamRefusal::BatchRefused))?;
let pending = pending.as_mut().ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
let store = self.log.store();
block_on(async {
self.log
.stage_bootstrap_head(pending, positions.next_log_sequence)
.await?;
self.outbox_log
.stage_bootstrap_head(pending, positions.extension_sequence)
.await?;
pending
.stage_borrowed_checkpoint(
store.as_ref(),
positions,
history_root,
live,
prefix_digest,
)
.await
.map_err(OperationLogError::from)?;
Ok::<(), StateError>(())
})?
}
pub(super) fn has_staged(&self) -> Result<bool, OperationLogError> {
let Some(batch) = self.batch else {
return Ok(false);
};
let pending = batch
.try_borrow()
.map_err(|_| OperationLogError::RoutedStream(RoutedStreamRefusal::BatchRefused))?;
let operation = pending.as_ref().ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
Ok(!operation.is_empty()?)
}
pub(super) fn commit_staged(&self) -> Result<(), OperationLogError> {
let Some(batch) = self.batch else {
return Err(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
));
};
let pending = {
let mut pending = batch
.try_borrow_mut()
.map_err(|_| OperationLogError::RoutedStream(RoutedStreamRefusal::BatchRefused))?;
pending.take().ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?
};
let store = self.log.store();
let mut pending = pending;
let prune = pending.take_prune();
block_on(pending.commit(store.as_ref(), || self.log.issuance().mark_submitted()))??;
if let Some(plan) = prune {
for outcome in block_on(super::prune::cut(store.as_ref(), &plan))? {
match outcome.refused {
None => tracing::info!(
stream = ?outcome.stream,
source = ?outcome.source,
cut_before = outcome.cut_before,
removed = outcome.removed,
"pruned acknowledged rows behind the checkpoint"
),
Some(refusal) => tracing::warn!(
stream = ?outcome.stream,
source = ?outcome.source,
cut_before = outcome.cut_before,
refusal = %refusal,
"prune receipt written, cut refused by the store"
),
}
}
}
Ok(())
}
pub(super) fn outstanding_extension_rows(&self) -> u64 {
self.outstanding_extension_rows.get()
}
}
impl DurableAppend for LogAppender<'_> {
fn append_extension(
&self,
row: &crate::server::participant::production::outbox_log::OutboxRow,
expected_sequence: u64,
) -> Result<(), crate::server::participant::production::state::ExtensionRowCause> {
if let Some(batch) = self.batch {
let mut pending = batch.try_borrow_mut().map_err(|_| {
super::state::ExtensionRowCause::Log(OutboxLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))
})?;
let pending = pending.as_mut().ok_or_else(|| {
super::state::ExtensionRowCause::Log(OutboxLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))
})?;
return block_on(self.outbox_log.stage_into(
pending,
row,
expected_sequence,
self.timestamp,
))
.map_err(super::state::ExtensionRowCause::Bridge)?
.map_err(super::state::ExtensionRowCause::Log);
}
let Some(log) = self.extension_log() else {
return Err(
crate::server::participant::production::state::ExtensionRowCause::Log(
crate::server::participant::production::outbox_log::OutboxLogError::Durability(
liminal::durability::DurabilityError::BatchUnsupported {
store: std::any::type_name::<Self>(),
},
),
),
);
};
liminal::durability::bridge::block_on(log.append_flat(row, expected_sequence))
.map_err(crate::server::participant::production::state::ExtensionRowCause::Bridge)?
.map_err(crate::server::participant::production::state::ExtensionRowCause::Log)
}
fn append(
&self,
operation: &StoredOperation,
expected_sequence: u64,
) -> Result<(), OperationLogError> {
if expected_sequence == 0 && matches!(operation, StoredOperation::Genesis { .. }) {
self.registry
.register_with_issuance(self.conversation_id, self.log.issuance())?;
}
if let Some(batch) = self.batch {
let mut pending = batch
.try_borrow_mut()
.map_err(|_| OperationLogError::RoutedStream(RoutedStreamRefusal::BatchRefused))?;
let pending = pending.as_mut().ok_or(OperationLogError::RoutedStream(
RoutedStreamRefusal::BatchRefused,
))?;
block_on(
self.log
.stage_into(pending, operation, expected_sequence, self.timestamp),
)??;
} else {
block_on(self.log.append_flat(operation, expected_sequence))??;
}
if owes_extension_row(operation) {
self.outstanding_extension_rows
.set(self.outstanding_extension_rows.get().saturating_add(1));
}
Ok(())
}
fn extension_log(&self) -> Option<&OutboxLog> {
Some(self.outbox_log)
}
fn owed_extension_rows(&self) -> u64 {
self.outstanding_extension_rows.get()
}
fn discharge_owed_extension_row(&self) {
self.outstanding_extension_rows
.set(self.outstanding_extension_rows.get().saturating_sub(1));
}
}
pub(super) fn elapsed_ms(started: Instant) -> u64 {
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
}
const fn unsettled_write(error: &DurabilityError) -> bool {
match error {
DurabilityError::EmptyOperationBatch
| DurabilityError::BatchUnsupported { .. }
| DurabilityError::AppendUnresolved { .. }
| DurabilityError::PendingAppendUnreadable { .. }
| DurabilityError::OutcomeUnknown { .. }
| DurabilityError::NotStarted { .. }
| DurabilityError::StoreError(_)
| DurabilityError::SequenceConflict { .. }
| DurabilityError::CursorRegression { .. }
| DurabilityError::DedupCollision { .. }
| DurabilityError::ConfigError(_)
| DurabilityError::EphemeralStoreOpen(_)
| DurabilityError::EphemeralStoreDetached
| DurabilityError::TruncateUnsupported { .. }
| DurabilityError::RoutedScanUnsupported { .. }
| DurabilityError::RoutedRemoveUnsupported { .. }
| DurabilityError::EnvelopeError(_) => true,
}
}
const fn unsettled_outbox_write(error: &OutboxLogError) -> bool {
matches!(error, OutboxLogError::Durability(error) if unsettled_write(error))
}
pub(super) fn state_error(error: &StateError) -> ParticipantSemanticError {
let issued = match error {
StateError::Log(error) => {
matches!(error, OperationLogError::Durability(error) if unsettled_write(error))
}
StateError::OutboxLog(error) => unsettled_outbox_write(error),
StateError::Outbox(error) => {
matches!(error, ConversationOutboxError::Log(error) if unsettled_outbox_write(error))
}
StateError::ExtensionRowsOwed { .. } | StateError::ExtensionRowUnconfirmed { .. } => true,
StateError::Bridge(_)
| StateError::Facts(_)
| StateError::ObserverProgressConformance(_)
| StateError::FateOccurrenceConflict(_)
| StateError::ShellRefused { .. }
| StateError::ShellUnavailable
| StateError::FrontierUnavailable
| StateError::ObligationDebtOwner { .. }
| StateError::ReplayedEventDrift { .. }
| StateError::ReplayedDiedTerminalHistory { .. }
| StateError::BindingTerminalAdmissionRefused { .. }
| StateError::ConversationSealed { .. }
| StateError::PresentedRefusal(_)
| StateError::Invariant { .. }
| StateError::AllocationExhausted { .. } => false,
};
classify_state_error(error, issued)
}
fn state_error_with_issuance(
error: &StateError,
issuance: &WriteIssuance,
) -> ParticipantSemanticError {
classify_state_error(error, issuance.was_submitted())
}
fn classify_state_error(error: &StateError, issued: bool) -> ParticipantSemanticError {
let uncertain = match error {
StateError::ReplayedDiedTerminalHistory { .. } => false,
StateError::BindingTerminalAdmissionRefused { error } => {
return ParticipantSemanticError::BindingTerminalAdmissionRefused { error: *error };
}
StateError::ConversationSealed { conversation_id } => {
return ParticipantSemanticError::ConversationSealed {
conversation_id: *conversation_id,
};
}
StateError::ExtensionRowsOwed { .. }
| StateError::ExtensionRowUnconfirmed { .. }
| StateError::Log(_)
| StateError::Outbox(_)
| StateError::OutboxLog(_)
| StateError::Bridge(_)
| StateError::Facts(_)
| StateError::ObserverProgressConformance(_)
| StateError::FateOccurrenceConflict(_)
| StateError::ShellRefused { .. }
| StateError::ShellUnavailable
| StateError::FrontierUnavailable
| StateError::ObligationDebtOwner { .. }
| StateError::ReplayedEventDrift { .. }
| StateError::PresentedRefusal(_)
| StateError::Invariant { .. }
| StateError::AllocationExhausted { .. } => issued,
};
let message = format!("participant production operation failed: {error}");
if uncertain {
ParticipantSemanticError::WriteOutcomeUnknown { message }
} else {
ParticipantSemanticError::Internal { message }
}
}
pub(super) fn log_error(error: &OperationLogError) -> ParticipantSemanticError {
let message = format!("participant production log failed: {error}");
if matches!(error, OperationLogError::Durability(error) if unsettled_write(error)) {
return ParticipantSemanticError::WriteOutcomeUnknown { message };
}
ParticipantSemanticError::Internal { message }
}
#[cfg(test)]
pub(super) fn outbox_log_error(error: &OutboxLogError) -> ParticipantSemanticError {
let message = format!("participant Unit 2 extension log failed: {error}");
if unsettled_outbox_write(error) {
return ParticipantSemanticError::WriteOutcomeUnknown { message };
}
ParticipantSemanticError::Internal { message }
}
pub(super) fn bridge_error(
error: &liminal::durability::bridge::BridgeError,
) -> ParticipantSemanticError {
ParticipantSemanticError::Internal {
message: format!("participant durability bridge failed: {error}"),
}
}
#[cfg(test)]
mod batch_tests;