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