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> {
92 let config = self.build_inner();
93 config.validate()?;
94 Ok(config)
95 }
96}
97
98impl Default for CacheConfig {
99 fn default() -> Self {
100 Self::builder()
101 .build()
102 .expect("default `CacheConfig` should be valid")
103 }
104}
105
106impl CacheConfig {
107 #[expect(clippy::too_many_arguments)]
113 #[must_use]
114 pub fn new(
115 encoding: SerializationEncoding,
116 timestamps_as_iso8601: bool,
117 buffer_interval_ms: Option<usize>,
118 bulk_read_batch_size: Option<usize>,
119 use_trader_prefix: bool,
120 use_instance_id: bool,
121 flush_on_start: bool,
122 drop_instruments_on_reset: bool,
123 tick_capacity: usize,
124 bar_capacity: usize,
125 persist_account_events: bool,
126 save_market_data: bool,
127 ) -> Self {
128 check_cache_data_capacity(tick_capacity, stringify!(tick_capacity)).expect_display(FAILED);
129 check_cache_data_capacity(bar_capacity, stringify!(bar_capacity)).expect_display(FAILED);
130
131 Self {
132 encoding,
133 timestamps_as_iso8601,
134 buffer_interval_ms,
135 bulk_read_batch_size,
136 use_trader_prefix,
137 use_instance_id,
138 flush_on_start,
139 drop_instruments_on_reset,
140 tick_capacity,
141 bar_capacity,
142 persist_account_events,
143 save_market_data,
144 }
145 }
146
147 pub fn validate(&self) -> ConfigResult<()> {
153 let mut errors = ConfigErrorCollector::new();
154
155 for (field, value) in [
156 ("tick_capacity", self.tick_capacity),
157 ("bar_capacity", self.bar_capacity),
158 ] {
159 errors.check(
160 check_cache_data_capacity(value, field).is_ok(),
161 ConfigError::range(
162 field,
163 format!("must be in range [1, {MAX_CACHE_DATA_CAPACITY}], was {value}"),
164 ),
165 );
166 }
167
168 errors.into_result()
169 }
170}
171
172fn deserialize_cache_data_capacity<'de, D>(deserializer: D) -> Result<usize, D::Error>
173where
174 D: Deserializer<'de>,
175{
176 let value = usize::deserialize(deserializer)?;
177 check_cache_data_capacity(value, "capacity").map_err(D::Error::custom)?;
178 Ok(value)
179}
180
181#[cfg(test)]
182mod tests {
183 use rstest::rstest;
184
185 use super::*;
186
187 #[rstest]
188 fn test_default_uses_json_encoding() {
189 let config = CacheConfig::default();
190
191 assert_eq!(config.encoding, SerializationEncoding::Json);
192 }
193
194 #[rstest]
195 #[case(0, 1)]
196 #[case(1, 0)]
197 #[case(MAX_CACHE_DATA_CAPACITY + 1, 1)]
198 #[case(1, MAX_CACHE_DATA_CAPACITY + 1)]
199 #[case(usize::MAX, 1)]
200 #[case(1, usize::MAX)]
201 #[should_panic]
202 fn test_new_rejects_invalid_capacities(
203 #[case] tick_capacity: usize,
204 #[case] bar_capacity: usize,
205 ) {
206 let _ = CacheConfig::new(
207 SerializationEncoding::MsgPack,
208 false,
209 None,
210 None,
211 true,
212 false,
213 false,
214 true,
215 tick_capacity,
216 bar_capacity,
217 true,
218 false,
219 );
220 }
221
222 #[rstest]
223 fn test_new_accepts_maximum_capacities() {
224 let config = CacheConfig::new(
225 SerializationEncoding::MsgPack,
226 false,
227 None,
228 None,
229 true,
230 false,
231 false,
232 true,
233 MAX_CACHE_DATA_CAPACITY,
234 MAX_CACHE_DATA_CAPACITY,
235 true,
236 false,
237 );
238
239 assert_eq!(config.tick_capacity, MAX_CACHE_DATA_CAPACITY);
240 assert_eq!(config.bar_capacity, MAX_CACHE_DATA_CAPACITY);
241 }
242
243 #[rstest]
244 fn test_builder_rejects_zero_tick_capacity() {
245 let result = CacheConfig::builder().tick_capacity(0).build();
246 assert!(
247 matches!(result, Err(ConfigError::Range { field, .. }) if field == "tick_capacity")
248 );
249 }
250
251 #[rstest]
252 fn test_builder_rejects_zero_bar_capacity() {
253 let result = CacheConfig::builder().bar_capacity(0).build();
254 assert!(matches!(result, Err(ConfigError::Range { field, .. }) if field == "bar_capacity"));
255 }
256
257 #[rstest]
258 fn test_builder_accepts_maximum_capacities() {
259 let config = CacheConfig::builder()
260 .tick_capacity(MAX_CACHE_DATA_CAPACITY)
261 .bar_capacity(MAX_CACHE_DATA_CAPACITY)
262 .build()
263 .unwrap();
264
265 assert_eq!(config.tick_capacity, MAX_CACHE_DATA_CAPACITY);
266 assert_eq!(config.bar_capacity, MAX_CACHE_DATA_CAPACITY);
267 }
268
269 #[rstest]
270 #[case(MAX_CACHE_DATA_CAPACITY + 1, 1, "tick_capacity")]
271 #[case(1, MAX_CACHE_DATA_CAPACITY + 1, "bar_capacity")]
272 #[case(usize::MAX, 1, "tick_capacity")]
273 #[case(1, usize::MAX, "bar_capacity")]
274 fn test_validate_rejects_oversized_capacities(
275 #[case] tick_capacity: usize,
276 #[case] bar_capacity: usize,
277 #[case] expected_field: &str,
278 ) {
279 let config = CacheConfig {
280 tick_capacity,
281 bar_capacity,
282 ..Default::default()
283 };
284
285 let err = config
286 .validate()
287 .expect_err("oversized capacity is invalid");
288
289 assert!(matches!(err, ConfigError::Range { field, .. } if field == expected_field));
290 }
291
292 #[rstest]
293 #[case(0, 1, "tick_capacity")]
294 #[case(1, 0, "bar_capacity")]
295 fn test_validate_rejects_zero_capacities(
296 #[case] tick_capacity: usize,
297 #[case] bar_capacity: usize,
298 #[case] expected_field: &str,
299 ) {
300 let config = CacheConfig {
301 tick_capacity,
302 bar_capacity,
303 ..Default::default()
304 };
305
306 let err = config.validate().expect_err("zero capacity is invalid");
307
308 assert!(matches!(err, ConfigError::Range { field, .. } if field == expected_field));
309 }
310
311 #[rstest]
312 #[case("tick_capacity", 0)]
313 #[case("bar_capacity", 0)]
314 #[case("tick_capacity", MAX_CACHE_DATA_CAPACITY + 1)]
315 #[case("bar_capacity", MAX_CACHE_DATA_CAPACITY + 1)]
316 fn test_deserialize_rejects_invalid_capacities(#[case] field: &str, #[case] capacity: usize) {
317 let raw = format!(r#"{{"{field}":{capacity}}}"#);
318 let err = serde_json::from_str::<CacheConfig>(&raw)
319 .expect_err("invalid capacity should fail deserialization");
320
321 assert!(err.to_string().contains(&format!(
322 "invalid usize for 'capacity' not in range [1, {MAX_CACHE_DATA_CAPACITY}], was {capacity}"
323 )));
324 }
325
326 #[rstest]
327 fn test_deserialize_uses_positive_default_capacities() {
328 let config = serde_json::from_str::<CacheConfig>("{}").unwrap();
329
330 assert_eq!(config.tick_capacity, 10_000);
331 assert_eq!(config.bar_capacity, 10_000);
332 }
333}