1use std::{
10 collections::{HashMap, HashSet},
11 sync::Arc,
12 time::{Instant, SystemTime, UNIX_EPOCH},
13};
14
15use async_trait::async_trait;
16use futures::future::join_all;
17use metrics::{counter, gauge, histogram};
18use tokio::sync::{broadcast, RwLock};
19use tracing::{error, info, trace, warn};
20use tycho_simulation::tycho_common::models::Address;
21
22use crate::types::ComponentId;
23
24#[derive(Debug, Clone, Default)]
29pub struct ChangedComponents {
30 pub added: HashMap<ComponentId, Vec<Address>>,
32 pub removed: Vec<ComponentId>,
34 pub updated: Vec<ComponentId>,
36 pub is_full_recompute: bool,
38}
39
40impl ChangedComponents {
41 pub fn all(market: MarketDataView) -> Self {
45 Self {
46 added: market.component_topology().clone(),
47 removed: vec![],
48 updated: vec![],
49 is_full_recompute: true,
50 }
51 }
52
53 pub fn is_topology_change(&self) -> bool {
55 !self.added.is_empty() || !self.removed.is_empty()
56 }
57
58 pub fn all_changed_ids(&self) -> HashSet<ComponentId> {
60 let mut all = HashSet::new();
61 all.extend(self.added.keys().cloned());
62 all.extend(self.removed.iter().cloned());
63 all.extend(self.updated.iter().cloned());
64 all
65 }
66}
67
68use super::{
69 computation::{ComputationId, ComputationRequirements, DerivedComputation},
70 computations::{PoolDepthComputation, SpotPriceComputation, TokenGasPriceComputation},
71 error::ComputationError,
72 events::DerivedDataEvent,
73 registry::ErasedComputation,
74 store::DerivedData,
75};
76use crate::feed::{
77 events::{EventError, MarketEvent, MarketEventHandler},
78 market_data::{MarketData, MarketDataView},
79};
80
81pub type SharedDerivedDataRef = Arc<RwLock<DerivedData>>;
83
84#[derive(Debug, Clone)]
86pub struct ComputationManagerConfig {
87 gas_token: Address,
89 max_hop: usize,
91 depth_slippage_threshold: f64,
93}
94
95impl ComputationManagerConfig {
96 pub fn new() -> Self {
98 Self::default()
99 }
100
101 pub fn with_depth_slippage_threshold(mut self, threshold: f64) -> Self {
103 self.depth_slippage_threshold = threshold;
104 self
105 }
106
107 pub fn with_max_hop(mut self, hop_count: usize) -> Self {
109 self.max_hop = hop_count;
110 self
111 }
112
113 pub fn with_gas_token(mut self, gas_token: Address) -> Self {
115 self.gas_token = gas_token;
116 self
117 }
118
119 pub fn gas_token(&self) -> &Address {
121 &self.gas_token
122 }
123
124 pub fn max_hop(&self) -> usize {
126 self.max_hop
127 }
128
129 pub fn depth_slippage_threshold(&self) -> f64 {
131 self.depth_slippage_threshold
132 }
133}
134
135impl Default for ComputationManagerConfig {
136 fn default() -> Self {
137 Self { gas_token: Address::zero(20), max_hop: 2, depth_slippage_threshold: 0.01 }
138 }
139}
140
141pub struct ComputationManager {
143 market_data: MarketData,
145 store: SharedDerivedDataRef,
147 computations: Vec<Box<dyn ErasedComputation>>,
149 event_tx: broadcast::Sender<DerivedDataEvent>,
151}
152
153struct ComputationSchedule {
155 stages: Vec<Vec<usize>>,
157 unscheduled: Vec<usize>,
159}
160
161impl ComputationManager {
162 pub fn new(
168 config: ComputationManagerConfig,
169 market_data: MarketData,
170 ) -> Result<(Self, broadcast::Receiver<DerivedDataEvent>), ComputationError> {
171 let (mut manager, event_rx) = Self::empty(market_data);
172 manager.register(SpotPriceComputation::new())?;
173 manager.register(
174 TokenGasPriceComputation::default()
175 .with_max_hops(config.max_hop)
176 .with_gas_token(config.gas_token),
177 )?;
178 manager.register(PoolDepthComputation::new(config.depth_slippage_threshold)?)?;
179 Ok((manager, event_rx))
180 }
181
182 pub(crate) fn empty(market_data: MarketData) -> (Self, broadcast::Receiver<DerivedDataEvent>) {
187 let (event_tx, event_rx) = broadcast::channel(64);
188 (
189 Self {
190 market_data,
191 store: DerivedData::new_shared(),
192 computations: Vec::new(),
193 event_tx,
194 },
195 event_rx,
196 )
197 }
198
199 pub(crate) fn register<C: DerivedComputation>(
210 &mut self,
211 computation: C,
212 ) -> Result<(), ComputationError> {
213 if self
214 .computations
215 .iter()
216 .any(|existing| existing.id() == C::ID)
217 {
218 return Err(ComputationError::DuplicateComputationId(C::ID));
219 }
220 self.computations
221 .push(Box::new(computation));
222 Ok(())
223 }
224
225 pub fn store(&self) -> SharedDerivedDataRef {
227 Arc::clone(&self.store)
228 }
229
230 pub fn event_sender(&self) -> broadcast::Sender<DerivedDataEvent> {
232 self.event_tx.clone()
233 }
234
235 pub async fn run(
239 mut self,
240 mut event_rx: broadcast::Receiver<MarketEvent>,
241 mut shutdown_rx: broadcast::Receiver<()>,
242 ) {
243 info!("computation manager started");
244
245 loop {
246 tokio::select! {
247 biased;
248
249 _ = shutdown_rx.recv() => {
250 info!("computation manager shutting down");
251 break;
252 }
253
254 event_result = event_rx.recv() => {
255 match event_result {
256 Ok(event) => {
257 if let Err(e) = self.handle_event(&event).await {
258 warn!(error = ?e, "failed to handle market event");
259 }
260 }
261 Err(broadcast::error::RecvError::Closed) => {
262 info!("event channel closed, computation manager shutting down");
263 break;
264 }
265 Err(broadcast::error::RecvError::Lagged(skipped)) => {
266 warn!(
267 skipped,
268 "computation manager lagged, skipped {} events. Recomputing from current state.",
269 skipped
270 );
271 let market = self.market_data.read().await;
272 let changed = ChangedComponents::all(market);
273 self.compute_all(&changed).await;
274 }
275 }
276 }
277 }
278 }
279 }
280
281 async fn compute_all(&self, changed: &ChangedComponents) {
289 let total_start = Instant::now();
290
291 let Some(block) = self
293 .market_data
294 .read()
295 .await
296 .last_updated()
297 .map(|b| b.number())
298 else {
299 warn!("market data has no last updated block, skipping computations");
300 return;
301 };
302
303 let _ = self
305 .event_tx
306 .send(DerivedDataEvent::NewBlock { block });
307
308 let nodes: Vec<(ComputationId, ComputationRequirements)> = self
309 .computations
310 .iter()
311 .map(|computation| (computation.id(), computation.requirements()))
312 .collect();
313 let schedule = build_schedule(&nodes);
314 for &idx in &schedule.unscheduled {
315 let computation_id = nodes[idx].0;
316 error!(computation = computation_id, "computation skipped: requirement cycle");
317 counter!(
318 "derived_computation_failures_total",
319 "computation" => computation_id,
320 "reason" => "cycle"
321 )
322 .increment(1);
323 let _ = self
324 .event_tx
325 .send(DerivedDataEvent::ComputationFailed { computation_id, block });
326 }
327
328 let mut succeeded: HashSet<ComputationId> = HashSet::new();
329 for stage in &schedule.stages {
330 let mut runnable = Vec::new();
333 {
334 let store = self.store.read().await;
335 for &idx in stage {
336 let reqs = &nodes[idx].1;
337 let fresh_ready = reqs
338 .fresh_requirements()
339 .iter()
340 .all(|id| succeeded.contains(id));
341 let stale_ready = reqs
342 .stale_requirements()
343 .iter()
344 .all(|id| succeeded.contains(id) || store.output_block(id).is_some());
345 if fresh_ready && stale_ready {
346 runnable.push(idx);
347 } else {
348 let computation_id = nodes[idx].0;
349 counter!(
350 "derived_computation_failures_total",
351 "computation" => computation_id,
352 "reason" => "upstream_failed"
353 )
354 .increment(1);
355 let _ = self
356 .event_tx
357 .send(DerivedDataEvent::ComputationFailed { computation_id, block });
358 }
359 }
360 }
361
362 if runnable.is_empty() {
363 continue;
364 }
365
366 let results = join_all(runnable.iter().map(|&idx| async move {
368 let start = Instant::now();
369 let result = self.computations[idx]
370 .compute_erased(&self.market_data, &self.store, changed, block)
371 .await;
372 (idx, result, start.elapsed())
373 }))
374 .await;
375
376 let mut store = self.store.write().await;
378 for (idx, result, elapsed) in results {
379 let computation_id = nodes[idx].0;
380 match result {
381 Ok(write) => {
382 (write.persist)(&mut store);
383 histogram!(
384 "derived_computation_duration_seconds",
385 "computation" => computation_id
386 )
387 .record(elapsed.as_secs_f64());
388 gauge!(
389 "derived_last_success_timestamp_seconds",
390 "computation" => computation_id
391 )
392 .set(unix_now_seconds());
393 info!(
394 computation = computation_id,
395 failed = write.failed_items.len(),
396 elapsed_ms = elapsed.as_millis(),
397 "computation complete"
398 );
399 let _ = self
400 .event_tx
401 .send(DerivedDataEvent::ComputationComplete {
402 computation_id,
403 block,
404 failed_items: write.failed_items,
405 });
406 succeeded.insert(computation_id);
407 }
408 Err(e) => {
409 counter!(
410 "derived_computation_failures_total",
411 "computation" => computation_id,
412 "reason" => "error"
413 )
414 .increment(1);
415 warn!(
416 error = ?e,
417 computation = computation_id,
418 elapsed_ms = elapsed.as_millis(),
419 "computation failed"
420 );
421 let _ = self
422 .event_tx
423 .send(DerivedDataEvent::ComputationFailed { computation_id, block });
424 }
425 }
426 }
427 }
428
429 info!(
430 block,
431 total_ms = total_start.elapsed().as_millis(),
432 "all derived computations complete"
433 );
434 }
435}
436
437fn unix_now_seconds() -> f64 {
439 SystemTime::now()
440 .duration_since(UNIX_EPOCH)
441 .map(|elapsed| elapsed.as_secs_f64())
442 .unwrap_or(0.0)
443}
444
445fn build_schedule(nodes: &[(ComputationId, ComputationRequirements)]) -> ComputationSchedule {
453 let ids: Vec<ComputationId> = nodes
454 .iter()
455 .map(|(id, _)| *id)
456 .collect();
457 let mut stage_of: Vec<Option<usize>> = vec![None; nodes.len()];
458
459 loop {
460 let mut progressed = false;
461 for (idx, (_, reqs)) in nodes.iter().enumerate() {
462 if stage_of[idx].is_some() {
463 continue;
464 }
465 let mut stage = 0;
466 let mut ready = true;
467 for dep in reqs
468 .fresh_requirements()
469 .iter()
470 .chain(reqs.stale_requirements().iter())
471 {
472 let Some(dep_idx) = ids.iter().position(|id| id == dep) else {
473 continue;
474 };
475 match stage_of[dep_idx] {
476 Some(dep_stage) => stage = stage.max(dep_stage + 1),
477 None => {
478 ready = false;
479 break;
480 }
481 }
482 }
483 if ready {
484 stage_of[idx] = Some(stage);
485 progressed = true;
486 }
487 }
488 if !progressed {
489 break;
490 }
491 }
492
493 let stage_count = stage_of
494 .iter()
495 .filter_map(|stage| *stage)
496 .max()
497 .map_or(0, |max| max + 1);
498 let mut stages = vec![Vec::new(); stage_count];
499 let mut unscheduled = Vec::new();
500 for (idx, stage) in stage_of.iter().enumerate() {
501 match stage {
502 Some(stage) => stages[*stage].push(idx),
503 None => unscheduled.push(idx),
504 }
505 }
506 ComputationSchedule { stages, unscheduled }
507}
508
509#[async_trait]
510impl MarketEventHandler for ComputationManager {
511 async fn handle_event(&mut self, event: &MarketEvent) -> Result<(), EventError> {
512 match event {
513 MarketEvent::MarketUpdated {
514 added_components,
515 removed_components,
516 updated_components,
517 } if !added_components.is_empty() ||
518 !removed_components.is_empty() ||
519 !updated_components.is_empty() =>
520 {
521 trace!(
522 added = added_components.len(),
523 removed = removed_components.len(),
524 updated = updated_components.len(),
525 "market updated, running incremental computations"
526 );
527
528 let changed = ChangedComponents {
529 added: added_components.clone(),
530 removed: removed_components.clone(),
531 updated: updated_components.clone(),
532 is_full_recompute: false,
533 };
534 self.compute_all(&changed).await;
535 }
536 _ => {
537 trace!("empty market update, skipping computations");
538 }
539 }
540
541 Ok(())
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use std::{
548 collections::HashMap,
549 sync::{
550 atomic::{AtomicBool, Ordering},
551 Arc,
552 },
553 };
554
555 use tokio::sync::broadcast;
556
557 use super::*;
558 use crate::{
559 algorithm::test_utils::{component, setup_market_weighted, token, MockProtocolSim},
560 derived::computation::{ComputationOutput, FailedItem, FailedItemError},
561 feed::market_data::{MarketData, MarketState},
562 types::BlockInfo,
563 };
564
565 fn drain_events(rx: &mut broadcast::Receiver<DerivedDataEvent>) -> Vec<DerivedDataEvent> {
567 let mut events = vec![];
568 loop {
569 match rx.try_recv() {
570 Ok(e) => events.push(e),
571 Err(broadcast::error::TryRecvError::Empty) => break,
572 Err(broadcast::error::TryRecvError::Lagged(_)) => continue,
573 Err(broadcast::error::TryRecvError::Closed) => break,
574 }
575 }
576 events
577 }
578
579 #[test]
582 fn schedule_empty_has_no_stages() {
583 let schedule = build_schedule(&[]);
584 assert!(schedule.stages.is_empty());
585 assert!(schedule.unscheduled.is_empty());
586 }
587
588 #[test]
589 fn schedule_single_root_is_one_stage() {
590 let schedule = build_schedule(&[("a", ComputationRequirements::none())]);
591 assert_eq!(schedule.stages, vec![vec![0]]);
592 assert!(schedule.unscheduled.is_empty());
593 }
594
595 #[test]
596 fn schedule_independent_roots_share_one_stage() {
597 let schedule = build_schedule(&[
598 ("a", ComputationRequirements::none()),
599 ("b", ComputationRequirements::none()),
600 ]);
601 assert_eq!(schedule.stages, vec![vec![0, 1]]);
602 assert!(schedule.unscheduled.is_empty());
603 }
604
605 #[test]
606 fn schedule_chain_orders_into_successive_stages() {
607 let schedule = build_schedule(&[
609 ("a", ComputationRequirements::none()),
610 ("b", ComputationRequirements::fresh(["a"])),
611 ("c", ComputationRequirements::fresh(["b"])),
612 ]);
613 assert_eq!(schedule.stages, vec![vec![0], vec![1], vec![2]]);
614 assert!(schedule.unscheduled.is_empty());
615 }
616
617 #[test]
618 fn schedule_diamond_places_join_after_both_parents() {
619 let schedule = build_schedule(&[
621 ("a", ComputationRequirements::none()),
622 ("b", ComputationRequirements::fresh(["a"])),
623 ("c", ComputationRequirements::fresh(["a"])),
624 ("d", ComputationRequirements::fresh(["b", "c"])),
625 ]);
626 assert_eq!(schedule.stages, vec![vec![0], vec![1, 2], vec![3]]);
627 assert!(schedule.unscheduled.is_empty());
628 }
629
630 #[test]
631 fn schedule_preserves_input_order_within_a_stage() {
632 let schedule = build_schedule(&[
633 ("a", ComputationRequirements::none()),
634 ("b", ComputationRequirements::fresh(["a"])),
635 ("c", ComputationRequirements::fresh(["a"])),
636 ]);
637 assert_eq!(schedule.stages, vec![vec![0], vec![1, 2]]);
639 }
640
641 #[test]
642 fn schedule_stale_requirement_orders_after_its_producer() {
643 let schedule = build_schedule(&[
644 ("a", ComputationRequirements::none()),
645 ("b", ComputationRequirements::stale(["a"])),
646 ]);
647 assert_eq!(schedule.stages, vec![vec![0], vec![1]]);
648 }
649
650 #[test]
651 fn schedule_requirement_on_unregistered_id_does_not_affect_ordering() {
652 let schedule = build_schedule(&[("a", ComputationRequirements::fresh(["ghost"]))]);
654 assert_eq!(schedule.stages, vec![vec![0]]);
655 assert!(schedule.unscheduled.is_empty());
656 }
657
658 #[test]
659 fn schedule_two_node_cycle_is_unscheduled() {
660 let schedule = build_schedule(&[
661 ("a", ComputationRequirements::fresh(["b"])),
662 ("b", ComputationRequirements::fresh(["a"])),
663 ]);
664 assert!(schedule.stages.is_empty());
665 assert_eq!(schedule.unscheduled, vec![0, 1]);
666 }
667
668 #[test]
669 fn schedule_isolates_cycle_from_schedulable_nodes() {
670 let schedule = build_schedule(&[
672 ("root", ComputationRequirements::none()),
673 ("x", ComputationRequirements::fresh(["y"])),
674 ("y", ComputationRequirements::fresh(["x"])),
675 ]);
676 assert_eq!(schedule.stages, vec![vec![0]]);
677 assert_eq!(schedule.unscheduled, vec![1, 2]);
678 }
679
680 #[test]
681 fn invalid_slippage_threshold_returns_error() {
682 let (market, _) = setup_market_weighted(vec![]);
683 let config = ComputationManagerConfig::new().with_depth_slippage_threshold(1.5);
684
685 let result = ComputationManager::new(config, market);
686 assert!(matches!(result, Err(ComputationError::InvalidConfiguration(_))));
687 }
688
689 #[tokio::test]
690 async fn handle_event_runs_computations_on_market_update() {
691 let eth = token(1, "ETH");
692 let usdc = token(2, "USDC");
693
694 let (market, _) = setup_market_weighted(vec![(
695 "eth_usdc",
696 ð,
697 &usdc,
698 MockProtocolSim::new(2000.0).with_gas(0),
699 )]);
700
701 let config = ComputationManagerConfig::new().with_gas_token(eth.address.clone());
702 let (mut manager, _event_rx) = ComputationManager::new(config, market).unwrap();
703
704 let event = MarketEvent::MarketUpdated {
705 added_components: HashMap::from([(
706 "eth_usdc".to_string(),
707 vec![eth.address.clone(), usdc.address.clone()],
708 )]),
709 removed_components: vec![],
710 updated_components: vec![],
711 };
712
713 manager
714 .handle_event(&event)
715 .await
716 .unwrap();
717
718 let store = manager.store();
719 let guard = store.read().await;
720 assert!(guard.token_prices().is_some());
721 assert!(guard.spot_prices().is_some());
722 }
723
724 #[tokio::test]
725 async fn handle_event_skips_empty_update() {
726 let (market, _) = setup_market_weighted(vec![]);
727 let config = ComputationManagerConfig::new();
728 let (mut manager, _event_rx) = ComputationManager::new(config, market).unwrap();
729
730 let event = MarketEvent::MarketUpdated {
731 added_components: HashMap::new(),
732 removed_components: vec![],
733 updated_components: vec![],
734 };
735
736 manager
737 .handle_event(&event)
738 .await
739 .unwrap();
740
741 let store = manager.store();
742 let guard = store.read().await;
743 assert!(guard.token_prices().is_none());
744 }
745
746 #[tokio::test]
747 async fn run_shuts_down_on_signal() {
748 let (market, _) = setup_market_weighted(vec![]);
749 let config = ComputationManagerConfig::new();
750 let (manager, _event_rx) = ComputationManager::new(config, market).unwrap();
751
752 let (_event_tx, event_rx) = broadcast::channel::<MarketEvent>(16);
753 let (shutdown_tx, shutdown_rx) = broadcast::channel::<()>(1);
754
755 let handle = tokio::spawn(async move {
756 manager.run(event_rx, shutdown_rx).await;
757 });
758
759 shutdown_tx.send(()).unwrap();
760
761 tokio::time::timeout(tokio::time::Duration::from_secs(1), handle)
762 .await
763 .expect("manager should shutdown")
764 .expect("task should complete successfully");
765 }
766
767 #[derive(Clone, Debug, PartialEq)]
770 struct CounterOutput(u32);
771
772 struct CounterComputation;
775
776 #[async_trait::async_trait]
777 impl DerivedComputation for CounterComputation {
778 type Output = CounterOutput;
779 const ID: ComputationId = "counter";
780
781 async fn compute(
782 &self,
783 _market: &MarketData,
784 _store: &SharedDerivedDataRef,
785 _changed: &ChangedComponents,
786 ) -> Result<ComputationOutput<Self::Output>, ComputationError> {
787 Ok(ComputationOutput::success(CounterOutput(7)))
788 }
789 }
790
791 fn market_with_block() -> MarketData {
793 let eth = token(1, "ETH");
794 let usdc = token(2, "USDC");
795 let (market, _) = setup_market_weighted(vec![(
796 "eth_usdc",
797 ð,
798 &usdc,
799 MockProtocolSim::new(2000.0).with_gas(0),
800 )]);
801 market
802 }
803
804 #[tokio::test]
805 async fn registered_custom_computation_runs_and_persists_via_default_slot() {
806 let (mut manager, mut event_rx) = ComputationManager::empty(market_with_block());
807 manager
808 .register(CounterComputation)
809 .unwrap();
810
811 manager
812 .compute_all(&ChangedComponents { is_full_recompute: true, ..Default::default() })
813 .await;
814
815 let store = manager.store();
816 let guard = store.read().await;
817 assert_eq!(
818 guard.output::<CounterOutput>(CounterComputation::ID),
819 Some(&CounterOutput(7)),
820 "default persist should write the output into the generic slot"
821 );
822 assert!(guard
823 .output_block(CounterComputation::ID)
824 .is_some());
825
826 let events = drain_events(&mut event_rx);
827 assert!(
828 events.iter().any(|e| matches!(
829 e,
830 DerivedDataEvent::ComputationComplete { computation_id: "counter", .. }
831 )),
832 "expected ComputationComplete(counter), got: {events:?}"
833 );
834 }
835
836 #[test]
837 fn registering_duplicate_id_is_rejected() {
838 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
839 manager
840 .register(CounterComputation)
841 .unwrap();
842
843 let result = manager.register(CounterComputation);
844
845 assert!(matches!(result, Err(ComputationError::DuplicateComputationId("counter"))));
846 }
847
848 fn event_summary(events: &[DerivedDataEvent]) -> Vec<(&'static str, &'static str)> {
852 events
853 .iter()
854 .map(|event| match event {
855 DerivedDataEvent::NewBlock { .. } => ("new_block", ""),
856 DerivedDataEvent::ComputationComplete { computation_id, .. } => {
857 ("complete", *computation_id)
858 }
859 DerivedDataEvent::ComputationFailed { computation_id, .. } => {
860 ("failed", *computation_id)
861 }
862 })
863 .collect()
864 }
865
866 async fn run_full_recompute(manager: &ComputationManager) -> Vec<DerivedDataEvent> {
868 let mut event_rx = manager.event_sender().subscribe();
869 manager
870 .compute_all(&ChangedComponents { is_full_recompute: true, ..Default::default() })
871 .await;
872 drain_events(&mut event_rx)
873 }
874
875 macro_rules! test_computation {
877 ($name:ident, $id:literal, $reqs:expr, $result:expr) => {
878 struct $name;
879
880 #[async_trait::async_trait]
881 impl DerivedComputation for $name {
882 type Output = ();
883 const ID: ComputationId = $id;
884
885 fn requirements(&self) -> ComputationRequirements {
886 $reqs
887 }
888
889 async fn compute(
890 &self,
891 _market: &MarketData,
892 _store: &SharedDerivedDataRef,
893 _changed: &ChangedComponents,
894 ) -> Result<ComputationOutput<Self::Output>, ComputationError> {
895 $result
896 }
897 }
898 };
899 }
900
901 test_computation!(
902 RootOk,
903 "root",
904 ComputationRequirements::none(),
905 Ok(ComputationOutput::success(()))
906 );
907 test_computation!(
908 DepOnRoot,
909 "dep",
910 ComputationRequirements::fresh(["root"]),
911 Ok(ComputationOutput::success(()))
912 );
913 test_computation!(
914 SecondDepOnRoot,
915 "dep2",
916 ComputationRequirements::fresh(["root"]),
917 Ok(ComputationOutput::success(()))
918 );
919 test_computation!(
920 RootErr,
921 "boom",
922 ComputationRequirements::none(),
923 Err(ComputationError::InvalidConfiguration("boom".to_string()))
924 );
925 test_computation!(
926 DepOnBoom,
927 "dep_boom",
928 ComputationRequirements::fresh(["boom"]),
929 Ok(ComputationOutput::success(()))
930 );
931 test_computation!(
932 ThirdOnBoom,
933 "third",
934 ComputationRequirements::fresh(["dep_boom"]),
935 Ok(ComputationOutput::success(()))
936 );
937 test_computation!(
938 StaleDepOnFlaky,
939 "stale_dep",
940 ComputationRequirements::stale(["flaky"]),
941 Ok(ComputationOutput::success(()))
942 );
943 test_computation!(
944 GhostDependent,
945 "needs_ghost",
946 ComputationRequirements::fresh(["ghost"]),
947 Ok(ComputationOutput::success(()))
948 );
949 test_computation!(
950 PartialProducer,
951 "partial",
952 ComputationRequirements::none(),
953 Ok(ComputationOutput::with_failures(
954 (),
955 vec![FailedItem { key: "x".to_string(), error: FailedItemError::MissingSpotPrice }]
956 ))
957 );
958 test_computation!(
959 DepOnPartial,
960 "dep_partial",
961 ComputationRequirements::fresh(["partial"]),
962 Ok(ComputationOutput::success(()))
963 );
964
965 struct FlakyProducer {
969 succeed: Arc<AtomicBool>,
970 }
971
972 #[async_trait::async_trait]
973 impl DerivedComputation for FlakyProducer {
974 type Output = ();
975 const ID: ComputationId = "flaky";
976
977 async fn compute(
978 &self,
979 _market: &MarketData,
980 _store: &SharedDerivedDataRef,
981 _changed: &ChangedComponents,
982 ) -> Result<ComputationOutput<Self::Output>, ComputationError> {
983 if self.succeed.load(Ordering::SeqCst) {
984 Ok(ComputationOutput::success(()))
985 } else {
986 Err(ComputationError::InvalidConfiguration("flaky".to_string()))
987 }
988 }
989 }
990
991 #[tokio::test]
992 async fn events_follow_dependency_order_across_stages() {
993 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
994 manager.register(RootOk).unwrap();
995 manager.register(DepOnRoot).unwrap();
996
997 let events = run_full_recompute(&manager).await;
998
999 assert_eq!(
1000 event_summary(&events),
1001 vec![("new_block", ""), ("complete", "root"), ("complete", "dep")]
1002 );
1003 }
1004
1005 #[tokio::test]
1006 async fn events_preserve_registration_order_within_a_stage() {
1007 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1008 manager.register(RootOk).unwrap();
1009 manager.register(DepOnRoot).unwrap();
1010 manager
1011 .register(SecondDepOnRoot)
1012 .unwrap();
1013
1014 let events = run_full_recompute(&manager).await;
1015
1016 assert_eq!(
1018 event_summary(&events),
1019 vec![
1020 ("new_block", ""),
1021 ("complete", "root"),
1022 ("complete", "dep"),
1023 ("complete", "dep2"),
1024 ]
1025 );
1026 }
1027
1028 #[tokio::test]
1029 async fn failed_dependency_cascades_to_dependents() {
1030 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1031 manager.register(RootErr).unwrap();
1032 manager.register(DepOnBoom).unwrap();
1033
1034 let events = run_full_recompute(&manager).await;
1035
1036 assert_eq!(
1038 event_summary(&events),
1039 vec![("new_block", ""), ("failed", "boom"), ("failed", "dep_boom")]
1040 );
1041 }
1042
1043 #[tokio::test]
1044 async fn computation_with_unregistered_requirement_is_skipped() {
1045 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1046 manager
1047 .register(GhostDependent)
1048 .unwrap();
1049
1050 let events = run_full_recompute(&manager).await;
1051
1052 assert_eq!(event_summary(&events), vec![("new_block", ""), ("failed", "needs_ghost")]);
1054 }
1055
1056 #[tokio::test]
1057 async fn fresh_dependent_runs_when_producer_succeeds_partially() {
1058 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1059 manager
1060 .register(PartialProducer)
1061 .unwrap();
1062 manager.register(DepOnPartial).unwrap();
1063
1064 let events = run_full_recompute(&manager).await;
1065
1066 assert_eq!(
1069 event_summary(&events),
1070 vec![("new_block", ""), ("complete", "partial"), ("complete", "dep_partial"),]
1071 );
1072 }
1073
1074 #[tokio::test]
1075 async fn failure_cascade_propagates_through_three_levels() {
1076 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1077 manager.register(RootErr).unwrap();
1078 manager.register(DepOnBoom).unwrap();
1079 manager.register(ThirdOnBoom).unwrap();
1080
1081 let events = run_full_recompute(&manager).await;
1082
1083 assert_eq!(
1085 event_summary(&events),
1086 vec![
1087 ("new_block", ""),
1088 ("failed", "boom"),
1089 ("failed", "dep_boom"),
1090 ("failed", "third"),
1091 ]
1092 );
1093 }
1094
1095 #[tokio::test]
1096 async fn stale_dependency_runs_on_prior_value_after_producer_fails() {
1097 let succeed = Arc::new(AtomicBool::new(true));
1098 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1099 manager
1100 .register(FlakyProducer { succeed: Arc::clone(&succeed) })
1101 .unwrap();
1102 manager
1103 .register(StaleDepOnFlaky)
1104 .unwrap();
1105
1106 let first = run_full_recompute(&manager).await;
1108 assert_eq!(
1109 event_summary(&first),
1110 vec![("new_block", ""), ("complete", "flaky"), ("complete", "stale_dep")]
1111 );
1112
1113 succeed.store(false, Ordering::SeqCst);
1116 let second = run_full_recompute(&manager).await;
1117 assert_eq!(
1118 event_summary(&second),
1119 vec![("new_block", ""), ("failed", "flaky"), ("complete", "stale_dep")]
1120 );
1121 }
1122
1123 #[tokio::test]
1124 async fn default_computations_cascade_failure_in_registration_order() {
1125 let (manager, _event_rx) = ComputationManager::new(
1128 ComputationManagerConfig::new(),
1129 market_with_component_no_sim_state(),
1130 )
1131 .unwrap();
1132
1133 let events = run_full_recompute(&manager).await;
1134
1135 assert_eq!(
1136 event_summary(&events),
1137 vec![
1138 ("new_block", ""),
1139 ("failed", "spot_prices"),
1140 ("failed", "token_prices"),
1141 ("failed", "pool_depths"),
1142 ]
1143 );
1144 }
1145
1146 fn market_with_component_no_sim_state() -> MarketData {
1151 let eth = token(1, "ETH");
1152 let usdc = token(2, "USDC");
1153 let pool = component("pool", &[eth.clone(), usdc.clone()]);
1154
1155 let mut market = MarketState::new();
1156 market.update_last_updated(BlockInfo::new(10, "0xhash".into(), 0));
1157 market.upsert_components(std::iter::once(pool));
1158 market.upsert_tokens([eth, usdc]);
1160 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
1161 }
1162
1163 fn market_with_mixed_sim_states() -> MarketData {
1166 let eth = token(1, "ETH");
1167 let usdc = token(2, "USDC");
1168 let dai = token(3, "DAI");
1169
1170 let pool1 = component("eth_usdc", &[eth.clone(), usdc.clone()]);
1171 let pool2 = component("eth_dai", &[eth.clone(), dai.clone()]);
1172
1173 let mut market = MarketState::new();
1174 market.update_last_updated(BlockInfo::new(10, "0xhash".into(), 0));
1175 market.upsert_components([pool1, pool2]);
1176 market
1178 .update_states([("eth_usdc".to_string(), Box::new(MockProtocolSim::new(2000.0)) as _)]);
1179 market.upsert_tokens([eth, usdc, dai]);
1180 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
1181 }
1182
1183 fn market_with_sim_state_no_gas_price() -> MarketData {
1188 let eth = token(1, "ETH");
1189 let usdc = token(2, "USDC");
1190 let pool = component("pool", &[eth.clone(), usdc.clone()]);
1191
1192 let mut market = MarketState::new();
1193 market.update_last_updated(BlockInfo::new(10, "0xhash".into(), 0));
1195 market.upsert_components(std::iter::once(pool));
1196 market.update_states([("pool".to_string(), Box::new(MockProtocolSim::new(2000.0)) as _)]);
1197 market.upsert_tokens([eth, usdc]);
1198 MarketData::new(std::sync::Arc::new(tokio::sync::RwLock::new(market)))
1199 }
1200
1201 #[tokio::test]
1202 async fn test_spot_price_failure_broadcasts_computation_failed() {
1203 let market = market_with_component_no_sim_state();
1204 let config = ComputationManagerConfig::new();
1205 let (manager, mut event_rx) = ComputationManager::new(config, market).unwrap();
1206
1207 let changed = ChangedComponents { is_full_recompute: true, ..Default::default() };
1209 manager.compute_all(&changed).await;
1210
1211 let events = drain_events(&mut event_rx);
1212
1213 assert!(
1214 events.iter().any(|e| matches!(
1215 e,
1216 DerivedDataEvent::ComputationFailed { computation_id: "spot_prices", .. }
1217 )),
1218 "expected ComputationFailed(spot_prices) in events: {events:?}"
1219 );
1220 }
1221
1222 #[tokio::test]
1223 async fn test_token_price_failure_broadcasts_computation_failed() {
1224 let eth = token(1, "ETH");
1225 let usdc = token(2, "USDC");
1226 let market = market_with_sim_state_no_gas_price();
1227 let config = ComputationManagerConfig::new().with_gas_token(eth.address.clone());
1228 let (mut manager, mut event_rx) = ComputationManager::new(config, market).unwrap();
1229
1230 let event = MarketEvent::MarketUpdated {
1232 added_components: HashMap::from([(
1233 "pool".to_string(),
1234 vec![eth.address.clone(), usdc.address.clone()],
1235 )]),
1236 removed_components: vec![],
1237 updated_components: vec![],
1238 };
1239 manager
1240 .handle_event(&event)
1241 .await
1242 .unwrap();
1243
1244 let events = drain_events(&mut event_rx);
1245 assert!(
1246 events.iter().any(|e| matches!(
1247 e,
1248 DerivedDataEvent::ComputationFailed { computation_id: "token_prices", .. }
1249 )),
1250 "expected ComputationFailed(token_prices) in events: {events:?}"
1251 );
1252 }
1253
1254 #[tokio::test]
1255 async fn run_shuts_down_on_channel_close() {
1256 let (market, _) = setup_market_weighted(vec![]);
1257 let config = ComputationManagerConfig::new();
1258 let (manager, _event_rx) = ComputationManager::new(config, market).unwrap();
1259
1260 let (event_tx, event_rx) = broadcast::channel::<MarketEvent>(16);
1261 let (_shutdown_tx, shutdown_rx) = broadcast::channel::<()>(1);
1262
1263 let handle = tokio::spawn(async move {
1264 manager.run(event_rx, shutdown_rx).await;
1265 });
1266
1267 drop(event_tx);
1268
1269 tokio::time::timeout(tokio::time::Duration::from_secs(1), handle)
1270 .await
1271 .expect("manager should shutdown on channel close")
1272 .expect("task should complete successfully");
1273 }
1274
1275 #[tokio::test]
1276 async fn partial_spot_price_failure_broadcasts_computation_complete() {
1277 let market = market_with_mixed_sim_states();
1280 let config = ComputationManagerConfig::new();
1281 let (manager, mut event_rx) = ComputationManager::new(config, market).unwrap();
1282
1283 let changed = ChangedComponents { is_full_recompute: true, ..Default::default() };
1284 manager.compute_all(&changed).await;
1285
1286 let events = drain_events(&mut event_rx);
1287
1288 assert!(
1290 events.iter().any(|e| matches!(
1291 e,
1292 DerivedDataEvent::ComputationComplete { computation_id: "spot_prices", .. }
1293 )),
1294 "expected ComputationComplete(spot_prices), got: {events:?}"
1295 );
1296 assert!(
1297 !events.iter().any(|e| matches!(
1298 e,
1299 DerivedDataEvent::ComputationFailed { computation_id: "spot_prices", .. }
1300 )),
1301 "should not broadcast ComputationFailed for partial failure"
1302 );
1303
1304 let complete = events.iter().find(|e| {
1306 matches!(e, DerivedDataEvent::ComputationComplete { computation_id: "spot_prices", .. })
1307 });
1308 if let Some(DerivedDataEvent::ComputationComplete { failed_items, .. }) = complete {
1309 assert!(
1310 !failed_items.is_empty(),
1311 "ComputationComplete should carry failed_items for pool2"
1312 );
1313 }
1314
1315 let eth = token(1, "ETH");
1318 let dai = token(3, "DAI");
1319 let store = manager.store();
1320 let guard = store.read().await;
1321 let key_eth_dai = ("eth_dai".to_string(), eth.address.clone(), dai.address.clone());
1322 let key_dai_eth = ("eth_dai".to_string(), dai.address.clone(), eth.address.clone());
1323 assert!(
1324 guard
1325 .spot_price_failure(&key_eth_dai)
1326 .is_some() ||
1327 guard
1328 .spot_price_failure(&key_dai_eth)
1329 .is_some(),
1330 "store should persist failure reason for eth_dai (missing sim state)"
1331 );
1332 }
1333
1334 struct FailingComputation;
1338
1339 #[async_trait::async_trait]
1340 impl DerivedComputation for FailingComputation {
1341 type Output = ();
1342 const ID: ComputationId = "failing";
1343
1344 async fn compute(
1345 &self,
1346 _market: &MarketData,
1347 _store: &SharedDerivedDataRef,
1348 _changed: &ChangedComponents,
1349 ) -> Result<ComputationOutput<Self::Output>, ComputationError> {
1350 Err(ComputationError::InvalidConfiguration("always fails".to_string()))
1351 }
1352 }
1353
1354 fn find_metric<'a>(
1356 recorded: &'a [(
1357 metrics_util::CompositeKey,
1358 Option<metrics::Unit>,
1359 Option<metrics::SharedString>,
1360 metrics_util::debugging::DebugValue,
1361 )],
1362 name: &str,
1363 labels: &[(&str, &str)],
1364 ) -> &'a metrics_util::debugging::DebugValue {
1365 recorded
1366 .iter()
1367 .find(|(key, _, _, _)| {
1368 key.key().name() == name &&
1369 labels
1370 .iter()
1371 .all(|(label_key, label_value)| {
1372 key.key()
1373 .labels()
1374 .any(|l| l.key() == *label_key && l.value() == *label_value)
1375 })
1376 })
1377 .map(|(_, _, _, value)| value)
1378 .unwrap_or_else(|| panic!("missing {name}{labels:?}, got {recorded:?}"))
1379 }
1380
1381 #[test]
1382 fn compute_all_records_derived_metrics() {
1383 use metrics_util::debugging::DebugValue;
1384
1385 let recorder = metrics_util::debugging::DebuggingRecorder::new();
1386 let snapshotter = recorder.snapshotter();
1387 let rt = tokio::runtime::Builder::new_current_thread()
1388 .enable_all()
1389 .build()
1390 .expect("runtime builds");
1391
1392 metrics::with_local_recorder(&recorder, || {
1393 rt.block_on(async {
1394 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1395 manager
1396 .register(CounterComputation)
1397 .unwrap();
1398 manager
1399 .compute_all(&ChangedComponents {
1400 is_full_recompute: true,
1401 ..Default::default()
1402 })
1403 .await;
1404 })
1405 });
1406
1407 let recorded = snapshotter.snapshot().into_vec();
1408 let recorded_names: Vec<(String, Vec<String>)> = recorded
1409 .iter()
1410 .map(|(key, _, _, _)| {
1411 (
1412 key.key().name().to_string(),
1413 key.key()
1414 .labels()
1415 .map(|l| format!("{}={}", l.key(), l.value()))
1416 .collect(),
1417 )
1418 })
1419 .collect();
1420 for expected in
1421 ["derived_computation_duration_seconds", "derived_last_success_timestamp_seconds"]
1422 {
1423 assert!(
1424 recorded_names
1425 .iter()
1426 .any(|(name, labels)| name == expected &&
1427 labels.contains(&"computation=counter".to_string())),
1428 "missing {expected}{{computation=counter}}, got {recorded_names:?}"
1429 );
1430 }
1431
1432 match find_metric(
1433 &recorded,
1434 "derived_last_success_timestamp_seconds",
1435 &[("computation", "counter")],
1436 ) {
1437 DebugValue::Gauge(value) => {
1438 assert!(value.0 > 1.7e9, "gauge value {} not a sane unix timestamp", value.0);
1439 }
1440 other => panic!("derived_last_success_timestamp_seconds is not a gauge: {other:?}"),
1441 }
1442
1443 match find_metric(
1444 &recorded,
1445 "derived_computation_duration_seconds",
1446 &[("computation", "counter")],
1447 ) {
1448 DebugValue::Histogram(samples) => {
1449 assert!(!samples.is_empty(), "expected at least one recorded duration sample");
1450 }
1451 other => panic!("derived_computation_duration_seconds is not a histogram: {other:?}"),
1452 }
1453 }
1454
1455 #[test]
1456 fn compute_all_records_failure_metric() {
1457 use metrics_util::debugging::DebugValue;
1458
1459 let recorder = metrics_util::debugging::DebuggingRecorder::new();
1460 let snapshotter = recorder.snapshotter();
1461 let rt = tokio::runtime::Builder::new_current_thread()
1462 .enable_all()
1463 .build()
1464 .expect("runtime builds");
1465
1466 metrics::with_local_recorder(&recorder, || {
1467 rt.block_on(async {
1468 let (mut manager, _event_rx) = ComputationManager::empty(market_with_block());
1469 manager
1470 .register(FailingComputation)
1471 .unwrap();
1472 manager
1473 .compute_all(&ChangedComponents {
1474 is_full_recompute: true,
1475 ..Default::default()
1476 })
1477 .await;
1478 })
1479 });
1480
1481 let recorded = snapshotter.snapshot().into_vec();
1482 match find_metric(
1483 &recorded,
1484 "derived_computation_failures_total",
1485 &[("computation", "failing"), ("reason", "error")],
1486 ) {
1487 DebugValue::Counter(value) => {
1488 assert!(*value >= 1, "expected failure counter >= 1, got {value}");
1489 }
1490 other => panic!("derived_computation_failures_total is not a counter: {other:?}"),
1491 }
1492 }
1493}