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