1use tokio::{sync::mpsc, task::JoinSet};
40use tokio_util::sync::CancellationToken;
41use tracing::{debug, error, info, warn};
42
43use crate::{Result, events::MarketEvent, sink::EventSink, source::DataSource};
44
45type EventChannel = (
51 mpsc::Sender<Vec<MarketEvent>>,
52 mpsc::Receiver<Vec<MarketEvent>>,
53);
54
55#[derive(Debug, Clone)]
61pub struct LoaderConfig {
62 pub channel_capacity: usize,
65 pub batch_size: usize,
68 pub continue_on_sink_error: bool,
71}
72
73impl Default for LoaderConfig {
74 fn default() -> Self {
75 Self {
76 channel_capacity: 4096,
77 batch_size: 256,
78 continue_on_sink_error: false,
79 }
80 }
81}
82
83pub struct DataLoader<S: DataSource = crate::source::tradingview::TradingViewSource> {
102 source: Option<S>,
103 sinks: Vec<Box<dyn EventSink>>,
104 sink_channels: Vec<EventChannel>,
106 config: LoaderConfig,
107 cancel: CancellationToken,
108 tasks: JoinSet<Result<()>>,
109}
110
111impl<S: DataSource> DataLoader<S> {
112 pub fn builder() -> DataLoaderBuilder<S> {
114 DataLoaderBuilder::new()
115 }
116
117 pub async fn start(&mut self) -> Result<()> {
122 let source = self
123 .source
124 .take()
125 .ok_or_else(|| crate::Error::Internal(ustr::ustr("loader already started")))?;
126
127 if self.sinks.is_empty() {
128 return Err(crate::Error::Internal(ustr::ustr(
129 "no sinks registered — add at least one sink before starting",
130 )));
131 }
132
133 let sink_count = self.sinks.len();
134 info!(
135 source = %source.name(),
136 sink_count,
137 channel_capacity = self.config.channel_capacity,
138 "starting data loader",
139 );
140
141 let (source_tx, source_rx) =
143 mpsc::channel::<Vec<MarketEvent>>(self.config.channel_capacity);
144
145 let cancel_src = self.cancel.clone();
147 let source_name = source.name().to_string();
148 self.tasks.spawn(async move {
149 debug!(source = %source_name, "source task started");
150 source.run(source_tx, cancel_src).await.inspect_err(|e| {
151 error!(source = %source_name, error = %e, "source failed");
152 })
153 });
154
155 let sink_txs: Vec<mpsc::Sender<Vec<MarketEvent>>> = self
157 .sink_channels
158 .iter()
159 .map(|(tx, _)| tx.clone())
160 .collect();
161 let fan_cancel = self.cancel.clone();
162 let fan_config = self.config.clone();
163 self.tasks
164 .spawn(async move { fan_out_task(source_rx, sink_txs, fan_config, fan_cancel).await });
165
166 let sink_rxs: Vec<(usize, mpsc::Receiver<Vec<MarketEvent>>)> = self
168 .sink_channels
169 .drain(..)
170 .enumerate()
171 .map(|(i, (_, rx))| (i, rx))
172 .collect();
173
174 let mut sinks = std::mem::take(&mut self.sinks);
175
176 for (idx, mut rx) in sink_rxs {
177 let sink = sinks.remove(0);
179 let cancel_s = self.cancel.clone();
180 let sink_name = sink.name().to_string();
181 let cont_on_err = self.config.continue_on_sink_error;
182
183 self.tasks.spawn(async move {
184 sink_task(idx, sink, &mut rx, sink_name, cont_on_err, cancel_s).await
185 });
186 }
187
188 Ok(())
189 }
190
191 pub async fn shutdown(&mut self) -> Result<()> {
195 info!("initiating loader shutdown");
196 self.cancel.cancel();
197
198 while let Some(result) = self.tasks.join_next().await {
199 match result {
200 Ok(Ok(())) => debug!("task completed successfully"),
201 Ok(Err(e)) => warn!(error = %e, "task completed with error"),
202 Err(e) => warn!(error = %e, "task join error"),
203 }
204 }
205
206 info!("loader shutdown complete");
207 Ok(())
208 }
209
210 pub fn cancel_token(&self) -> CancellationToken {
212 self.cancel.clone()
213 }
214}
215
216async fn fan_out_task(
225 mut source_rx: mpsc::Receiver<Vec<MarketEvent>>,
226 sink_txs: Vec<mpsc::Sender<Vec<MarketEvent>>>,
227 config: LoaderConfig,
228 cancel: CancellationToken,
229) -> Result<()> {
230 loop {
231 tokio::select! {
232 biased;
233
234 _ = cancel.cancelled() => {
235 debug!("fan-out task cancelled");
236 break;
237 }
238
239 result = source_rx.recv() => {
240 match result {
241 Some(events) => {
242 if sink_txs.is_empty() {
243 continue;
244 }
245
246 if !config.continue_on_sink_error {
247 for (i, tx) in sink_txs.iter().enumerate() {
248 if tx.is_closed() {
249 return Err(crate::Error::Internal(ustr::ustr(
250 &format!("sink channel {i} closed")
251 )));
252 }
253 }
254 }
255
256 let active_indices: Vec<usize> = sink_txs
257 .iter()
258 .enumerate()
259 .filter_map(|(i, tx)| if !tx.is_closed() { Some(i) } else { None })
260 .collect();
261
262 if active_indices.is_empty() {
263 if config.continue_on_sink_error {
264 for i in 0..sink_txs.len() {
265 warn!(sink_index = i, "sink channel dropped");
266 }
267 continue;
268 } else {
269 return Err(crate::Error::Internal(ustr::ustr("all sink channels closed")));
270 }
271 }
272
273 let (&last_idx, head_indices) = active_indices
274 .split_last()
275 .expect("active_indices is not empty");
276
277 for &i in head_indices {
278 let tx = &sink_txs[i];
279 if let Err(e) = tx.send(events.clone()).await {
280 if config.continue_on_sink_error {
281 warn!(sink_index = i, error = %e, "sink channel dropped");
282 } else {
283 return Err(crate::Error::Internal(ustr::ustr(
284 &format!("sink channel {i} closed: {e}")
285 )));
286 }
287 }
288 }
289
290 let last_tx = &sink_txs[last_idx];
291 if let Err(e) = last_tx.send(events).await {
292 if config.continue_on_sink_error {
293 warn!(sink_index = last_idx, error = %e, "sink channel dropped");
294 } else {
295 return Err(crate::Error::Internal(ustr::ustr(
296 &format!("sink channel {last_idx} closed: {e}")
297 )));
298 }
299 }
300
301 if config.continue_on_sink_error {
302 for (i, tx) in sink_txs.iter().enumerate() {
303 if tx.is_closed() {
304 warn!(sink_index = i, "sink channel dropped");
305 }
306 }
307 }
308 }
309 None => {
310 info!("source channel closed — fan-out exiting");
311 break;
312 }
313 }
314 }
315 }
316 }
317
318 Ok(())
319}
320
321async fn sink_task(
323 idx: usize,
324 sink: Box<dyn EventSink>,
325 rx: &mut mpsc::Receiver<Vec<MarketEvent>>,
326 name: String,
327 continue_on_error: bool,
328 cancel: CancellationToken,
329) -> Result<()> {
330 debug!(sink_index = idx, sink = %name, "sink task started");
331
332 loop {
333 tokio::select! {
334 biased;
335
336 _ = cancel.cancelled() => {
337 debug!(sink = %name, "sink task cancelled");
338 let _ = sink.shutdown(cancel).await;
340 break;
341 }
342
343 result = rx.recv() => {
344 match result {
345 Some(events) => {
346 if let Err(e) = sink.accept(&events).await {
347 warn!(sink = %name, error = %e, "sink accept failed");
348 if !continue_on_error {
349 return Err(e);
350 }
351 }
352 }
353 None => {
354 debug!(sink = %name, "sink channel closed");
355 break;
356 }
357 }
358 }
359 }
360 }
361
362 debug!(sink = %name, "sink task exiting");
363 Ok(())
364}
365
366#[must_use]
382pub struct DataLoaderBuilder<S: DataSource> {
383 source: Option<S>,
384 sinks: Vec<Box<dyn EventSink>>,
385 sink_channels: Vec<EventChannel>,
386 config: LoaderConfig,
387}
388
389impl<S: DataSource> DataLoaderBuilder<S> {
390 pub fn new() -> Self {
392 Self {
393 source: None,
394 sinks: Vec::new(),
395 sink_channels: Vec::new(),
396 config: LoaderConfig::default(),
397 }
398 }
399
400 pub fn source(mut self, source: S) -> Self {
402 self.source = Some(source);
403 self
404 }
405
406 pub fn sink(mut self, sink: impl EventSink) -> Self {
409 let (tx, rx) = mpsc::channel::<Vec<MarketEvent>>(self.config.channel_capacity);
410 self.sink_channels.push((tx, rx));
411 let boxed: Box<dyn EventSink> = Box::new(sink);
412 self.sinks.push(boxed);
413 self
414 }
415
416 pub fn channel_capacity(mut self, capacity: usize) -> Self {
418 self.config.channel_capacity = capacity;
419 self
420 }
421
422 pub fn continue_on_sink_error(mut self, val: bool) -> Self {
424 self.config.continue_on_sink_error = val;
425 self
426 }
427
428 pub fn build(self) -> Result<DataLoader<S>> {
435 let source = self
436 .source
437 .ok_or_else(|| crate::Error::Internal(ustr::ustr("no data source configured")))?;
438
439 if self.sinks.is_empty() {
440 return Err(crate::Error::Internal(ustr::ustr(
441 "at least one sink is required",
442 )));
443 }
444
445 Ok(DataLoader {
446 source: Some(source),
447 sinks: self.sinks,
448 sink_channels: self.sink_channels,
449 config: self.config,
450 cancel: CancellationToken::new(),
451 tasks: JoinSet::new(),
452 })
453 }
454}
455
456impl<S: DataSource> Default for DataLoaderBuilder<S> {
457 fn default() -> Self {
458 Self::new()
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use crate::events::CandleData;
466
467 fn test_candle(ts: i64) -> MarketEvent {
468 MarketEvent::Candle(CandleData::new(
469 ts,
470 "AAPL",
471 "1D",
472 150.0,
473 155.0,
474 149.0,
475 153.0,
476 1_000_000.0,
477 ))
478 }
479
480 #[tokio::test]
481 async fn test_fanout_single_sink_avoids_cloning() {
482 let (source_tx, source_rx) = mpsc::channel(16);
483 let (sink_tx, mut sink_rx) = mpsc::channel(16);
484 let cancel = CancellationToken::new();
485
486 let handle = tokio::spawn(fan_out_task(
487 source_rx,
488 vec![sink_tx],
489 LoaderConfig::default(),
490 cancel.clone(),
491 ));
492
493 let batch = vec![test_candle(1000)];
494 let original_ptr = batch.as_ptr();
495 source_tx.send(batch).await.unwrap();
496 drop(source_tx);
497
498 let received = sink_rx.recv().await.expect("received event batch");
499 assert_eq!(received.len(), 1);
500 assert_eq!(
501 received.as_ptr(),
502 original_ptr,
503 "single sink must move the batch buffer without cloning"
504 );
505
506 handle.await.unwrap().unwrap();
507 }
508
509 #[tokio::test]
510 async fn test_fanout_identical_observable_events_one_two_three_sinks() {
511 for sink_count in 1..=3 {
512 let (source_tx, source_rx) = mpsc::channel(16);
513 let mut sink_rxs = Vec::new();
514 let mut sink_txs = Vec::new();
515 for _ in 0..sink_count {
516 let (tx, rx) = mpsc::channel(16);
517 sink_txs.push(tx);
518 sink_rxs.push(rx);
519 }
520 let cancel = CancellationToken::new();
521
522 let handle = tokio::spawn(fan_out_task(
523 source_rx,
524 sink_txs,
525 LoaderConfig::default(),
526 cancel.clone(),
527 ));
528
529 let batch = vec![test_candle(1000), test_candle(2000)];
530 source_tx.send(batch.clone()).await.unwrap();
531 drop(source_tx);
532
533 for (sink_idx, rx) in sink_rxs.iter_mut().enumerate() {
534 let received = rx.recv().await.expect("sink received batch");
535 assert_eq!(
536 received, batch,
537 "sink {sink_idx}/{sink_count} must receive identical observable events"
538 );
539 }
540
541 handle.await.unwrap().unwrap();
542 }
543 }
544
545 #[tokio::test]
546 async fn test_fanout_trailing_closed_sink_fails_fast_when_continue_false() {
547 let (source_tx, source_rx) = mpsc::channel(16);
548 let (sink_tx_active, mut sink_rx_active) = mpsc::channel(16);
549 let (sink_tx_closed, sink_rx_closed) = mpsc::channel(16);
550 drop(sink_rx_closed); let cancel = CancellationToken::new();
553 let handle = tokio::spawn(fan_out_task(
554 source_rx,
555 vec![sink_tx_active, sink_tx_closed],
556 LoaderConfig {
557 continue_on_sink_error: false,
558 ..Default::default()
559 },
560 cancel.clone(),
561 ));
562
563 let batch = vec![test_candle(1000)];
564 source_tx.send(batch).await.unwrap();
565 drop(source_tx);
566
567 let task_result = handle.await.unwrap();
568 assert!(
569 task_result.is_err(),
570 "expected error due to closed trailing sink when continue_on_sink_error is false"
571 );
572 assert!(sink_rx_active.try_recv().is_err());
574 }
575
576 #[tokio::test]
577 async fn test_fanout_trailing_closed_sink_continues_when_continue_true() {
578 let (source_tx, source_rx) = mpsc::channel(16);
579 let (sink_tx_active, mut sink_rx_active) = mpsc::channel(16);
580 let (sink_tx_closed, sink_rx_closed) = mpsc::channel(16);
581 drop(sink_rx_closed); let cancel = CancellationToken::new();
584 let handle = tokio::spawn(fan_out_task(
585 source_rx,
586 vec![sink_tx_active, sink_tx_closed],
587 LoaderConfig {
588 continue_on_sink_error: true,
589 ..Default::default()
590 },
591 cancel.clone(),
592 ));
593
594 let batch = vec![test_candle(1000)];
595 let original_ptr = batch.as_ptr();
596 source_tx.send(batch).await.unwrap();
597 drop(source_tx);
598
599 let task_result = handle.await.unwrap();
600 assert!(
601 task_result.is_ok(),
602 "expected success when continue_on_sink_error is true"
603 );
604
605 let received = sink_rx_active
606 .recv()
607 .await
608 .expect("active sink received batch");
609 assert_eq!(received.len(), 1);
610 assert_eq!(
611 received.as_ptr(),
612 original_ptr,
613 "single remaining active sink must move the batch buffer without cloning"
614 );
615 }
616}