Skip to main content

tuwunel_service/federation/feds/
fold.rs

1//! Terminal folds for federation fanout outcomes.
2//!
3//! Ordered maps and sets make diagnostic output deterministic without changing
4//! completion-ordered request execution. These folds summarize unique room
5//! destinations; callers using repeated origins consume raw outcomes instead.
6
7use std::collections::{BTreeMap, BTreeSet};
8
9use futures::{Stream, pin_mut};
10use ruma::OwnedServerName;
11use tuwunel_core::utils::stream::ReadyExt;
12
13use super::{Fault, Outcome};
14
15/// Servers contributing the same datum.
16///
17/// Ordered storage keeps diagnostic output stable across runs.
18pub type Origins = BTreeSet<OwnedServerName>;
19
20/// Failed destinations indexed by server.
21///
22/// Each unique destination retains its terminal failure reason.
23pub type Faults = BTreeMap<OwnedServerName, Fault>;
24
25/// Grid of response data and the servers reporting it.
26///
27/// Empty successful responses remain distinct from faults so all destinations
28/// can be accounted for after the fold.
29pub struct Grid<K> {
30	/// Maps each response datum to the origins reporting it.
31	///
32	/// A response that contributes several data appears in every matching set.
33	pub data: BTreeMap<K, Origins>,
34
35	/// Contains origins whose successful response yielded no data.
36	///
37	/// These origins remain distinct from destinations that failed.
38	pub empty: Origins,
39
40	/// Contains origins that produced no successful response.
41	///
42	/// The mapped value preserves the terminal failure class.
43	pub faults: Faults,
44}
45
46/// Successful and failed destinations from a completed fanout.
47///
48/// The successful origin set retains more information than a count while
49/// keeping the broadcast verdict compact.
50pub struct Tally {
51	/// Contains origins that returned a successful response.
52	///
53	/// The set preserves origin identity rather than reducing it to a count.
54	pub ok: Origins,
55
56	/// Contains origins that produced no successful response.
57	///
58	/// The mapped value preserves the terminal failure class.
59	pub faults: Faults,
60}
61
62/// Adds terminal aggregations to a federation outcome stream.
63///
64/// Each fold preserves per-origin failures while applying caller policy only
65/// to successful typed responses.
66pub trait OutcomeExt<R>
67where
68	Self: Stream<Item = Outcome<R>> + Send + Sized,
69	R: Send,
70{
71	/// Merges successful responses and retains per-origin faults.
72	///
73	/// Successful values are folded in completion order from the supplied seed.
74	fn merge<T, F>(self, init: T, merge: F) -> impl Future<Output = (T, Faults)> + Send
75	where
76		T: Send,
77		F: Fn(T, R) -> T + Send;
78
79	/// Inverts extracted response data into origin sets.
80	///
81	/// Successful responses with no extracted data enter the empty-origin set.
82	fn grid<K, I, F>(self, extract: F) -> impl Future<Output = Grid<K>> + Send
83	where
84		K: Ord + Send,
85		I: IntoIterator<Item = K>,
86		F: Fn(R) -> I + Send;
87
88	/// Partitions destinations into successful origins and faults.
89	///
90	/// The partition retains origin identity and each terminal failure reason.
91	fn tally(self) -> impl Future<Output = Tally> + Send;
92
93	/// Returns the first completed successful response accepted by a predicate.
94	///
95	/// Dropping the search future cancels any remaining stream work.
96	fn first_acceptable<F>(
97		self,
98		accept: F,
99	) -> impl Future<Output = Option<(OwnedServerName, R)>> + Send
100	where
101		F: Fn(&R) -> bool + Send;
102}
103
104impl<S, R> OutcomeExt<R> for S
105where
106	S: Stream<Item = Outcome<R>> + Send + Sized,
107	R: Send,
108{
109	fn merge<T, F>(self, init: T, merge: F) -> impl Future<Output = (T, Faults)> + Send
110	where
111		T: Send,
112		F: Fn(T, R) -> T + Send,
113	{
114		self.ready_fold((init, Faults::new()), move |(merged, mut faults), outcome| match outcome
115			.result
116		{
117			| Ok(response) => (merge(merged, response), faults),
118			| Err(fault) => {
119				faults.insert(outcome.origin, fault);
120				(merged, faults)
121			},
122		})
123	}
124
125	fn grid<K, I, F>(self, extract: F) -> impl Future<Output = Grid<K>> + Send
126	where
127		K: Ord + Send,
128		I: IntoIterator<Item = K>,
129		F: Fn(R) -> I + Send,
130	{
131		let grid = Grid {
132			data: BTreeMap::new(),
133			empty: Origins::new(),
134			faults: Faults::new(),
135		};
136
137		self.ready_fold(grid, move |mut grid, outcome| {
138			match outcome.result {
139				| Ok(response) => {
140					let mut data = extract(response).into_iter();
141
142					if let Some(mut datum) = data.next() {
143						for next in data {
144							grid.data
145								.entry(datum)
146								.or_default()
147								.insert(outcome.origin.clone());
148
149							datum = next;
150						}
151
152						grid.data
153							.entry(datum)
154							.or_default()
155							.insert(outcome.origin);
156					} else {
157						grid.empty.insert(outcome.origin);
158					}
159				},
160				| Err(fault) => {
161					grid.faults.insert(outcome.origin, fault);
162				},
163			}
164
165			grid
166		})
167	}
168
169	fn tally(self) -> impl Future<Output = Tally> + Send {
170		let tally = Tally {
171			ok: Origins::new(),
172			faults: Faults::new(),
173		};
174
175		self.ready_fold(tally, |mut tally, outcome| {
176			match outcome.result {
177				| Ok(_response) => {
178					tally.ok.insert(outcome.origin);
179				},
180				| Err(fault) => {
181					tally.faults.insert(outcome.origin, fault);
182				},
183			}
184
185			tally
186		})
187	}
188
189	async fn first_acceptable<F>(self, accept: F) -> Option<(OwnedServerName, R)>
190	where
191		F: Fn(&R) -> bool + Send,
192	{
193		let outcomes = self;
194
195		pin_mut!(outcomes);
196		outcomes
197			.ready_find_map(move |outcome| match outcome.result {
198				| Ok(response) if accept(&response) => Some((outcome.origin, response)),
199				| _ => None,
200			})
201			.await
202	}
203}