Skip to main content

tollgate_client/
lib.rs

1//! Instance-side quota runtime.
2//!
3//! Background tasks run *off* the request path (INVARIANTS.md GL-6): a
4//! [`LeaseManager`] task keeps an account's [`LeaseSlot`] stocked from a
5//! [`LeaseAllocator`], and a [`UsageWriter`] task drains a bounded channel of
6//! usage events into a [`UsageSink`] in idempotent batches. The request path
7//! touches published state (lock-free loads) and the channel (permit
8//! reservation) — when the channel is full, admission sheds *before* work is
9//! accepted (INVARIANTS.md GL-8) via [`UsageRecorder::try_reserve`].
10//!
11//! Timestamps come from a [`Clock`] so every behavior is testable with a
12//! manual clock; production uses [`SystemClock`].
13//!
14//! [`InstanceRuntime`] owns discovery, stable account slots, dynamically
15//! supervised lease managers, and the bounded usage writer. Its cloneable
16//! [`RuntimeHandle`] provides staged admission, readiness, and reports; retain
17//! the unique runtime owner and await its shutdown. Lower-level managers remain
18//! available for specialized embeddings.
19//! Direct-store applications with budget schedules also own a [`PeriodRoller`]
20//! beside the admission runtime. Its monitor reports rollover health and
21//! confirmed progress; its shutdown is independent of usage and lease cleanup.
22//! HTTP-backed applications leave period maintenance to `tollgate-server`.
23//! Own a [`KeyManager`] beside the runtime to refresh customer credentials from
24//! a read-only `KeySource`. Use its [`KeyVerifier`] with
25//! `tollgate_auth::SessionCredential`, combine both monitors' readiness, and
26//! include key-manager shutdown in the application's budget. Refresh runs on
27//! the snapshot cadence; cached evidence expires within the configured key
28//! freshness window even when a key is removed or the feed becomes unavailable.
29//!
30//! The runtime enforces one total shutdown deadline. It closes accounting
31//! admission, pauses refills, drains issued permits and guards, and releases
32//! account leases concurrently. Embedders stop their HTTP listeners when they
33//! request runtime shutdown and bound their own request-task quiescence by
34//! the returned deadline.
35//!
36//! Graceful shutdown has one safe order: stop admitting, quiesce the request
37//! tasks still holding permits or committed
38//! [`tollgate_admission::Committed`] guards, await
39//! [`UsageWriter::shutdown`] (which refuses new reservations, then drains
40//! outstanding permits under its configured deadline and reports anything
41//! unresolved), and only then shut the [`LeaseManager`] down — usage events
42//! must land while their lease is live (INVARIANTS.md GL-12).
43//!
44//! [`LeaseAllocator`]: tollgate_store::LeaseAllocator
45//! [`UsageSink`]: tollgate_store::UsageSink
46//! [`LeaseSlot`]: tollgate_admission::LeaseSlot
47
48#![deny(missing_docs)]
49
50pub mod key_manager;
51pub mod lease_manager;
52pub mod period_roller;
53mod registry;
54pub mod runtime;
55pub mod snapshot_manager;
56mod task_health;
57pub mod usage_writer;
58
59#[cfg(feature = "http")]
60pub mod http;
61#[cfg(feature = "http")]
62pub mod http_security;
63
64pub use key_manager::{
65    KeyManager, KeyManagerConfig, KeyManagerConfigError, KeyManagerHealth, KeyManagerMonitor,
66    KeyManagerReport, KeyManagerShutdownReport, KeyManagerStats, KeyVerifier,
67};
68pub use period_roller::{
69    PeriodRoller, PeriodRollerConfig, PeriodRollerConfigError, PeriodRollerHealth,
70    PeriodRollerMonitor, PeriodRollerReport, PeriodRollerShutdownReport, PeriodRollerStats,
71};
72pub use runtime::RuntimeFundingReport;
73pub use runtime::{
74    AccountPhase, AccountReport, ContentionReport, InstanceRuntime, InstanceRuntimeConfig,
75    InstanceRuntimeConfigError, RuntimeHandle, RuntimeReadiness, RuntimeReport,
76    RuntimeShutdownReport, RuntimeWriterError,
77};
78pub use tollgate_store::{Clock, ManualClock, SystemClock};
79
80#[cfg(feature = "http")]
81pub use http::HttpStore;
82#[cfg(feature = "http")]
83pub use http_security::{
84    BearerProvider, BearerToken, GoogleIdentity, HttpStoreConfig, StaticBearer,
85};
86pub use lease_manager::{
87    AccountLeaseConfig, LeaseCounters, LeaseManager, LeaseManagerConfig, LeaseManagerConfigError,
88    LeaseManagerReport, LeaseStats,
89};
90pub use snapshot_manager::{
91    SlotRegistry, SnapshotCounters, SnapshotManager, SnapshotManagerConfig,
92    SnapshotManagerConfigError, SnapshotManagerReport, SnapshotStats, TrackedPrincipals,
93};
94pub use usage_writer::{
95    UsagePermit, UsageRecorder, UsageWriter, UsageWriterConfig, UsageWriterConfigError,
96    WriterCounters, WriterHealth, WriterShutdownError, WriterStats,
97};
98
99/// A control-plane deadline shared with the task that performs cleanup.
100/// Setting it can only shorten the remaining budget. Never read by requests.
101#[derive(Default)]
102pub(crate) struct ShutdownDeadline(std::sync::Mutex<Option<tokio::time::Instant>>);
103
104impl ShutdownDeadline {
105    pub(crate) fn constrain(&self, deadline: tokio::time::Instant) {
106        let mut current = self.0.lock().expect("shutdown deadline poisoned");
107        *current = Some(current.map_or(deadline, |old| old.min(deadline)));
108    }
109
110    pub(crate) fn within(&self, budget: std::time::Duration) -> tokio::time::Instant {
111        let local = tokio::time::Instant::now() + budget;
112        self.0
113            .lock()
114            .expect("shutdown deadline poisoned")
115            .map_or(local, |deadline| deadline.min(local))
116    }
117}
118
119/// Set a watch channel, reporting the one way it can fail.
120///
121/// A `watch` send fails only when every receiver has been dropped, which
122/// means the observer this signal was for is already gone. That is never
123/// actionable by itself — the caller's own report carries the outcome — but
124/// it is a breadcrumb, and discarding it silently is the habit issue GL-36
125/// exists to break.
126pub(crate) fn signal(tx: &tokio::sync::watch::Sender<bool>, value: bool, signal: &'static str) {
127    if tx.send(value).is_err() {
128        tracing::debug!(signal, value, "no receivers remain for signal");
129    }
130}
131
132#[cfg(test)]
133mod deadline_tests {
134    use super::ShutdownDeadline;
135    use std::time::Duration;
136    use tokio::time::Instant;
137
138    #[tokio::test(start_paused = true)]
139    async fn a_shared_shutdown_deadline_can_only_shorten_a_components_budget() {
140        let deadline = ShutdownDeadline::default();
141        let now = Instant::now();
142        let second = Duration::from_secs(1);
143        assert_eq!(deadline.within(second), now + second);
144        deadline.constrain(now + second * 3);
145        deadline.constrain(now + second * 2);
146        deadline.constrain(now + second * 4);
147        assert_eq!(deadline.within(second * 10), now + second * 2);
148        assert_eq!(deadline.within(second), now + second);
149        tokio::time::advance(second * 2).await;
150        assert_eq!(deadline.within(second * 10), now + second * 2);
151    }
152}