liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
/// Operational bounds (§5, scout Q4 — rule-2 items).
///
/// Each field is a hard per-scope cap with a typed refusal and a
/// certifying-pair-signed default (the numbers below are §5's). The struct is
/// the single wire surface for `[limits]`; [`LimitsConfig::validate`] rejects any
/// zero value as a typed config error (a zero cap would gate nothing — the exact
/// unlimited-by-silence state §5 outlaws). Defaults come from the `default_*`
/// free functions so an absent key resolves to the signed number, not zero.
#[derive(Debug, Clone, Copy, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LimitsConfig {
    /// Total live connections the listener admits before refusing (§5: 256 — a
    /// worker-bus, an order of magnitude above any observed fleet).
    #[serde(default = "default_max_connections")]
    pub max_connections: usize,
    /// Subscriptions one connection may hold (§5: 32).
    #[serde(default = "default_max_subscriptions_per_connection")]
    pub max_subscriptions_per_connection: usize,
    /// Open conversations one connection may hold (§5: 32).
    #[serde(default = "default_max_conversations_per_connection")]
    pub max_conversations_per_connection: usize,
    /// In-flight server→client correlated pushes per connection (§5: 32).
    #[serde(default = "default_max_pending_pushes_per_connection")]
    pub max_pending_pushes_per_connection: usize,
    /// Entries in the per-connection pending-reply table (§1.2(3b)/§5: 32 —
    /// distinct from server-push slots).
    #[serde(default = "default_max_pending_conversation_replies_per_connection")]
    pub max_pending_conversation_replies_per_connection: usize,
    /// Per-conversation sub-cap that confines tombstone ambiguity to its own
    /// conversation (§1.2(3b)/§5: 8). Pending entries count against BOTH this and
    /// the connection table; tombstones against THIS alone.
    #[serde(default = "default_max_pending_replies_per_conversation")]
    pub max_pending_replies_per_conversation: usize,
    /// One shared inbox-byte budget per connection, spent across ALL its
    /// subscription inboxes (§5: 4 MiB — deliberately mirroring the outbound 4 MiB
    /// bound). Accounting unit: serialized envelope bytes as admitted, charged at
    /// enqueue and released at dequeue.
    #[serde(default = "default_max_connection_inbox_bytes")]
    pub max_connection_inbox_bytes: usize,
    /// One outbound byte buffer per connection, holding every server-originated
    /// frame (acks, errors, `Push`, `Deliver`, `Disconnect`, `Pong`) until the
    /// connection's scheduler slice drains it to the socket.
    ///
    /// This is a DELIVERY bound, not a fairness one: a single frame larger than
    /// the whole buffer can never be queued, so this value is the largest frame
    /// the server can send on one connection. Raising it raises that ceiling at
    /// the cost of per-connection memory; lowering it lowers the ceiling for
    /// every connection at once.
    #[serde(default = "default_max_connection_outbound_bytes")]
    pub max_connection_outbound_bytes: usize,
    /// Per-inbox envelope-count secondary fairness trip — stops one subscription
    /// starving its siblings inside the shared byte budget. See
    /// [`LimitsConfig::DEFAULT_MAX_SUBSCRIPTION_INBOX_DEPTH`] for why the default
    /// is what it is; the short version is that this cap is the CRUDE bound and
    /// [`LimitsConfig::max_connection_inbox_bytes`] is the real one.
    #[serde(default = "default_max_subscription_inbox_depth")]
    pub max_subscription_inbox_depth: usize,
    /// Per-slice cap on `Deliver` frames one connection may enqueue across all of
    /// its subscriptions, before the scheduler moves on to its peers.
    ///
    /// Operator-visible because P0 #55 proved it decides whether a subscriber
    /// behind a burst survives — but see
    /// [`LimitsConfig::DEFAULT_DELIVERY_SLICE_BUDGET`] for why the DEFAULT is not
    /// raised. Raising it trades one starvation for another, and the measurement
    /// that showed it moves the outcome could not see the starvation it causes.
    #[serde(default = "default_delivery_slice_budget")]
    pub delivery_slice_budget: usize,
    /// Runtime-registered channels this deployment admits.
    ///
    /// # Why this cap departs the uniform pattern
    ///
    /// Every other field here carries `#[serde(default = "…")]` naming a §5
    /// constant, because each of those numbers is a signed §5 bound. **There is
    /// no signed §5 bound for channel count**, and inventing one is barred: a
    /// number nobody certified, presented in the same shape as eight numbers
    /// somebody did, is a forged citation. So the type is `Option<usize>` and
    /// the serde default is the ABSENCE itself (`None`), never a value —
    /// `#[serde(default)]` here resolves a missing key to "no bound declared",
    /// which is a different statement from any number.
    ///
    /// Nor may this be a `usize` with a large default: unbounded-by-default is
    /// not a bound, it is the gap wearing a number.
    ///
    /// `None` refuses every runtime channel registration with a typed error
    /// naming this key, so a deployment that wants runtime registration declares
    /// its own bound and a deployment that never registers needs no config
    /// change at all. The cap bounds RUNTIME-registered channels only:
    /// `[[channels]]` entries are the bound the operator already wrote.
    ///
    /// `Some(0)` is a validation error like every other zero cap here — see
    /// [`LimitsConfig::collect_errors`].
    #[serde(default)]
    pub max_channels: Option<usize>,
    /// How long a client's correlated conversation request waits for its reply
    /// before the server answers it timed out, in milliseconds (default 5,000).
    /// A zero would time every request out on its first slice.
    #[serde(default = "default_reply_timeout_ms")]
    pub reply_timeout_ms: u64,
}

impl LimitsConfig {
    /// §5 default: total live connections before the listener refuses.
    pub const DEFAULT_MAX_CONNECTIONS: usize = 256;
    /// §5 default: subscriptions per connection.
    pub const DEFAULT_MAX_SUBSCRIPTIONS_PER_CONNECTION: usize = 32;
    /// §5 default: open conversations per connection.
    pub const DEFAULT_MAX_CONVERSATIONS_PER_CONNECTION: usize = 32;
    /// §5 default: in-flight server pushes per connection.
    pub const DEFAULT_MAX_PENDING_PUSHES_PER_CONNECTION: usize = 32;
    /// §5 default: pending-reply table entries per connection.
    pub const DEFAULT_MAX_PENDING_CONVERSATION_REPLIES_PER_CONNECTION: usize = 32;
    /// §5 default: per-conversation pending-reply sub-cap.
    pub const DEFAULT_MAX_PENDING_REPLIES_PER_CONVERSATION: usize = 8;
    /// §5 default: shared per-connection inbox byte budget (4 MiB).
    pub const DEFAULT_MAX_CONNECTION_INBOX_BYTES: usize = 4 * 1024 * 1024;
    /// Default per-connection OUTBOUND byte budget (4 MiB) — the twin the inbound
    /// budget was always described as mirroring, now declared in its own right.
    ///
    /// It is the honest single-frame delivery bound: a frame up to this size is
    /// delivered intact across as many scheduler slices as the peer's read rate
    /// needs, and a frame LARGER than the whole buffer can never be queued at all.
    /// The number is unchanged from the buffer's former private constant; what
    /// changes is that an operator can now see it and set it, instead of meeting
    /// it only as a connection that stopped carrying something.
    pub const DEFAULT_MAX_CONNECTION_OUTBOUND_BYTES: usize = 4 * 1024 * 1024;
    /// Default per-inbox envelope-count fairness trip.
    ///
    /// # Why 4096 and not the §5-era 256 (P0 #55)
    ///
    /// A subscription inbox is bounded TWICE: by
    /// [`Self::DEFAULT_MAX_CONNECTION_INBOX_BYTES`] (4 MiB, shared across all of
    /// one connection's inboxes) and by this envelope COUNT. Memory is the real
    /// resource, so the byte budget is the bound that should bind and this one is
    /// meant to be a fairness backstop behind it.
    ///
    /// Which one binds first is decided by record size. For a connection holding a
    /// single subscription, the crossover — the record size at which the two caps
    /// trip together — is `4 MiB / depth`:
    ///
    /// | depth | crossover record size  | binds first for a 1 KiB record |
    /// |-------|------------------------|--------------------------------|
    /// | 256   | 4194304/256 = 16 KiB   | the COUNT (at 256 KiB, 6% of 4 MiB) |
    /// | 4096  | 4194304/4096 = 1 KiB   | the BYTES (as intended)        |
    ///
    /// At 256 the count cap therefore binds for every record smaller than 16 KiB —
    /// which is nearly all real traffic — and it binds at a small fraction of the
    /// memory the connection is actually permitted: at a 164-byte record, 256
    /// envelopes is 41 KiB, ~1% of the 4 MiB budget. A replay burst is thousands of
    /// SMALL records, which is precisely the shape that hits the crude bound while
    /// the real bound sits untouched. That is the P0: a live subscriber shed for
    /// exceeding a fairness trip it reached at 1% of its memory allowance.
    ///
    /// At 4096 the crossover falls to 1 KiB, so bytes bind for realistic records
    /// and this cap returns to backstop duty.
    ///
    /// Honest bound on that arithmetic: the byte budget is per CONNECTION and this
    /// cap is per SUBSCRIPTION, so with `S` subscriptions sharing one connection
    /// the effective crossover is `4 MiB / S / depth`. The table above is the `S=1`
    /// case. A connection holding many subscriptions of small records can still
    /// reach the count cap first — which is exactly the sibling-starvation case
    /// this cap exists to catch.
    pub const DEFAULT_MAX_SUBSCRIPTION_INBOX_DEPTH: usize = 4096;
    /// Default per-slice `Deliver` budget for one connection — the source of truth
    /// for [`crate::config::LimitsConfig::delivery_slice_budget`] and for the
    /// delivery pump's own `DELIVERY_SLICE_BUDGET`.
    ///
    /// # Why this is NOT raised (P0 #55)
    ///
    /// This is the cross-connection FAIRNESS bound: it caps how long one connection
    /// may hold a shared scheduler thread before its peers get a turn. Raising it
    /// makes a burst subscriber's own drain finish in fewer scheduling round trips,
    /// and the P0 #55 2x2 measured exactly that — 0/192 boots lost a subscriber at
    /// 256 against roughly half of them at 32.
    ///
    /// That experiment does not license raising the default. It ran two or three
    /// connections, so the peer starvation a raise would CAUSE was unobservable BY
    /// CONSTRUCTION: with no queue of waiting peers there is nothing for a longer
    /// slice to delay. What was measured is that this knob moves the outcome, not
    /// that moving it is safe.
    ///
    /// So the number stays 32 and becomes an operator DECISION instead: a
    /// deployment that knows its connection count and its burst shape can raise it
    /// deliberately. The default does not make that trade on anyone's behalf.
    /// The P0's actual fix is [`Self::DEFAULT_MAX_SUBSCRIPTION_INBOX_DEPTH`].
    pub const DEFAULT_DELIVERY_SLICE_BUDGET: usize = 32;
    /// Default wait for a correlated conversation reply: 5 s, the bound a
    /// client has always been answered within.
    pub const DEFAULT_REPLY_TIMEOUT_MS: u64 = 5_000;

    /// Validates the caps: every value must be non-zero (a zero cap gates nothing
    /// — the unlimited-by-silence state §5 outlaws). Errors are accumulated into
    /// `errors` (one per offending field) so an operator sees every bad cap at
    /// once, matching the rest of config validation.
    pub(crate) fn collect_errors(&self, errors: &mut Vec<String>) {
        let checks: [(&str, usize); 10] = [
            ("max_connections", self.max_connections),
            (
                "max_subscriptions_per_connection",
                self.max_subscriptions_per_connection,
            ),
            (
                "max_conversations_per_connection",
                self.max_conversations_per_connection,
            ),
            (
                "max_pending_pushes_per_connection",
                self.max_pending_pushes_per_connection,
            ),
            (
                "max_pending_conversation_replies_per_connection",
                self.max_pending_conversation_replies_per_connection,
            ),
            (
                "max_pending_replies_per_conversation",
                self.max_pending_replies_per_conversation,
            ),
            (
                "max_connection_inbox_bytes",
                self.max_connection_inbox_bytes,
            ),
            // A zero here would leave a connection unable to queue ANY frame at
            // all — every push and every delivery refused on its first byte.
            (
                "max_connection_outbound_bytes",
                self.max_connection_outbound_bytes,
            ),
            (
                "max_subscription_inbox_depth",
                self.max_subscription_inbox_depth,
            ),
            // A zero here would make the delivery pump enqueue nothing on any
            // slice — every subscription silently stalled forever, which is the
            // unlimited-by-silence failure wearing the opposite sign.
            ("delivery_slice_budget", self.delivery_slice_budget),
        ];
        for (field, value) in checks {
            if value == 0 {
                errors.push(zero_cap_error(field));
            }
        }
        // `max_channels` is checked BESIDE the array rather than inside it,
        // because it is the one optional cap and the array's element type has no
        // way to say "absent". Forcing it in would need a stand-in value for
        // `None` — a number standing where a declaration is not — and any
        // stand-in would either invent a bound or make an undeclared cap look
        // declared. So the shape stays honest and the RULE stays uniform: a
        // declared zero is refused with the same message as the other eight.
        //
        // The parenthetical about unlimited-by-silence is the other eight caps'
        // ground; for this one a zero would refuse every registration rather
        // than admit every registration. The message is kept identical anyway:
        // an operator reading nine cap errors should learn one rule — zero is
        // not a legal cap value anywhere in this section — not two.
        if self.max_channels == Some(0) {
            errors.push(zero_cap_error("max_channels"));
        }
        // A duration, not a count, so it sits beside the array too; zero is
        // refused by the same rule.
        if self.reply_timeout_ms == 0 {
            errors.push(zero_cap_error("reply_timeout_ms"));
        }
    }
}

/// The typed refusal for a zero-valued cap, shared by every field in `[limits]`
/// so the wording cannot drift between them.
fn zero_cap_error(field: &str) -> String {
    format!(
        "limits.{field}: must be greater than zero (a zero cap would be \
         unlimited-by-silence, which §5 forbids)"
    )
}

impl Default for LimitsConfig {
    fn default() -> Self {
        Self {
            max_connections: default_max_connections(),
            max_subscriptions_per_connection: default_max_subscriptions_per_connection(),
            max_conversations_per_connection: default_max_conversations_per_connection(),
            max_pending_pushes_per_connection: default_max_pending_pushes_per_connection(),
            max_pending_conversation_replies_per_connection:
                default_max_pending_conversation_replies_per_connection(),
            max_pending_replies_per_conversation: default_max_pending_replies_per_conversation(),
            max_connection_inbox_bytes: default_max_connection_inbox_bytes(),
            max_connection_outbound_bytes: default_max_connection_outbound_bytes(),
            max_subscription_inbox_depth: default_max_subscription_inbox_depth(),
            delivery_slice_budget: default_delivery_slice_budget(),
            // No signed bound exists for this cap, so the default is the
            // absence of a declaration — never a number.
            max_channels: None,
            reply_timeout_ms: default_reply_timeout_ms(),
        }
    }
}

const fn default_max_connections() -> usize {
    LimitsConfig::DEFAULT_MAX_CONNECTIONS
}
const fn default_max_subscriptions_per_connection() -> usize {
    LimitsConfig::DEFAULT_MAX_SUBSCRIPTIONS_PER_CONNECTION
}
const fn default_max_conversations_per_connection() -> usize {
    LimitsConfig::DEFAULT_MAX_CONVERSATIONS_PER_CONNECTION
}
const fn default_max_pending_pushes_per_connection() -> usize {
    LimitsConfig::DEFAULT_MAX_PENDING_PUSHES_PER_CONNECTION
}
const fn default_max_pending_conversation_replies_per_connection() -> usize {
    LimitsConfig::DEFAULT_MAX_PENDING_CONVERSATION_REPLIES_PER_CONNECTION
}
const fn default_max_pending_replies_per_conversation() -> usize {
    LimitsConfig::DEFAULT_MAX_PENDING_REPLIES_PER_CONVERSATION
}
const fn default_max_connection_inbox_bytes() -> usize {
    LimitsConfig::DEFAULT_MAX_CONNECTION_INBOX_BYTES
}
const fn default_max_connection_outbound_bytes() -> usize {
    LimitsConfig::DEFAULT_MAX_CONNECTION_OUTBOUND_BYTES
}
const fn default_max_subscription_inbox_depth() -> usize {
    LimitsConfig::DEFAULT_MAX_SUBSCRIPTION_INBOX_DEPTH
}
const fn default_delivery_slice_budget() -> usize {
    LimitsConfig::DEFAULT_DELIVERY_SLICE_BUDGET
}

const fn default_reply_timeout_ms() -> u64 {
    LimitsConfig::DEFAULT_REPLY_TIMEOUT_MS
}