1use nautilus_core::correctness::{
17 CorrectnessResult, CorrectnessResultExt, FAILED, check_in_range_inclusive_usize,
18};
19use serde::{Deserialize, Deserializer, Serialize, de::Error};
20
21use crate::{
22 config::{ConfigError, ConfigErrorCollector, ConfigResult},
23 enums::SerializationEncoding,
24};
25
26pub(super) const MAX_CACHE_DATA_CAPACITY: usize = 1_000_000;
27
28pub(super) fn check_cache_data_capacity(capacity: usize, parameter: &str) -> CorrectnessResult<()> {
29 check_in_range_inclusive_usize(capacity, 1, MAX_CACHE_DATA_CAPACITY, parameter)
30}
31
32#[cfg_attr(
34 feature = "python",
35 pyo3::pyclass(module = "nautilus_trader.common", from_py_object)
36)]
37#[cfg_attr(
38 feature = "python",
39 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
40)]
41#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
42#[builder(finish_fn(name = build_inner, vis = ""))]
43#[serde(default, deny_unknown_fields)]
44pub struct CacheConfig {
45 #[builder(default = SerializationEncoding::Json)]
47 pub encoding: SerializationEncoding,
48 #[builder(default)]
50 pub timestamps_as_iso8601: bool,
51 pub buffer_interval_ms: Option<usize>,
53 pub bulk_read_batch_size: Option<usize>,
56 #[builder(default = true)]
58 pub use_trader_prefix: bool,
59 #[builder(default)]
61 pub use_instance_id: bool,
62 #[builder(default)]
64 pub flush_on_start: bool,
65 #[builder(default = true)]
67 pub drop_instruments_on_reset: bool,
68 #[builder(default = 10_000)]
70 #[serde(deserialize_with = "deserialize_cache_data_capacity")]
71 pub tick_capacity: usize,
72 #[builder(default = 10_000)]
74 #[serde(deserialize_with = "deserialize_cache_data_capacity")]
75 pub bar_capacity: usize,
76 #[builder(default = true)]
78 pub persist_account_events: bool,
79 #[builder(default)]
81 pub save_market_data: bool,
82}
83
84impl<S: cache_config_builder::IsComplete> CacheConfigBuilder<S> {
85 pub fn build(self) -> ConfigResult<CacheConfig> {
91 let config = self.build_inner();
92 config.validate()?;
93 Ok(config)
94 }
95}
96
97impl Default for CacheConfig {
98 fn default() -> Self {
99 Self::builder()
100 .build()
101 .expect("default `CacheConfig` should be valid")
102 }
103}
104
105impl CacheConfig {
106 #[expect(clippy::too_many_arguments)]
112 #[must_use]
113 pub fn new(
114 encoding: SerializationEncoding,
115 timestamps_as_iso8601: bool,
116 buffer_interval_ms: Option<usize>,
117 bulk_read_batch_size: Option<usize>,
118 use_trader_prefix: bool,
119 use_instance_id: bool,
120 flush_on_start: bool,
121 drop_instruments_on_reset: bool,
122 tick_capacity: usize,
123 bar_capacity: usize,
124 persist_account_events: bool,
125 save_market_data: bool,
126 ) -> Self {
127 check_cache_data_capacity(tick_capacity, stringify!(tick_capacity)).expect_display(FAILED);
128 check_cache_data_capacity(bar_capacity, stringify!(bar_capacity)).expect_display(FAILED);
129
130 Self {
131 encoding,
132 timestamps_as_iso8601,
133 buffer_interval_ms,
134 bulk_read_batch_size,
135 use_trader_prefix,
136 use_instance_id,
137 flush_on_start,
138 drop_instruments_on_reset,
139 tick_capacity,
140 bar_capacity,
141 persist_account_events,
142 save_market_data,
143 }
144 }
145
146 pub fn validate(&self) -> ConfigResult<()> {
152 let mut errors = ConfigErrorCollector::new();
153
154 for (field, value) in [
155 ("tick_capacity", self.tick_capacity),
156 ("bar_capacity", self.bar_capacity),
157 ] {
158 errors.check(
159 check_cache_data_capacity(value, field).is_ok(),
160 ConfigError::range(
161 field,
162 format!("must be in range [1, {MAX_CACHE_DATA_CAPACITY}], was {value}"),
163 ),
164 );
165 }
166
167 errors.into_result()
168 }
169}
170
171fn deserialize_cache_data_capacity<'de, D>(deserializer: D) -> Result<usize, D::Error>
172where
173 D: Deserializer<'de>,
174{
175 let value = usize::deserialize(deserializer)?;
176 check_cache_data_capacity(value, "capacity").map_err(D::Error::custom)?;
177 Ok(value)
178}
179
180#[cfg(test)]
181mod tests {
182 use rstest::rstest;
183
184 use super::*;
185
186 #[rstest]
187 fn test_default_uses_json_encoding() {
188 let config = CacheConfig::default();
189
190 assert_eq!(config.encoding, SerializationEncoding::Json);
191 }
192
193 #[rstest]
194 #[case(0, 1)]
195 #[case(1, 0)]
196 #[case(MAX_CACHE_DATA_CAPACITY + 1, 1)]
197 #[case(1, MAX_CACHE_DATA_CAPACITY + 1)]
198 #[case(usize::MAX, 1)]
199 #[case(1, usize::MAX)]
200 #[should_panic]
201 fn test_new_rejects_invalid_capacities(
202 #[case] tick_capacity: usize,
203 #[case] bar_capacity: usize,
204 ) {
205 let _ = CacheConfig::new(
206 SerializationEncoding::MsgPack,
207 false,
208 None,
209 None,
210 true,
211 false,
212 false,
213 true,
214 tick_capacity,
215 bar_capacity,
216 true,
217 false,
218 );
219 }
220
221 #[rstest]
222 fn test_new_accepts_maximum_capacities() {
223 let config = CacheConfig::new(
224 SerializationEncoding::MsgPack,
225 false,
226 None,
227 None,
228 true,
229 false,
230 false,
231 true,
232 MAX_CACHE_DATA_CAPACITY,
233 MAX_CACHE_DATA_CAPACITY,
234 true,
235 false,
236 );
237
238 assert_eq!(config.tick_capacity, MAX_CACHE_DATA_CAPACITY);
239 assert_eq!(config.bar_capacity, MAX_CACHE_DATA_CAPACITY);
240 }
241
242 #[rstest]
243 fn test_builder_rejects_zero_tick_capacity() {
244 let result = CacheConfig::builder().tick_capacity(0).build();
245 assert!(
246 matches!(result, Err(ConfigError::Range { field, .. }) if field == "tick_capacity")
247 );
248 }
249
250 #[rstest]
251 fn test_builder_rejects_zero_bar_capacity() {
252 let result = CacheConfig::builder().bar_capacity(0).build();
253 assert!(matches!(result, Err(ConfigError::Range { field, .. }) if field == "bar_capacity"));
254 }
255
256 #[rstest]
257 fn test_builder_accepts_maximum_capacities() {
258 let config = CacheConfig::builder()
259 .tick_capacity(MAX_CACHE_DATA_CAPACITY)
260 .bar_capacity(MAX_CACHE_DATA_CAPACITY)
261 .build()
262 .unwrap();
263
264 assert_eq!(config.tick_capacity, MAX_CACHE_DATA_CAPACITY);
265 assert_eq!(config.bar_capacity, MAX_CACHE_DATA_CAPACITY);
266 }
267
268 #[rstest]
269 #[case(MAX_CACHE_DATA_CAPACITY + 1, 1, "tick_capacity")]
270 #[case(1, MAX_CACHE_DATA_CAPACITY + 1, "bar_capacity")]
271 #[case(usize::MAX, 1, "tick_capacity")]
272 #[case(1, usize::MAX, "bar_capacity")]
273 fn test_validate_rejects_oversized_capacities(
274 #[case] tick_capacity: usize,
275 #[case] bar_capacity: usize,
276 #[case] expected_field: &str,
277 ) {
278 let config = CacheConfig {
279 tick_capacity,
280 bar_capacity,
281 ..Default::default()
282 };
283
284 let err = config
285 .validate()
286 .expect_err("oversized capacity is invalid");
287
288 assert!(matches!(err, ConfigError::Range { field, .. } if field == expected_field));
289 }
290
291 #[rstest]
292 #[case(0, 1, "tick_capacity")]
293 #[case(1, 0, "bar_capacity")]
294 fn test_validate_rejects_zero_capacities(
295 #[case] tick_capacity: usize,
296 #[case] bar_capacity: usize,
297 #[case] expected_field: &str,
298 ) {
299 let config = CacheConfig {
300 tick_capacity,
301 bar_capacity,
302 ..Default::default()
303 };
304
305 let err = config.validate().expect_err("zero capacity is invalid");
306
307 assert!(matches!(err, ConfigError::Range { field, .. } if field == expected_field));
308 }
309
310 #[rstest]
311 #[case("tick_capacity", 0)]
312 #[case("bar_capacity", 0)]
313 #[case("tick_capacity", MAX_CACHE_DATA_CAPACITY + 1)]
314 #[case("bar_capacity", MAX_CACHE_DATA_CAPACITY + 1)]
315 fn test_deserialize_rejects_invalid_capacities(#[case] field: &str, #[case] capacity: usize) {
316 let raw = format!(r#"{{"{field}":{capacity}}}"#);
317 let err = serde_json::from_str::<CacheConfig>(&raw)
318 .expect_err("invalid capacity should fail deserialization");
319
320 assert!(err.to_string().contains(&format!(
321 "invalid usize for 'capacity' not in range [1, {MAX_CACHE_DATA_CAPACITY}], was {capacity}"
322 )));
323 }
324
325 #[rstest]
326 fn test_deserialize_uses_positive_default_capacities() {
327 let config = serde_json::from_str::<CacheConfig>("{}").unwrap();
328
329 assert_eq!(config.tick_capacity, 10_000);
330 assert_eq!(config.bar_capacity, 10_000);
331 }
332}