Skip to main content

brokapi_alor/
limits.rs

1//! Loads `limits.toml` (section `[alor]`) through `BrokerLimits::from_toml_section`.
2//!
3//! The broker publishes one number for requests (100 a second per user) and the sizes of a WebSocket connection.
4//! The rest of its protection (the social rating) is not published, so the bundled values are the published ones
5//! and nothing more. One `BrokerLimiter` per login: give every connector of one ALOR login the same
6//! `Arc<BrokerLimiter>`.
7
8use brokapi_core::ConfigError;
9use brokapi_runtime::BrokerLimits;
10
11/// The text of the bundled limits file `limits.toml`, compiled into the crate.
12///
13/// Parse it with [`limits_from_toml_str`], or take the result directly from [`default_limits`].
14pub const DEFAULT_LIMITS_TOML: &str = include_str!("../limits.toml");
15
16/// Reads the `[alor]` section of a limits file given as text.
17///
18/// Other sections of the file (other brokers) are ignored.
19///
20/// # Errors
21///
22/// [`brokapi_core::ConfigError::Message`] when the text is not valid TOML, when there is no `[alor]` section, or
23/// when the section is not a valid set of limits (see [`BrokerLimits::from_toml_section`]).
24///
25/// # Examples
26///
27/// ```
28/// use brokapi_alor::limits::{limits_from_toml_str, DEFAULT_LIMITS_TOML};
29///
30/// assert!(limits_from_toml_str(DEFAULT_LIMITS_TOML).is_ok());
31/// // A file for another broker has no [alor] section.
32/// assert!(limits_from_toml_str("[tbank.buckets.global]\nrate = 1").is_err());
33/// ```
34pub fn limits_from_toml_str(raw: &str) -> Result<BrokerLimits, ConfigError> {
35    BrokerLimits::from_toml_section(raw, "alor")
36}
37
38/// Reads the `[alor]` section of a limits file on disk, to tune the limits without recompiling.
39///
40/// # Errors
41///
42/// [`brokapi_core::ConfigError::Message`] when the file cannot be read (the message names the path), and for
43/// every reason [`limits_from_toml_str`] lists.
44///
45/// # Examples
46///
47/// ```
48/// use brokapi_alor::limits::limits_from_toml_path;
49///
50/// let err = limits_from_toml_path("/definitely/not/here.toml").unwrap_err();
51/// assert!(err.to_string().contains("/definitely/not/here.toml"));
52/// ```
53pub fn limits_from_toml_path(path: &str) -> Result<BrokerLimits, ConfigError> {
54    let raw = std::fs::read_to_string(path)
55        .map_err(|e| ConfigError::Message(format!("read {path}: {e}")))?;
56    limits_from_toml_str(&raw)
57}
58
59/// The bundled ALOR limits, parsed.
60///
61/// A method with no route of its own spends the default route `"*"`, so no call is free. Placing goes through a
62/// soft bucket of its own that cancel does not pass.
63///
64/// # Panics
65///
66/// Never in practice: the bundled file is checked by a test of this crate.
67///
68/// # Examples
69///
70/// ```
71/// use brokapi_alor::limits::default_limits;
72/// use brokapi_runtime::BrokerLimiter;
73/// use std::time::Instant;
74///
75/// let limiter = BrokerLimiter::new(&default_limits());
76/// assert!(limiter.acquire("PlaceOrder", Instant::now()).is_ok());
77/// ```
78pub fn default_limits() -> BrokerLimits {
79    limits_from_toml_str(DEFAULT_LIMITS_TOML).expect("bundled alor limits.toml is valid")
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn bundled_limits_parse_and_every_route_points_to_a_bucket() {
88        let l = default_limits();
89        for method in [
90            "PlaceOrder",
91            "CancelOrder",
92            "ReplaceOrder",
93            "PlaceStopOrder",
94            "CancelStopOrder",
95            "GetOrder",
96            "GetOrders",
97            "GetStopOrders",
98            "GetTrades",
99            "GetPositions",
100            "GetSummary",
101            "GetOrderBook",
102            "GetLastTrades",
103            "GetCandles",
104            "GetSecurity",
105            "GetSecurities",
106            "GetTradingStatus",
107            "Reconcile",
108        ] {
109            let buckets = l.buckets_for(method);
110            assert!(!buckets.is_empty(), "{method} is not routed");
111            for b in buckets {
112                assert!(l.buckets.contains_key(&b), "{method}: unknown bucket {b}");
113            }
114        }
115        assert!(!l.buckets_for("SomethingNew").is_empty());
116        // Placing goes through a soft gate that cancel does not pass.
117        assert!(l.route["PlaceOrder"].contains(&"place".to_string()));
118        assert!(!l.route["CancelOrder"].contains(&"place".to_string()));
119        let (place, http) = (&l.buckets["place"], &l.buckets["http"]);
120        assert!(place.rate < http.rate && place.burst < http.burst);
121    }
122
123    #[test]
124    fn a_file_with_no_alor_section_is_refused() {
125        assert!(limits_from_toml_str("[tbank.buckets.global]\nrate=1").is_err());
126    }
127}