brokapi_runtime/command_gate.rs
1//! Trading command admission control.
2
3use brokapi_core::Error;
4
5/// Decides whether a trading command may go out now.
6///
7/// A connector asks the gate before it sends a place, cancel or replace. The gate refuses in three
8/// cases, checked in this order: the connector is shutting down, it is degraded, it has not been
9/// reconciled yet. The gate starts closed (not reconciled), so the first reconcile with the broker
10/// opens it. Why: before the client knows which orders already exist at the broker, a new command
11/// could duplicate or fight an order it cannot see.
12///
13/// The gate only holds flags. The connector sets them: [`mark_reconciled`](Self::mark_reconciled)
14/// after a successful reconcile, [`mark_degraded`](Self::mark_degraded) when the
15/// [`Emitter`](crate::Emitter) reports an overflow, [`begin_shutdown`](Self::begin_shutdown) when
16/// closing. Reads (order book, portfolio, active orders) are not gated.
17///
18/// # Examples
19///
20/// ```
21/// use brokapi_core::Error;
22/// use brokapi_runtime::CommandGate;
23///
24/// let mut gate = CommandGate::new();
25/// // A new gate refuses commands until the first reconcile.
26/// assert!(matches!(gate.check_command(), Err(Error::NotReconciled)));
27///
28/// gate.mark_reconciled();
29/// assert!(gate.check_command().is_ok());
30///
31/// // An overflow of a lossless stream closes it again until the next reconcile.
32/// gate.mark_degraded();
33/// assert!(matches!(gate.check_command(), Err(Error::Degraded)));
34/// gate.clear_degraded();
35/// assert!(gate.check_command().is_ok());
36///
37/// // Shutdown wins over everything else and is final.
38/// gate.begin_shutdown();
39/// assert!(matches!(gate.check_command(), Err(Error::ShuttingDown)));
40/// ```
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct CommandGate {
43 reconciled: bool,
44 degraded: bool,
45 shutting_down: bool,
46}
47
48impl Default for CommandGate {
49 /// A closed gate: not reconciled, not degraded, not shutting down.
50 fn default() -> Self {
51 Self {
52 reconciled: false,
53 degraded: false,
54 shutting_down: false,
55 }
56 }
57}
58
59impl CommandGate {
60 /// Creates a closed gate. Same as [`Default::default`].
61 pub fn new() -> Self {
62 Self::default()
63 }
64
65 /// Whether [`mark_reconciled`](Self::mark_reconciled) was called.
66 pub fn is_reconciled(&self) -> bool {
67 self.reconciled
68 }
69
70 /// Whether the gate is in the degraded state.
71 pub fn is_degraded(&self) -> bool {
72 self.degraded
73 }
74
75 /// Records a successful reconcile. There is no way to undo it.
76 pub fn mark_reconciled(&mut self) {
77 self.reconciled = true;
78 }
79
80 /// Records that events were lost (see [`Emitter::is_degraded`](crate::Emitter::is_degraded)).
81 /// Commands are refused until [`clear_degraded`](Self::clear_degraded).
82 pub fn mark_degraded(&mut self) {
83 self.degraded = true;
84 }
85
86 /// Leaves the degraded state. Call it after the reconcile that made up for the lost events.
87 pub fn clear_degraded(&mut self) {
88 self.degraded = false;
89 }
90
91 /// Closes the gate for good: every later check fails with [`Error::ShuttingDown`].
92 pub fn begin_shutdown(&mut self) {
93 self.shutting_down = true;
94 }
95
96 /// Says whether a trading command may be sent now.
97 ///
98 /// # Errors
99 ///
100 /// In this order of priority:
101 ///
102 /// - [`Error::ShuttingDown`] after [`begin_shutdown`](Self::begin_shutdown).
103 /// - [`Error::Degraded`] while the gate is degraded.
104 /// - [`Error::NotReconciled`] until [`mark_reconciled`](Self::mark_reconciled).
105 ///
106 /// All three mean nothing was sent, so the command may be repeated once the cause is gone
107 /// (except after a shutdown).
108 pub fn check_command(&self) -> Result<(), Error> {
109 if self.shutting_down {
110 return Err(Error::ShuttingDown);
111 }
112 if self.degraded {
113 return Err(Error::Degraded);
114 }
115 if !self.reconciled {
116 return Err(Error::NotReconciled);
117 }
118 Ok(())
119 }
120}