liminal-server 0.20.1

Standalone server for the liminal messaging bus
Documentation
//! Live connection-fate progress without turning lane occupancy into corruption.
//!
//! State transitions (LIMINAL-FATE-1):
//! - `OpenDurable` -> `CompleteDurable`: all targets folded, then Complete fsynced.
//! - `OpenDurable` -> `ParkedPrecedence`: the typed candidate-lane refusal preserves
//!   the exact Open sequence, connection, class and targets; teardown may finish.
//! - `ParkedPrecedence` -> `ParkedPrecedence`: a retry still finds occupancy.
//! - `ParkedPrecedence` -> `CompleteDurable`: a drain of one of the fate's
//!   conversations' candidate lanes TELLS this runtime
//!   ([`CandidateLaneListener`]), replay folds the remaining targets, then
//!   Complete fsync removes the index entry.
//! - Any non-Precedence fold failure or Complete persistence failure retains the
//!   existing fatal policy. A lock failure is a named coordinator error.
//!
//! The index is only a live scheduling aid: the already-fsynced Open is the WAL
//! recovery token. Process loss before Complete replays that same named intent
//! at startup. Committed targets are idempotent on replay.
//!
//! No retry is driven by a timer or by unrelated traffic: the drain that removes
//! the refusal is the only event that retries, and it retries only the fates
//! naming the drained conversation. The index lock is never held across a fold
//! or a durable write; a fold arms the drain generation before it runs and parks
//! only if no drain was told meanwhile, so a drain landing between the refusal
//! and the park is never lost. A clean stop drains the lanes its parked fates
//! wait on ([`ConnectionRuntime::settle_parked_connection_fates`]) and names any
//! fate it still could not complete.

use std::collections::{BTreeMap, BTreeSet};
use std::sync::MutexGuard;

use liminal_protocol::lifecycle::BindingTerminalAdmitError;
use liminal_protocol::wire::ConversationId;

use crate::ServerError;
use crate::server::participant::incarnation_stream::ConnectionFateIntent;
use crate::server::participant::{
    CandidateLaneListener, ParticipantSemanticError, ParticipantSemanticHandler,
};

use super::ConnectionRuntime;

/// Connection fates refused by candidate-lane `Precedence`, by Open sequence.
#[derive(Debug, Default)]
pub(super) struct ParkedConnectionFates {
    intents: BTreeMap<u64, ConnectionFateIntent>,
    /// Candidate-lane drains told so far. A fold reads it before trying and
    /// parks only if it has not moved, so a drain told while the fold ran
    /// (and which therefore found nothing parked to retry) is retried by the
    /// fold itself.
    lane_drains: u64,
}

impl ConnectionRuntime {
    pub(super) fn fold_or_park_connection_fate(
        &self,
        intent: ConnectionFateIntent,
    ) -> Result<(), ServerError> {
        loop {
            let armed = self.lock_parked_connection_fates("connection-fate progress lock")?;
            let observed = armed.lane_drains;
            drop(armed);
            if self.try_complete_connection_fate(&intent)? {
                return Ok(());
            }
            let mut parked = self.lock_parked_connection_fates("connection-fate park lock")?;
            if parked.lane_drains == observed {
                tracing::debug!(
                    open_sequence = intent.open_sequence,
                    "connection-fate intent parked for candidate-lane progress"
                );
                parked.intents.insert(intent.open_sequence, intent);
                crate::metrics::connection_fate_parked();
                drop(parked);
                return Ok(());
            }
            // A lane drained while this fold ran. Its tell found this intent
            // not yet parked, so this fold is that drain's retry.
            drop(parked);
        }
    }

    /// Settles every parked fate before a clean stop reports success.
    ///
    /// Drains each conversation a parked fate waits on, exactly as the next
    /// boot would, so the drain's tell completes the fate here. Whatever is
    /// still parked afterwards is named, and the stop is not clean.
    ///
    /// # Errors
    ///
    /// Returns [`ServerError::ShutdownUnsettledConnectionFates`] naming every
    /// Open left for the next boot to reconcile, or a named lock failure.
    pub(super) fn settle_parked_connection_fates(&self) -> Result<(), ServerError> {
        let parked = self.lock_parked_connection_fates("connection-fate stop lock")?;
        let conversations: BTreeSet<ConversationId> = parked
            .intents
            .values()
            .flat_map(|intent| intent.conversations.iter().copied())
            .collect();
        drop(parked);
        if !conversations.is_empty() {
            let Some(service) = self.participant_service() else {
                return Err(ServerError::ParticipantIncarnation {
                    phase: "connection-fate stop drain",
                    message: "parked connection fates lack an installed participant service"
                        .to_owned(),
                });
            };
            for conversation_id in conversations {
                if let Err(error) = service.drain_candidate_lane(conversation_id) {
                    tracing::error!(
                        conversation_id,
                        %error,
                        "clean stop could not drain a candidate lane a parked connection fate waits on"
                    );
                }
            }
        }
        let parked = self.lock_parked_connection_fates("connection-fate stop lock")?;
        let unsettled: Vec<u64> = parked.intents.keys().copied().collect();
        for intent in parked.intents.values() {
            tracing::error!(
                open_sequence = intent.open_sequence,
                conversations = ?intent.conversations,
                "clean stop left a parked connection fate unsettled; the next boot reconciles its Open"
            );
        }
        drop(parked);
        if unsettled.is_empty() {
            Ok(())
        } else {
            Err(ServerError::ShutdownUnsettledConnectionFates {
                open_sequences: unsettled,
            })
        }
    }

    /// Open sequences currently parked, for pins that need the index itself.
    #[cfg(test)]
    pub(super) fn parked_connection_fate_sequences(&self) -> Result<Vec<u64>, ServerError> {
        let parked = self.lock_parked_connection_fates("connection-fate test lock")?;
        Ok(parked.intents.keys().copied().collect())
    }

    fn lock_parked_connection_fates(
        &self,
        phase: &'static str,
    ) -> Result<MutexGuard<'_, ParkedConnectionFates>, ServerError> {
        self.parked_connection_fates
            .lock()
            .map_err(|error| ServerError::ParticipantIncarnation {
                phase,
                message: error.to_string(),
            })
    }

    /// False is only the typed occupancy refusal; all other failures retain
    /// their existing fatal handling. Complete is never appended for a refusal.
    fn try_complete_connection_fate(
        &self,
        intent: &ConnectionFateIntent,
    ) -> Result<bool, ServerError> {
        let (Some(service), Some(authority)) =
            (self.participant_service(), self.incarnations.as_ref())
        else {
            return Err(ServerError::ParticipantIncarnation {
                phase: "connection-fate progress authority",
                message: "durable fate work lost its service/incarnation pair".to_owned(),
            });
        };
        if let Err(error) = service.handle_connection_fate(intent.work_item()) {
            if matches!(
                error,
                ParticipantSemanticError::BindingTerminalAdmissionRefused {
                    error: BindingTerminalAdmitError::Precedence,
                }
            ) {
                return Ok(false);
            }
            return Err(self.complete_connection_fate_fatal(
                intent.open_sequence,
                &intent.work_item().tracked_conversations,
                "handler",
                &error,
            ));
        }
        if let Err(error) = authority.complete_connection_fate(intent.open_sequence) {
            return Err(self.complete_connection_fate_fatal(
                intent.open_sequence,
                &intent.work_item().tracked_conversations,
                "Complete",
                &error,
            ));
        }
        Ok(true)
    }
}

impl CandidateLaneListener for ConnectionRuntime {
    /// Retries every parked fate naming the drained conversation.
    ///
    /// Runs on the thread whose request committed the drain, after its
    /// conversation lock is released. A retry's failure belongs to the parked
    /// fate, not to that request: the fatal policy has already latched inside
    /// the retry, so it is logged here by Open sequence and the request that
    /// happened to drain the lane keeps its reply.
    fn candidate_lane_drained(&self, conversation_id: ConversationId) {
        let ready = match self.lock_parked_connection_fates("connection-fate retry lock") {
            Ok(mut parked) => {
                parked.lane_drains = parked.lane_drains.wrapping_add(1);
                let open_sequences: Vec<u64> = parked
                    .intents
                    .values()
                    .filter(|intent| intent.conversations.contains(&conversation_id))
                    .map(|intent| intent.open_sequence)
                    .collect();
                let ready: Vec<ConnectionFateIntent> = open_sequences
                    .iter()
                    .filter_map(|open_sequence| parked.intents.remove(open_sequence))
                    .collect();
                drop(parked);
                ready
            }
            Err(error) => {
                tracing::error!(
                    conversation_id,
                    %error,
                    "parked connection fates could not be read after a candidate-lane drain"
                );
                return;
            }
        };
        for intent in ready {
            crate::metrics::connection_fate_unparked();
            let open_sequence = intent.open_sequence;
            match self.fold_or_park_connection_fate(intent) {
                Ok(()) => tracing::debug!(
                    open_sequence,
                    conversation_id,
                    "parked connection-fate intent retried after a candidate-lane drain"
                ),
                Err(error) => tracing::error!(
                    open_sequence,
                    conversation_id,
                    %error,
                    "parked connection-fate retry failed"
                ),
            }
        }
    }
}