1use std::{
10 collections::{HashMap, HashSet},
11 sync::Arc,
12 time::Instant,
13};
14
15use async_trait::async_trait;
16use futures::future::join_all;
17use tokio::sync::{broadcast, RwLock};
18use tracing::{error, info, trace, warn};
19use tycho_simulation::tycho_common::models::Address;
20
21use crate::types::ComponentId;
22
23#[derive(Debug, Clone, Default)]
28pub struct ChangedComponents {
29 pub added: HashMap<ComponentId, Vec<Address>>,
31 pub removed: Vec<ComponentId>,
33 pub updated: Vec<ComponentId>,
35 pub is_full_recompute: bool,
37}
38
39impl ChangedComponents {
40 pub fn all(market: MarketDataView) -> Self {
44 Self {
45 added: market.component_topology().clone(),
46 removed: vec![],
47 updated: vec![],
48 is_full_recompute: true,
49 }
50 }
51
52 pub fn is_topology_change(&self) -> bool {
54 !self.added.is_empty() || !self.removed.is_empty()
55 }
56
57 pub fn all_changed_ids(&self) -> HashSet<ComponentId> {
59 let mut all = HashSet::new();
60 all.extend(self.added.keys().cloned());
61 all.extend(self.removed.iter().cloned());
62 all.extend(self.updated.iter().cloned());
63 all
64 }
65}
66
67use super::{
68 computation::{ComputationId, ComputationRequirements, DerivedComputation},
69 computations::{PoolDepthComputation, SpotPriceComputation, TokenGasPriceComputation},
70 error::ComputationError,
71 events::DerivedDataEvent,
72 registry::ErasedComputation,
73 store::DerivedData,
74};
75use crate::feed::{
76 events::{EventError, MarketEvent, MarketEventHandler},
77 market_data::{MarketData, MarketDataView},
78};
79
80pub type SharedDerivedDataRef = Arc<RwLock<DerivedData>>;
82
83#[derive(Debug, Clone)]
85pub struct ComputationManagerConfig {
86 gas_token: Address,
88 max_hop: usize,
90 depth_slippage_threshold: f64,
92}
93
94impl ComputationManagerConfig {
95 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn with_depth_slippage_threshold(mut self, threshold: f64) -> Self {
102 self.depth_slippage_threshold = threshold;
103 self
104 }
105
106 pub fn with_max_hop(mut self, hop_count: usize) -> Self {
108 self.max_hop = hop_count;
109 self
110 }
111
112 pub fn with_gas_token(mut self, gas_token: Address) -> Self {
114 self.gas_token = gas_token;
115 self
116 }
117
118 pub fn gas_token(&self) -> &Address {
120 &self.gas_token
121 }
122
123 pub fn max_hop(&self) -> usize {
125 self.max_hop
126 }
127
128 pub fn depth_slippage_threshold(&self) -> f64 {
130 self.depth_slippage_threshold
131 }
132}
133
134impl Default for ComputationManagerConfig {
135 fn default() -> Self {
136 Self { gas_token: Address::zero(20), max_hop: 2, depth_slippage_threshold: 0.01 }
137 }
138}
139
140pub struct ComputationManager {
142 market_data: MarketData,
144 store: SharedDerivedDataRef,
146 computations: Vec<Box<dyn ErasedComputation>>,
148 event_tx: broadcast::Sender<DerivedDataEvent>,
150}
151
152struct ComputationSchedule {
154 stages: Vec<Vec<usize>>,
156 unscheduled: Vec<usize>,
158}
159
160impl ComputationManager {
161 pub fn new(
167 config: ComputationManagerConfig,
168 market_data: MarketData,
169 ) -> Result<(Self, broadcast::Receiver<DerivedDataEvent>), ComputationError> {
170 let (mut manager, event_rx) = Self::empty(market_data);
171 manager.register(SpotPriceComputation::new())?;
172 manager.register(
173 TokenGasPriceComputation::default()
174 .with_max_hops(config.max_hop)
175 .with_gas_token(config.gas_token),
176 )?;
177 manager.register(PoolDepthComputation::new(config.depth_slippage_threshold)?)?;
178 Ok((manager, event_rx))
179 }
180
181 pub(crate) fn empty(market_data: MarketData) -> (Self, broadcast::Receiver<DerivedDataEvent>) {
186 let (event_tx, event_rx) = broadcast::channel(64);
187 (
188 Self {
189 market_data,
190 store: DerivedData::new_shared(),
191 computations: Vec::new(),
192 event_tx,
193 },
194 event_rx,
195 )
196 }
197
198 pub(crate) fn register<C: DerivedComputation>(
209 &mut self,
210 computation: C,
211 ) -> Result<(), ComputationError> {
212 if self
213 .computations
214 .iter()
215 .any(|existing| existing.id() == C::ID)
216 {
217 return Err(ComputationError::DuplicateComputationId(C::ID));
218 }
219 self.computations
220 .push(Box::new(computation));
221 Ok(())
222 }
223
224 pub fn store(&self) -> SharedDerivedDataRef {
226 Arc::clone(&self.store)
227 }
228
229 pub fn event_sender(&self) -> broadcast::Sender<DerivedDataEvent> {
231 self.event_tx.clone()
232 }
233
234 pub async fn run(
238 mut self,
239 mut event_rx: broadcast::Receiver<MarketEvent>,
240 mut shutdown_rx: broadcast::Receiver<()>,
241 ) {
242 info!("computation manager started");
243
244 loop {
245 tokio::select! {
246 biased;
247
248 _ = shutdown_rx.recv() => {
249 info!("computation manager shutting down");
250 break;
251 }
252
253 event_result = event_rx.recv() => {
254 match event_result {
255 Ok(event) => {
256 if let Err(e) = self.handle_event(&event).await {
257 warn!(error = ?e, "failed to handle market event");
258 }
259 }
260 Err(broadcast::error::RecvError::Closed) => {
261 info!("event channel closed, computation manager shutting down");
262 break;
263 }
264 Err(broadcast::error::RecvError::Lagged(skipped)) => {
265 warn!(
266 skipped,
267 "computation manager lagged, skipped {} events. Recomputing from current state.",
268 skipped
269 );
270 let market = self.market_data.read().await;
271 let changed = ChangedComponents::all(market);
272 self.compute_all(&changed).await;
273 }
274 }
275 }
276 }
277 }
278 }
279
280 async fn compute_all(&self, changed: &ChangedComponents) {
288 let total_start = Instant::now();
289
290 let Some(block) = self
292 .market_data
293 .read()
294 .await
295 .last_updated()
296 .map(|b| b.number())
297 else {
298 warn!("market data has no last updated block, skipping computations");
299 return;
300 };
301
302 let _ = self
304 .event_tx
305 .send(DerivedDataEvent::NewBlock { block });
306
307 let nodes: Vec<(ComputationId, ComputationRequirements)> = self
308 .computations
309 .iter()
310 .map(|computation| (computation.id(), computation.requirements()))
311 .collect();
312 let schedule = build_schedule(&nodes);
313 for &idx in &schedule.unscheduled {
314 let computation_id = nodes[idx].0;
315 error!(computation = computation_id, "computation skipped: requirement cycle");
316 let _ = self
317 .event_tx
318 .send(DerivedDataEvent::ComputationFailed { computation_id, block });
319 }
320
321 let mut succeeded: HashSet<ComputationId> = HashSet::new();
322 for stage in &schedule.stages {
323 let mut runnable = Vec::new();
326 {
327 let store = self.store.read().await;
328 for &idx in stage {
329 let reqs = &nodes[idx].1;
330 let fresh_ready = reqs
331 .fresh_requirements()
332 .iter()
333 .all(|id| succeeded.contains(id));
334 let stale_ready = reqs
335 .stale_requirements()
336 .iter()
337 .all(|id| succeeded.contains(id) || store.output_block(id).is_some());
338 if fresh_ready && stale_ready {
339 runnable.push(idx);
340 } else {
341 let _ = self
342 .event_tx
343 .send(DerivedDataEvent::ComputationFailed {
344 computation_id: nodes[idx].0,
345 block,
346 });
347 }
348 }
349 }
350
351 if runnable.is_empty() {
352 continue;
353 }
354
355 let results = join_all(runnable.iter().map(|&idx| async move {
357 let start = Instant::now();
358 let result = self.computations[idx]
359 .compute_erased(&self.market_data, &self.store, changed, block)
360 .await;
361 (idx, result, start.elapsed())
362 }))
363 .await;
364
365 let mut store = self.store.write().await;
367 for (idx, result, elapsed) in results {
368 let computation_id = nodes[idx].0;
369 match result {
370 Ok(write) => {
371 (write.persist)(&mut store);
372 info!(
373 computation = computation_id,
374 failed = write.failed_items.len(),
375 elapsed_ms = elapsed.as_millis(),
376 "computation complete"
377 );
378 let _ = self
379 .event_tx
380 .send(DerivedDataEvent::ComputationComplete {
381 computation_id,
382 block,
383 failed_items: write.failed_items,
384 });
385 succeeded.insert(computation_id);
386 }
387 Err(e) => {
388 warn!(
389 error = ?e,
390 computation = computation_id,
391 elapsed_ms = elapsed.as_millis(),
392 "computation failed"
393 );
394 let _ = self
395 .event_tx
396 .send(DerivedDataEvent::ComputationFailed { computation_id, block });
397 }
398 }
399 }
400 }
401
402 info!(
403 block,
404 total_ms = total_start.elapsed().as_millis(),
405 "all derived computations complete"
406 );
407 }
408}
409
410fn build_schedule(nodes: &[(ComputationId, ComputationRequirements)]) -> ComputationSchedule {
418 let ids: Vec<ComputationId> = nodes
419 .iter()
420 .map(|(id, _)| *id)
421 .collect();
422 let mut stage_of: Vec<Option<usize>> = vec![None; nodes.len()];
423
424 loop {
425 let mut progressed = false;
426 for (idx, (_, reqs)) in nodes.iter().enumerate() {
427 if stage_of[idx].is_some() {
428 continue;
429 }
430 let mut stage = 0;
431 let mut ready = true;
432 for dep in reqs
433 .fresh_requirements()
434 .iter()
435 .chain(reqs.stale_requirements().iter())
436 {
437 let Some(dep_idx) = ids.iter().position(|id| id == dep) else {
438 continue;
439 };
440 match stage_of[dep_idx] {
441 Some(dep_stage) => stage = stage.max(dep_stage + 1),
442 None => {
443 ready = false;
444 break;
445 }
446 }
447 }
448 if ready {
449 stage_of[idx] = Some(stage);
450 progressed = true;
451 }
452 }
453 if !progressed {
454 break;
455 }
456 }
457
458 let stage_count = stage_of
459 .iter()
460 .filter_map(|stage| *stage)
461 .max()
462 .map_or(0, |max| max + 1);
463 let mut stages = vec![Vec::new(); stage_count];
464 let mut unscheduled = Vec::new();
465 for (idx, stage) in stage_of.iter().enumerate() {
466 match stage {
467 Some(stage) => stages[*stage].push(idx),
468 None => unscheduled.push(idx),
469 }
470 }
471 ComputationSchedule { stages, unscheduled }
472}
473
474#[async_trait]
475impl MarketEventHandler for ComputationManager {
476 async fn handle_event(&mut self, event: &MarketEvent) -> Result<(), EventError> {
477 match event {
478 MarketEvent::MarketUpdated {
479 added_components,
480 removed_components,
481 updated_components,
482 } if !added_components.is_empty() ||
483 !removed_components.is_empty() ||
484 !updated_components.is_empty() =>
485 {
486 trace!(
487 added = added_components.len(),
488 removed = removed_components.len(),
489 updated = updated_components.len(),
490 "market updated, running incremental computations"
491 );
492
493 let changed = ChangedComponents {
494 added: added_components.clone(),
495 removed: removed_components.clone(),
496 updated: updated_components.clone(),
497 is_full_recompute: false,
498 };
499 self.compute_all(&changed).await;
500 }
501 _ => {
502 trace!("empty market update, skipping computations");
503 }
504 }
505
506 Ok(())
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use std::{
513 collections::HashMap,
514 sync::{
515 atomic::{AtomicBool, Ordering},
516 Arc,
517 },
518 };
519
520 use tokio::sync::broadcast;
521
522 use super::*;
523 use crate::{
524 algorithm::test_utils::{component, setup_market_weighted, token, MockProtocolSim},
525 derived::computation::{ComputationOutput, FailedItem, FailedItemError},
526 feed::market_data::{MarketData, MarketState},
527 types::BlockInfo,
528 };
529
530 fn drain_events(rx: &mut broadcast::Receiver<DerivedDataEvent>) -> Vec<DerivedDataEvent> {
532 let mut events = vec![];
533 loop {
534 match rx.try_recv() {
535 Ok(e) => events.push(e),
536 Err(broadcast::error::TryRecvError::Empty) => break,
537 Err(broadcast::error::TryRecvError::Lagged(_)) => continue,
538 Err(broadcast::error::TryRecvError::Closed) => break,
539 }
540 }
541 events
542 }
543
544 #[test]
547 fn schedule_empty_has_no_stages() {
548 let schedule = build_schedule(&[]);
549 assert!(schedule.stages.is_empty());
550 assert!(schedule.unscheduled.is_empty());
551 }
552
553 #[test]
554 fn schedule_single_root_is_one_stage() {
555 let schedule = build_schedule(&[("a", ComputationRequirements::none())]);
556 assert_eq!(schedule.stages, vec![vec![0]]);
557 assert!(schedule.unscheduled.is_empty());
558 }
559
560 #[test]
561 fn schedule_independent_roots_share_one_stage() {
562 let schedule = build_schedule(&[
563 ("a", ComputationRequirements::none()),
564 ("b", ComputationRequirements::none()),
565 ]);
566 assert_eq!(schedule.stages, vec![vec![0, 1]]);
567 assert!(schedule.unscheduled.is_empty());
568 }
569
570 #[test]
571 fn schedule_chain_orders_into_successive_stages() {
572 let schedule = build_schedule(&[
574 ("a", ComputationRequirements::none()),
575 ("b", ComputationRequirements::fresh(["a"])),
576 ("c", ComputationRequirements::fresh(["b"])),
577 ]);
578 assert_eq!(schedule.stages, vec![vec![0], vec![1], vec![2]]);
579 assert!(schedule.unscheduled.is_empty());
580 }
581
582 #[test]
583 fn schedule_diamond_places_join_after_both_parents() {
584 let schedule = build_schedule(&[
586 ("a", ComputationRequirements::none()),
587 ("b", ComputationRequirements::fresh(["a"])),
588 ("c", ComputationRequirements::fresh(["a"])),
589 ("d", ComputationRequirements::fresh(["b", "c"])),
590 ]);
591 assert_eq!(schedule.stages, vec![vec![0], vec![1, 2], vec![3]]);
592 assert!(schedule.unscheduled.is_empty());
593 }
594
595 #[test]
596 fn schedule_preserves_input_order_within_a_stage() {
597 let schedule = build_schedule(&[
598 ("a", ComputationRequirements::none()),
599 ("b", ComputationRequirements::fresh(["a"])),
600 ("c", ComputationRequirements::fresh(["a"])),
601 ]);
602 assert_eq!(schedule.stages, vec![vec![0], vec![1, 2]]);
604 }
605
606 #[test]
607 fn schedule_stale_requirement_orders_after_its_producer() {
608 let schedule = build_schedule(&[
609 ("a", ComputationRequirements::none()),
610 ("b", ComputationRequirements::stale(["a"])),
611 ]);
612 assert_eq!(schedule.stages, vec![vec![0], vec![1]]);
613 }
614
615 #[test]
616 fn schedule_requirement_on_unregistered_id_does_not_affect_ordering() {
617 let schedule = build_schedule(&[("a", ComputationRequirements::fresh(["ghost"]))]);
619 assert_eq!(schedule.stages, vec![vec![0]]);
620 assert!(schedule.unscheduled.is_empty());
621 }
622
623 #[test]
624 fn schedule_two_node_cycle_is_unscheduled() {
625 let schedule = build_schedule(&[
626 ("a", ComputationRequirements::fresh(["b"])),
627 ("b", ComputationRequirements::fresh(["a"])),
628 ]);
629 assert!(schedule.stages.is_empty());
630 assert_eq!(schedule.unscheduled, vec![0, 1]);
631 }
632
633 #[test]
634 fn schedule_isolates_cycle_from_schedulable_nodes() {
635 let schedule = build_schedule(&[
637 ("root", ComputationRequirements::none()),
638 ("x", ComputationRequirements::fresh(["y"])),
639 ("y", ComputationRequirements::fresh(["x"])),
640 ]);
641 assert_eq!(schedule.stages, vec![vec![0]]);
642 assert_eq!(schedule.unscheduled, vec![1, 2]);
643 }
644
645 #[test]
646 fn invalid_slippage_threshold_returns_error() {
647 let (market, _) = setup_market_weighted(vec![]);
648 let config = ComputationManagerConfig::new().with_depth_slippage_threshold(1.5);
649
650 let result = ComputationManager::new(config, market);
651 assert!(matches!(result, Err(ComputationError::InvalidConfiguration(_))));
652 }
653
654 #[tokio::test]
655 async fn handle_event_runs_computations_on_market_update() {
656 let eth = token(1, "ETH");
657 let usdc = token(2, "USDC");
658
659 let (market, _) = setup_market_weighted(vec![(
660 "eth_usdc",
661 ð,
662 &usdc,
663 MockProtocolSim::new(2000.0).with_gas(0),
664 )]);
665
666 let config = ComputationManagerConfig::new().with_gas_token(eth.address.clone());
667 let (mut manager, _event_rx) = ComputationManager::new(config, market).unwrap();
668
669 let event = MarketEvent::MarketUpdated {
670 added_components: HashMap::from([(
671 "eth_usdc".to_string(),
672 vec![eth.address.clone(), usdc.address.clone()],
673 )]),
674 removed_components: vec![],
675 updated_components: vec![],
676 };
677
678 manager
679 .handle_event(&event)
680 .await
681 .unwrap();
682
683 let store = manager.store();
684 let guard = store.read().await;
685 assert!(guard.token_prices().is_some());
686 assert!(guard.spot_prices().is_some());
687 }
688
689 #[tokio::test]
690 async fn handle_event_skips_empty_update() {
691 let (market, _) = setup_market_weighted(vec![]);
692 let config = ComputationManagerConfig::new();
693 let (mut manager, _event_rx) = ComputationManager::new(config, market).unwrap();
694
695 let event = MarketEvent::MarketUpdated {
696 added_components: HashMap::new(),
697 removed_components: vec![],
698 updated_components: vec![],
699 };
700
701 manager
702 .handle_event(&event)
703 .await
704 .unwrap();
705
706 let store = manager.store();
707 let guard = store.read().await;
708 assert!(guard.token_prices().is_none());
709 }
710
711 #[tokio::test]
712 async fn run_shuts_down_on_signal() {
713 let (market, _) = setup_market_weighted(vec![]);
714 let config = ComputationManagerConfig::new();
715 let (manager, _event_rx) = ComputationManager::new(config, market).unwrap();
716
717 let (_event_tx, event_rx) = broadcast::channel::<MarketEvent>(16);
718 let (shutdown_tx, shutdown_rx) = broadcast::channel::<()>(1);
719
720 let handle = tokio::spawn(async move {
721 manager.run(event_rx, shutdown_rx).await;
722 });
723
724 shutdown_tx.send(()).unwrap();
725
726 tokio::time::timeout(tokio::time::Duration::from_secs(1), handle)
727 .await
728 .expect("manager should shutdown")
729 .expect("task should complete successfully");
730 }
731
732 #[derive(Clone, Debug, PartialEq)]
735 struct CounterOutput(u32);
736
737 struct CounterComputation;
740
741 #[async_trait::async_trait]
742 impl DerivedComputation for CounterComputation {
743 type Output = CounterOutput;
744 const ID: ComputationId = "counter";
745
746 async fn compute(
747 &self,
748 _market: &MarketData,
749 _store: &SharedDerivedDataRef,
750 _changed: &ChangedComponents,
751 ) -> Result<ComputationOutput<Self::Output>, ComputationError> {
752 Ok(ComputationOutput::success(CounterOutput(7)))
753 }
754 }
755
756 fn market_with_block() -> MarketData {
758 let eth = token(1, "ETH");
759 let usdc = token(2, "USDC");
760 let (market, _) = setup_market_weighted(vec![(
761 "eth_usdc",
762 ð,
763 &usdc,
764 MockProtocolSim::new(2000.0).with_gas(0),
765 )]);
766 market
767 }
768
769 #[tokio::test]
770 async fn registered_custom_computation_runs_and_persists_via_default_slot() {
771 let (mut manager, mut event_rx) = ComputationManager::empty(market_with_block());
772 manager
773 .register(CounterComputation)
774 .unwrap();
775
776 manager
777 .compute_all(&ChangedComponents { is_full_recompute: true, ..Default::default() })
778 .await;
779
780 let store = manager.store();
781 let guard = store.read().await;
782 assert_eq!(
783 guard.output::<CounterOutput>(CounterComputation::ID),
784 Some(&CounterOutput(7)),
785 "default persist should write the output into the generic slot"
786 );
787 assert!(guard
788 .output_block(CounterComputation::ID)
789 .is_some());
790
791 let events = drain_events(&mut event_rx);
792 assert!(
793 events.iter().any(|e| matches!(
794 e,
795 DerivedDataEvent::ComputationComplete { computation_id: "counter", .. }
796 )),
797 "expected ComputationComplete(counter), got: {events:?}"
798 );
799 }
800
801 #[test]
802 fn registering_duplicate_id_is_rejected() {
803 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
804 manager
805 .register(CounterComputation)
806 .unwrap();
807
808 let result = manager.register(CounterComputation);
809
810 assert!(matches!(result, Err(ComputationError::DuplicateComputationId("counter"))));
811 }
812
813 fn event_summary(events: &[DerivedDataEvent]) -> Vec<(&'static str, &'static str)> {
817 events
818 .iter()
819 .map(|event| match event {
820 DerivedDataEvent::NewBlock { .. } => ("new_block", ""),
821 DerivedDataEvent::ComputationComplete { computation_id, .. } => {
822 ("complete", *computation_id)
823 }
824 DerivedDataEvent::ComputationFailed { computation_id, .. } => {
825 ("failed", *computation_id)
826 }
827 })
828 .collect()
829 }
830
831 async fn run_full_recompute(manager: &ComputationManager) -> Vec<DerivedDataEvent> {
833 let mut event_rx = manager.event_sender().subscribe();
834 manager
835 .compute_all(&ChangedComponents { is_full_recompute: true, ..Default::default() })
836 .await;
837 drain_events(&mut event_rx)
838 }
839
840 macro_rules! test_computation {
842 ($name:ident, $id:literal, $reqs:expr, $result:expr) => {
843 struct $name;
844
845 #[async_trait::async_trait]
846 impl DerivedComputation for $name {
847 type Output = ();
848 const ID: ComputationId = $id;
849
850 fn requirements(&self) -> ComputationRequirements {
851 $reqs
852 }
853
854 async fn compute(
855 &self,
856 _market: &MarketData,
857 _store: &SharedDerivedDataRef,
858 _changed: &ChangedComponents,
859 ) -> Result<ComputationOutput<Self::Output>, ComputationError> {
860 $result
861 }
862 }
863 };
864 }
865
866 test_computation!(
867 RootOk,
868 "root",
869 ComputationRequirements::none(),
870 Ok(ComputationOutput::success(()))
871 );
872 test_computation!(
873 DepOnRoot,
874 "dep",
875 ComputationRequirements::fresh(["root"]),
876 Ok(ComputationOutput::success(()))
877 );
878 test_computation!(
879 SecondDepOnRoot,
880 "dep2",
881 ComputationRequirements::fresh(["root"]),
882 Ok(ComputationOutput::success(()))
883 );
884 test_computation!(
885 RootErr,
886 "boom",
887 ComputationRequirements::none(),
888 Err(ComputationError::InvalidConfiguration("boom".to_string()))
889 );
890 test_computation!(
891 DepOnBoom,
892 "dep_boom",
893 ComputationRequirements::fresh(["boom"]),
894 Ok(ComputationOutput::success(()))
895 );
896 test_computation!(
897 ThirdOnBoom,
898 "third",
899 ComputationRequirements::fresh(["dep_boom"]),
900 Ok(ComputationOutput::success(()))
901 );
902 test_computation!(
903 StaleDepOnFlaky,
904 "stale_dep",
905 ComputationRequirements::stale(["flaky"]),
906 Ok(ComputationOutput::success(()))
907 );
908 test_computation!(
909 GhostDependent,
910 "needs_ghost",
911 ComputationRequirements::fresh(["ghost"]),
912 Ok(ComputationOutput::success(()))
913 );
914 test_computation!(
915 PartialProducer,
916 "partial",
917 ComputationRequirements::none(),
918 Ok(ComputationOutput::with_failures(
919 (),
920 vec![FailedItem { key: "x".to_string(), error: FailedItemError::MissingSpotPrice }]
921 ))
922 );
923 test_computation!(
924 DepOnPartial,
925 "dep_partial",
926 ComputationRequirements::fresh(["partial"]),
927 Ok(ComputationOutput::success(()))
928 );
929
930 struct FlakyProducer {
934 succeed: Arc<AtomicBool>,
935 }
936
937 #[async_trait::async_trait]
938 impl DerivedComputation for FlakyProducer {
939 type Output = ();
940 const ID: ComputationId = "flaky";
941
942 async fn compute(
943 &self,
944 _market: &MarketData,
945 _store: &SharedDerivedDataRef,
946 _changed: &ChangedComponents,
947 ) -> Result<ComputationOutput<Self::Output>, ComputationError> {
948 if self.succeed.load(Ordering::SeqCst) {
949 Ok(ComputationOutput::success(()))
950 } else {
951 Err(ComputationError::InvalidConfiguration("flaky".to_string()))
952 }
953 }
954 }
955
956 #[tokio::test]
957 async fn events_follow_dependency_order_across_stages() {
958 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
959 manager.register(RootOk).unwrap();
960 manager.register(DepOnRoot).unwrap();
961
962 let events = run_full_recompute(&manager).await;
963
964 assert_eq!(
965 event_summary(&events),
966 vec![("new_block", ""), ("complete", "root"), ("complete", "dep")]
967 );
968 }
969
970 #[tokio::test]
971 async fn events_preserve_registration_order_within_a_stage() {
972 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
973 manager.register(RootOk).unwrap();
974 manager.register(DepOnRoot).unwrap();
975 manager
976 .register(SecondDepOnRoot)
977 .unwrap();
978
979 let events = run_full_recompute(&manager).await;
980
981 assert_eq!(
983 event_summary(&events),
984 vec![
985 ("new_block", ""),
986 ("complete", "root"),
987 ("complete", "dep"),
988 ("complete", "dep2"),
989 ]
990 );
991 }
992
993 #[tokio::test]
994 async fn failed_dependency_cascades_to_dependents() {
995 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
996 manager.register(RootErr).unwrap();
997 manager.register(DepOnBoom).unwrap();
998
999 let events = run_full_recompute(&manager).await;
1000
1001 assert_eq!(
1003 event_summary(&events),
1004 vec![("new_block", ""), ("failed", "boom"), ("failed", "dep_boom")]
1005 );
1006 }
1007
1008 #[tokio::test]
1009 async fn computation_with_unregistered_requirement_is_skipped() {
1010 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1011 manager
1012 .register(GhostDependent)
1013 .unwrap();
1014
1015 let events = run_full_recompute(&manager).await;
1016
1017 assert_eq!(event_summary(&events), vec![("new_block", ""), ("failed", "needs_ghost")]);
1019 }
1020
1021 #[tokio::test]
1022 async fn fresh_dependent_runs_when_producer_succeeds_partially() {
1023 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1024 manager
1025 .register(PartialProducer)
1026 .unwrap();
1027 manager.register(DepOnPartial).unwrap();
1028
1029 let events = run_full_recompute(&manager).await;
1030
1031 assert_eq!(
1034 event_summary(&events),
1035 vec![("new_block", ""), ("complete", "partial"), ("complete", "dep_partial"),]
1036 );
1037 }
1038
1039 #[tokio::test]
1040 async fn failure_cascade_propagates_through_three_levels() {
1041 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1042 manager.register(RootErr).unwrap();
1043 manager.register(DepOnBoom).unwrap();
1044 manager.register(ThirdOnBoom).unwrap();
1045
1046 let events = run_full_recompute(&manager).await;
1047
1048 assert_eq!(
1050 event_summary(&events),
1051 vec![
1052 ("new_block", ""),
1053 ("failed", "boom"),
1054 ("failed", "dep_boom"),
1055 ("failed", "third"),
1056 ]
1057 );
1058 }
1059
1060 #[tokio::test]
1061 async fn stale_dependency_runs_on_prior_value_after_producer_fails() {
1062 let succeed = Arc::new(AtomicBool::new(true));
1063 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1064 manager
1065 .register(FlakyProducer { succeed: Arc::clone(&succeed) })
1066 .unwrap();
1067 manager
1068 .register(StaleDepOnFlaky)
1069 .unwrap();
1070
1071 let first = run_full_recompute(&manager).await;
1073 assert_eq!(
1074 event_summary(&first),
1075 vec![("new_block", ""), ("complete", "flaky"), ("complete", "stale_dep")]
1076 );
1077
1078 succeed.store(false, Ordering::SeqCst);
1081 let second = run_full_recompute(&manager).await;
1082 assert_eq!(
1083 event_summary(&second),
1084 vec![("new_block", ""), ("failed", "flaky"), ("complete", "stale_dep")]
1085 );
1086 }
1087
1088 #[tokio::test]
1089 async fn default_computations_cascade_failure_in_registration_order() {
1090 let (manager, _event_rx) = ComputationManager::new(
1093 ComputationManagerConfig::new(),
1094 market_with_component_no_sim_state(),
1095 )
1096 .unwrap();
1097
1098 let events = run_full_recompute(&manager).await;
1099
1100 assert_eq!(
1101 event_summary(&events),
1102 vec![
1103 ("new_block", ""),
1104 ("failed", "spot_prices"),
1105 ("failed", "token_prices"),
1106 ("failed", "pool_depths"),
1107 ]
1108 );
1109 }
1110
1111 fn market_with_component_no_sim_state() -> MarketData {
1116 let eth = token(1, "ETH");
1117 let usdc = token(2, "USDC");
1118 let pool = component("pool", &[eth.clone(), usdc.clone()]);
1119
1120 let mut market = MarketState::new();
1121 market.update_last_updated(BlockInfo::new(10, "0xhash".into(), 0));
1122 market.upsert_components(std::iter::once(pool));
1123 market.upsert_tokens([eth, usdc]);
1125 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
1126 }
1127
1128 fn market_with_mixed_sim_states() -> MarketData {
1131 let eth = token(1, "ETH");
1132 let usdc = token(2, "USDC");
1133 let dai = token(3, "DAI");
1134
1135 let pool1 = component("eth_usdc", &[eth.clone(), usdc.clone()]);
1136 let pool2 = component("eth_dai", &[eth.clone(), dai.clone()]);
1137
1138 let mut market = MarketState::new();
1139 market.update_last_updated(BlockInfo::new(10, "0xhash".into(), 0));
1140 market.upsert_components([pool1, pool2]);
1141 market
1143 .update_states([("eth_usdc".to_string(), Box::new(MockProtocolSim::new(2000.0)) as _)]);
1144 market.upsert_tokens([eth, usdc, dai]);
1145 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
1146 }
1147
1148 fn market_with_sim_state_no_gas_price() -> MarketData {
1153 let eth = token(1, "ETH");
1154 let usdc = token(2, "USDC");
1155 let pool = component("pool", &[eth.clone(), usdc.clone()]);
1156
1157 let mut market = MarketState::new();
1158 market.update_last_updated(BlockInfo::new(10, "0xhash".into(), 0));
1160 market.upsert_components(std::iter::once(pool));
1161 market.update_states([("pool".to_string(), Box::new(MockProtocolSim::new(2000.0)) as _)]);
1162 market.upsert_tokens([eth, usdc]);
1163 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
1164 }
1165
1166 #[tokio::test]
1167 async fn test_spot_price_failure_broadcasts_computation_failed() {
1168 let market = market_with_component_no_sim_state();
1169 let config = ComputationManagerConfig::new();
1170 let (manager, mut event_rx) = ComputationManager::new(config, market).unwrap();
1171
1172 let changed = ChangedComponents { is_full_recompute: true, ..Default::default() };
1174 manager.compute_all(&changed).await;
1175
1176 let events = drain_events(&mut event_rx);
1177
1178 assert!(
1179 events.iter().any(|e| matches!(
1180 e,
1181 DerivedDataEvent::ComputationFailed { computation_id: "spot_prices", .. }
1182 )),
1183 "expected ComputationFailed(spot_prices) in events: {events:?}"
1184 );
1185 }
1186
1187 #[tokio::test]
1188 async fn test_token_price_failure_broadcasts_computation_failed() {
1189 let eth = token(1, "ETH");
1190 let usdc = token(2, "USDC");
1191 let market = market_with_sim_state_no_gas_price();
1192 let config = ComputationManagerConfig::new().with_gas_token(eth.address.clone());
1193 let (mut manager, mut event_rx) = ComputationManager::new(config, market).unwrap();
1194
1195 let event = MarketEvent::MarketUpdated {
1197 added_components: HashMap::from([(
1198 "pool".to_string(),
1199 vec![eth.address.clone(), usdc.address.clone()],
1200 )]),
1201 removed_components: vec![],
1202 updated_components: vec![],
1203 };
1204 manager
1205 .handle_event(&event)
1206 .await
1207 .unwrap();
1208
1209 let events = drain_events(&mut event_rx);
1210 assert!(
1211 events.iter().any(|e| matches!(
1212 e,
1213 DerivedDataEvent::ComputationFailed { computation_id: "token_prices", .. }
1214 )),
1215 "expected ComputationFailed(token_prices) in events: {events:?}"
1216 );
1217 }
1218
1219 #[tokio::test]
1220 async fn run_shuts_down_on_channel_close() {
1221 let (market, _) = setup_market_weighted(vec![]);
1222 let config = ComputationManagerConfig::new();
1223 let (manager, _event_rx) = ComputationManager::new(config, market).unwrap();
1224
1225 let (event_tx, event_rx) = broadcast::channel::<MarketEvent>(16);
1226 let (_shutdown_tx, shutdown_rx) = broadcast::channel::<()>(1);
1227
1228 let handle = tokio::spawn(async move {
1229 manager.run(event_rx, shutdown_rx).await;
1230 });
1231
1232 drop(event_tx);
1233
1234 tokio::time::timeout(tokio::time::Duration::from_secs(1), handle)
1235 .await
1236 .expect("manager should shutdown on channel close")
1237 .expect("task should complete successfully");
1238 }
1239
1240 #[tokio::test]
1241 async fn partial_spot_price_failure_broadcasts_computation_complete() {
1242 let market = market_with_mixed_sim_states();
1245 let config = ComputationManagerConfig::new();
1246 let (manager, mut event_rx) = ComputationManager::new(config, market).unwrap();
1247
1248 let changed = ChangedComponents { is_full_recompute: true, ..Default::default() };
1249 manager.compute_all(&changed).await;
1250
1251 let events = drain_events(&mut event_rx);
1252
1253 assert!(
1255 events.iter().any(|e| matches!(
1256 e,
1257 DerivedDataEvent::ComputationComplete { computation_id: "spot_prices", .. }
1258 )),
1259 "expected ComputationComplete(spot_prices), got: {events:?}"
1260 );
1261 assert!(
1262 !events.iter().any(|e| matches!(
1263 e,
1264 DerivedDataEvent::ComputationFailed { computation_id: "spot_prices", .. }
1265 )),
1266 "should not broadcast ComputationFailed for partial failure"
1267 );
1268
1269 let complete = events.iter().find(|e| {
1271 matches!(e, DerivedDataEvent::ComputationComplete { computation_id: "spot_prices", .. })
1272 });
1273 if let Some(DerivedDataEvent::ComputationComplete { failed_items, .. }) = complete {
1274 assert!(
1275 !failed_items.is_empty(),
1276 "ComputationComplete should carry failed_items for pool2"
1277 );
1278 }
1279
1280 let eth = token(1, "ETH");
1283 let dai = token(3, "DAI");
1284 let store = manager.store();
1285 let guard = store.read().await;
1286 let key_eth_dai = ("eth_dai".to_string(), eth.address.clone(), dai.address.clone());
1287 let key_dai_eth = ("eth_dai".to_string(), dai.address.clone(), eth.address.clone());
1288 assert!(
1289 guard
1290 .spot_price_failure(&key_eth_dai)
1291 .is_some() ||
1292 guard
1293 .spot_price_failure(&key_dai_eth)
1294 .is_some(),
1295 "store should persist failure reason for eth_dai (missing sim state)"
1296 );
1297 }
1298}