1use std::future::Future;
2use std::sync::Arc;
3use std::time::Duration;
4
5use futures::{StreamExt, stream};
6use tokio_util::sync::CancellationToken;
7
8use crate::{HostId, HostRecord};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct FanoutPolicy {
13 max_concurrency: usize,
14 per_target_timeout: Duration,
15}
16
17impl FanoutPolicy {
18 pub fn new(
20 max_concurrency: usize,
21 per_target_timeout: Duration,
22 ) -> Result<Self, FanoutPolicyError> {
23 if max_concurrency == 0 {
24 return Err(FanoutPolicyError::ZeroConcurrency);
25 }
26 if per_target_timeout.is_zero() {
27 return Err(FanoutPolicyError::ZeroTimeout);
28 }
29 Ok(Self {
30 max_concurrency,
31 per_target_timeout,
32 })
33 }
34
35 #[must_use]
37 pub const fn max_concurrency(self) -> usize {
38 self.max_concurrency
39 }
40
41 #[must_use]
43 pub const fn per_target_timeout(self) -> Duration {
44 self.per_target_timeout
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
50pub enum FanoutPolicyError {
51 #[error("fanout concurrency must be greater than zero")]
53 ZeroConcurrency,
54 #[error("fanout per-target timeout must be greater than zero")]
56 ZeroTimeout,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum TargetOutcomeKind<T, E> {
62 Succeeded(T),
64 Failed(E),
66 Cancelled,
68 TimedOut,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct TargetOutcome<T, E> {
75 index: usize,
76 host: HostId,
77 kind: TargetOutcomeKind<T, E>,
78}
79
80impl<T, E> TargetOutcome<T, E> {
81 #[must_use]
83 pub const fn index(&self) -> usize {
84 self.index
85 }
86
87 #[must_use]
89 pub fn host(&self) -> &HostId {
90 &self.host
91 }
92
93 #[must_use]
95 pub fn kind(&self) -> &TargetOutcomeKind<T, E> {
96 &self.kind
97 }
98
99 #[must_use]
101 pub fn into_parts(self) -> (usize, HostId, TargetOutcomeKind<T, E>) {
102 (self.index, self.host, self.kind)
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct FanoutReport<T, E> {
109 outcomes: Vec<TargetOutcome<T, E>>,
110}
111
112impl<T, E> FanoutReport<T, E> {
113 #[must_use]
115 pub fn outcomes(&self) -> &[TargetOutcome<T, E>] {
116 &self.outcomes
117 }
118
119 #[must_use]
121 pub fn into_outcomes(self) -> Vec<TargetOutcome<T, E>> {
122 self.outcomes
123 }
124
125 #[must_use]
127 pub fn success_count(&self) -> usize {
128 self.outcomes
129 .iter()
130 .filter(|outcome| matches!(outcome.kind, TargetOutcomeKind::Succeeded(_)))
131 .count()
132 }
133
134 #[must_use]
136 pub fn failure_count(&self) -> usize {
137 self.outcomes
138 .iter()
139 .filter(|outcome| matches!(outcome.kind, TargetOutcomeKind::Failed(_)))
140 .count()
141 }
142
143 #[must_use]
145 pub fn cancelled_count(&self) -> usize {
146 self.outcomes
147 .iter()
148 .filter(|outcome| matches!(outcome.kind, TargetOutcomeKind::Cancelled))
149 .count()
150 }
151
152 #[must_use]
154 pub fn timed_out_count(&self) -> usize {
155 self.outcomes
156 .iter()
157 .filter(|outcome| matches!(outcome.kind, TargetOutcomeKind::TimedOut))
158 .count()
159 }
160
161 #[must_use]
163 pub fn all_succeeded(&self) -> bool {
164 self.success_count() == self.outcomes.len()
165 }
166}
167
168#[derive(Debug, Clone, Copy)]
170pub struct FanoutScheduler {
171 policy: FanoutPolicy,
172}
173
174impl FanoutScheduler {
175 #[must_use]
177 pub const fn new(policy: FanoutPolicy) -> Self {
178 Self { policy }
179 }
180
181 pub async fn run<T, E, F, Fut>(
187 &self,
188 targets: Vec<HostRecord>,
189 cancellation: CancellationToken,
190 operation: F,
191 ) -> FanoutReport<T, E>
192 where
193 T: Send,
194 E: Send,
195 F: Fn(HostRecord, CancellationToken) -> Fut + Send + Sync,
196 Fut: Future<Output = Result<T, E>> + Send,
197 {
198 self.run_with_payload(
199 targets.into_iter().map(|host| (host, ())).collect(),
200 cancellation,
201 move |host, (), child| operation(host, child),
202 )
203 .await
204 }
205
206 pub async fn run_with_payload<P, T, E, F, Fut>(
211 &self,
212 targets: Vec<(HostRecord, P)>,
213 cancellation: CancellationToken,
214 operation: F,
215 ) -> FanoutReport<T, E>
216 where
217 P: Send,
218 T: Send,
219 E: Send,
220 F: Fn(HostRecord, P, CancellationToken) -> Fut + Send + Sync,
221 Fut: Future<Output = Result<T, E>> + Send,
222 {
223 let operation = Arc::new(operation);
224 let timeout = self.policy.per_target_timeout;
225 let mut outcomes = stream::iter(targets.into_iter().enumerate())
226 .map(|(index, (host, payload))| {
227 let operation = Arc::clone(&operation);
228 let child = cancellation.child_token();
229 async move {
230 let host_id = host.id().clone();
231 let future = operation(host, payload, child.clone());
232 let kind = tokio::select! {
233 () = child.cancelled() => TargetOutcomeKind::Cancelled,
234 result = tokio::time::timeout(timeout, future) => match result {
235 Ok(Ok(value)) => TargetOutcomeKind::Succeeded(value),
236 Ok(Err(error)) => TargetOutcomeKind::Failed(error),
237 Err(_) => {
238 child.cancel();
239 TargetOutcomeKind::TimedOut
240 }
241 }
242 };
243 TargetOutcome {
244 index,
245 host: host_id,
246 kind,
247 }
248 }
249 })
250 .buffer_unordered(self.policy.max_concurrency)
251 .collect::<Vec<_>>()
252 .await;
253 outcomes.sort_by_key(TargetOutcome::index);
254 FanoutReport { outcomes }
255 }
256}
257
258#[cfg(test)]
259#[path = "fanout_tests.rs"]
260mod tests;