use std::collections::{BTreeMap, HashMap};
#[cfg(test)]
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use liminal::durability::DurableStore;
use liminal::durability::bridge::block_on;
use liminal_protocol::lifecycle::{CapacityCounter, ObserverRecoveryAggregate};
use liminal_protocol::wire::ConversationId;
use crate::config::types::ParticipantConfig;
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::outbox::ConversationOutboxLimits;
use super::outbox_log::{OutboxLog, OutboxLogError};
use super::outbox_replay::RestoreError;
use super::registry::ConversationRegistry;
use super::state::{ConversationAuthority, DurableAppend, StateError};
#[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)]
pub struct ProductionParticipantHandler {
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) capacity: ServerCapacity,
registry: ConversationRegistry,
#[cfg(test)]
pub(super) obligation_dispatch_work: ObligationDispatchWorkCounters,
#[cfg(test)]
now_override: AtomicU64,
}
impl ProductionParticipantHandler {
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 {
store,
config,
outbox_limits,
conversations: Mutex::new(HashMap::new()),
service_fatal: Mutex::new(None),
observer: Mutex::new(None),
capacity: ServerCapacity::default(),
registry,
#[cfg(test)]
obligation_dispatch_work: ObligationDispatchWorkCounters::default(),
#[cfg(test)]
now_override: AtomicU64::new(0),
};
handler.restore_all_conversations()?;
Ok(handler)
}
#[cfg(not(test))]
#[allow(clippy::unused_self)]
fn now_ms(&self) -> Result<u64, facts::FactsError> {
facts::now_unix_millis()
}
#[cfg(test)]
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 conversation_ids = self.registry.restore().map_err(|error| log_error(&error))?;
for conversation_id in conversation_ids {
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 replayed = self.replay_and_repair(conversation_id, &log)?;
let durably_empty = replayed.next_log_sequence == 0;
if durably_empty {
drop(owner);
self.evict_uncommitted(conversation_id, &cell)?;
continue;
}
*owner = Some(replayed);
}
drop(owner);
}
Ok(())
}
pub(super) fn registered_conversation_ids(
&self,
) -> Result<Vec<ConversationId>, ParticipantSemanticError> {
self.registry.restore().map_err(|error| log_error(&error))
}
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 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: MutexGuard<'_, Option<ConversationAuthority>> =
cell.lock()
.map_err(|_| ParticipantSemanticError::Internal {
message: format!(
"participant conversation {conversation_id} owner lock is poisoned"
),
})?;
let log = OperationLog::new(Arc::clone(&self.store), conversation_id);
if owner.is_none() {
let replayed = self.replay_and_repair(conversation_id, &log)?;
*owner = Some(replayed);
}
let Some(authority) = owner.as_mut() else {
return Err(ParticipantSemanticError::Internal {
message: format!("participant conversation {conversation_id} owner is absent"),
});
};
let appender = LogAppender {
log: &log,
registry: &self.registry,
conversation_id,
};
let starting_log_sequence = authority.next_log_sequence;
let operation_result = operation(authority, &appender, impact.as_deref_mut());
let (result, durably_empty) = match operation_result {
Ok(value)
if reconcile_appended_source
&& authority.next_log_sequence > starting_log_sequence =>
{
match self.replay_and_repair(conversation_id, &log) {
Ok(reconciled) => {
let durably_empty = reconciled.next_log_sequence == 0;
if let Some(impact) = impact.as_deref_mut() {
impact.install_staged();
}
*owner = Some(reconciled);
(Ok(value), durably_empty)
}
Err(error) => {
*owner = None;
(Err(error), false)
}
}
}
Ok(value) => {
if let Some(impact) = impact.as_deref_mut() {
impact.install_staged();
}
let durably_empty = authority.next_log_sequence == 0;
(Ok(value), durably_empty)
}
Err(error) => {
let mut durably_empty = authority.next_log_sequence == 0;
let staged = impact
.as_deref()
.is_some_and(DispatchImpactAccumulator::has_staged);
if staged {
match self.replay_and_repair(conversation_id, &log) {
Ok(reconciled) => {
durably_empty = reconciled.next_log_sequence == 0;
*owner = Some(reconciled);
if let Some(impact) = impact.as_mut() {
impact.install_staged();
}
}
Err(_) => {
*owner = None;
}
}
} else {
*owner = None;
}
(Err(state_error(&error)), durably_empty)
}
};
drop(owner);
if durably_empty {
self.evict_uncommitted(conversation_id, &cell)?;
}
result
}
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);
block_on(outbox_log.restore_cursor().validate_all())
.map_err(|error| bridge_error(&error))?
.map_err(|error| outbox_log_error(&error))?;
let mut replayed = block_on(ConversationAuthority::replay(
conversation_id,
log,
&outbox_log,
&self.config,
self.outbox_limits,
))
.map_err(|error| bridge_error(&error))?
.map_err(|error| match error {
RestoreError::Extension(error) => outbox_log_error(&error),
RestoreError::Semantic(error) => state_error(&error),
})?;
let appender = LogAppender {
log,
registry: &self.registry,
conversation_id,
};
replayed
.repair_pending_specific_fates(&appender)
.map_err(|error| state_error(&error))?;
let observer_witnesses = replayed.take_observer_progress_witnesses();
if !replayed.tokens.is_empty() {
self.reconcile_observer_progress(
conversation_id,
&observer_witnesses,
replayed.observer_progress,
)?;
} 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(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 {
identity_server: self.config.max_retired_identity_slots_server,
live_receipts_server: self.config.max_live_attach_receipts_server,
live_receipts_per_participant: self.config.max_live_attach_receipts_per_participant,
provenance_server: self.config.max_receipt_provenance_server,
provenance_per_conversation: self.config.max_receipt_provenance_per_conversation,
provenance_per_participant: self.config.max_receipt_provenance_per_participant,
},
connection_tracking: conversations.tracking(conversation_id),
connection_capacity,
})
}
}
struct LogAppender<'a> {
log: &'a OperationLog,
registry: &'a ConversationRegistry,
conversation_id: ConversationId,
}
impl DurableAppend for LogAppender<'_> {
fn append(
&self,
operation: &StoredOperation,
expected_sequence: u64,
) -> Result<(), OperationLogError> {
if expected_sequence == 0 && matches!(operation, StoredOperation::Genesis { .. }) {
self.registry.register(self.conversation_id)?;
}
block_on(self.log.append(operation, expected_sequence))?
}
}
pub(super) fn state_error(error: &StateError) -> ParticipantSemanticError {
ParticipantSemanticError::Internal {
message: format!("participant production operation failed: {error}"),
}
}
pub(super) fn log_error(error: &OperationLogError) -> ParticipantSemanticError {
ParticipantSemanticError::Internal {
message: format!("participant production log failed: {error}"),
}
}
pub(super) fn outbox_log_error(error: &OutboxLogError) -> ParticipantSemanticError {
ParticipantSemanticError::Internal {
message: format!("participant Unit 2 extension log failed: {error}"),
}
}
pub(super) fn bridge_error(
error: &liminal::durability::bridge::BridgeError,
) -> ParticipantSemanticError {
ParticipantSemanticError::Internal {
message: format!("participant durability bridge failed: {error}"),
}
}