Skip to main content

nautilus_infrastructure/redis/
cache.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Redis-backed cache database for the system.
17//!
18//! # Architecture
19//!
20//! Uses two Redis connections with distinct roles:
21//! - **READ** (`self.con`): synchronous queries (`keys`, `read`, `load_all`),
22//!   owned by the main struct.
23//! - **WRITE**: owned by a background task on `get_runtime()`, receives
24//!   commands via an unbounded `tokio::sync::mpsc` channel.
25//!
26//! All write operations (`insert`, `update`, `delete`, `flush`) are routed
27//! through the command channel so they execute on the WRITE connection. This
28//! avoids cross-runtime I/O issues since the WRITE connection is always
29//! created on the Nautilus runtime.
30//!
31//! Synchronous callers (`close`, `flushdb_sync`) use `std::sync::mpsc` reply
32//! channels to block until the background task confirms completion. When
33//! called from the Nautilus runtime itself, `block_in_place` is used
34//! automatically to avoid stalling the worker thread.
35
36use std::{
37    collections::VecDeque,
38    fmt::{Debug, Write as _},
39    ops::ControlFlow,
40    pin::Pin,
41    sync::mpsc::{self, SyncSender},
42    time::Duration,
43};
44
45use ahash::AHashMap;
46use anyhow::Context;
47use bytes::Bytes;
48use nautilus_common::{
49    cache::{
50        CacheConfig,
51        database::{CacheDatabaseAdapter, CacheDatabaseFactory, CacheMap},
52    },
53    enums::SerializationEncoding,
54    live::get_runtime,
55    logging::{log_task_awaiting, log_task_started, log_task_stopped},
56    signal::Signal,
57};
58use nautilus_core::{UUID4, UnixNanos, correctness::check_slice_not_empty};
59use nautilus_cryptography::providers::install_cryptographic_provider;
60use nautilus_model::{
61    accounts::AccountAny,
62    data::{
63        Bar, CustomData, DataType, FundingRateUpdate, HasTsInit, InstrumentClose, QuoteTick,
64        TradeTick,
65    },
66    events::{
67        AccountState, OrderEventAny, OrderFilled, OrderSnapshot,
68        position::snapshot::PositionSnapshot,
69    },
70    identifiers::{
71        AccountId, ActorId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId,
72        TraderId, VenueOrderId,
73    },
74    instruments::{Instrument, InstrumentAny, SyntheticInstrument},
75    orderbook::OrderBook,
76    orders::{Order, OrderAny},
77    position::Position,
78    types::{Currency, Money},
79};
80use redis::{AsyncCommands, Pipeline, aio::ConnectionManager};
81use serde::{Deserialize, Serialize};
82use ustr::Ustr;
83
84use super::{REDIS_DELIMITER, REDIS_FLUSHDB, get_index_key};
85use crate::redis::{RedisConnectionConfig, create_redis_connection, queries::DatabaseQueries};
86
87// Task and connection names
88const CACHE_READ: &str = "cache-read";
89const CACHE_WRITE: &str = "cache-write";
90const CACHE_PROCESS: &str = "cache-process";
91
92// Error constants
93const FAILED_TX_CHANNEL: &str = "Failed to send to channel";
94
95// Collection keys
96const INDEX: &str = "index";
97const GENERAL: &str = "general";
98const CURRENCIES: &str = "currencies";
99const INSTRUMENTS: &str = "instruments";
100const INSTRUMENT_CLOSES: &str = "instrument_closes";
101const SYNTHETICS: &str = "synthetics";
102const ACCOUNTS: &str = "accounts";
103const ORDERS: &str = "orders";
104const POSITIONS: &str = "positions";
105const ACTORS: &str = "actors";
106const STRATEGIES: &str = "strategies";
107const SNAPSHOTS: &str = "snapshots";
108const HEALTH: &str = "health";
109const CUSTOM: &str = "custom";
110
111// Index keys
112const INDEX_ORDER_IDS: &str = "index:order_ids";
113const INDEX_ORDER_POSITION: &str = "index:order_position";
114const INDEX_ORDER_CLIENT: &str = "index:order_client";
115const INDEX_ORDERS: &str = "index:orders";
116const INDEX_ORDERS_OPEN: &str = "index:orders_open";
117const INDEX_ORDERS_CLOSED: &str = "index:orders_closed";
118const INDEX_ORDERS_EMULATED: &str = "index:orders_emulated";
119const INDEX_ORDERS_INFLIGHT: &str = "index:orders_inflight";
120const INDEX_POSITIONS: &str = "index:positions";
121const INDEX_POSITIONS_OPEN: &str = "index:positions_open";
122const INDEX_POSITIONS_CLOSED: &str = "index:positions_closed";
123
124/// Configuration for a Redis-backed cache database.
125///
126/// Redis 6.2 or higher is required for correct operation.
127#[cfg_attr(
128    feature = "python",
129    expect(
130        clippy::unsafe_derive_deserialize,
131        reason = "config deserializes plain fields; unsafe methods come from generated PyO3 integration"
132    )
133)]
134#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(default, deny_unknown_fields)]
136#[cfg_attr(
137    feature = "python",
138    pyo3::pyclass(module = "nautilus_trader.infrastructure", from_py_object)
139)]
140#[cfg_attr(
141    feature = "python",
142    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.infrastructure")
143)]
144pub struct RedisCacheConfig {
145    /// The Redis host address. If `None`, `127.0.0.1` is used.
146    pub host: Option<String>,
147    /// The Redis port. If `None`, `6379` is used.
148    pub port: Option<u16>,
149    /// The Redis account username.
150    pub username: Option<String>,
151    /// The Redis account password.
152    pub password: Option<String>,
153    /// If Redis should use an SSL-enabled connection.
154    pub ssl: bool,
155    /// The timeout (in seconds) to wait for a new connection.
156    pub connection_timeout: u16,
157    /// The timeout (in seconds) to wait for a response.
158    pub response_timeout: u16,
159    /// The number of retry attempts with exponential backoff for connection attempts.
160    pub number_of_retries: usize,
161    /// The base value for exponential backoff calculation.
162    pub exponent_base: u64,
163    /// The maximum delay between retry attempts (in seconds).
164    pub max_delay: u64,
165    /// The multiplication factor for retry delay calculation.
166    pub factor: u64,
167}
168
169impl Debug for RedisCacheConfig {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        let redacted = self.password.as_ref().map(|_| "***");
172        f.debug_struct(stringify!(RedisCacheConfig))
173            .field("host", &self.host)
174            .field("port", &self.port)
175            .field("username", &self.username)
176            .field("password", &redacted)
177            .field("ssl", &self.ssl)
178            .field("connection_timeout", &self.connection_timeout)
179            .field("response_timeout", &self.response_timeout)
180            .field("number_of_retries", &self.number_of_retries)
181            .field("exponent_base", &self.exponent_base)
182            .field("max_delay", &self.max_delay)
183            .field("factor", &self.factor)
184            .finish()
185    }
186}
187
188impl Default for RedisCacheConfig {
189    fn default() -> Self {
190        Self {
191            host: None,
192            port: None,
193            username: None,
194            password: None,
195            ssl: false,
196            connection_timeout: 20,
197            response_timeout: 20,
198            number_of_retries: 100,
199            exponent_base: 2,
200            max_delay: 1000,
201            factor: 2,
202        }
203    }
204}
205
206impl RedisConnectionConfig for RedisCacheConfig {
207    fn host(&self) -> Option<&str> {
208        self.host.as_deref()
209    }
210
211    fn port(&self) -> Option<u16> {
212        self.port
213    }
214
215    fn username(&self) -> Option<&str> {
216        self.username.as_deref()
217    }
218
219    fn password(&self) -> Option<&str> {
220        self.password.as_deref()
221    }
222
223    fn ssl(&self) -> bool {
224        self.ssl
225    }
226
227    fn connection_timeout(&self) -> u16 {
228        self.connection_timeout
229    }
230
231    fn response_timeout(&self) -> u16 {
232        self.response_timeout
233    }
234
235    fn number_of_retries(&self) -> usize {
236        self.number_of_retries
237    }
238
239    fn exponent_base(&self) -> u64 {
240        self.exponent_base
241    }
242
243    fn max_delay(&self) -> u64 {
244        self.max_delay
245    }
246
247    fn factor(&self) -> u64 {
248        self.factor
249    }
250}
251
252/// A type of database operation.
253#[derive(Clone, Debug)]
254pub enum DatabaseOperation {
255    Insert,
256    Update,
257    UpdateOrder,
258    ReplaceList,
259    Delete,
260    Flush(SyncSender<()>),
261    Close,
262}
263
264/// Represents a database command to be performed which may be executed in a task.
265#[derive(Clone, Debug)]
266pub struct DatabaseCommand {
267    /// The database operation type.
268    pub op_type: DatabaseOperation,
269    /// The primary key for the operation.
270    pub key: Option<String>,
271    /// The data payload for the operation.
272    pub payload: Option<Vec<Bytes>>,
273}
274
275impl DatabaseCommand {
276    /// Creates a new [`DatabaseCommand`] instance.
277    #[must_use]
278    pub const fn new(op_type: DatabaseOperation, key: String, payload: Option<Vec<Bytes>>) -> Self {
279        Self {
280            op_type,
281            key: Some(key),
282            payload,
283        }
284    }
285
286    /// Initialize a `Close` database command, this is meant to close the database cache channel.
287    #[must_use]
288    pub const fn close() -> Self {
289        Self {
290            op_type: DatabaseOperation::Close,
291            key: None,
292            payload: None,
293        }
294    }
295}
296
297#[cfg_attr(
298    feature = "python",
299    pyo3::pyclass(module = "nautilus_trader.infrastructure")
300)]
301pub struct RedisCacheDatabase {
302    pub con: ConnectionManager,
303    pub trader_id: TraderId,
304    pub trader_key: String,
305    pub encoding: SerializationEncoding,
306    pub bulk_read_batch_size: Option<usize>,
307    tx: tokio::sync::mpsc::UnboundedSender<DatabaseCommand>,
308    handle: Option<tokio::task::JoinHandle<()>>,
309}
310
311impl Debug for RedisCacheDatabase {
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        f.debug_struct(stringify!(RedisCacheDatabase))
314            .field("trader_id", &self.trader_id)
315            .field("encoding", &self.encoding)
316            .finish_non_exhaustive()
317    }
318}
319
320impl RedisCacheDatabase {
321    /// Creates a new [`RedisCacheDatabase`] instance for the given `trader_id`, `instance_id`, and `config`.
322    ///
323    /// # Errors
324    ///
325    /// Returns an error if:
326    /// - The database configuration is missing in `config`.
327    /// - Establishing the Redis connection fails.
328    /// - The command processing task cannot be spawned.
329    pub async fn new(
330        trader_id: TraderId,
331        instance_id: UUID4,
332        config: CacheConfig,
333        database: RedisCacheConfig,
334    ) -> anyhow::Result<Self> {
335        install_cryptographic_provider();
336
337        let con = create_redis_connection(CACHE_READ, &database).await?;
338
339        let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<DatabaseCommand>();
340        let trader_key = get_trader_key(trader_id, instance_id, &config);
341        let trader_key_clone = trader_key.clone();
342        let encoding = config.encoding;
343        let bulk_read_batch_size = config.bulk_read_batch_size;
344
345        let handle = get_runtime().spawn(async move {
346            if let Err(e) =
347                process_commands(rx, trader_key_clone, config.clone(), database.clone()).await
348            {
349                log::error!("Error in task '{CACHE_PROCESS}': {e}");
350            }
351        });
352
353        Ok(Self {
354            con,
355            trader_id,
356            trader_key,
357            encoding,
358            bulk_read_batch_size,
359            tx,
360            handle: Some(handle),
361        })
362    }
363
364    #[must_use]
365    pub const fn get_encoding(&self) -> SerializationEncoding {
366        self.encoding
367    }
368
369    #[must_use]
370    pub fn get_trader_key(&self) -> &str {
371        &self.trader_key
372    }
373
374    pub fn close(&mut self) {
375        log::debug!("Closing");
376
377        let Some(handle) = self.handle.take() else {
378            log::debug!("Already closed");
379            return;
380        };
381
382        if let Err(e) = self.tx.send(DatabaseCommand::close()) {
383            log::debug!("Error sending close command: {e:?}");
384        }
385
386        log_task_awaiting(CACHE_PROCESS);
387
388        let (tx, rx) = mpsc::sync_channel(1);
389
390        get_runtime().spawn(async move {
391            if let Err(e) = handle.await {
392                log::error!("Error awaiting task '{CACHE_PROCESS}': {e:?}");
393            }
394            let _ = tx.send(());
395        });
396        let _ = blocking_recv(&rx);
397
398        log::debug!("Closed");
399    }
400
401    pub async fn flushdb(&mut self) {
402        if let Err(e) = redis::cmd(REDIS_FLUSHDB)
403            .query_async::<()>(&mut self.con)
404            .await
405        {
406            log::error!("Failed to flush database: {e:?}");
407        }
408    }
409
410    /// Sends a flush command through the background task channel and blocks
411    /// until it completes. Safe to call from any runtime context.
412    ///
413    /// # Errors
414    ///
415    /// Returns an error if the command channel is closed or the reply is lost.
416    pub fn flushdb_sync(&self) -> anyhow::Result<()> {
417        let (reply_tx, reply_rx) = mpsc::sync_channel(1);
418        let cmd = DatabaseCommand {
419            op_type: DatabaseOperation::Flush(reply_tx),
420            key: None,
421            payload: None,
422        };
423        self.tx
424            .send(cmd)
425            .map_err(|e| anyhow::anyhow!("{FAILED_TX_CHANNEL}: {e}"))?;
426        blocking_recv(&reply_rx).map_err(|e| anyhow::anyhow!("Failed to flush database: {e}"))?;
427        Ok(())
428    }
429
430    /// Retrieves all keys matching the given `pattern` from Redis for this trader.
431    ///
432    /// # Errors
433    ///
434    /// Returns an error if the underlying Redis scan operation fails.
435    pub async fn keys(&mut self, pattern: &str) -> anyhow::Result<Vec<String>> {
436        let pattern = format!("{}{REDIS_DELIMITER}{pattern}", self.trader_key);
437        DatabaseQueries::scan_keys(&mut self.con, pattern).await
438    }
439
440    /// Reads the value(s) associated with `key` for this trader from Redis.
441    ///
442    /// # Errors
443    ///
444    /// Returns an error if the underlying Redis read operation fails.
445    pub async fn read(&mut self, key: &str) -> anyhow::Result<Vec<Bytes>> {
446        DatabaseQueries::read(&self.con, &self.trader_key, key).await
447    }
448
449    /// Reads multiple values using bulk operations for efficiency.
450    ///
451    /// # Errors
452    ///
453    /// Returns an error if the underlying Redis read operation fails.
454    pub async fn read_bulk(&mut self, keys: &[String]) -> anyhow::Result<Vec<Option<Bytes>>> {
455        match self.bulk_read_batch_size {
456            Some(batch_size) => {
457                DatabaseQueries::read_bulk_batched(&self.con, keys, batch_size).await
458            }
459            None => DatabaseQueries::read_bulk(&self.con, keys).await,
460        }
461    }
462
463    /// Loads custom data from Redis matching the given `data_type` (blocking).
464    ///
465    /// Spawns the async query on the global Nautilus runtime and blocks until
466    /// the result arrives via a channel. Safe from any thread context (Python,
467    /// test runtimes, plain threads).
468    ///
469    /// # Errors
470    ///
471    /// Returns an error if the query fails or the reply channel is closed.
472    pub fn load_custom_data(&self, data_type: &DataType) -> anyhow::Result<Vec<CustomData>> {
473        let con = self.con.clone();
474        let trader_key = self.trader_key.clone();
475        let data_type = data_type.clone();
476        let (tx, rx) = mpsc::channel();
477
478        get_runtime().spawn(async move {
479            let result = DatabaseQueries::load_custom_data(&con, &trader_key, &data_type).await;
480            if let Err(e) = tx.send(result) {
481                log::error!("Failed to send custom data result for '{data_type}': {e:?}");
482            }
483        });
484
485        blocking_recv(&rx).map_err(|e| anyhow::anyhow!("load_custom_data channel closed: {e}"))?
486    }
487
488    /// Sends an insert command for `key` with optional `payload` to Redis via the background task.
489    ///
490    /// # Errors
491    ///
492    /// Returns an error if the command cannot be sent to the background task channel.
493    pub fn insert(&self, key: String, payload: Option<Vec<Bytes>>) -> anyhow::Result<()> {
494        let op = DatabaseCommand::new(DatabaseOperation::Insert, key, payload);
495        match self.tx.send(op) {
496            Ok(()) => Ok(()),
497            Err(e) => anyhow::bail!("{FAILED_TX_CHANNEL}: {e}"),
498        }
499    }
500
501    /// Stores custom data in Redis (key format: `custom:<ts_init_020>:<uuid>`, value: full JSON).
502    ///
503    /// # Errors
504    ///
505    /// Returns an error if serialization fails or the insert command cannot be sent.
506    pub fn add_custom_data(&self, data: &CustomData) -> anyhow::Result<()> {
507        let json_bytes = serde_json::to_vec(data)
508            .map_err(|e| anyhow::anyhow!("CustomData serialization failed: {e}"))?;
509        let ts_init = data.ts_init().as_u64();
510        let key = format!(
511            "{CUSTOM}{REDIS_DELIMITER}{:020}{REDIS_DELIMITER}{}",
512            ts_init,
513            UUID4::new()
514        );
515        self.insert(key, Some(vec![Bytes::from(json_bytes)]))
516    }
517
518    /// Sends an update command for `key` with optional `payload` to Redis via the background task.
519    ///
520    /// # Errors
521    ///
522    /// Returns an error if the command cannot be sent to the background task channel.
523    pub fn update(&mut self, key: String, payload: Option<Vec<Bytes>>) -> anyhow::Result<()> {
524        let op = DatabaseCommand::new(DatabaseOperation::Update, key, payload);
525        match self.tx.send(op) {
526            Ok(()) => Ok(()),
527            Err(e) => anyhow::bail!("{FAILED_TX_CHANNEL}: {e}"),
528        }
529    }
530
531    /// Sends a delete command for `key` with optional `payload` to Redis via the background task.
532    ///
533    /// # Errors
534    ///
535    /// Returns an error if the command cannot be sent to the background task channel.
536    pub fn delete(&mut self, key: String, payload: Option<Vec<Bytes>>) -> anyhow::Result<()> {
537        let op = DatabaseCommand::new(DatabaseOperation::Delete, key, payload);
538        match self.tx.send(op) {
539            Ok(()) => Ok(()),
540            Err(e) => anyhow::bail!("{FAILED_TX_CHANNEL}: {e}"),
541        }
542    }
543
544    /// Delete the given order from the database with full index cleanup.
545    ///
546    /// # Errors
547    ///
548    /// Returns an error if the command cannot be sent to the background task channel.
549    pub fn delete_order(&self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
550        let order_id_bytes = Bytes::from(client_order_id.to_string());
551
552        // Delete the order itself
553        let key = format!("{ORDERS}{REDIS_DELIMITER}{client_order_id}");
554        let op = DatabaseCommand::new(DatabaseOperation::Delete, key, None);
555        self.tx
556            .send(op)
557            .map_err(|e| anyhow::anyhow!("Failed to send delete order command: {e}"))?;
558
559        // Delete from all order indexes
560        let index_keys = [
561            INDEX_ORDER_IDS,
562            INDEX_ORDERS,
563            INDEX_ORDERS_OPEN,
564            INDEX_ORDERS_CLOSED,
565            INDEX_ORDERS_EMULATED,
566            INDEX_ORDERS_INFLIGHT,
567        ];
568
569        for index_key in &index_keys {
570            let key = (*index_key).to_string();
571            let payload = vec![order_id_bytes.clone()];
572            let op = DatabaseCommand::new(DatabaseOperation::Delete, key, Some(payload));
573            self.tx
574                .send(op)
575                .map_err(|e| anyhow::anyhow!("Failed to send delete order index command: {e}"))?;
576        }
577
578        // Delete from hash indexes
579        let hash_indexes = [INDEX_ORDER_POSITION, INDEX_ORDER_CLIENT];
580        for index_key in &hash_indexes {
581            let key = (*index_key).to_string();
582            let payload = vec![order_id_bytes.clone()];
583            let op = DatabaseCommand::new(DatabaseOperation::Delete, key, Some(payload));
584            self.tx.send(op).map_err(|e| {
585                anyhow::anyhow!("Failed to send delete order hash index command: {e}")
586            })?;
587        }
588
589        Ok(())
590    }
591
592    /// Delete the given position from the database with full index cleanup.
593    ///
594    /// # Errors
595    ///
596    /// Returns an error if the command cannot be sent to the background task channel.
597    pub fn delete_position(&self, position_id: &PositionId) -> anyhow::Result<()> {
598        let position_id_bytes = Bytes::from(position_id.to_string());
599
600        // Delete the position itself
601        let key = format!("{POSITIONS}{REDIS_DELIMITER}{position_id}");
602        let op = DatabaseCommand::new(DatabaseOperation::Delete, key, None);
603        self.tx
604            .send(op)
605            .map_err(|e| anyhow::anyhow!("Failed to send delete position command: {e}"))?;
606
607        // Delete from all position indexes
608        let index_keys = [
609            INDEX_POSITIONS,
610            INDEX_POSITIONS_OPEN,
611            INDEX_POSITIONS_CLOSED,
612        ];
613
614        for index_key in &index_keys {
615            let key = (*index_key).to_string();
616            let payload = vec![position_id_bytes.clone()];
617            let op = DatabaseCommand::new(DatabaseOperation::Delete, key, Some(payload));
618            self.tx.send(op).map_err(|e| {
619                anyhow::anyhow!("Failed to send delete position index command: {e}")
620            })?;
621        }
622
623        Ok(())
624    }
625
626    /// Delete the given account event from the database.
627    ///
628    /// # Errors
629    ///
630    /// Returns an error if the command cannot be sent to the background task channel.
631    pub fn delete_account_event(
632        &self,
633        account_id: &AccountId,
634        event_id: &str,
635    ) -> anyhow::Result<()> {
636        log::warn!(
637            "Deleting account events currently a no-op (pending redesign), {account_id}: {event_id}"
638        );
639        Ok(())
640    }
641}
642
643/// Receives a reply, handing off the worker first when called from the Nautilus runtime.
644///
645/// The check is whether a runtime handle is current, not whether this thread is a runtime worker.
646/// Both branches block the caller, so a caller that must not block, such as a live node driven by
647/// a host event loop, cannot use these paths at all and is rejected before it reaches them.
648fn blocking_recv<T>(rx: &mpsc::Receiver<T>) -> Result<T, mpsc::RecvError> {
649    let on_nautilus_runtime =
650        tokio::runtime::Handle::try_current().is_ok_and(|h| h.id() == get_runtime().handle().id());
651
652    if on_nautilus_runtime {
653        tokio::task::block_in_place(|| rx.recv())
654    } else {
655        rx.recv()
656    }
657}
658
659async fn process_commands(
660    mut rx: tokio::sync::mpsc::UnboundedReceiver<DatabaseCommand>,
661    trader_key: String,
662    config: CacheConfig,
663    database: RedisCacheConfig,
664) -> anyhow::Result<()> {
665    log_task_started(CACHE_PROCESS);
666
667    let mut con = create_redis_connection(CACHE_WRITE, &database).await?;
668
669    // Buffering
670    let mut buffer: VecDeque<DatabaseCommand> = VecDeque::new();
671    let buffer_interval = Duration::from_millis(config.buffer_interval_ms.unwrap_or(0) as u64);
672
673    // A sleep used to trigger periodic flushing of the buffer.
674    // When `buffer_interval` is zero we skip using the timer and flush immediately
675    // after every message.
676    let flush_timer = tokio::time::sleep(buffer_interval);
677    tokio::pin!(flush_timer);
678
679    // Continue to receive and handle messages until channel is hung up
680    loop {
681        tokio::select! {
682            maybe_cmd = rx.recv() => {
683                let result = handle_command(
684                    maybe_cmd,
685                    &mut buffer,
686                    buffer_interval,
687                    &mut con,
688                    &trader_key,
689                    config.encoding,
690                ).await;
691
692                if result.is_break() {
693                    break;
694                }
695            }
696            () = &mut flush_timer, if !buffer_interval.is_zero() => {
697                flush_buffer(
698                    &mut buffer,
699                    &mut con,
700                    &trader_key,
701                    config.encoding,
702                    &mut flush_timer,
703                    buffer_interval,
704                ).await;
705            }
706        }
707    }
708
709    // Drain any remaining messages
710    if !buffer.is_empty() {
711        drain_buffer(&mut con, &trader_key, config.encoding, &mut buffer).await;
712    }
713
714    log_task_stopped(CACHE_PROCESS);
715    Ok(())
716}
717
718async fn handle_command(
719    maybe_cmd: Option<DatabaseCommand>,
720    buffer: &mut VecDeque<DatabaseCommand>,
721    buffer_interval: Duration,
722    con: &mut ConnectionManager,
723    trader_key: &str,
724    encoding: SerializationEncoding,
725) -> ControlFlow<()> {
726    let Some(cmd) = maybe_cmd else {
727        log::debug!("Command channel closed");
728        return ControlFlow::Break(());
729    };
730
731    log::trace!("Received {cmd:?}");
732
733    match cmd.op_type {
734        DatabaseOperation::Close => {
735            if !buffer.is_empty() {
736                drain_buffer(con, trader_key, encoding, buffer).await;
737            }
738            return ControlFlow::Break(());
739        }
740        DatabaseOperation::Flush(reply_tx) => {
741            if !buffer.is_empty() {
742                drain_buffer(con, trader_key, encoding, buffer).await;
743            }
744
745            if let Err(e) = redis::cmd(REDIS_FLUSHDB).query_async::<()>(con).await {
746                log::error!("Failed to flush database: {e:?}");
747            }
748            let _ = reply_tx.send(());
749            return ControlFlow::Continue(());
750        }
751        _ => {}
752    }
753
754    buffer.push_back(cmd);
755
756    if buffer_interval.is_zero() {
757        drain_buffer(con, trader_key, encoding, buffer).await;
758    }
759
760    ControlFlow::Continue(())
761}
762
763async fn flush_buffer(
764    buffer: &mut VecDeque<DatabaseCommand>,
765    con: &mut ConnectionManager,
766    trader_key: &str,
767    encoding: SerializationEncoding,
768    flush_timer: &mut Pin<&mut tokio::time::Sleep>,
769    buffer_interval: Duration,
770) {
771    if !buffer.is_empty() {
772        drain_buffer(con, trader_key, encoding, buffer).await;
773    }
774    flush_timer
775        .as_mut()
776        .reset(tokio::time::Instant::now() + buffer_interval);
777}
778
779async fn drain_buffer(
780    conn: &mut ConnectionManager,
781    trader_key: &str,
782    encoding: SerializationEncoding,
783    buffer: &mut VecDeque<DatabaseCommand>,
784) {
785    let mut pipe = redis::pipe();
786    pipe.atomic();
787    let mut has_pending_ops = false;
788
789    for msg in buffer.drain(..) {
790        let Some(key) = msg.key else {
791            log::error!("Null key found for message: {msg:?}");
792            continue;
793        };
794        let collection = match get_collection_key(&key) {
795            Ok(collection) => collection,
796            Err(e) => {
797                log::error!("{e}");
798                continue; // Continue to next message
799            }
800        };
801
802        let key = format!("{trader_key}{REDIS_DELIMITER}{key}");
803
804        match msg.op_type {
805            DatabaseOperation::Insert => {
806                if let Some(payload) = msg.payload {
807                    log::debug!("Processing INSERT for collection: {collection}, key: {key}");
808                    if let Err(e) = insert(&mut pipe, collection, &key, &payload) {
809                        log::error!("{e}");
810                    } else {
811                        has_pending_ops = true;
812                    }
813                } else {
814                    log::error!("Null `payload` for `insert`");
815                }
816            }
817            DatabaseOperation::Update => {
818                if let Some(payload) = msg.payload {
819                    log::debug!("Processing UPDATE for collection: {collection}, key: {key}");
820                    if let Err(e) = update(&mut pipe, collection, &key, &payload) {
821                        log::error!("{e}");
822                    } else {
823                        has_pending_ops = true;
824                    }
825                } else {
826                    log::error!("Null `payload` for `update`");
827                }
828            }
829            DatabaseOperation::UpdateOrder => {
830                flush_pending_pipeline(conn, &mut pipe, &mut has_pending_ops).await;
831
832                if let Some(payload) = msg.payload {
833                    log::debug!("Processing UPDATE_ORDER for key: {key}");
834                    if let Err(e) =
835                        update_order_event_log(conn, trader_key, encoding, &key, &payload).await
836                    {
837                        log::error!("{e}");
838                    }
839                } else {
840                    log::error!("Null `payload` for `update_order`");
841                }
842            }
843            DatabaseOperation::ReplaceList => {
844                if let Some(payload) = msg.payload {
845                    log::debug!("Processing REPLACE_LIST for key: {key}");
846                    if let Err(e) = replace_list_operation(&mut pipe, collection, &key, &payload) {
847                        log::error!("{e}");
848                    } else {
849                        has_pending_ops = true;
850                    }
851                } else {
852                    log::error!("Null `payload` for `replace_list`");
853                }
854            }
855            DatabaseOperation::Delete => {
856                log::debug!(
857                    "Processing DELETE for collection: {}, key: {}, payload: {:?}",
858                    collection,
859                    key,
860                    msg.payload.as_ref().map(std::vec::Vec::len)
861                );
862                // `payload` can be `None` for a delete operation
863                if let Err(e) = delete(&mut pipe, collection, &key, msg.payload) {
864                    log::error!("{e}");
865                } else {
866                    has_pending_ops = true;
867                }
868            }
869            DatabaseOperation::Close => panic!("Close command should not be drained"),
870            DatabaseOperation::Flush(_) => panic!("Flush command should not be drained"),
871        }
872    }
873
874    flush_pending_pipeline(conn, &mut pipe, &mut has_pending_ops).await;
875}
876
877async fn flush_pending_pipeline(
878    conn: &mut ConnectionManager,
879    pipe: &mut Pipeline,
880    has_pending_ops: &mut bool,
881) {
882    if !*has_pending_ops {
883        return;
884    }
885
886    if let Err(e) = pipe.query_async::<()>(conn).await {
887        log::error!("{e}");
888    }
889
890    *pipe = redis::pipe();
891    pipe.atomic();
892    *has_pending_ops = false;
893}
894
895async fn update_order_event_log(
896    conn: &mut ConnectionManager,
897    trader_key: &str,
898    encoding: SerializationEncoding,
899    key: &str,
900    value: &[Bytes],
901) -> anyhow::Result<()> {
902    check_slice_not_empty(value, stringify!(value))?;
903
904    let result: Vec<Bytes> = conn.lrange(key, 0, -1).await?;
905    if result.is_empty() {
906        log::warn!("Cannot update order in Redis, no existing state at {key}");
907        return Ok(());
908    }
909
910    let mut append_pipe = redis::pipe();
911    append_pipe.atomic();
912    update_list(&mut append_pipe, key, value[0].as_ref());
913    append_pipe.query_async::<()>(conn).await?;
914
915    let mut events: Vec<OrderEventAny> = result
916        .iter()
917        .map(|payload| DatabaseQueries::deserialize_payload(encoding, payload))
918        .collect::<anyhow::Result<_>>()
919        .with_context(|| {
920            format!(
921                "Order event append succeeded for {key}, but index replay failed decoding history"
922            )
923        })?;
924    let event: OrderEventAny = DatabaseQueries::deserialize_payload(encoding, value[0].as_ref())
925        .with_context(|| {
926            format!(
927                "Order event append succeeded for {key}, but index replay failed decoding appended event"
928            )
929        })?;
930    events.push(event);
931    let order = OrderAny::from_events(events).with_context(|| {
932        format!("Order event append succeeded for {key}, but index replay failed rebuilding order")
933    })?;
934
935    let mut pipe = redis::pipe();
936    pipe.atomic();
937    update_order_indexes(&mut pipe, trader_key, &order);
938    pipe.query_async::<()>(conn).await?;
939
940    Ok(())
941}
942
943fn insert(pipe: &mut Pipeline, collection: &str, key: &str, value: &[Bytes]) -> anyhow::Result<()> {
944    check_slice_not_empty(value, stringify!(value))?;
945
946    match collection {
947        INDEX => insert_index(pipe, key, value),
948        GENERAL | CURRENCIES | INSTRUMENTS | INSTRUMENT_CLOSES | SYNTHETICS | ACTORS
949        | STRATEGIES | HEALTH | CUSTOM => {
950            insert_string(pipe, key, value[0].as_ref());
951            Ok(())
952        }
953        ACCOUNTS | ORDERS | POSITIONS | SNAPSHOTS => {
954            insert_list(pipe, key, value[0].as_ref());
955            Ok(())
956        }
957        _ => anyhow::bail!("Unsupported operation: `insert` for collection '{collection}'"),
958    }
959}
960
961fn insert_index(pipe: &mut Pipeline, key: &str, value: &[Bytes]) -> anyhow::Result<()> {
962    let index_key = get_index_key(key)?;
963    match index_key {
964        INDEX_ORDER_IDS
965        | INDEX_ORDERS
966        | INDEX_ORDERS_OPEN
967        | INDEX_ORDERS_CLOSED
968        | INDEX_ORDERS_EMULATED
969        | INDEX_ORDERS_INFLIGHT
970        | INDEX_POSITIONS
971        | INDEX_POSITIONS_OPEN
972        | INDEX_POSITIONS_CLOSED => {
973            insert_set(pipe, key, value[0].as_ref());
974            Ok(())
975        }
976        INDEX_ORDER_POSITION => {
977            insert_hset(pipe, key, value[0].as_ref(), value[1].as_ref());
978            Ok(())
979        }
980        INDEX_ORDER_CLIENT => {
981            if !value.len().is_multiple_of(2) {
982                anyhow::bail!(
983                    "Invalid hash index payload for '{index_key}': expected field-value pairs"
984                );
985            }
986
987            let entries = value
988                .as_chunks::<2>()
989                .0
990                .iter()
991                .map(|entry| (entry[0].as_ref(), entry[1].as_ref()))
992                .collect::<Vec<(&[u8], &[u8])>>();
993            pipe.hset_multiple(key, &entries);
994            Ok(())
995        }
996        _ => anyhow::bail!("Index unknown '{index_key}' on insert"),
997    }
998}
999
1000fn insert_string(pipe: &mut Pipeline, key: &str, value: &[u8]) {
1001    pipe.set(key, value);
1002}
1003
1004fn insert_set(pipe: &mut Pipeline, key: &str, value: &[u8]) {
1005    pipe.sadd(key, value);
1006}
1007
1008fn insert_hset(pipe: &mut Pipeline, key: &str, name: &[u8], value: &[u8]) {
1009    pipe.hset(key, name, value);
1010}
1011
1012fn insert_list(pipe: &mut Pipeline, key: &str, value: &[u8]) {
1013    pipe.rpush(key, value);
1014}
1015
1016fn replace_list(pipe: &mut Pipeline, key: &str, value: &[u8]) {
1017    pipe.del(key);
1018    pipe.rpush(key, value);
1019}
1020
1021fn replace_list_operation(
1022    pipe: &mut Pipeline,
1023    collection: &str,
1024    key: &str,
1025    value: &[Bytes],
1026) -> anyhow::Result<()> {
1027    check_slice_not_empty(value, stringify!(value))?;
1028
1029    match collection {
1030        ACCOUNTS | ORDERS | POSITIONS => {
1031            replace_list(pipe, key, value[0].as_ref());
1032            Ok(())
1033        }
1034        _ => anyhow::bail!("Unsupported operation: `replace_list` for collection '{collection}'"),
1035    }
1036}
1037
1038fn update(pipe: &mut Pipeline, collection: &str, key: &str, value: &[Bytes]) -> anyhow::Result<()> {
1039    check_slice_not_empty(value, stringify!(value))?;
1040
1041    match collection {
1042        ACCOUNTS | ORDERS | POSITIONS => {
1043            update_list(pipe, key, value[0].as_ref());
1044            Ok(())
1045        }
1046        _ => anyhow::bail!("Unsupported operation: `update` for collection '{collection}'"),
1047    }
1048}
1049
1050fn update_list(pipe: &mut Pipeline, key: &str, value: &[u8]) {
1051    pipe.rpush_exists(key, value);
1052}
1053
1054fn delete(
1055    pipe: &mut Pipeline,
1056    collection: &str,
1057    key: &str,
1058    value: Option<Vec<Bytes>>,
1059) -> anyhow::Result<()> {
1060    log::debug!(
1061        "delete: collection={}, key={}, has_payload={}",
1062        collection,
1063        key,
1064        value.is_some()
1065    );
1066
1067    match collection {
1068        INDEX => delete_from_index(pipe, key, value),
1069        ORDERS | POSITIONS | ACCOUNTS | ACTORS | STRATEGIES => {
1070            delete_string(pipe, key);
1071            Ok(())
1072        }
1073        _ => anyhow::bail!("Unsupported operation: `delete` for collection '{collection}'"),
1074    }
1075}
1076
1077fn delete_from_index(
1078    pipe: &mut Pipeline,
1079    key: &str,
1080    value: Option<Vec<Bytes>>,
1081) -> anyhow::Result<()> {
1082    let value = value.ok_or_else(|| anyhow::anyhow!("Empty `payload` for `delete` '{key}'"))?;
1083    let index_key = get_index_key(key)?;
1084
1085    match index_key {
1086        INDEX_ORDER_IDS
1087        | INDEX_ORDERS
1088        | INDEX_ORDERS_OPEN
1089        | INDEX_ORDERS_CLOSED
1090        | INDEX_ORDERS_EMULATED
1091        | INDEX_ORDERS_INFLIGHT
1092        | INDEX_POSITIONS
1093        | INDEX_POSITIONS_OPEN
1094        | INDEX_POSITIONS_CLOSED => {
1095            remove_from_set(pipe, key, value[0].as_ref());
1096            Ok(())
1097        }
1098        INDEX_ORDER_POSITION | INDEX_ORDER_CLIENT => {
1099            remove_from_hash(pipe, key, value[0].as_ref());
1100            Ok(())
1101        }
1102        _ => anyhow::bail!("Unsupported index operation: remove from '{index_key}'"),
1103    }
1104}
1105
1106fn remove_from_set(pipe: &mut Pipeline, key: &str, member: &[u8]) {
1107    pipe.srem(key, member);
1108}
1109
1110fn remove_from_hash(pipe: &mut Pipeline, key: &str, field: &[u8]) {
1111    pipe.hdel(key, field);
1112}
1113
1114fn delete_string(pipe: &mut Pipeline, key: &str) {
1115    pipe.del(key);
1116}
1117
1118fn full_redis_key(trader_key: &str, key: &str) -> String {
1119    format!("{trader_key}{REDIS_DELIMITER}{key}")
1120}
1121
1122fn update_order_indexes(pipe: &mut Pipeline, trader_key: &str, order: &OrderAny) {
1123    let client_order_id = order.client_order_id();
1124    let order_id_bytes = client_order_id.to_string();
1125
1126    insert_set(
1127        pipe,
1128        &full_redis_key(trader_key, INDEX_ORDERS),
1129        order_id_bytes.as_bytes(),
1130    );
1131
1132    if order.venue_order_id().is_some() {
1133        insert_set(
1134            pipe,
1135            &full_redis_key(trader_key, INDEX_ORDER_IDS),
1136            order_id_bytes.as_bytes(),
1137        );
1138    }
1139
1140    if order.is_inflight() {
1141        insert_set(
1142            pipe,
1143            &full_redis_key(trader_key, INDEX_ORDERS_INFLIGHT),
1144            order_id_bytes.as_bytes(),
1145        );
1146    } else {
1147        remove_from_set(
1148            pipe,
1149            &full_redis_key(trader_key, INDEX_ORDERS_INFLIGHT),
1150            order_id_bytes.as_bytes(),
1151        );
1152    }
1153
1154    if order.is_open() {
1155        remove_from_set(
1156            pipe,
1157            &full_redis_key(trader_key, INDEX_ORDERS_CLOSED),
1158            order_id_bytes.as_bytes(),
1159        );
1160        insert_set(
1161            pipe,
1162            &full_redis_key(trader_key, INDEX_ORDERS_OPEN),
1163            order_id_bytes.as_bytes(),
1164        );
1165    } else if order.is_closed() {
1166        remove_from_set(
1167            pipe,
1168            &full_redis_key(trader_key, INDEX_ORDERS_OPEN),
1169            order_id_bytes.as_bytes(),
1170        );
1171        insert_set(
1172            pipe,
1173            &full_redis_key(trader_key, INDEX_ORDERS_CLOSED),
1174            order_id_bytes.as_bytes(),
1175        );
1176    }
1177
1178    if order.emulation_trigger().is_some() && !order.is_closed() {
1179        insert_set(
1180            pipe,
1181            &full_redis_key(trader_key, INDEX_ORDERS_EMULATED),
1182            order_id_bytes.as_bytes(),
1183        );
1184    } else {
1185        remove_from_set(
1186            pipe,
1187            &full_redis_key(trader_key, INDEX_ORDERS_EMULATED),
1188            order_id_bytes.as_bytes(),
1189        );
1190    }
1191}
1192
1193fn format_timestamp(timestamp: UnixNanos) -> String {
1194    format!("{:.9}", timestamp.to_datetime_utc())
1195}
1196
1197fn get_trader_key(trader_id: TraderId, instance_id: UUID4, config: &CacheConfig) -> String {
1198    let mut key = String::new();
1199
1200    if config.use_trader_prefix {
1201        key.push_str("trader-");
1202    }
1203
1204    key.push_str(trader_id.as_str());
1205
1206    if config.use_instance_id {
1207        key.push(REDIS_DELIMITER);
1208        write!(key, "{instance_id}").expect("writing to String cannot fail");
1209    }
1210
1211    key
1212}
1213
1214fn get_collection_key(key: &str) -> anyhow::Result<&str> {
1215    key.split_once(REDIS_DELIMITER)
1216        .map(|(collection, _)| collection)
1217        .ok_or_else(|| {
1218            anyhow::anyhow!("Invalid `key`, missing a '{REDIS_DELIMITER}' delimiter, was {key}")
1219        })
1220}
1221
1222#[derive(Debug)]
1223pub struct RedisCacheDatabaseAdapter {
1224    pub database: RedisCacheDatabase,
1225}
1226
1227impl RedisCacheDatabaseAdapter {
1228    fn encoding(&self) -> SerializationEncoding {
1229        self.database.get_encoding()
1230    }
1231
1232    fn send_command(
1233        &self,
1234        op_type: DatabaseOperation,
1235        key: String,
1236        payload: Option<Vec<Bytes>>,
1237    ) -> anyhow::Result<()> {
1238        let op = DatabaseCommand::new(op_type, key, payload);
1239        self.database
1240            .tx
1241            .send(op)
1242            .map_err(|e| anyhow::anyhow!("{FAILED_TX_CHANNEL}: {e}"))
1243    }
1244
1245    fn append_list(&self, key: String, payload: Bytes) -> anyhow::Result<()> {
1246        self.send_command(DatabaseOperation::Update, key, Some(vec![payload]))
1247    }
1248
1249    fn serialize_account_event(&self, account: &AccountAny) -> anyhow::Result<Bytes> {
1250        let event: AccountState = account.last_event().ok_or_else(|| {
1251            anyhow::anyhow!("Cannot persist account with no events: {}", account.id())
1252        })?;
1253        let payload = DatabaseQueries::serialize_payload(self.encoding(), &event)?;
1254        Ok(Bytes::from(payload))
1255    }
1256
1257    fn serialize_order_event(&self, order_event: &OrderEventAny) -> anyhow::Result<Bytes> {
1258        let payload = DatabaseQueries::serialize_payload(self.encoding(), order_event)?;
1259        Ok(Bytes::from(payload))
1260    }
1261
1262    fn serialize_position_event(&self, position: &Position) -> anyhow::Result<Bytes> {
1263        let event: OrderFilled = position.last_event().ok_or_else(|| {
1264            anyhow::anyhow!("Cannot persist position with no events: {}", position.id)
1265        })?;
1266        let payload = DatabaseQueries::serialize_payload(self.encoding(), &event)?;
1267        Ok(Bytes::from(payload))
1268    }
1269
1270    fn load_state(&self, key: String) -> anyhow::Result<AHashMap<String, Bytes>> {
1271        let mut con = self.database.con.clone();
1272        let trader_key = self.database.trader_key.clone();
1273        let encoding = self.encoding();
1274        let (tx, rx) = mpsc::channel();
1275
1276        get_runtime().spawn(async move {
1277            let result = async {
1278                let full_key = format!("{trader_key}{REDIS_DELIMITER}{key}");
1279                let value: Option<Bytes> = con.get(&full_key).await?;
1280                let Some(value) = value else {
1281                    return Ok(AHashMap::new());
1282                };
1283
1284                DatabaseQueries::deserialize_payload(encoding, &value)
1285            }
1286            .await;
1287
1288            if let Err(e) = tx.send(result) {
1289                log::error!("Failed to send state load result for '{key}': {e:?}");
1290            }
1291        });
1292
1293        blocking_recv(&rx).map_err(|e| anyhow::anyhow!("load_state channel closed: {e}"))?
1294    }
1295
1296    fn update_state(&self, key: String, state: &AHashMap<String, Bytes>) -> anyhow::Result<()> {
1297        let payload = DatabaseQueries::serialize_payload(self.encoding(), state)?;
1298        self.database.insert(key, Some(vec![Bytes::from(payload)]))
1299    }
1300
1301    fn replace_list(&self, key: String, payload: Bytes) -> anyhow::Result<()> {
1302        self.send_command(DatabaseOperation::ReplaceList, key, Some(vec![payload]))
1303    }
1304}
1305
1306#[async_trait::async_trait]
1307impl CacheDatabaseFactory for RedisCacheConfig {
1308    async fn create(
1309        &self,
1310        trader_id: TraderId,
1311        instance_id: UUID4,
1312        config: CacheConfig,
1313    ) -> anyhow::Result<Box<dyn CacheDatabaseAdapter>> {
1314        let database =
1315            RedisCacheDatabase::new(trader_id, instance_id, config, self.clone()).await?;
1316        Ok(Box::new(RedisCacheDatabaseAdapter { database }))
1317    }
1318}
1319
1320#[async_trait::async_trait]
1321impl CacheDatabaseAdapter for RedisCacheDatabaseAdapter {
1322    fn close(&mut self) -> anyhow::Result<()> {
1323        self.database.close();
1324        Ok(())
1325    }
1326
1327    fn flush(&mut self) -> anyhow::Result<()> {
1328        self.database.flushdb_sync()
1329    }
1330
1331    async fn load_all(&self) -> anyhow::Result<CacheMap> {
1332        log::debug!("Loading all data");
1333
1334        let (
1335            currencies,
1336            instruments,
1337            instrument_closes,
1338            synthetics,
1339            accounts,
1340            orders,
1341            positions,
1342            greeks,
1343            yield_curves,
1344        ) = tokio::try_join!(
1345            self.load_currencies(),
1346            self.load_instruments(),
1347            self.load_instrument_closes(),
1348            self.load_synthetics(),
1349            self.load_accounts(),
1350            self.load_orders(),
1351            self.load_positions(),
1352            self.load_greeks(),
1353            self.load_yield_curves()
1354        )
1355        .map_err(|e| anyhow::anyhow!("Error loading cache data: {e}"))?;
1356
1357        Ok(CacheMap {
1358            currencies,
1359            instruments,
1360            instrument_closes,
1361            synthetics,
1362            accounts,
1363            orders,
1364            positions,
1365            greeks,
1366            yield_curves,
1367        })
1368    }
1369
1370    fn load(&self) -> anyhow::Result<AHashMap<String, Bytes>> {
1371        let con = self.database.con.clone();
1372        let trader_key = self.database.trader_key.clone();
1373        let (tx, rx) = mpsc::channel();
1374
1375        get_runtime().spawn(async move {
1376            let result = async {
1377                let pattern = format!("{trader_key}{REDIS_DELIMITER}{GENERAL}:*");
1378                let mut con_scan = con.clone();
1379                let keys = DatabaseQueries::scan_keys(&mut con_scan, pattern).await?;
1380                if keys.is_empty() {
1381                    return Ok(AHashMap::new());
1382                }
1383
1384                let values = DatabaseQueries::read_bulk(&con, &keys).await?;
1385                let prefix = format!("{trader_key}{REDIS_DELIMITER}{GENERAL}{REDIS_DELIMITER}");
1386                let mut general = AHashMap::new();
1387
1388                for (key, value) in keys.into_iter().zip(values) {
1389                    let Some(value) = value else {
1390                        continue;
1391                    };
1392
1393                    if let Some(clean_key) = key.strip_prefix(&prefix) {
1394                        general.insert(clean_key.to_string(), value);
1395                    }
1396                }
1397
1398                Ok(general)
1399            }
1400            .await;
1401
1402            if let Err(e) = tx.send(result) {
1403                log::error!("Failed to send general load result: {e:?}");
1404            }
1405        });
1406
1407        blocking_recv(&rx).map_err(|e| anyhow::anyhow!("load channel closed: {e}"))?
1408    }
1409
1410    async fn load_currencies(&self) -> anyhow::Result<AHashMap<Ustr, Currency>> {
1411        DatabaseQueries::load_currencies(
1412            &self.database.con,
1413            &self.database.trader_key,
1414            self.encoding(),
1415        )
1416        .await
1417    }
1418
1419    async fn load_instruments(&self) -> anyhow::Result<AHashMap<InstrumentId, InstrumentAny>> {
1420        DatabaseQueries::load_instruments(
1421            &self.database.con,
1422            &self.database.trader_key,
1423            self.encoding(),
1424        )
1425        .await
1426    }
1427
1428    async fn load_instrument_closes(
1429        &self,
1430    ) -> anyhow::Result<AHashMap<InstrumentId, InstrumentClose>> {
1431        DatabaseQueries::load_instrument_closes(
1432            &self.database.con,
1433            &self.database.trader_key,
1434            self.encoding(),
1435        )
1436        .await
1437    }
1438
1439    async fn load_synthetics(&self) -> anyhow::Result<AHashMap<InstrumentId, SyntheticInstrument>> {
1440        DatabaseQueries::load_synthetics(
1441            &self.database.con,
1442            &self.database.trader_key,
1443            self.encoding(),
1444        )
1445        .await
1446    }
1447
1448    async fn load_accounts(&self) -> anyhow::Result<AHashMap<AccountId, AccountAny>> {
1449        DatabaseQueries::load_accounts(
1450            &self.database.con,
1451            &self.database.trader_key,
1452            self.encoding(),
1453        )
1454        .await
1455    }
1456
1457    async fn load_orders(&self) -> anyhow::Result<AHashMap<ClientOrderId, OrderAny>> {
1458        DatabaseQueries::load_orders(
1459            &self.database.con,
1460            &self.database.trader_key,
1461            self.encoding(),
1462        )
1463        .await
1464    }
1465
1466    async fn load_positions(&self) -> anyhow::Result<AHashMap<PositionId, Position>> {
1467        DatabaseQueries::load_positions(
1468            &self.database.con,
1469            &self.database.trader_key,
1470            self.encoding(),
1471        )
1472        .await
1473    }
1474
1475    fn load_index_order_position(&self) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
1476        let con = self.database.con.clone();
1477        let trader_key = self.database.trader_key.clone();
1478        let (tx, rx) = mpsc::channel();
1479
1480        get_runtime().spawn(async move {
1481            let result = DatabaseQueries::load_index_order_position(&con, &trader_key).await;
1482            if let Err(e) = tx.send(result) {
1483                log::error!("Failed to send load_index_order_position result: {e:?}");
1484            }
1485        });
1486
1487        blocking_recv(&rx)
1488            .map_err(|e| anyhow::anyhow!("load_index_order_position channel closed: {e}"))?
1489    }
1490
1491    fn load_index_order_client(&self) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
1492        let con = self.database.con.clone();
1493        let trader_key = self.database.trader_key.clone();
1494        let (tx, rx) = mpsc::channel();
1495
1496        get_runtime().spawn(async move {
1497            let result = DatabaseQueries::load_index_order_client(&con, &trader_key).await;
1498            if let Err(e) = tx.send(result) {
1499                log::error!("Failed to send load_index_order_client result: {e:?}");
1500            }
1501        });
1502
1503        blocking_recv(&rx)
1504            .map_err(|e| anyhow::anyhow!("load_index_order_client channel closed: {e}"))?
1505    }
1506
1507    async fn load_currency(&self, code: &Ustr) -> anyhow::Result<Option<Currency>> {
1508        DatabaseQueries::load_currency(
1509            &self.database.con,
1510            &self.database.trader_key,
1511            code,
1512            self.encoding(),
1513        )
1514        .await
1515    }
1516
1517    async fn load_instrument(
1518        &self,
1519        instrument_id: &InstrumentId,
1520    ) -> anyhow::Result<Option<InstrumentAny>> {
1521        DatabaseQueries::load_instrument(
1522            &self.database.con,
1523            &self.database.trader_key,
1524            instrument_id,
1525            self.encoding(),
1526        )
1527        .await
1528    }
1529
1530    async fn load_synthetic(
1531        &self,
1532        instrument_id: &InstrumentId,
1533    ) -> anyhow::Result<Option<SyntheticInstrument>> {
1534        DatabaseQueries::load_synthetic(
1535            &self.database.con,
1536            &self.database.trader_key,
1537            instrument_id,
1538            self.encoding(),
1539        )
1540        .await
1541    }
1542
1543    async fn load_account(&self, account_id: &AccountId) -> anyhow::Result<Option<AccountAny>> {
1544        DatabaseQueries::load_account(
1545            &self.database.con,
1546            &self.database.trader_key,
1547            account_id,
1548            self.encoding(),
1549        )
1550        .await
1551    }
1552
1553    async fn load_order(
1554        &self,
1555        client_order_id: &ClientOrderId,
1556    ) -> anyhow::Result<Option<OrderAny>> {
1557        DatabaseQueries::load_order(
1558            &self.database.con,
1559            &self.database.trader_key,
1560            client_order_id,
1561            self.encoding(),
1562        )
1563        .await
1564    }
1565
1566    async fn load_position(&self, position_id: &PositionId) -> anyhow::Result<Option<Position>> {
1567        DatabaseQueries::load_position(
1568            &self.database.con,
1569            &self.database.trader_key,
1570            position_id,
1571            self.encoding(),
1572        )
1573        .await
1574    }
1575
1576    fn load_actor(&self, actor_id: &ActorId) -> anyhow::Result<AHashMap<String, Bytes>> {
1577        let key = format!("{ACTORS}{REDIS_DELIMITER}{actor_id}{REDIS_DELIMITER}state");
1578        self.load_state(key)
1579    }
1580
1581    fn load_strategy(&self, strategy_id: &StrategyId) -> anyhow::Result<AHashMap<String, Bytes>> {
1582        let key = format!("{STRATEGIES}{REDIS_DELIMITER}{strategy_id}{REDIS_DELIMITER}state");
1583        self.load_state(key)
1584    }
1585
1586    fn load_signals(&self, _name: &str) -> anyhow::Result<Vec<Signal>> {
1587        anyhow::bail!("Loading signals from Redis cache adapter not supported")
1588    }
1589
1590    fn load_custom_data(&self, data_type: &DataType) -> anyhow::Result<Vec<CustomData>> {
1591        self.database.load_custom_data(data_type)
1592    }
1593
1594    fn load_order_snapshot(
1595        &self,
1596        _client_order_id: &ClientOrderId,
1597    ) -> anyhow::Result<Option<OrderSnapshot>> {
1598        anyhow::bail!("Loading order snapshots from Redis cache adapter not supported")
1599    }
1600
1601    fn load_position_snapshot(
1602        &self,
1603        _position_id: &PositionId,
1604    ) -> anyhow::Result<Option<PositionSnapshot>> {
1605        anyhow::bail!("Loading position snapshots from Redis cache adapter not supported")
1606    }
1607
1608    fn load_quotes(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<QuoteTick>> {
1609        anyhow::bail!("Loading quote data for Redis cache adapter not supported")
1610    }
1611
1612    fn load_trades(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<TradeTick>> {
1613        anyhow::bail!("Loading market data for Redis cache adapter not supported")
1614    }
1615
1616    fn load_funding_rates(
1617        &self,
1618        _instrument_id: &InstrumentId,
1619    ) -> anyhow::Result<Vec<FundingRateUpdate>> {
1620        anyhow::bail!("Loading market data for Redis cache adapter not supported")
1621    }
1622
1623    fn load_bars(&self, _instrument_id: &InstrumentId) -> anyhow::Result<Vec<Bar>> {
1624        anyhow::bail!("Loading market data for Redis cache adapter not supported")
1625    }
1626
1627    fn add(&self, key: String, value: Bytes) -> anyhow::Result<()> {
1628        let key = format!("{GENERAL}{REDIS_DELIMITER}{key}");
1629        self.database.insert(key, Some(vec![value]))
1630    }
1631
1632    fn add_currency(&self, currency: &Currency) -> anyhow::Result<()> {
1633        let key = format!("{CURRENCIES}{REDIS_DELIMITER}{}", currency.code);
1634        let payload = DatabaseQueries::serialize_payload(self.encoding(), currency)?;
1635        self.database.insert(key, Some(vec![Bytes::from(payload)]))
1636    }
1637
1638    fn add_instrument(&self, instrument: &InstrumentAny) -> anyhow::Result<()> {
1639        let key = format!("{INSTRUMENTS}{REDIS_DELIMITER}{}", instrument.id());
1640        let payload = DatabaseQueries::serialize_payload(self.encoding(), instrument)?;
1641        self.database.insert(key, Some(vec![Bytes::from(payload)]))
1642    }
1643
1644    fn add_instrument_close(&self, close: &InstrumentClose) -> anyhow::Result<()> {
1645        let key = format!(
1646            "{INSTRUMENT_CLOSES}{REDIS_DELIMITER}{}",
1647            close.instrument_id
1648        );
1649        let payload = DatabaseQueries::serialize_payload(self.encoding(), close)?;
1650        self.database.insert(key, Some(vec![Bytes::from(payload)]))
1651    }
1652
1653    fn add_synthetic(&self, synthetic: &SyntheticInstrument) -> anyhow::Result<()> {
1654        let key = format!("{SYNTHETICS}{REDIS_DELIMITER}{}", synthetic.id);
1655        let payload = DatabaseQueries::serialize_payload(self.encoding(), synthetic)?;
1656        self.database.insert(key, Some(vec![Bytes::from(payload)]))
1657    }
1658
1659    fn add_account(&self, account: &AccountAny) -> anyhow::Result<()> {
1660        let account_id = account.id();
1661        let key = format!("{ACCOUNTS}{REDIS_DELIMITER}{account_id}");
1662
1663        let payload = self.serialize_account_event(account)?;
1664        self.database.insert(key, Some(vec![payload]))
1665    }
1666
1667    fn add_order(&self, order: &OrderAny, client_id: Option<ClientId>) -> anyhow::Result<()> {
1668        let client_order_id = order.client_order_id();
1669        let key = format!("{ORDERS}{REDIS_DELIMITER}{client_order_id}");
1670
1671        let event = OrderEventAny::Initialized(order.init_event().clone());
1672        let payload = self.serialize_order_event(&event)?;
1673        self.replace_list(key, payload)?;
1674
1675        let order_id_bytes = Bytes::from(client_order_id.to_string());
1676        self.database
1677            .insert(INDEX_ORDERS.to_string(), Some(vec![order_id_bytes.clone()]))?;
1678
1679        if order.emulation_trigger().is_some() {
1680            self.database.insert(
1681                INDEX_ORDERS_EMULATED.to_string(),
1682                Some(vec![order_id_bytes.clone()]),
1683            )?;
1684        }
1685
1686        if let Some(client_id) = client_id {
1687            self.database.insert(
1688                INDEX_ORDER_CLIENT.to_string(),
1689                Some(vec![order_id_bytes, Bytes::from(client_id.to_string())]),
1690            )?;
1691        }
1692
1693        Ok(())
1694    }
1695
1696    fn add_order_snapshot(&self, snapshot: &OrderSnapshot) -> anyhow::Result<()> {
1697        let key = format!(
1698            "{SNAPSHOTS}{REDIS_DELIMITER}{ORDERS}{REDIS_DELIMITER}{}",
1699            snapshot.client_order_id
1700        );
1701        let payload = DatabaseQueries::serialize_payload(self.encoding(), snapshot)?;
1702        self.database.insert(key, Some(vec![Bytes::from(payload)]))
1703    }
1704
1705    fn add_position(&self, position: &Position) -> anyhow::Result<()> {
1706        let position_id = position.id;
1707        let key = format!("{POSITIONS}{REDIS_DELIMITER}{position_id}");
1708
1709        let payload = self.serialize_position_event(position)?;
1710        self.replace_list(key, payload)?;
1711
1712        let position_id_bytes = Bytes::from(position_id.to_string());
1713        self.database.insert(
1714            INDEX_POSITIONS.to_string(),
1715            Some(vec![position_id_bytes.clone()]),
1716        )?;
1717        self.database.insert(
1718            INDEX_POSITIONS_OPEN.to_string(),
1719            Some(vec![position_id_bytes.clone()]),
1720        )?;
1721        self.send_command(
1722            DatabaseOperation::Delete,
1723            INDEX_POSITIONS_CLOSED.to_string(),
1724            Some(vec![position_id_bytes]),
1725        )?;
1726
1727        Ok(())
1728    }
1729
1730    fn add_position_snapshot(&self, snapshot: &PositionSnapshot) -> anyhow::Result<()> {
1731        let key = format!(
1732            "{SNAPSHOTS}{REDIS_DELIMITER}{POSITIONS}{REDIS_DELIMITER}{}",
1733            snapshot.position_id
1734        );
1735        let payload = DatabaseQueries::serialize_payload(self.encoding(), snapshot)?;
1736        self.database.insert(key, Some(vec![Bytes::from(payload)]))
1737    }
1738
1739    fn add_order_book(&self, _order_book: &OrderBook) -> anyhow::Result<()> {
1740        anyhow::bail!("Saving market data for Redis cache adapter not supported")
1741    }
1742
1743    fn add_signal(&self, _signal: &Signal) -> anyhow::Result<()> {
1744        anyhow::bail!("Saving signals for Redis cache adapter not supported")
1745    }
1746
1747    fn add_custom_data(&self, data: &CustomData) -> anyhow::Result<()> {
1748        self.database.add_custom_data(data)
1749    }
1750
1751    fn add_quote(&self, _quote: &QuoteTick) -> anyhow::Result<()> {
1752        anyhow::bail!("Saving market data for Redis cache adapter not supported")
1753    }
1754
1755    fn add_trade(&self, _trade: &TradeTick) -> anyhow::Result<()> {
1756        anyhow::bail!("Saving market data for Redis cache adapter not supported")
1757    }
1758
1759    fn add_funding_rate(&self, _funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
1760        anyhow::bail!("Saving market data for Redis cache adapter not supported")
1761    }
1762
1763    fn add_bar(&self, _bar: &Bar) -> anyhow::Result<()> {
1764        anyhow::bail!("Saving market data for Redis cache adapter not supported")
1765    }
1766
1767    fn delete_actor(&self, actor_id: &ActorId) -> anyhow::Result<()> {
1768        let key = format!("{ACTORS}{REDIS_DELIMITER}{actor_id}{REDIS_DELIMITER}state");
1769        let op = DatabaseCommand::new(DatabaseOperation::Delete, key, None);
1770        self.database
1771            .tx
1772            .send(op)
1773            .map_err(|e| anyhow::anyhow!("{FAILED_TX_CHANNEL}: {e}"))
1774    }
1775
1776    fn delete_strategy(&self, component_id: &StrategyId) -> anyhow::Result<()> {
1777        let key = format!("{STRATEGIES}{REDIS_DELIMITER}{component_id}{REDIS_DELIMITER}state");
1778        let op = DatabaseCommand::new(DatabaseOperation::Delete, key, None);
1779        self.database
1780            .tx
1781            .send(op)
1782            .map_err(|e| anyhow::anyhow!("{FAILED_TX_CHANNEL}: {e}"))
1783    }
1784
1785    fn delete_order(&self, client_order_id: &ClientOrderId) -> anyhow::Result<()> {
1786        self.database.delete_order(client_order_id)
1787    }
1788
1789    fn delete_position(&self, position_id: &PositionId) -> anyhow::Result<()> {
1790        self.database.delete_position(position_id)
1791    }
1792
1793    fn delete_account_event(&self, account_id: &AccountId, event_id: &str) -> anyhow::Result<()> {
1794        self.database.delete_account_event(account_id, event_id)
1795    }
1796
1797    fn index_venue_order_id(
1798        &self,
1799        client_order_id: ClientOrderId,
1800        venue_order_id: VenueOrderId,
1801    ) -> anyhow::Result<()> {
1802        self.database.insert(
1803            INDEX_ORDER_IDS.to_string(),
1804            Some(vec![Bytes::from(client_order_id.to_string())]),
1805        )?;
1806        log::debug!("Indexed {client_order_id:?} -> {venue_order_id:?}");
1807        Ok(())
1808    }
1809
1810    fn index_order_position(
1811        &self,
1812        client_order_id: ClientOrderId,
1813        position_id: PositionId,
1814    ) -> anyhow::Result<()> {
1815        self.database.insert(
1816            INDEX_ORDER_POSITION.to_string(),
1817            Some(vec![
1818                Bytes::from(client_order_id.to_string()),
1819                Bytes::from(position_id.to_string()),
1820            ]),
1821        )
1822    }
1823
1824    fn index_order_clients(&self, claims: &[(ClientOrderId, ClientId)]) -> anyhow::Result<()> {
1825        if claims.is_empty() {
1826            return Ok(());
1827        }
1828
1829        let mut payload = Vec::with_capacity(claims.len() * 2);
1830        for (client_order_id, client_id) in claims {
1831            payload.push(Bytes::from(client_order_id.to_string()));
1832            payload.push(Bytes::from(client_id.to_string()));
1833        }
1834
1835        self.database
1836            .insert(INDEX_ORDER_CLIENT.to_string(), Some(payload))
1837    }
1838
1839    fn update_actor(
1840        &self,
1841        actor_id: &ActorId,
1842        state: &AHashMap<String, Bytes>,
1843    ) -> anyhow::Result<()> {
1844        let key = format!("{ACTORS}{REDIS_DELIMITER}{actor_id}{REDIS_DELIMITER}state");
1845        self.update_state(key, state)
1846    }
1847
1848    fn update_strategy(
1849        &self,
1850        strategy_id: &StrategyId,
1851        state: &AHashMap<String, Bytes>,
1852    ) -> anyhow::Result<()> {
1853        let key = format!("{STRATEGIES}{REDIS_DELIMITER}{strategy_id}{REDIS_DELIMITER}state");
1854        self.update_state(key, state)
1855    }
1856
1857    fn update_account(&self, account: &AccountAny) -> anyhow::Result<()> {
1858        let account_id = account.id();
1859        let key = format!("{ACCOUNTS}{REDIS_DELIMITER}{account_id}");
1860        let payload = self.serialize_account_event(account)?;
1861        self.append_list(key, payload)
1862    }
1863
1864    fn update_order(&self, order_event: &OrderEventAny) -> anyhow::Result<()> {
1865        let client_order_id = order_event.client_order_id();
1866        let key = format!("{ORDERS}{REDIS_DELIMITER}{client_order_id}");
1867        let payload = DatabaseQueries::serialize_payload(self.encoding(), order_event)?;
1868        let op = DatabaseCommand::new(
1869            DatabaseOperation::UpdateOrder,
1870            key,
1871            Some(vec![Bytes::from(payload)]),
1872        );
1873        self.database
1874            .tx
1875            .send(op)
1876            .map_err(|e| anyhow::anyhow!("{FAILED_TX_CHANNEL}: {e}"))
1877    }
1878
1879    fn update_position(&self, position: &Position) -> anyhow::Result<()> {
1880        let position_id = position.id;
1881        if position.fill_voids.is_empty() {
1882            let key = format!("{POSITIONS}{REDIS_DELIMITER}{position_id}");
1883            let payload = self.serialize_position_event(position)?;
1884            self.append_list(key, payload)?;
1885        } else {
1886            self.add_position_snapshot(&PositionSnapshot::from_replay_state(position, None))?;
1887        }
1888
1889        let position_id_bytes = Bytes::from(position_id.to_string());
1890
1891        if position.is_open() {
1892            self.database.insert(
1893                INDEX_POSITIONS_OPEN.to_string(),
1894                Some(vec![position_id_bytes.clone()]),
1895            )?;
1896            self.send_command(
1897                DatabaseOperation::Delete,
1898                INDEX_POSITIONS_CLOSED.to_string(),
1899                Some(vec![position_id_bytes]),
1900            )?;
1901        } else if position.is_closed() {
1902            self.database.insert(
1903                INDEX_POSITIONS_CLOSED.to_string(),
1904                Some(vec![position_id_bytes.clone()]),
1905            )?;
1906            self.send_command(
1907                DatabaseOperation::Delete,
1908                INDEX_POSITIONS_OPEN.to_string(),
1909                Some(vec![position_id_bytes]),
1910            )?;
1911        }
1912
1913        Ok(())
1914    }
1915
1916    fn snapshot_order_state(&self, order: &OrderAny) -> anyhow::Result<()> {
1917        let snapshot = OrderSnapshot::from(order.clone());
1918        self.add_order_snapshot(&snapshot)
1919    }
1920
1921    fn snapshot_position_state(
1922        &self,
1923        position: &Position,
1924        ts_snapshot: UnixNanos,
1925        unrealized_pnl: Option<Money>,
1926    ) -> anyhow::Result<()> {
1927        let mut snapshot = PositionSnapshot::from(position, unrealized_pnl);
1928        snapshot.ts_init = ts_snapshot;
1929        self.add_position_snapshot(&snapshot)
1930    }
1931
1932    fn heartbeat(&self, timestamp: UnixNanos) -> anyhow::Result<()> {
1933        let timestamp = format_timestamp(timestamp);
1934        self.database.insert(
1935            format!("{HEALTH}{REDIS_DELIMITER}heartbeat"),
1936            Some(vec![Bytes::from(timestamp)]),
1937        )
1938    }
1939}
1940
1941#[cfg(test)]
1942mod tests {
1943    use rstest::rstest;
1944
1945    use super::*;
1946
1947    #[rstest]
1948    fn test_get_trader_key_with_prefix_and_instance_id() {
1949        let trader_id = TraderId::from("tester-123");
1950        let instance_id = UUID4::new();
1951        let config = CacheConfig {
1952            use_instance_id: true,
1953            ..Default::default()
1954        };
1955
1956        let key = get_trader_key(trader_id, instance_id, &config);
1957        assert!(key.starts_with("trader-tester-123:"));
1958        assert!(key.ends_with(&instance_id.to_string()));
1959    }
1960
1961    #[rstest]
1962    fn test_get_collection_key_valid() {
1963        let key = "collection:123";
1964        assert_eq!(get_collection_key(key).unwrap(), "collection");
1965    }
1966
1967    #[rstest]
1968    fn test_get_collection_key_invalid() {
1969        let key = "no_delimiter";
1970        assert!(get_collection_key(key).is_err());
1971    }
1972}