Skip to main content

monoloop_loop/transaction/
bootstrap.rs

1//! Runtime bootstrap inputs (Transaction Runtime v2).
2
3use super::channel_registry::ChannelRegistry;
4use super::host_tools::HostToolRegistry;
5use monoloop_contracts::TransactionLimits;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::sync::watch;
10
11/// Test/prod gate that defers the supervisor's `Stopped` transition until
12/// [`StoppedGate::release`] (v2 §22.5 TimedOut determinism).
13///
14/// Uses [`watch`] so a release that races ahead of the waiter is not lost
15/// (unlike `Notify`, which can miss a wakeup between check and await).
16#[derive(Debug)]
17pub struct StoppedGate {
18    tx: watch::Sender<bool>,
19    rx: watch::Receiver<bool>,
20}
21
22impl Default for StoppedGate {
23    fn default() -> Self {
24        let (tx, rx) = watch::channel(false);
25        Self { tx, rx }
26    }
27}
28
29impl StoppedGate {
30    /// New unreleased gate.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Allow the supervisor to enter `Stopped`.
36    pub fn release(&self) {
37        let _ = self.tx.send(true);
38    }
39
40    /// Wait until [`Self::release`] has been called.
41    pub async fn wait_released(&self) {
42        let mut rx = self.rx.clone();
43        // `wait_for` observes the current value first — no lost-wakeup race.
44        let _ = rx.wait_for(|released| *released).await;
45    }
46}
47
48/// Test-only gate that pauses supervisor drain of the start queue (D-040).
49///
50/// While held, `Start` commands remain queued so admission can observe
51/// start-queue-full rollback without the supervisor racing to drain.
52#[derive(Debug, Default)]
53pub struct StartHoldGate {
54    held: AtomicBool,
55}
56
57impl StartHoldGate {
58    /// New gate, initially not holding (start drain enabled).
59    pub fn new() -> Self {
60        Self {
61            held: AtomicBool::new(false),
62        }
63    }
64
65    /// Pause start-queue drain.
66    pub fn hold(&self) {
67        self.held.store(true, Ordering::SeqCst);
68    }
69
70    /// Resume start-queue drain.
71    pub fn release(&self) {
72        self.held.store(false, Ordering::SeqCst);
73    }
74
75    /// Whether start drain is currently paused.
76    pub fn is_held(&self) -> bool {
77        self.held.load(Ordering::SeqCst)
78    }
79}
80
81/// Test-only gate that pauses supervisor drain of the control queue (§23).
82///
83/// While held, Cancel / ForceTerminate / BeginShutdown remain queued so
84/// `TransactionLimits.max_actor_commands` plus-one can observe
85/// `ControlCapacityExceeded` without the preferential control drain racing.
86#[derive(Debug, Default)]
87pub struct ControlHoldGate {
88    held: AtomicBool,
89}
90
91impl ControlHoldGate {
92    /// New gate, initially not holding (control drain enabled).
93    pub fn new() -> Self {
94        Self {
95            held: AtomicBool::new(false),
96        }
97    }
98
99    /// Pause control-queue drain.
100    pub fn hold(&self) {
101        self.held.store(true, Ordering::SeqCst);
102    }
103
104    /// Resume control-queue drain.
105    pub fn release(&self) {
106        self.held.store(false, Ordering::SeqCst);
107    }
108
109    /// Whether control drain is currently paused.
110    pub fn is_held(&self) -> bool {
111        self.held.load(Ordering::SeqCst)
112    }
113}
114
115/// Test-only gate that pauses the Finalizer between Seal and completion send (§22.2).
116///
117/// Proves shutdown / hard-grace cannot drop the ledger row or the one completion
118/// attempt while Seal has already run.
119#[derive(Debug)]
120pub struct FinalizerHoldGate {
121    released: AtomicBool,
122    notify: tokio::sync::Notify,
123}
124
125impl Default for FinalizerHoldGate {
126    fn default() -> Self {
127        Self {
128            released: AtomicBool::new(false),
129            notify: tokio::sync::Notify::new(),
130        }
131    }
132}
133
134impl FinalizerHoldGate {
135    /// New gate; Finalizer blocks after Seal until [`Self::release`].
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    /// Allow Finalizer to publish completion.
141    pub fn release(&self) {
142        self.released.store(true, Ordering::SeqCst);
143        self.notify.notify_waiters();
144    }
145
146    /// Wait until release (used by Finalizer).
147    pub async fn wait_released(&self) {
148        loop {
149            if self.released.load(Ordering::SeqCst) {
150                return;
151            }
152            // Subscribe before re-check to avoid lost wakeup.
153            let notified = self.notify.notified();
154            if self.released.load(Ordering::SeqCst) {
155                return;
156            }
157            notified.await;
158        }
159    }
160}
161
162/// Test-only inject: TaskSupervisor-owned JoinOnly-style work (§22.4 / Law 23 / M5.4).
163///
164/// Registers a `RuntimeService` that parks the worker thread until
165/// [`JoinOnlySpillInject::release`] (abort cannot join a non-awaiting park —
166/// same shape as the §22.3 sacrificial). Proves `wait_stopped` stays Quiescing
167/// with `owned_tasks > 0`, then reaches Stopped after release.
168/// Production leaves [`RuntimeConfig::inject_join_only_spill`] as `None`.
169///
170/// Name retains “Spill” for API stability; ownership is TaskSupervisor, not
171/// [`crate::transaction::dispatcher::OrphanToolPermitSet`].
172#[derive(Debug)]
173pub struct JoinOnlySpillInject {
174    entered: AtomicBool,
175    released: AtomicBool,
176    parked_thread: std::sync::Mutex<Option<std::thread::Thread>>,
177}
178
179impl Default for JoinOnlySpillInject {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185impl JoinOnlySpillInject {
186    /// New inject; JoinOnly wait starts blocked until [`Self::release`].
187    pub fn new() -> Self {
188        Self {
189            entered: AtomicBool::new(false),
190            released: AtomicBool::new(false),
191            parked_thread: std::sync::Mutex::new(None),
192        }
193    }
194
195    /// True once the supervised task has entered its park loop.
196    pub fn is_entered(&self) -> bool {
197        self.entered.load(Ordering::SeqCst)
198    }
199
200    pub(crate) fn is_released(&self) -> bool {
201        self.released.load(Ordering::SeqCst)
202    }
203
204    pub(crate) fn mark_entered(&self) {
205        self.entered.store(true, Ordering::SeqCst);
206    }
207
208    pub(crate) fn store_parked_thread(&self, thread: std::thread::Thread) {
209        *self.parked_thread.lock().unwrap_or_else(|e| e.into_inner()) = Some(thread);
210    }
211
212    /// Allow the supervised JoinOnly task to finish (unblocks Stopped).
213    pub fn release(&self) {
214        self.released.store(true, Ordering::SeqCst);
215        if let Some(thread) = self
216            .parked_thread
217            .lock()
218            .unwrap_or_else(|e| e.into_inner())
219            .take()
220        {
221            thread.unpark();
222        }
223    }
224}
225
226/// Runtime-wide configuration validated at startup.
227#[derive(Clone, Debug)]
228pub struct RuntimeConfig {
229    /// Transaction / event / callback bounds.
230    pub transaction_limits: TransactionLimits,
231    /// When true, bind a loopback MCP gateway as TaskSupervisor RuntimeService.
232    pub enable_mcp_listener: bool,
233    /// Maximum time to wait for graceful drain during shutdown when not specified.
234    pub default_shutdown_deadline: Duration,
235    /// When `Some`, supervisor defers drain-complete until the gate is released.
236    /// Production leaves this `None`; §22.5 TimedOut proofs set it.
237    pub block_stopped: Option<Arc<StoppedGate>>,
238    /// When `Some`, supervisor skips draining `Start` while the gate is held.
239    /// Production leaves this `None`; D-040 parked-Start proofs set it.
240    pub hold_start: Option<Arc<StartHoldGate>>,
241    /// When `Some`, supervisor skips draining control while the gate is held.
242    /// Production leaves this `None`; §23 `max_actor_commands` proofs set it.
243    pub hold_control: Option<Arc<ControlHoldGate>>,
244    /// Override start-queue capacity (tests). `None` ⇒ `max_active_transactions`.
245    /// Use a value smaller than reservation capacity to prove start-full rollback
246    /// while the reservation pool still has headroom (D-040 / §22.1).
247    pub start_queue_capacity: Option<usize>,
248    /// When `Some`, Finalizer waits after Seal before completion send (§22.2).
249    /// Production leaves this `None`.
250    pub hold_finalizer_after_seal: Option<Arc<FinalizerHoldGate>>,
251    /// When `Some`, the executor OS thread waits here after supervisor drain and
252    /// before `shutdown_timeout` (D-049). Production leaves this `None`.
253    pub hold_executor_teardown: Option<Arc<StoppedGate>>,
254    /// When `Some`, supervisor registers a never-awaiting `RuntimeService` that
255    /// stores `true` on this flag immediately before parking (§22.3 sacrificial).
256    /// Production leaves this `None`.
257    pub inject_non_yielding_service: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
258    /// When `Some`, supervisor registers TaskSupervisor-owned JoinOnly-style
259    /// work at start (Stopped-vs-owned-task proof). Production leaves this `None`.
260    pub inject_join_only_spill: Option<Arc<JoinOnlySpillInject>>,
261}
262
263impl Default for RuntimeConfig {
264    fn default() -> Self {
265        Self {
266            transaction_limits: TransactionLimits::default(),
267            // Default off; hosts that need MCP set true at bootstrap.
268            enable_mcp_listener: false,
269            default_shutdown_deadline: Duration::from_secs(30),
270            block_stopped: None,
271            hold_start: None,
272            hold_control: None,
273            start_queue_capacity: None,
274            hold_finalizer_after_seal: None,
275            hold_executor_teardown: None,
276            inject_non_yielding_service: None,
277            inject_join_only_spill: None,
278        }
279    }
280}
281
282impl RuntimeConfig {
283    /// Validate non-zero and consistent bounds.
284    pub fn validate(&self) -> Result<(), super::StartupError> {
285        self.transaction_limits.validate().map_err(|e| match e {
286            monoloop_contracts::LimitsError::ZeroCapacity(f) => {
287                super::StartupError::InvalidConfig(f)
288            }
289            monoloop_contracts::LimitsError::Inconsistent(_) => {
290                super::StartupError::InvalidConfig("inconsistent transaction limits")
291            }
292        })?;
293        if self.default_shutdown_deadline.is_zero() {
294            return Err(super::StartupError::InvalidConfig(
295                "default_shutdown_deadline",
296            ));
297        }
298        // Reject durations that cannot form an Instant absolute deadline
299        // (`Instant + Duration` would panic for Duration::MAX-class values).
300        const MAX_TX_DEADLINE: std::time::Duration =
301            std::time::Duration::from_secs(365 * 24 * 3600);
302        if self.transaction_limits.transaction_deadline > MAX_TX_DEADLINE
303            || std::time::Instant::now()
304                .checked_add(self.transaction_limits.transaction_deadline)
305                .is_none()
306        {
307            return Err(super::StartupError::InvalidConfig(
308                "transaction_deadline exceeds Instant-representable bound",
309            ));
310        }
311        if let Some(cap) = self.start_queue_capacity {
312            if cap == 0 {
313                return Err(super::StartupError::InvalidConfig(
314                    "start_queue_capacity must be nonzero when set",
315                ));
316            }
317        }
318        Ok(())
319    }
320}
321
322/// Production bootstrap for [`super::lifecycle::StartedRuntime::start`].
323///
324/// The runtime constructs and owns its Tokio executor (v2 §7.2). There is no
325/// external `Handle` on this struct.
326pub struct RuntimeBootstrap {
327    /// Limits and feature flags.
328    pub config: RuntimeConfig,
329    /// Immutable Channel bindings (factories realized at start).
330    pub channels: ChannelRegistry,
331    /// Immutable host tool shell (empty allowed).
332    pub tools: HostToolRegistry,
333}