liminal-server 0.23.1

Standalone server for the liminal messaging bus
Documentation
//! Routing a request to the worker serving its subject (SRV-027 R2).
//!
//! A `ConversationMessage` on a subject the directory names is pushed to that
//! worker on the subject, the slot carrying this connection's ready waker.
//! With the reply-requested flag the operation is admitted in the
//! connection's pending-reply table first, exactly as the participant path
//! admits one (same caps, deadline, release binding and tombstones), and the
//! awaiter is held against the operation id; the slice reads it after the
//! wake and relays the outcome: the worker's reply as the correlated
//! conversation reply, or a refusal, an uncarriable frame or the worker gone
//! as a `ConversationError` in words on the request's stream. Without the
//! flag the push is made and nothing is held: the worker's reply is
//! discarded at its slot. Nothing here reaches the channel.

use std::time::Instant;

use liminal::protocol::{
    CONVERSATION_REPLY_REQUESTED_FLAG, CausalContext, Frame, MessageEnvelope, SchemaId,
};

use super::state::{ConnectionProcessState, FrameAction};
use super::supervisor::handle::PushReplyAwaiter;
use super::supervisor::runtime::ConnectionRuntime;
use crate::ServerError;
use crate::auth_membership::MembershipAction;

/// The conversation paths' server-error reason code (apply.rs); no code is
/// allocated for routing.
const SERVER_ERROR_CODE: u16 = 0xFFFF;

/// One routed request awaiting its worker's answer, held by operation id.
#[derive(Debug)]
pub(super) struct RoutedCall {
    conversation_id: u64,
    stream_id: u32,
    /// The caller's exact subject, also the selected worker's task queue.
    subject: String,
    awaiter: PushReplyAwaiter,
    schema_id: SchemaId,
    causal_context: CausalContext,
}

/// The frame a request arrived on: its flags, stream and conversation.
#[derive(Debug, Clone, Copy)]
pub(super) struct Request {
    /// The `ConversationMessage` flags; the reply-requested bit decides
    /// whether an operation is admitted.
    pub(super) flags: u8,
    /// The stream the request and its answer travel on.
    pub(super) stream_id: u32,
    /// The conversation the request was sent on.
    pub(super) conversation_id: u64,
}

impl Request {
    /// The frame's flags, stream and conversation, as received.
    pub(super) const fn new(flags: u8, stream_id: u32, conversation_id: u64) -> Self {
        Self {
            flags,
            stream_id,
            conversation_id,
        }
    }
}

/// Routes `envelope`, received as `request` on `subject`, to `worker`, the
/// connection the directory names for the subject. Called after the Lys
/// `Call` decision on the subject.
pub(super) fn routed(
    pid: u64,
    runtime: &ConnectionRuntime,
    state: &mut ConnectionProcessState,
    request: Request,
    subject: &str,
    envelope: &MessageEnvelope,
    worker: u64,
) -> FrameAction {
    let Request {
        flags,
        stream_id,
        conversation_id,
    } = request;
    // ACCESS-006 R3: the reply is data released to this caller; decided
    // before the request is pushed and again at the reply's release.
    if let Err(refusal) = state.membership.check(subject, MembershipAction::Read) {
        tracing::warn!(
            refusal = refusal.name(),
            action = MembershipAction::Read.as_str(),
            "refused a membership-protected act"
        );
        return error_frame(stream_id, conversation_id, refusal.to_string());
    }
    if flags & CONVERSATION_REPLY_REQUESTED_FLAG == 0 {
        // A one-way journey: pushed once; the reply resolves the slot and is
        // discarded there, nothing held on this side.
        return match runtime.route_push(worker, subject, envelope.payload.clone(), None) {
            Ok(push) => {
                state.call_station = super::service_tap::CallDestination::Queued(push.station);
                drop(push.awaiter);
                FrameAction::NoResponse
            }
            Err(error) => error_frame(stream_id, conversation_id, error.to_string()),
        };
    }
    let release = state
        .membership
        .release_check(subject, MembershipAction::Read);
    // Admission before the push: a cap-refused request never reaches the
    // worker, so no reply can arrive for an operation the table never held.
    let op_id = match state
        .pending_replies
        .admit(conversation_id, stream_id, Instant::now())
    {
        Ok(op_id) => op_id,
        Err(error) => {
            return FrameAction::Respond(super::pending_reply::cap_refusal_frame(
                stream_id,
                conversation_id,
                &error,
            ));
        }
    };
    if let Some(release) = release {
        state.pending_replies.bind_release(op_id, release);
    }
    match runtime.route_push(
        worker,
        subject,
        envelope.payload.clone(),
        runtime.ready_waker(pid),
    ) {
        Ok(push) => {
            state.call_station = super::service_tap::CallDestination::Queued(push.station);
            state.pending_replies.dispatched(op_id, Instant::now());
            state.routed.insert(
                op_id,
                RoutedCall {
                    conversation_id,
                    stream_id,
                    subject: subject.to_owned(),
                    awaiter: push.awaiter,
                    schema_id: envelope.schema_id,
                    causal_context: envelope.causal_context.clone(),
                },
            );
            FrameAction::NoResponse
        }
        Err(error) => {
            // Nothing was pushed: the reservation is rolled back exactly.
            state.pending_replies.cancel(op_id);
            error_frame(stream_id, conversation_id, error.to_string())
        }
    }
}

/// The frames to write for the routed calls that have settled: each resolved
/// slot's reply, correlated through the pending-reply table, or its named
/// error; a call whose operation the table no longer holds pending (its
/// deadline passed, or its conversation closed) releases its slot by
/// correlation id and writes nothing, the table having already named the
/// timeout. Non-blocking: a slot still empty is left for the next wake.
pub(super) fn settled(
    state: &mut ConnectionProcessState,
    runtime: &ConnectionRuntime,
) -> Vec<Frame> {
    let mut frames = Vec::new();
    let mut done = Vec::new();
    for (op_id, call) in &state.routed {
        if !state.pending_replies.is_pending(*op_id) {
            runtime.cancel_push(call.awaiter.correlation_id());
            done.push((*op_id, None));
        } else if let Some(outcome) = call.awaiter.try_receive() {
            done.push((*op_id, Some(outcome)));
        }
    }
    for (op_id, outcome) in done {
        let Some(call) = state.routed.remove(&op_id) else {
            continue;
        };
        match outcome {
            None => {}
            Some(Ok(payload)) => {
                let reply = MessageEnvelope::new(call.schema_id, call.causal_context, payload);
                if let Some(frame) =
                    state
                        .pending_replies
                        .match_reply(call.conversation_id, op_id, reply)
                {
                    frames.push(frame);
                }
            }
            Some(Err(error)) => {
                frames.push(error.into_frame(
                    state,
                    op_id,
                    call.stream_id,
                    call.conversation_id,
                    &call.subject,
                    words,
                ));
            }
        }
    }
    frames
}

/// The error in the caller's terms: the service, not the slot.
fn words(subject: &str, error: &ServerError) -> String {
    match error {
        ServerError::PushReplyDisconnected { .. } => {
            format!("the service serving {subject} left before answering")
        }
        ServerError::PushReplyRefused { reason, .. } => {
            format!("the service serving {subject} had its reply refused: {reason}")
        }
        other => format!("the service serving {subject} could not answer: {other}"),
    }
}

const fn error_frame(stream_id: u32, conversation_id: u64, message: String) -> FrameAction {
    FrameAction::Respond(Frame::ConversationError {
        flags: 0,
        stream_id,
        conversation_id,
        reason_code: SERVER_ERROR_CODE,
        message: Some(message),
    })
}