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