pub struct StorageManager { /* private fields */ }Expand description
Manages rotating daily log files for multiple concurrent streams.
§Example
use digdigdig3::core::storage::{StorageManager, StorageConfig, StreamKey};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mgr = StorageManager::new(StorageConfig::default())?;
let key = StreamKey {
exchange: "binance".into(),
account: "spot".into(),
symbol: "BTCUSDT".into(),
stream_kind: "ticker".into(),
};
mgr.append(&key, 1_700_000_000_000, b"payload").await?;
Ok(())
}Implementations§
Source§impl StorageManager
impl StorageManager
Sourcepub fn new(config: StorageConfig) -> Result<Self>
pub fn new(config: StorageConfig) -> Result<Self>
Create a StorageManager. Creates config.root if it doesn’t exist.
Examples found in repository?
examples/e2e_storage_read.rs (line 11)
5async fn main() -> Result<(), Box<dyn std::error::Error>> {
6 let cfg = StorageConfig {
7 root: PathBuf::from("./e2e_test_data"),
8 default_retention_days: 365,
9 orderbook_snapshot_interval_secs: 0,
10 };
11 let mgr = StorageManager::new(cfg)?;
12
13 for (exchange, account, sym, stream) in [
14 ("binance", "spot", "BTCUSDT", "trade"),
15 ("binance", "spot", "BTCUSDT", "ticker"),
16 ("bybit", "spot", "BTCUSDT", "trade"),
17 ("bybit", "spot", "BTCUSDT", "ticker"),
18 ("okx", "spot", "BTC-USDT", "trade"),
19 ("okx", "spot", "BTC-USDT", "ticker"),
20 ] {
21 let key = StreamKey {
22 exchange: exchange.to_string(),
23 account: account.to_string(),
24 symbol: sym.to_string(),
25 stream_kind: stream.to_string(),
26 };
27 // read today only — avoid iterating thousands of days from epoch
28 let now_ms = chrono::Utc::now().timestamp_millis();
29 let day_start = now_ms - (now_ms % 86_400_000);
30 let records = mgr.read_range(&key, day_start, now_ms).await?;
31 let first_ts = records.first().map(|(t, _)| *t);
32 let last_ts = records.last().map(|(t, _)| *t);
33 let span_secs = match (first_ts, last_ts) {
34 (Some(f), Some(l)) => (l - f) / 1000,
35 _ => 0,
36 };
37 println!("{exchange}:{sym}:{stream:8} records={} span={span_secs}s first_ts={first_ts:?} last_ts={last_ts:?}",
38 records.len());
39
40 if let Some((_, payload)) = records.first() {
41 match serde_json::from_slice::<serde_json::Value>(payload) {
42 Ok(v) => {
43 let keys: Vec<_> = v.as_object()
44 .map(|o| o.keys().map(|k| k.as_str()).collect())
45 .unwrap_or_default();
46 println!(" first event keys: {:?}", keys);
47 }
48 Err(e) => println!(" first event PARSE FAIL: {e}"),
49 }
50 }
51 }
52 Ok(())
53}More examples
examples/e2e_canonical.rs (line 14)
8async fn main() -> Result<(), Box<dyn std::error::Error>> {
9 let cfg = StorageConfig {
10 root: PathBuf::from("./e2e_test_data"),
11 default_retention_days: 365,
12 orderbook_snapshot_interval_secs: 0,
13 };
14 let mgr = StorageManager::new(cfg)?;
15
16 let key = StreamKey {
17 exchange: "binance".to_string(),
18 account: "spot".to_string(),
19 symbol: "BTCUSDT".to_string(),
20 stream_kind: "trade".to_string(),
21 };
22
23 let now_ms = chrono::Utc::now().timestamp_millis();
24 let day_start = now_ms - (now_ms % 86_400_000);
25 let records = mgr.read_range(&key, day_start, now_ms).await?;
26
27 println!("Total Binance trade records: {}", records.len());
28
29 let sample: Vec<_> = records.iter().take(10).collect();
30 let mut ok = 0u32;
31 let mut failed = 0u32;
32
33 for (i, (ts_ms, payload)) in sample.iter().enumerate() {
34 match serde_json::from_slice::<StreamEvent>(payload) {
35 Ok(event) => {
36 match event.canonicalize() {
37 Some(CanonicalEvent::Trade(t)) => {
38 let price_ok = t.price > rust_decimal::Decimal::ZERO;
39 let qty_ok = t.quantity > rust_decimal::Decimal::ZERO;
40 let ts_ok = t.timestamp_ms > 1_700_000_000_000;
41 if price_ok && qty_ok && ts_ok {
42 ok += 1;
43 println!("[{i}] OK price={} qty={} ts_ms={} side={:?}",
44 t.price, t.quantity, t.timestamp_ms, t.side);
45 } else {
46 failed += 1;
47 println!("[{i}] BAD price={} qty={} ts_ms={} (record ts_ms={})",
48 t.price, t.quantity, t.timestamp_ms, ts_ms);
49 }
50 }
51 Some(other) => {
52 failed += 1;
53 println!("[{i}] WRONG VARIANT: {:?}", other);
54 }
55 None => {
56 failed += 1;
57 println!("[{i}] canonicalize() returned None (record ts_ms={})", ts_ms);
58 }
59 }
60 }
61 Err(e) => {
62 failed += 1;
63 println!("[{i}] DESERIALIZE FAIL: {e}");
64 }
65 }
66 }
67
68 println!("\nResult: {ok}/10 canonical OK, {failed} failed");
69 Ok(())
70}examples/replay_demo.rs (lines 29-33)
18async fn main() -> Result<(), Box<dyn std::error::Error>> {
19 // ── 1. Write synthetic events ─────────────────────────────────────────────
20
21 let tmp_dir = {
22 let mut d = std::env::temp_dir();
23 d.push("dig3_replay_demo");
24 d
25 };
26 std::fs::remove_dir_all(&tmp_dir).ok();
27 std::fs::create_dir_all(&tmp_dir)?;
28
29 let storage = StorageManager::new(StorageConfig {
30 root: tmp_dir.clone(),
31 default_retention_days: 365,
32 orderbook_snapshot_interval_secs: 0,
33 })?;
34
35 let key = StreamKey {
36 exchange: "binance".into(),
37 account: "spot".into(),
38 symbol: "BTCUSDT".into(),
39 stream_kind: "ticker".into(),
40 };
41
42 let base_ms = chrono::Utc::now().timestamp_millis();
43 let n_events = 200usize;
44
45 for i in 0..n_events {
46 let ts_ms = base_ms + (i as i64) * 100; // 100 ms apart → 20 s simulated
47 let price = 50_000.0 + i as f64;
48 let ev = StreamEvent::Ticker(Ticker {
49 symbol: "BTCUSDT".into(),
50 last_price: price,
51 bid_price: Some(price - 1.0),
52 ask_price: Some(price + 1.0),
53 volume_24h: Some(1000.0),
54 quote_volume_24h: None,
55 price_change_24h: None,
56 price_change_percent_24h: None,
57 high_24h: None,
58 low_24h: None,
59 timestamp: ts_ms,
60 });
61 let payload = serde_json::to_vec(&ev)?;
62 storage.append(&key, ts_ms, &payload).await?;
63 }
64 storage.flush_all().await?;
65 println!("Wrote {n_events} synthetic ticker events to {}", tmp_dir.display());
66
67 // ── 2. Replay at 10× speed ────────────────────────────────────────────────
68
69 let config = ReplayConfig {
70 storage_root: PathBuf::from(&tmp_dir),
71 rate: ReplayRate::Accelerated(10.0),
72 from_ms: Some(base_ms),
73 to_ms: Some(base_ms + (n_events as i64) * 100),
74 };
75 let hub = ReplayHub::new(config).await?;
76 hub.connect_full(ExchangeId::Binance, &[AccountType::Spot], false)
77 .await?;
78
79 let ws = hub.ws(ExchangeId::Binance, AccountType::Spot).unwrap();
80 ws.subscribe(SubscriptionRequest::ticker(Symbol::with_raw(
81 "BTC",
82 "USDT",
83 "BTCUSDT".into(),
84 )))
85 .await?;
86
87 let mut stream = ws.event_stream();
88 let mut count = 0usize;
89 let start = std::time::Instant::now();
90
91 while let Some(ev) = stream.next().await {
92 match ev {
93 Ok(StreamEvent::Ticker(_t)) => {
94 count += 1;
95 if count % 50 == 0 {
96 println!(" {count} events received…");
97 }
98 if count >= n_events {
99 break;
100 }
101 }
102 Ok(_other) => {}
103 Err(e) => eprintln!("stream error: {e}"),
104 }
105 }
106
107 let elapsed = start.elapsed();
108 println!(
109 "Replay done: {count} events in {:.2}s (simulated 20s at 10×)",
110 elapsed.as_secs_f64()
111 );
112
113 // ── 3. Cleanup ────────────────────────────────────────────────────────────
114 std::fs::remove_dir_all(&tmp_dir).ok();
115
116 Ok(())
117}Sourcepub async fn append(
&self,
key: &StreamKey,
ts_ms: i64,
payload: &[u8],
) -> Result<()>
pub async fn append( &self, key: &StreamKey, ts_ms: i64, payload: &[u8], ) -> Result<()>
Append one record to the stream identified by key.
Rotates to a new daily file automatically at UTC midnight.
Examples found in repository?
examples/replay_demo.rs (line 62)
18async fn main() -> Result<(), Box<dyn std::error::Error>> {
19 // ── 1. Write synthetic events ─────────────────────────────────────────────
20
21 let tmp_dir = {
22 let mut d = std::env::temp_dir();
23 d.push("dig3_replay_demo");
24 d
25 };
26 std::fs::remove_dir_all(&tmp_dir).ok();
27 std::fs::create_dir_all(&tmp_dir)?;
28
29 let storage = StorageManager::new(StorageConfig {
30 root: tmp_dir.clone(),
31 default_retention_days: 365,
32 orderbook_snapshot_interval_secs: 0,
33 })?;
34
35 let key = StreamKey {
36 exchange: "binance".into(),
37 account: "spot".into(),
38 symbol: "BTCUSDT".into(),
39 stream_kind: "ticker".into(),
40 };
41
42 let base_ms = chrono::Utc::now().timestamp_millis();
43 let n_events = 200usize;
44
45 for i in 0..n_events {
46 let ts_ms = base_ms + (i as i64) * 100; // 100 ms apart → 20 s simulated
47 let price = 50_000.0 + i as f64;
48 let ev = StreamEvent::Ticker(Ticker {
49 symbol: "BTCUSDT".into(),
50 last_price: price,
51 bid_price: Some(price - 1.0),
52 ask_price: Some(price + 1.0),
53 volume_24h: Some(1000.0),
54 quote_volume_24h: None,
55 price_change_24h: None,
56 price_change_percent_24h: None,
57 high_24h: None,
58 low_24h: None,
59 timestamp: ts_ms,
60 });
61 let payload = serde_json::to_vec(&ev)?;
62 storage.append(&key, ts_ms, &payload).await?;
63 }
64 storage.flush_all().await?;
65 println!("Wrote {n_events} synthetic ticker events to {}", tmp_dir.display());
66
67 // ── 2. Replay at 10× speed ────────────────────────────────────────────────
68
69 let config = ReplayConfig {
70 storage_root: PathBuf::from(&tmp_dir),
71 rate: ReplayRate::Accelerated(10.0),
72 from_ms: Some(base_ms),
73 to_ms: Some(base_ms + (n_events as i64) * 100),
74 };
75 let hub = ReplayHub::new(config).await?;
76 hub.connect_full(ExchangeId::Binance, &[AccountType::Spot], false)
77 .await?;
78
79 let ws = hub.ws(ExchangeId::Binance, AccountType::Spot).unwrap();
80 ws.subscribe(SubscriptionRequest::ticker(Symbol::with_raw(
81 "BTC",
82 "USDT",
83 "BTCUSDT".into(),
84 )))
85 .await?;
86
87 let mut stream = ws.event_stream();
88 let mut count = 0usize;
89 let start = std::time::Instant::now();
90
91 while let Some(ev) = stream.next().await {
92 match ev {
93 Ok(StreamEvent::Ticker(_t)) => {
94 count += 1;
95 if count % 50 == 0 {
96 println!(" {count} events received…");
97 }
98 if count >= n_events {
99 break;
100 }
101 }
102 Ok(_other) => {}
103 Err(e) => eprintln!("stream error: {e}"),
104 }
105 }
106
107 let elapsed = start.elapsed();
108 println!(
109 "Replay done: {count} events in {:.2}s (simulated 20s at 10×)",
110 elapsed.as_secs_f64()
111 );
112
113 // ── 3. Cleanup ────────────────────────────────────────────────────────────
114 std::fs::remove_dir_all(&tmp_dir).ok();
115
116 Ok(())
117}Sourcepub async fn flush_all(&self) -> Result<()>
pub async fn flush_all(&self) -> Result<()>
Flush all open writers to the OS.
Examples found in repository?
examples/replay_demo.rs (line 64)
18async fn main() -> Result<(), Box<dyn std::error::Error>> {
19 // ── 1. Write synthetic events ─────────────────────────────────────────────
20
21 let tmp_dir = {
22 let mut d = std::env::temp_dir();
23 d.push("dig3_replay_demo");
24 d
25 };
26 std::fs::remove_dir_all(&tmp_dir).ok();
27 std::fs::create_dir_all(&tmp_dir)?;
28
29 let storage = StorageManager::new(StorageConfig {
30 root: tmp_dir.clone(),
31 default_retention_days: 365,
32 orderbook_snapshot_interval_secs: 0,
33 })?;
34
35 let key = StreamKey {
36 exchange: "binance".into(),
37 account: "spot".into(),
38 symbol: "BTCUSDT".into(),
39 stream_kind: "ticker".into(),
40 };
41
42 let base_ms = chrono::Utc::now().timestamp_millis();
43 let n_events = 200usize;
44
45 for i in 0..n_events {
46 let ts_ms = base_ms + (i as i64) * 100; // 100 ms apart → 20 s simulated
47 let price = 50_000.0 + i as f64;
48 let ev = StreamEvent::Ticker(Ticker {
49 symbol: "BTCUSDT".into(),
50 last_price: price,
51 bid_price: Some(price - 1.0),
52 ask_price: Some(price + 1.0),
53 volume_24h: Some(1000.0),
54 quote_volume_24h: None,
55 price_change_24h: None,
56 price_change_percent_24h: None,
57 high_24h: None,
58 low_24h: None,
59 timestamp: ts_ms,
60 });
61 let payload = serde_json::to_vec(&ev)?;
62 storage.append(&key, ts_ms, &payload).await?;
63 }
64 storage.flush_all().await?;
65 println!("Wrote {n_events} synthetic ticker events to {}", tmp_dir.display());
66
67 // ── 2. Replay at 10× speed ────────────────────────────────────────────────
68
69 let config = ReplayConfig {
70 storage_root: PathBuf::from(&tmp_dir),
71 rate: ReplayRate::Accelerated(10.0),
72 from_ms: Some(base_ms),
73 to_ms: Some(base_ms + (n_events as i64) * 100),
74 };
75 let hub = ReplayHub::new(config).await?;
76 hub.connect_full(ExchangeId::Binance, &[AccountType::Spot], false)
77 .await?;
78
79 let ws = hub.ws(ExchangeId::Binance, AccountType::Spot).unwrap();
80 ws.subscribe(SubscriptionRequest::ticker(Symbol::with_raw(
81 "BTC",
82 "USDT",
83 "BTCUSDT".into(),
84 )))
85 .await?;
86
87 let mut stream = ws.event_stream();
88 let mut count = 0usize;
89 let start = std::time::Instant::now();
90
91 while let Some(ev) = stream.next().await {
92 match ev {
93 Ok(StreamEvent::Ticker(_t)) => {
94 count += 1;
95 if count % 50 == 0 {
96 println!(" {count} events received…");
97 }
98 if count >= n_events {
99 break;
100 }
101 }
102 Ok(_other) => {}
103 Err(e) => eprintln!("stream error: {e}"),
104 }
105 }
106
107 let elapsed = start.elapsed();
108 println!(
109 "Replay done: {count} events in {:.2}s (simulated 20s at 10×)",
110 elapsed.as_secs_f64()
111 );
112
113 // ── 3. Cleanup ────────────────────────────────────────────────────────────
114 std::fs::remove_dir_all(&tmp_dir).ok();
115
116 Ok(())
117}Sourcepub async fn read_range(
&self,
key: &StreamKey,
from_ms: i64,
to_ms: i64,
) -> Result<Vec<(i64, Vec<u8>)>>
pub async fn read_range( &self, key: &StreamKey, from_ms: i64, to_ms: i64, ) -> Result<Vec<(i64, Vec<u8>)>>
Read records in [from_ms, to_ms] (inclusive) for key.
Spans multiple daily files automatically. Accepts any i64 (including
i64::MAX sentinel for “read to end”) — values outside the
representable date range are clamped to [0, MAX_SAFE_MS].
Examples found in repository?
examples/e2e_storage_read.rs (line 30)
5async fn main() -> Result<(), Box<dyn std::error::Error>> {
6 let cfg = StorageConfig {
7 root: PathBuf::from("./e2e_test_data"),
8 default_retention_days: 365,
9 orderbook_snapshot_interval_secs: 0,
10 };
11 let mgr = StorageManager::new(cfg)?;
12
13 for (exchange, account, sym, stream) in [
14 ("binance", "spot", "BTCUSDT", "trade"),
15 ("binance", "spot", "BTCUSDT", "ticker"),
16 ("bybit", "spot", "BTCUSDT", "trade"),
17 ("bybit", "spot", "BTCUSDT", "ticker"),
18 ("okx", "spot", "BTC-USDT", "trade"),
19 ("okx", "spot", "BTC-USDT", "ticker"),
20 ] {
21 let key = StreamKey {
22 exchange: exchange.to_string(),
23 account: account.to_string(),
24 symbol: sym.to_string(),
25 stream_kind: stream.to_string(),
26 };
27 // read today only — avoid iterating thousands of days from epoch
28 let now_ms = chrono::Utc::now().timestamp_millis();
29 let day_start = now_ms - (now_ms % 86_400_000);
30 let records = mgr.read_range(&key, day_start, now_ms).await?;
31 let first_ts = records.first().map(|(t, _)| *t);
32 let last_ts = records.last().map(|(t, _)| *t);
33 let span_secs = match (first_ts, last_ts) {
34 (Some(f), Some(l)) => (l - f) / 1000,
35 _ => 0,
36 };
37 println!("{exchange}:{sym}:{stream:8} records={} span={span_secs}s first_ts={first_ts:?} last_ts={last_ts:?}",
38 records.len());
39
40 if let Some((_, payload)) = records.first() {
41 match serde_json::from_slice::<serde_json::Value>(payload) {
42 Ok(v) => {
43 let keys: Vec<_> = v.as_object()
44 .map(|o| o.keys().map(|k| k.as_str()).collect())
45 .unwrap_or_default();
46 println!(" first event keys: {:?}", keys);
47 }
48 Err(e) => println!(" first event PARSE FAIL: {e}"),
49 }
50 }
51 }
52 Ok(())
53}More examples
examples/e2e_canonical.rs (line 25)
8async fn main() -> Result<(), Box<dyn std::error::Error>> {
9 let cfg = StorageConfig {
10 root: PathBuf::from("./e2e_test_data"),
11 default_retention_days: 365,
12 orderbook_snapshot_interval_secs: 0,
13 };
14 let mgr = StorageManager::new(cfg)?;
15
16 let key = StreamKey {
17 exchange: "binance".to_string(),
18 account: "spot".to_string(),
19 symbol: "BTCUSDT".to_string(),
20 stream_kind: "trade".to_string(),
21 };
22
23 let now_ms = chrono::Utc::now().timestamp_millis();
24 let day_start = now_ms - (now_ms % 86_400_000);
25 let records = mgr.read_range(&key, day_start, now_ms).await?;
26
27 println!("Total Binance trade records: {}", records.len());
28
29 let sample: Vec<_> = records.iter().take(10).collect();
30 let mut ok = 0u32;
31 let mut failed = 0u32;
32
33 for (i, (ts_ms, payload)) in sample.iter().enumerate() {
34 match serde_json::from_slice::<StreamEvent>(payload) {
35 Ok(event) => {
36 match event.canonicalize() {
37 Some(CanonicalEvent::Trade(t)) => {
38 let price_ok = t.price > rust_decimal::Decimal::ZERO;
39 let qty_ok = t.quantity > rust_decimal::Decimal::ZERO;
40 let ts_ok = t.timestamp_ms > 1_700_000_000_000;
41 if price_ok && qty_ok && ts_ok {
42 ok += 1;
43 println!("[{i}] OK price={} qty={} ts_ms={} side={:?}",
44 t.price, t.quantity, t.timestamp_ms, t.side);
45 } else {
46 failed += 1;
47 println!("[{i}] BAD price={} qty={} ts_ms={} (record ts_ms={})",
48 t.price, t.quantity, t.timestamp_ms, ts_ms);
49 }
50 }
51 Some(other) => {
52 failed += 1;
53 println!("[{i}] WRONG VARIANT: {:?}", other);
54 }
55 None => {
56 failed += 1;
57 println!("[{i}] canonicalize() returned None (record ts_ms={})", ts_ms);
58 }
59 }
60 }
61 Err(e) => {
62 failed += 1;
63 println!("[{i}] DESERIALIZE FAIL: {e}");
64 }
65 }
66 }
67
68 println!("\nResult: {ok}/10 canonical OK, {failed} failed");
69 Ok(())
70}Sourcepub fn cleanup(&self, now: DateTime<Utc>) -> Result<usize>
pub fn cleanup(&self, now: DateTime<Utc>) -> Result<usize>
Run retention sweep — delete daily files older than config.default_retention_days.
Returns the count of deleted files.
Sourcepub fn stream_dir(&self, key: &StreamKey) -> PathBuf
pub fn stream_dir(&self, key: &StreamKey) -> PathBuf
Return the directory for a stream: {root}/{exchange}/{account}/{symbol}/{stream_kind}/
Sourcepub fn config(&self) -> &StorageConfig
pub fn config(&self) -> &StorageConfig
Return reference to config.
Auto Trait Implementations§
impl Freeze for StorageManager
impl !RefUnwindSafe for StorageManager
impl Send for StorageManager
impl Sync for StorageManager
impl Unpin for StorageManager
impl UnsafeUnpin for StorageManager
impl !UnwindSafe for StorageManager
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more