Skip to main content

brokapi_runtime/
instrument_queue.rs

1//! Per-instrument command serialization (ordered_command_channel: false).
2//!
3//! Commands on one instrument run one at a time, in arrival order (tokio mutexes are FIFO):
4//! a cancel followed by a place cannot be reordered on the wire. The guard is RAII, so the
5//! instrument is released on every exit path, including errors and dropped futures.
6
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9
10use brokapi_core::{ClientOrderId, InstrumentId, InstrumentKey};
11use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard};
12
13/// Held while a command on the instrument is on the wire.
14pub type InstrumentGuard = OwnedMutexGuard<()>;
15
16/// Per-instrument command serialization: one trading command at a time on each instrument.
17///
18/// The lock is keyed by [`InstrumentKey`] (symbol and mic), not by the whole [`InstrumentId`]: the
19/// same instrument reached through a ticker, a uid or a figi is still one instrument. Waiters are
20/// served in the order they asked (the lock is FIFO), so a cancel sent before a place reaches the
21/// broker before it. Why: the broker's command channel is not ordered, and a place that overtakes
22/// a cancel would act on a different book than the caller intended.
23///
24/// Different instruments do not wait for each other. The lock is released when the guard is dropped,
25/// on every exit path, including errors and cancelled futures.
26///
27/// [`bind`](Self::bind) and [`instrument_of`](Self::instrument_of) remember which instrument a cid
28/// belongs to, for commands that carry only a cid (cancel, replace).
29///
30/// # Examples
31///
32/// ```
33/// use brokapi_core::{ClientOrderId, InstrumentId, InstrumentKey, NativeId};
34/// use brokapi_runtime::InstrumentLocks;
35///
36/// # #[tokio::main(flavor = "current_thread")]
37/// # async fn main() {
38/// let sber = InstrumentId::new(
39///     InstrumentKey::new("SBER", "TQBR"),
40///     NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
41/// );
42/// let locks = InstrumentLocks::new();
43/// let cid = ClientOrderId::new_v4();
44/// locks.bind(cid, &sber);
45///
46/// // A place holds the instrument while its command is on the wire.
47/// let guard = locks.lock(&sber).await;
48/// assert!(locks.is_busy(&sber));
49/// drop(guard);
50/// assert!(!locks.is_busy(&sber));
51///
52/// // A cancel carries only the cid. It finds the instrument and takes the same lock.
53/// let guard = locks.lock_cid(cid).await;
54/// assert!(guard.is_some() && locks.is_busy(&sber));
55/// # }
56/// ```
57#[derive(Debug, Default)]
58pub struct InstrumentLocks {
59    locks: Mutex<HashMap<InstrumentKey, Arc<AsyncMutex<()>>>>,
60    by_cid: Mutex<Bound>,
61}
62
63/// The cid -> instrument map with the order the cids came in, so that the oldest can go.
64#[derive(Debug, Default)]
65struct Bound {
66    map: HashMap<ClientOrderId, InstrumentId>,
67    order: std::collections::VecDeque<ClientOrderId>,
68}
69
70/// How many cids [`InstrumentLocks`] remembers. The oldest are forgotten first: a cancel of such an old
71/// order is no longer serialized with the other commands of its instrument.
72pub const BOUND_CIDS: usize = 100_000;
73
74impl InstrumentLocks {
75    /// Creates an empty set of locks.
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Waits for earlier commands on `instrument`, then holds it until the guard drops.
81    pub async fn lock(&self, instrument: &InstrumentId) -> InstrumentGuard {
82        let lock = {
83            let mut map = self.locks.lock().expect("instrument locks poisoned");
84            Arc::clone(map.entry(instrument.key.clone()).or_default())
85        };
86        lock.lock_owned().await
87    }
88
89    /// Whether a command on the instrument is on the wire right now. For tests and diagnostics: the
90    /// answer can be stale by the time the caller acts on it.
91    pub fn is_busy(&self, instrument: &InstrumentId) -> bool {
92        let map = self.locks.lock().expect("instrument locks poisoned");
93        map.get(&instrument.key)
94            .is_some_and(|l| l.try_lock().is_err())
95    }
96
97    /// Remembers the instrument of an order we placed.
98    pub fn bind(&self, cid: ClientOrderId, instrument: &InstrumentId) {
99        let mut b = self.by_cid.lock().expect("instrument locks poisoned");
100        if b.map.insert(cid, instrument.clone()).is_none() {
101            b.order.push_back(cid);
102            while b.order.len() > BOUND_CIDS {
103                if let Some(old) = b.order.pop_front() {
104                    b.map.remove(&old);
105                }
106            }
107        }
108    }
109
110    /// Forgets the instrument of a cid, for an order that is long finished. A later cancel of that cid is
111    /// no longer serialized with the other commands of its instrument.
112    pub fn forget(&self, cid: ClientOrderId) {
113        self.by_cid
114            .lock()
115            .expect("instrument locks poisoned")
116            .map
117            .remove(&cid);
118    }
119
120    /// How many cids are bound to an instrument. For tests and metrics.
121    pub fn bound(&self) -> usize {
122        self.by_cid
123            .lock()
124            .expect("instrument locks poisoned")
125            .map
126            .len()
127    }
128
129    /// The instrument of a cid, if this process placed (or replaced) it.
130    pub fn instrument_of(&self, cid: ClientOrderId) -> Option<InstrumentId> {
131        self.by_cid
132            .lock()
133            .expect("instrument locks poisoned")
134            .map
135            .get(&cid)
136            .cloned()
137    }
138
139    /// Locks the instrument of `cid` when it is known. `None`: an order from before this process
140    /// started, its commands are not serialized with the others.
141    pub async fn lock_cid(&self, cid: ClientOrderId) -> Option<InstrumentGuard> {
142        match self.instrument_of(cid) {
143            Some(i) => Some(self.lock(&i).await),
144            None => None,
145        }
146    }
147}