Skip to main content

hopr_strategy/
strategy.rs

1//! ## Multi Strategy
2//!
3//! Runs multiple sub-strategies concurrently. Each sub-strategy manages its own
4//! event subscription and internal timers via the `Strategy::run` method.
5//!
6//! `MultiStrategy` is a pure combinator: it accepts any `Box<dyn Strategy + Send>` —
7//! including strategies defined outside this crate — and runs them all concurrently.
8//! Sub-strategies are fully isolated: a failure in one is logged and does not affect
9//! the others.
10use std::fmt::{Debug, Display, Formatter};
11
12use async_trait::async_trait;
13use tracing::warn;
14
15use crate::errors::Result;
16
17/// A strategy that runs until cancelled or a fatal error occurs.
18///
19/// Each implementation subscribes to the node's event stream and/or creates internal
20/// timers in [`run`](Strategy::run). The trait is trivially object-safe: `run` takes only
21/// `&mut self`, so strategies can be held as `Box<dyn Strategy + Send>`.
22///
23/// Any type implementing this trait can be composed into a [`MultiStrategy`] without
24/// any changes to this crate.
25#[async_trait]
26pub trait Strategy: Display + Send {
27    /// Run the strategy. Returns only on cancellation or fatal error.
28    async fn run(&mut self) -> Result<()>;
29}
30
31/// Runs a group of sub-strategies concurrently, each in its own async task.
32///
33/// `MultiStrategy` is strategy-kind-agnostic: it only knows about
34/// `Box<dyn Strategy + Send>`. Any type implementing [`Strategy`] — including
35/// ones defined outside this crate — can be composed here.
36pub struct MultiStrategy {
37    strategies: Vec<Box<dyn Strategy + Send>>,
38}
39
40impl MultiStrategy {
41    /// Creates a new `MultiStrategy` from pre-built strategy objects.
42    ///
43    /// Strategies are passed in already constructed; `MultiStrategy` does not know or
44    /// care about the concrete types. Pass an empty `strategies` vec to get a passive
45    /// strategy that blocks forever.
46    pub fn new(strategies: Vec<Box<dyn Strategy + Send>>) -> Self {
47        Self { strategies }
48    }
49}
50
51impl Debug for MultiStrategy {
52    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53        write!(f, "MultiStrategy({} sub-strategies)", self.strategies.len())
54    }
55}
56
57impl Display for MultiStrategy {
58    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
59        let names: Vec<String> = self.strategies.iter().map(|s| s.to_string()).collect();
60        if names.is_empty() {
61            write!(f, "multi_strategy(passive)")
62        } else {
63            write!(f, "multi_strategy({})", names.join(", "))
64        }
65    }
66}
67
68#[async_trait]
69impl Strategy for MultiStrategy {
70    async fn run(&mut self) -> Result<()> {
71        use futures::StreamExt as _;
72        use hopr_utils::runtime::prelude::{AbortHandle, abortable, spawn};
73
74        let strategies = std::mem::take(&mut self.strategies);
75
76        if strategies.is_empty() {
77            // Passive strategy: block forever until cancelled.
78            futures::future::pending::<()>().await;
79            return Ok(());
80        }
81
82        // Spawn each sub-strategy as an abortable task.
83        // Keeping all AbortHandles in a RAII guard ensures every sub-task is cancelled
84        // when MultiStrategy is dropped (graceful shutdown).
85        let mut join_handles = Vec::new();
86        let mut abort_handles: Vec<AbortHandle> = Vec::new();
87        for mut s in strategies {
88            let proc = hopr_utils::runtime::diagnostics::instrument(
89                async move { s.run().await },
90                "multi_strategy_sub_task",
91                module_path!(),
92                file!(),
93                line!(),
94            );
95            let (proc, abort_handle) = abortable(proc);
96            join_handles.push(spawn(proc));
97            abort_handles.push(abort_handle);
98        }
99
100        struct AbortGuard(Vec<AbortHandle>);
101        impl Drop for AbortGuard {
102            fn drop(&mut self) {
103                for h in &self.0 {
104                    h.abort();
105                }
106            }
107        }
108        let _guard = AbortGuard(abort_handles);
109
110        // Process completions as they arrive. Sub-strategies are fully isolated:
111        // a failure in one is logged but does not affect the others.
112        let mut pending: futures::stream::FuturesUnordered<_> = join_handles.into_iter().collect();
113
114        while let Some(join_result) = pending.next().await {
115            let strategy_result = match join_result {
116                Err(e) => Err(crate::errors::StrategyError::Other(e.into())),
117                Ok(Ok(result)) => result,
118                Ok(Err(_aborted)) => continue, // aborted by the guard — expected during shutdown
119            };
120
121            if let Err(e) = strategy_result {
122                warn!(%e, "sub-strategy failed");
123            }
124        }
125
126        Ok(())
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use std::fmt::{Display, Formatter};
133
134    use super::*;
135    use crate::errors::StrategyError;
136
137    struct OkStrategy;
138    impl Display for OkStrategy {
139        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140            write!(f, "ok")
141        }
142    }
143    #[async_trait]
144    impl Strategy for OkStrategy {
145        async fn run(&mut self) -> Result<()> {
146            Ok(())
147        }
148    }
149
150    struct FailStrategy;
151    impl Display for FailStrategy {
152        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
153            write!(f, "fail")
154        }
155    }
156    #[async_trait]
157    impl Strategy for FailStrategy {
158        async fn run(&mut self) -> Result<()> {
159            Err(StrategyError::Other(anyhow::anyhow!("error")))
160        }
161    }
162
163    /// An externally-defined strategy — simulates a plugin or application-defined strategy.
164    struct ExternalStrategy {
165        ran: bool,
166    }
167    impl Display for ExternalStrategy {
168        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
169            write!(f, "external")
170        }
171    }
172    #[async_trait]
173    impl Strategy for ExternalStrategy {
174        async fn run(&mut self) -> Result<()> {
175            self.ran = true;
176            Ok(())
177        }
178    }
179
180    #[tokio::test]
181    async fn test_multi_strategy_sub_failure_does_not_propagate() -> anyhow::Result<()> {
182        // A failing sub-strategy is isolated: the MultiStrategy still returns Ok.
183        let mut ms = MultiStrategy::new(vec![Box::new(FailStrategy), Box::new(OkStrategy)]);
184        ms.run().await?;
185        Ok(())
186    }
187
188    #[tokio::test]
189    async fn test_multi_strategy_accepts_external_strategy() -> anyhow::Result<()> {
190        // Demonstrates that any impl Strategy can be composed without modifying hopr-strategy.
191        let mut ms = MultiStrategy::new(vec![Box::new(OkStrategy), Box::new(ExternalStrategy { ran: false })]);
192        ms.run().await?;
193        Ok(())
194    }
195
196    #[tokio::test]
197    async fn test_multi_strategy_empty_is_passive() {
198        // An empty MultiStrategy blocks forever — verify it does not complete immediately.
199        let mut ms = MultiStrategy::new(vec![]);
200        let result =
201            futures_time::future::FutureExt::timeout(ms.run(), futures_time::time::Duration::from_millis(50)).await;
202        assert!(result.is_err(), "empty MultiStrategy should block (timeout expected)");
203    }
204
205    #[test]
206    fn test_multi_strategy_display() {
207        let ms = MultiStrategy::new(vec![Box::new(OkStrategy), Box::new(FailStrategy)]);
208        assert_eq!(ms.to_string(), "multi_strategy(ok, fail)");
209    }
210
211    #[test]
212    fn test_multi_strategy_display_passive() {
213        let ms = MultiStrategy::new(vec![]);
214        assert_eq!(ms.to_string(), "multi_strategy(passive)");
215    }
216}