1use std::sync::{Arc, Mutex};
19
20use ahash::AHashMap;
21use bytes::Bytes;
22use indexmap::IndexMap;
23use nautilus_common::{
24 cache::database::{CacheDatabaseAdapter, CacheMap},
25 signal::Signal,
26};
27use nautilus_core::UnixNanos;
28use nautilus_model::{
29 accounts::AccountAny,
30 data::{
31 Bar, CustomData, DataType, FundingRateUpdate, QuoteTick, TradeTick,
32 greeks::{GreeksData, YieldCurveData},
33 },
34 events::{OrderEventAny, OrderSnapshot, position::snapshot::PositionSnapshot},
35 identifiers::{
36 AccountId, ClientId, ClientOrderId, ComponentId, InstrumentId, PositionId, StrategyId,
37 VenueOrderId,
38 },
39 instruments::{InstrumentAny, SyntheticInstrument},
40 orderbook::OrderBook,
41 orders::OrderAny,
42 position::Position,
43 types::{Currency, Money},
44};
45use ustr::Ustr;
46
47#[expect(
48 clippy::struct_excessive_bools,
49 reason = "independent switches cover actor and strategy load and update failures"
50)]
51#[derive(Debug, Default)]
52struct TestCacheDatabaseState {
53 actors: AHashMap<ComponentId, AHashMap<String, Bytes>>,
54 strategies: AHashMap<StrategyId, AHashMap<String, Bytes>>,
55 events: Vec<String>,
56 fail_load_actor: bool,
57 fail_load_strategy: bool,
58 fail_update_actor: bool,
59 fail_update_strategy: bool,
60}
61
62#[derive(Clone, Debug, Default)]
64pub struct TestCacheDatabaseControl {
65 state: Arc<Mutex<TestCacheDatabaseState>>,
66}
67
68#[allow(
69 clippy::missing_panics_doc,
70 reason = "mutex poisoning is not expected in lifecycle tests"
71)]
72impl TestCacheDatabaseControl {
73 #[must_use]
75 pub fn create() -> (TestCacheDatabase, Self) {
76 let control = Self::default();
77 (
78 TestCacheDatabase {
79 control: control.clone(),
80 },
81 control,
82 )
83 }
84
85 pub fn record(&self, event: impl Into<String>) {
87 self.state.lock().unwrap().events.push(event.into());
88 }
89
90 #[must_use]
92 pub fn events(&self) -> Vec<String> {
93 self.state.lock().unwrap().events.clone()
94 }
95
96 pub fn set_actor_state(&self, component_id: ComponentId, state: &IndexMap<String, Vec<u8>>) {
98 self.state
99 .lock()
100 .unwrap()
101 .actors
102 .insert(component_id, encode_state(state));
103 }
104
105 pub fn set_strategy_state(&self, strategy_id: StrategyId, state: &IndexMap<String, Vec<u8>>) {
107 self.state
108 .lock()
109 .unwrap()
110 .strategies
111 .insert(strategy_id, encode_state(state));
112 }
113
114 #[must_use]
116 pub fn actor_state(&self, component_id: &ComponentId) -> Option<IndexMap<String, Vec<u8>>> {
117 self.state
118 .lock()
119 .unwrap()
120 .actors
121 .get(component_id)
122 .cloned()
123 .map(decode_state)
124 }
125
126 #[must_use]
128 pub fn strategy_state(&self, strategy_id: &StrategyId) -> Option<IndexMap<String, Vec<u8>>> {
129 self.state
130 .lock()
131 .unwrap()
132 .strategies
133 .get(strategy_id)
134 .cloned()
135 .map(decode_state)
136 }
137
138 pub fn set_fail_load_actor(&self, fail: bool) {
140 self.state.lock().unwrap().fail_load_actor = fail;
141 }
142
143 pub fn set_fail_load_strategy(&self, fail: bool) {
145 self.state.lock().unwrap().fail_load_strategy = fail;
146 }
147
148 pub fn set_fail_update_actor(&self, fail: bool) {
150 self.state.lock().unwrap().fail_update_actor = fail;
151 }
152
153 pub fn set_fail_update_strategy(&self, fail: bool) {
155 self.state.lock().unwrap().fail_update_strategy = fail;
156 }
157}
158
159#[derive(Debug)]
161pub struct TestCacheDatabase {
162 control: TestCacheDatabaseControl,
163}
164
165#[async_trait::async_trait]
166impl CacheDatabaseAdapter for TestCacheDatabase {
167 fn close(&mut self) -> anyhow::Result<()> {
168 self.control.record("database.close");
169 Ok(())
170 }
171
172 fn flush(&mut self) -> anyhow::Result<()> {
173 Ok(())
174 }
175
176 async fn load_all(&self) -> anyhow::Result<CacheMap> {
177 Ok(CacheMap::default())
178 }
179
180 fn load(&self) -> anyhow::Result<AHashMap<String, Bytes>> {
181 Ok(AHashMap::new())
182 }
183
184 async fn load_currencies(&self) -> anyhow::Result<AHashMap<Ustr, Currency>> {
185 Ok(AHashMap::new())
186 }
187
188 async fn load_instruments(&self) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
189 Ok(AHashMap::new())
190 }
191
192 async fn load_synthetics(&self) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
193 Ok(AHashMap::new())
194 }
195
196 async fn load_accounts(&self) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
197 Ok(AHashMap::new())
198 }
199
200 async fn load_orders(&self) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
201 Ok(AHashMap::new())
202 }
203
204 async fn load_positions(&self) -> anyhow::Result<AHashMap<PositionId, Position>> {
205 Ok(AHashMap::new())
206 }
207
208 fn load_index_order_position(&self) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
209 Ok(AHashMap::new())
210 }
211
212 fn load_index_order_client(&self) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
213 Ok(AHashMap::new())
214 }
215
216 async fn load_currency(&self, _code: &Ustr) -> anyhow::Result<Option<Currency>> {
217 Ok(None)
218 }
219
220 async fn load_instrument(
221 &self,
222 _instrument_id: &InstrumentId,
223 ) -> anyhow::Result<Option<InstrumentAny>> {
224 Ok(None)
225 }
226
227 async fn load_synthetic(
228 &self,
229 _instrument_id: &InstrumentId,
230 ) -> anyhow::Result<Option<SyntheticInstrument>> {
231 Ok(None)
232 }
233
234 async fn load_account(&self, _account_id: &AccountId) -> anyhow::Result<Option<AccountAny>> {
235 Ok(None)
236 }
237
238 async fn load_order(
239 &self,
240 _client_order_id: &ClientOrderId,
241 ) -> anyhow::Result<Option<OrderAny>> {
242 Ok(None)
243 }
244
245 async fn load_position(&self, _position_id: &PositionId) -> anyhow::Result<Option<Position>> {
246 Ok(None)
247 }
248
249 fn load_actor(&self, component_id: &ComponentId) -> anyhow::Result<AHashMap<String, Bytes>> {
250 self.control.record(format!("actor.load:{component_id}"));
251 let state = self.control.state.lock().unwrap();
252 if state.fail_load_actor {
253 anyhow::bail!("test actor load failure");
254 }
255 Ok(state.actors.get(component_id).cloned().unwrap_or_default())
256 }
257
258 fn load_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<AHashMap<String, Bytes>> {
259 self.control.record(format!("strategy.load:{strategy_id}"));
260 let state = self.control.state.lock().unwrap();
261 if state.fail_load_strategy {
262 anyhow::bail!("test strategy load failure");
263 }
264 Ok(state
265 .strategies
266 .get(strategy_id)
267 .cloned()
268 .unwrap_or_default())
269 }
270
271 fn load_signals(&self, _name: &str) -> anyhow::Result<Vec<Signal>> {
272 Ok(Vec::new())
273 }
274
275 fn load_custom_data(&self, _data_type: &DataType) -> anyhow::Result<Vec<CustomData>> {
276 Ok(Vec::new())
277 }
278
279 fn load_order_snapshot(
280 &self,
281 _client_order_id: &ClientOrderId,
282 ) -> anyhow::Result<Option<OrderSnapshot>> {
283 Ok(None)
284 }
285
286 fn load_position_snapshot(
287 &self,
288 _position_id: &PositionId,
289 ) -> anyhow::Result<Option<PositionSnapshot>> {
290 Ok(None)
291 }
292
293 fn load_quotes(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<QuoteTick>> {
294 Ok(Vec::new())
295 }
296
297 fn load_trades(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<TradeTick>> {
298 Ok(Vec::new())
299 }
300
301 fn load_funding_rates(
302 &self,
303 _instrument_id: &InstrumentId,
304 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
305 Ok(Vec::new())
306 }
307
308 fn load_bars(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<Bar>> {
309 Ok(Vec::new())
310 }
311
312 fn add(&self, _key: String, _value: Bytes) -> anyhow::Result<()> {
313 Ok(())
314 }
315
316 fn add_currency(&self, _currency: &Currency) -> anyhow::Result<()> {
317 Ok(())
318 }
319
320 fn add_instrument(&self, _instrument: &InstrumentAny) -> anyhow::Result<()> {
321 Ok(())
322 }
323
324 fn add_synthetic(&self, _synthetic: &SyntheticInstrument) -> anyhow::Result<()> {
325 Ok(())
326 }
327
328 fn add_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
329 Ok(())
330 }
331
332 fn add_order(&self, _order: &OrderAny, _client_id: Option<ClientId>) -> anyhow::Result<()> {
333 Ok(())
334 }
335
336 fn add_order_snapshot(&self, _snapshot: &OrderSnapshot) -> anyhow::Result<()> {
337 Ok(())
338 }
339
340 fn add_position(&self, _position: &Position) -> anyhow::Result<()> {
341 Ok(())
342 }
343
344 fn add_position_snapshot(&self, _snapshot: &PositionSnapshot) -> anyhow::Result<()> {
345 Ok(())
346 }
347
348 fn add_order_book(&self, _order_book: &OrderBook) -> anyhow::Result<()> {
349 Ok(())
350 }
351
352 fn add_signal(&self, _signal: &Signal) -> anyhow::Result<()> {
353 Ok(())
354 }
355
356 fn add_custom_data(&self, _data: &CustomData) -> anyhow::Result<()> {
357 Ok(())
358 }
359
360 fn add_quote(&self, _quote: &QuoteTick) -> anyhow::Result<()> {
361 Ok(())
362 }
363
364 fn add_trade(&self, _trade: &TradeTick) -> anyhow::Result<()> {
365 Ok(())
366 }
367
368 fn add_funding_rate(&self, _funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
369 Ok(())
370 }
371
372 fn add_bar(&self, _bar: &Bar) -> anyhow::Result<()> {
373 Ok(())
374 }
375
376 fn add_greeks(&self, _greeks: &GreeksData) -> anyhow::Result<()> {
377 Ok(())
378 }
379
380 fn add_yield_curve(&self, _yield_curve: &YieldCurveData) -> anyhow::Result<()> {
381 Ok(())
382 }
383
384 fn delete_actor(&self, _component_id: &ComponentId) -> anyhow::Result<()> {
385 Ok(())
386 }
387
388 fn delete_strategy(&self, _component_id: &StrategyId) -> anyhow::Result<()> {
389 Ok(())
390 }
391
392 fn delete_order(&self, _client_order_id: &ClientOrderId) -> anyhow::Result<()> {
393 Ok(())
394 }
395
396 fn delete_position(&self, _position_id: &PositionId) -> anyhow::Result<()> {
397 Ok(())
398 }
399
400 fn delete_account_event(&self, _account_id: &AccountId, _event_id: &str) -> anyhow::Result<()> {
401 Ok(())
402 }
403
404 fn index_venue_order_id(
405 &self,
406 _client_order_id: ClientOrderId,
407 _venue_order_id: VenueOrderId,
408 ) -> anyhow::Result<()> {
409 Ok(())
410 }
411
412 fn index_order_position(
413 &self,
414 _client_order_id: ClientOrderId,
415 _position_id: PositionId,
416 ) -> anyhow::Result<()> {
417 Ok(())
418 }
419
420 fn update_actor(
421 &self,
422 component_id: &ComponentId,
423 actor_state: &AHashMap<String, Bytes>,
424 ) -> anyhow::Result<()> {
425 self.control.record(format!("actor.update:{component_id}"));
426 let mut state = self.control.state.lock().unwrap();
427 if state.fail_update_actor {
428 anyhow::bail!("test actor update failure");
429 }
430 state.actors.insert(*component_id, actor_state.clone());
431 Ok(())
432 }
433
434 fn update_strategy(
435 &self,
436 strategy_id: &StrategyId,
437 strategy_state: &AHashMap<String, Bytes>,
438 ) -> anyhow::Result<()> {
439 self.control
440 .record(format!("strategy.update:{strategy_id}"));
441 let mut state = self.control.state.lock().unwrap();
442 if state.fail_update_strategy {
443 anyhow::bail!("test strategy update failure");
444 }
445 state
446 .strategies
447 .insert(*strategy_id, strategy_state.clone());
448 Ok(())
449 }
450
451 fn update_account(&self, _account: &AccountAny) -> anyhow::Result<()> {
452 Ok(())
453 }
454
455 fn update_order(&self, _order_event: &OrderEventAny) -> anyhow::Result<()> {
456 Ok(())
457 }
458
459 fn update_position(&self, _position: &Position) -> anyhow::Result<()> {
460 Ok(())
461 }
462
463 fn snapshot_order_state(&self, _order: &OrderAny) -> anyhow::Result<()> {
464 Ok(())
465 }
466
467 fn snapshot_position_state(
468 &self,
469 _position: &Position,
470 _ts_snapshot: UnixNanos,
471 _unrealized_pnl: Option<Money>,
472 ) -> anyhow::Result<()> {
473 Ok(())
474 }
475
476 fn heartbeat(&self, _timestamp: UnixNanos) -> anyhow::Result<()> {
477 Ok(())
478 }
479}
480
481fn decode_state(state: AHashMap<String, Bytes>) -> IndexMap<String, Vec<u8>> {
482 state
483 .into_iter()
484 .map(|(key, value)| (key, value.to_vec()))
485 .collect()
486}
487
488fn encode_state(state: &IndexMap<String, Vec<u8>>) -> AHashMap<String, Bytes> {
489 state
490 .iter()
491 .map(|(key, value)| (key.clone(), Bytes::copy_from_slice(value)))
492 .collect()
493}