use std::collections::{BTreeMap, HashMap};
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,
};
use super::barrier::{ArmOutcome, OperationFacts, ReceiptCapacityLimits};
use super::capacity::ServerCapacity;
use super::facts;
use super::log::{OperationLog, OperationLogError, StoredOperation};
use super::outbox::ConversationOutboxLimits;
use super::outbox_log::{OutboxLog, OutboxLogError};
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>>>>>,
pub(super) observer: Mutex<Option<ObserverOwner>>,
pub(super) capacity: ServerCapacity,
registry: ConversationRegistry,
}
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()),
observer: Mutex::new(None),
capacity: ServerCapacity::default(),
registry,
};
handler.restore_all_conversations()?;
Ok(handler)
}
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 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(
&self,
conversation_id: ConversationId,
operation: impl FnOnce(
&mut ConversationAuthority,
&dyn DurableAppend,
) -> Result<ArmOutcome, StateError>,
) -> Result<ArmOutcome, 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 (result, durably_empty) = match operation(authority, &appender) {
Ok(value) if 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;
*owner = Some(reconciled);
(Ok(value), durably_empty)
}
Err(error) => {
*owner = None;
(Err(error), false)
}
}
}
Ok(value) => {
let durably_empty = authority.next_log_sequence == 0;
(Ok(value), durably_empty)
}
Err(error) => {
let durably_empty = authority.next_log_sequence == 0;
*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);
let extension_rows = block_on(outbox_log.read_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,
extension_rows,
&self.config,
self.outbox_limits,
))
.map_err(|error| bridge_error(&error))?
.map_err(|error| state_error(&error))?;
let observer_projections = replayed.take_observer_progress_projections();
if !replayed.tokens.is_empty() {
self.ensure_observer_tracked(conversation_id)?;
self.reconcile_observer_progress(conversation_id, observer_projections)?;
} else if !observer_projections.is_empty() {
return Err(ParticipantSemanticError::Internal {
message: format!(
"unenrolled conversation {conversation_id} projected observer progress"
),
});
}
let now = facts::now_unix_millis().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)
}
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())
}
pub(super) fn operation_facts(
&self,
context: ParticipantConnectionContext,
conversation_id: ConversationId,
conversations: &ParticipantConnectionConversations,
) -> Result<OperationFacts, ParticipantSemanticError> {
let now_ms =
facts::now_unix_millis().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}"),
}
}