Skip to main content

brokapi_runtime/
shutdown.rs

1//! Graceful / immediate shutdown.
2
3use std::time::Instant;
4
5use brokapi_core::{ShutdownMode, ShutdownReport};
6use tokio_util::sync::CancellationToken;
7
8use crate::command_gate::CommandGate;
9use crate::orders::OrderRuntime;
10
11/// Shutdown coordinator for one connector instance: it closes the command gate, gives up waiting for
12/// in-flight commands and cancels the background tasks.
13///
14/// The controller does no network I/O. The connector supplies the work that needs the network (the
15/// unsubscribe calls) as a closure. Orders at the broker are never cancelled by a shutdown: only
16/// the local wait for an answer is given up (orders in `InFlight` become `Unknown`).
17///
18/// # Examples
19///
20/// ```
21/// use std::time::{Duration, Instant};
22/// use brokapi_core::{ClientOrderId, Error, ShutdownMode};
23/// use brokapi_runtime::{CommandGate, OrderRuntime, ShutdownController};
24///
25/// let mut gate = CommandGate::new();
26/// gate.mark_reconciled();
27/// let mut ctl = ShutdownController::new(gate);
28/// let task_token = ctl.child_token();
29///
30/// let mut orders = OrderRuntime::new();
31/// let now = Instant::now();
32/// let cid = ClientOrderId::new_v4();
33/// orders.register_place(cid, now);
34///
35/// // Two subscriptions were closed, none failed.
36/// let report = ctl.shutdown_local(
37///     ShutdownMode::Graceful { deadline: Duration::from_secs(5) },
38///     now,
39///     &mut orders,
40///     || (2, 0),
41/// );
42///
43/// assert_eq!((report.unsubscribed, report.unsubscribe_failed), (2, 0));
44/// assert_eq!(report.inflight_abandoned, vec![cid]);
45/// assert!(task_token.is_cancelled());
46/// assert!(matches!(ctl.gate().check_command(), Err(Error::ShuttingDown)));
47/// ```
48#[derive(Debug)]
49pub struct ShutdownController {
50    /// Parent cancellation token. Cancelled by [`shutdown_local`](Self::shutdown_local).
51    pub root: CancellationToken,
52    gate: CommandGate,
53}
54
55impl ShutdownController {
56    /// Creates a controller that owns the connector's command gate.
57    pub fn new(gate: CommandGate) -> Self {
58        Self {
59            root: CancellationToken::new(),
60            gate,
61        }
62    }
63
64    /// A token for one background task. It is cancelled when the controller shuts down.
65    pub fn child_token(&self) -> CancellationToken {
66        self.root.child_token()
67    }
68
69    /// The command gate, for checking commands.
70    pub fn gate(&self) -> &CommandGate {
71        &self.gate
72    }
73
74    /// The command gate, for marking it reconciled or degraded.
75    pub fn gate_mut(&mut self) -> &mut CommandGate {
76        &mut self.gate
77    }
78
79    /// Runs the local shutdown steps in order and returns a report.
80    ///
81    /// 1. Closes the command gate: new commands fail with [`brokapi_core::Error::ShuttingDown`].
82    /// 2. In [`ShutdownMode::Graceful`] calls `unsubscribe` once. It returns how many
83    ///    subscriptions were closed and how many failed. In [`ShutdownMode::Immediate`] it is not
84    ///    called and both counts are zero.
85    /// 3. Moves in-flight orders to `Unknown` (see [`OrderRuntime::abandon_inflight`]).
86    /// 4. Cancels the root token, which stops every task made with
87    ///    [`child_token`](Self::child_token).
88    ///
89    /// The `deadline` of `Graceful` is not applied here. This method runs to completion. Use
90    /// [`wait_graceful_deadline`](Self::wait_graceful_deadline) to wait for it. Calling the method
91    /// twice is harmless: the second report has no abandoned orders.
92    pub fn shutdown_local(
93        &mut self,
94        mode: ShutdownMode,
95        now: Instant,
96        orders: &mut OrderRuntime,
97        mut unsubscribe: impl FnMut() -> (usize, usize),
98    ) -> ShutdownReport {
99        self.gate.begin_shutdown();
100
101        let (unsubscribed, unsubscribe_failed) = match mode {
102            ShutdownMode::Graceful { .. } => unsubscribe(),
103            // `Immediate`, and a mode of a later version: close nothing in the protocol.
104            _ => (0, 0),
105        };
106
107        let inflight_abandoned = orders.abandon_inflight(now);
108        self.root.cancel();
109
110        ShutdownReport::new(unsubscribed, unsubscribe_failed, inflight_abandoned)
111    }
112
113    /// Sleeps for the `deadline` of a `Graceful` mode, and returns at once for `Immediate`.
114    ///
115    /// It always sleeps the full deadline. It does not return early when the work is done, so use
116    /// it only to bound a wait together with your own completion signal.
117    pub async fn wait_graceful_deadline(&self, mode: ShutdownMode) {
118        if let ShutdownMode::Graceful { deadline } = mode {
119            tokio::time::sleep(deadline).await;
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use brokapi_core::{ClientOrderId, OrderState, ShutdownMode};
128
129    fn cid(n: u8) -> ClientOrderId {
130        ClientOrderId::from_bytes([n; 16])
131    }
132
133    #[tokio::test(start_paused = true)]
134    async fn shutdown_abandons_inflight_orders() {
135        let gate = CommandGate::new();
136        let mut ctl = ShutdownController::new(gate);
137        let mut orders = OrderRuntime::new();
138        let now = Instant::now();
139        orders.register_place(cid(1), now);
140        orders.register_place(cid(2), now);
141
142        let report = ctl.shutdown_local(ShutdownMode::Immediate, now, &mut orders, || (0, 0));
143
144        assert_eq!(report.inflight_abandoned.len(), 2);
145        assert!(matches!(
146            orders.state(cid(1)),
147            Some(OrderState::Unknown { .. })
148        ));
149    }
150}