Skip to main content

tuwunel_matrix/room/state/resolution/
topological_sort.rs

1//! Sorts the given event graph using reverse topological power ordering.
2//!
3//! Definition in the specification:
4//!
5//! The reverse topological power ordering of a set of events is the
6//! lexicographically smallest topological ordering based on the DAG formed by
7//! referenced events (prev or auth, determined by caller). The reverse
8//! topological power ordering is ordered from earliest event to latest. For
9//! comparing two equal topological orderings to determine which is the
10//! lexicographically smallest, the following comparison relation on events is
11//! used: for events x and y, x < y if
12//!
13//! 1. x’s sender has greater power level than y’s sender, when looking at their
14//!    respective referenced events; or
15//! 2. the senders have the same power level, but x’s origin_server_ts is less
16//!    than y’s origin_server_ts; or
17//! 3. the senders have the same power level and the events have the same
18//!    origin_server_ts, but x’s event_id is less than y’s event_id.
19//!
20//! The reverse topological power ordering can be found by sorting the events
21//! using Kahn’s algorithm for topological sorting, and at each step selecting,
22//! among all the candidate vertices, the smallest vertex using the above
23//! comparison relation.
24
25use std::{
26	cmp::{Ordering, Reverse},
27	collections::{BinaryHeap, HashMap, HashSet},
28};
29
30use futures::{Stream, TryFutureExt, TryStreamExt, stream::try_unfold};
31use ruma::{
32	MilliSecondsSinceUnixEpoch, OwnedEventId, events::room::power_levels::UserPowerLevel,
33};
34use tuwunel_core::{
35	Error, Result, is_not_equal_to, smallvec::SmallVec, utils::stream::IterStream, validated,
36};
37
38/// Event identifiers referenced by an event.
39///
40/// The topological sorter uses these identifiers to follow graph edges.
41pub type ReferencedIds = SmallVec<[OwnedEventId; 3]>;
42type PduInfo = (UserPowerLevel, MilliSecondsSinceUnixEpoch);
43
44#[derive(PartialEq, Eq)]
45struct TieBreaker {
46	event_id: OwnedEventId,
47	power_level: UserPowerLevel,
48	origin_server_ts: MilliSecondsSinceUnixEpoch,
49}
50
51// NOTE: the power level comparison is "backwards" intentionally.
52impl Ord for TieBreaker {
53	fn cmp(&self, other: &Self) -> Ordering {
54		other
55			.power_level
56			.cmp(&self.power_level)
57			.then(self.origin_server_ts.cmp(&other.origin_server_ts))
58			.then(self.event_id.cmp(&other.event_id))
59	}
60}
61
62impl PartialOrd for TieBreaker {
63	fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
64}
65
66/// Sorts the given event graph using reverse topological power ordering.
67///
68/// ## Arguments
69///
70/// * `graph` - The graph to sort. A map of event ID to its referenced events
71///   that are in the full conflicted set.
72///
73/// * `query` - Function to obtain a (power level, origin_server_ts) of an event
74///   for breaking ties.
75///
76/// ## Returns
77///
78/// Returns the ordered list of event IDs from earliest to latest. Every event
79/// in the graph appears exactly once; a reference to an event absent from the
80/// graph is treated as a non-edge rather than dropping the referencing event.
81///
82/// We consider that the DAG is directed from most recent events to oldest
83/// events, so an event is an incoming edge to its referenced events.
84/// zero_outdegs: Vec of events that have an outdegree of zero (no outgoing
85/// edges), i.e. the oldest events. incoming_edges_map: Map of event to the list
86/// of events that reference it in its referenced events.
87#[tracing::instrument(
88	level = "debug",
89	skip_all,
90	fields(
91		graph = graph.len(),
92	)
93)]
94#[expect(clippy::implicit_hasher)]
95pub async fn topological_sort<Query, Fut>(
96	graph: HashMap<OwnedEventId, ReferencedIds>,
97	query: &Query,
98) -> Result<Vec<OwnedEventId>>
99where
100	Query: Fn(OwnedEventId) -> Fut + Sync,
101	Fut: Future<Output = Result<PduInfo>> + Send,
102{
103	let query = async |event_id: OwnedEventId| {
104		let (power_level, origin_server_ts) = query(event_id.clone()).await?;
105		Ok::<_, Error>(TieBreaker { event_id, power_level, origin_server_ts })
106	};
107
108	let max_edges = graph
109		.values()
110		.map(ReferencedIds::len)
111		.fold(graph.len(), |a, c| validated!(a + c));
112
113	let incoming = graph
114		.iter()
115		.flat_map(|(event_id, out)| {
116			out.iter()
117				.map(move |reference| (event_id, reference))
118		})
119		.fold(HashMap::with_capacity(max_edges), |mut incoming, (event_id, reference)| {
120			let references: &mut ReferencedIds = incoming.entry(reference.clone()).or_default();
121
122			if !references.contains(event_id) {
123				references.push(event_id.clone());
124			}
125
126			incoming
127		});
128
129	// A reference absent from the graph is unresolvable and not an out-edge.
130	let horizon = graph
131		.iter()
132		.filter(|(_, references)| {
133			!references
134				.iter()
135				.any(|reference| graph.contains_key(reference))
136		})
137		.try_stream()
138		.and_then(async |(event_id, _)| Ok(Reverse(query(event_id.clone()).await?)))
139		.try_collect::<BinaryHeap<Reverse<TieBreaker>>>()
140		.await?;
141
142	kahn_sort(horizon, graph, &incoming, &query)
143		.try_collect()
144		.await
145}
146
147// Apply Kahn's algorithm.
148// https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
149// Use a BinaryHeap to keep the events with an outdegree of zero sorted.
150#[tracing::instrument(
151	level = "debug",
152	skip_all,
153	fields(
154		heap = %heap.len(),
155		graph = %graph.len(),
156	)
157)]
158fn kahn_sort<Query, Fut>(
159	heap: BinaryHeap<Reverse<TieBreaker>>,
160	graph: HashMap<OwnedEventId, ReferencedIds>,
161	incoming: &HashMap<OwnedEventId, ReferencedIds>,
162	query: &Query,
163) -> impl Stream<Item = Result<OwnedEventId>> + Send
164where
165	Query: Fn(OwnedEventId) -> Fut + Sync,
166	Fut: Future<Output = Result<TieBreaker>> + Send,
167{
168	try_unfold((heap, graph), async move |(mut heap, graph)| {
169		let Some(Reverse(item)) = heap.pop() else {
170			return Ok(None);
171		};
172
173		let references = incoming.get(&item.event_id).cloned();
174		let state = (item.event_id, (heap, graph));
175		references
176			.into_iter()
177			.flatten()
178			.try_stream()
179			.try_fold(state, async |(event_id, (mut heap, mut graph)), parent_id| {
180				graph
181					.get_mut(&parent_id)
182					.expect("contains all parent_ids")
183					.retain(is_not_equal_to!(&event_id));
184
185				// References to absent events never resolve; gate on present out-edges only.
186				if !graph[&parent_id]
187					.iter()
188					.any(|reference| graph.contains_key(reference))
189				{
190					heap.push(Reverse(query(parent_id.clone()).await?));
191				}
192
193				Ok::<_, Error>((event_id, (heap, graph)))
194			})
195			.map_ok(Some)
196			.await
197	})
198}
199
200/// Tests whether the events in `order` are in reverse topological power
201/// ordering with respect to `graph`: each event appears after every event it
202/// references that is present in `graph`.
203///
204/// A reference absent from `graph` is a non-edge, as in [`topological_sort`].
205/// The check covers relative order only: it certifies that the events present
206/// in `order` are mutually consistent, not that `order` is complete or
207/// duplicate-free, and (lacking the tie-breaker) not that it equals the exact
208/// sequence [`topological_sort`] would select.
209#[expect(clippy::implicit_hasher)]
210pub fn is_topologically_sorted<'a, Order>(
211	order: Order,
212	graph: &HashMap<OwnedEventId, ReferencedIds>,
213) -> bool
214where
215	Order: IntoIterator<Item = &'a OwnedEventId>,
216{
217	order
218		.into_iter()
219		.try_fold(HashSet::with_capacity(graph.len()), |mut seen, event_id| {
220			let satisfied = graph
221				.get(event_id)
222				.into_iter()
223				.flatten()
224				.filter(|reference| graph.contains_key(*reference))
225				.all(|reference| seen.contains(reference));
226
227			seen.insert(event_id);
228			satisfied.then_some(seen)
229		})
230		.is_some()
231}
232
233/// Checks reverse topological reference order without building a graph.
234///
235/// Every referenced identifier that occurs in `items` must precede the item
236/// that references it; absent references are ignored. `id` extracts an item's
237/// identifier and `references` yields its outgoing references. The check does
238/// not apply [`topological_sort`]'s tie-breaker or reject unrelated duplicate
239/// identifiers.
240///
241/// This allocates nothing. Each yielded reference scans the remaining suffix,
242/// requiring at most `n * R` identifier comparisons for `n` items and `R`
243/// references. Prefer [`is_topologically_sorted`] for a large sequence or when
244/// its graph is already available.
245pub fn is_topologically_sorted_in_place<'a, T, Id, Refs, Ref>(
246	items: &'a [T],
247	id: Id,
248	references: Refs,
249) -> bool
250where
251	Id: Fn(&'a T) -> &'a str,
252	Refs: Fn(&'a T) -> Ref,
253	Ref: Iterator<Item = &'a str>,
254{
255	if items.len() < 2 {
256		return true;
257	}
258
259	items.iter().enumerate().all(|(i, item)| {
260		references(item).all(|reference| {
261			items[i..]
262				.iter()
263				.all(|other| id(other) != reference)
264		})
265	})
266}
267
268#[cfg(test)]
269mod tests {
270	use super::is_topologically_sorted_in_place;
271
272	fn sorted(items: &[(&str, &[&str])]) -> bool {
273		is_topologically_sorted_in_place(
274			items,
275			|item: &(&str, &[&str])| item.0,
276			|item: &(&str, &[&str])| item.1.iter().copied(),
277		)
278	}
279
280	#[test]
281	fn empty_or_single_is_sorted() {
282		assert!(sorted(&[]));
283		assert!(sorted(&[("a", &[])]));
284	}
285
286	#[test]
287	fn parents_before_children() {
288		assert!(sorted(&[("a", &[]), ("b", &["a"]), ("c", &["a", "b"])]));
289	}
290
291	#[test]
292	fn child_before_parent_is_unsorted() {
293		assert!(!sorted(&[("b", &["a"]), ("a", &[])]));
294	}
295
296	#[test]
297	fn absent_reference_is_a_non_edge() {
298		assert!(sorted(&[("b", &["x"]), ("a", &["x"])]));
299	}
300
301	#[test]
302	fn self_reference_is_unsorted() {
303		assert!(!sorted(&[("a", &["a"]), ("b", &[])]));
304	}
305
306	#[test]
307	fn later_duplicate_of_a_parent_is_unsorted() {
308		assert!(!sorted(&[("a", &[]), ("b", &["a"]), ("a", &[])]));
309	}
310}