Skip to main content

tuwunel_service/rooms/event_handler/
state_local_build.rs

1use std::{
2	borrow::Borrow,
3	collections::HashMap,
4	mem::take,
5	sync::{
6		Arc,
7		atomic::{AtomicBool, AtomicU64, Ordering},
8	},
9};
10
11use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::join};
12use ruma::{
13	EventId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId,
14	events::{StateEventType, TimelineEventType},
15	room_version_rules::RoomVersionRules,
16};
17use tracing::{Instrument, Span};
18use tuwunel_core::{
19	Result, debug, debug_warn, defer, err, implement,
20	matrix::{
21		Event, PduEvent,
22		pdu::PrevEvents,
23		room_version::{self, from_create_event},
24	},
25	trace,
26	utils::{
27		math::fetch_add_usize,
28		stream::{BroadbandExt, IterStream, ReadyExt, WidebandExt},
29	},
30};
31
32use crate::rooms::{
33	short::{ShortStateHash, ShortStateKey},
34	state::IdMapState,
35	state_compressor::CompressedState,
36	state_res::{AuthCheckOutcome, auth_check},
37};
38
39#[cfg(test)]
40mod tests;
41
42/// State before or after one event, in the shape the sibling builders return.
43type StateIds = HashMap<ShortStateKey, OwnedEventId>;
44
45/// Summary of one local build attempt, for the admin debug command.
46#[derive(Debug)]
47pub struct LocalBuildReport {
48	pub state_len: Option<usize>,
49	pub visited: usize,
50	pub forks: usize,
51	pub gate_drops: usize,
52	pub memo_hits: usize,
53	pub fallback: Option<String>,
54}
55
56/// Immutable process-lifetime totals for production local state builds.
57///
58/// Relaxed loads make each snapshot observational rather than a
59/// synchronization primitive.
60#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
61pub struct StateLocalMetrics {
62	/// Production local walks started.
63	pub walk_attempts: u64,
64	/// Local walks that produced complete state.
65	pub walk_resolved: u64,
66	/// Walks that fell back because an ancestor was absent.
67	pub fallback_absent: u64,
68	/// Walks that reached the configured node ceiling.
69	pub fallback_ceiling: u64,
70	/// Walks with an absent auth dependency.
71	pub fallback_auth_missing: u64,
72	/// Walks whose ancestors were all committed.
73	pub fallback_all_committed: u64,
74	/// Walks that reached the live-entry ceiling.
75	pub fallback_entries: u64,
76	/// Walks whose memo canary was absent.
77	pub fallback_canary: u64,
78	/// Walks whose create event did not match.
79	pub fallback_create_mismatch: u64,
80	/// Walks whose complete inputs could not be evaluated.
81	pub fallback_unevaluable: u64,
82	/// Walks that fell back after a recoverable lookup or state-build error.
83	pub fallback_error: u64,
84	/// Walks whose inner result errored or whose task did not complete.
85	pub walk_failures: u64,
86	/// State-event folds denied by the positional auth gate.
87	pub gate_denials: u64,
88}
89
90#[derive(Default)]
91pub(super) struct StateLocalCounters {
92	walk_attempts: AtomicU64,
93	walk_resolved: AtomicU64,
94	fallback_absent: AtomicU64,
95	fallback_ceiling: AtomicU64,
96	fallback_auth_missing: AtomicU64,
97	fallback_all_committed: AtomicU64,
98	fallback_entries: AtomicU64,
99	fallback_canary: AtomicU64,
100	fallback_create_mismatch: AtomicU64,
101	fallback_unevaluable: AtomicU64,
102	fallback_error: AtomicU64,
103	walk_failures: AtomicU64,
104	gate_denials: AtomicU64,
105}
106
107struct WalkAttempt {
108	counters: Arc<StateLocalCounters>,
109	settled: bool,
110}
111
112#[implement(StateLocalCounters)]
113fn start_walk(&self) { self.walk_attempts.fetch_add(1, Ordering::Relaxed); }
114
115#[implement(StateLocalCounters)]
116fn settle_walk(&self, outcome: WalkOutcome) {
117	match outcome {
118		| WalkOutcome::Resolved => {
119			self.walk_resolved.fetch_add(1, Ordering::Relaxed);
120		},
121		| WalkOutcome::Fallback(fallback) => self.record_fallback(fallback),
122		| WalkOutcome::Failure => {
123			self.walk_failures.fetch_add(1, Ordering::Relaxed);
124		},
125	}
126}
127
128#[implement(StateLocalCounters)]
129fn record_fallback(&self, fallback: Fallback) {
130	let counter = match fallback {
131		| Fallback::Absent => &self.fallback_absent,
132		| Fallback::Ceiling => &self.fallback_ceiling,
133		| Fallback::AuthMissing => &self.fallback_auth_missing,
134		| Fallback::AllCommitted => &self.fallback_all_committed,
135		| Fallback::Entries => &self.fallback_entries,
136		| Fallback::Canary => &self.fallback_canary,
137		| Fallback::CreateMismatch => &self.fallback_create_mismatch,
138		| Fallback::Unevaluable => &self.fallback_unevaluable,
139		| Fallback::Error => &self.fallback_error,
140	};
141
142	counter.fetch_add(1, Ordering::Relaxed);
143}
144
145#[implement(StateLocalCounters)]
146fn add_gate_denials(&self, gate_denials: usize) {
147	if gate_denials == 0 {
148		return;
149	}
150
151	fetch_add_usize(&self.gate_denials, gate_denials, Ordering::Relaxed);
152}
153
154#[implement(StateLocalCounters)]
155fn snapshot(&self) -> StateLocalMetrics {
156	StateLocalMetrics {
157		walk_attempts: self.walk_attempts.load(Ordering::Relaxed),
158		walk_resolved: self.walk_resolved.load(Ordering::Relaxed),
159		fallback_absent: self.fallback_absent.load(Ordering::Relaxed),
160		fallback_ceiling: self.fallback_ceiling.load(Ordering::Relaxed),
161		fallback_auth_missing: self.fallback_auth_missing.load(Ordering::Relaxed),
162		fallback_all_committed: self
163			.fallback_all_committed
164			.load(Ordering::Relaxed),
165		fallback_entries: self.fallback_entries.load(Ordering::Relaxed),
166		fallback_canary: self.fallback_canary.load(Ordering::Relaxed),
167		fallback_create_mismatch: self
168			.fallback_create_mismatch
169			.load(Ordering::Relaxed),
170		fallback_unevaluable: self.fallback_unevaluable.load(Ordering::Relaxed),
171		fallback_error: self.fallback_error.load(Ordering::Relaxed),
172		walk_failures: self.walk_failures.load(Ordering::Relaxed),
173		gate_denials: self.gate_denials.load(Ordering::Relaxed),
174	}
175}
176
177#[implement(WalkAttempt)]
178fn start(counters: Arc<StateLocalCounters>) -> Self {
179	counters.start_walk();
180
181	Self { counters, settled: false }
182}
183
184#[implement(WalkAttempt)]
185fn settle(mut self, outcome: WalkOutcome, gate_denials: usize) {
186	self.counters.add_gate_denials(gate_denials);
187	self.counters.settle_walk(outcome);
188	self.settled = true;
189}
190
191impl Drop for WalkAttempt {
192	fn drop(&mut self) {
193		if !self.settled {
194			self.counters.settle_walk(WalkOutcome::Failure);
195		}
196	}
197}
198
199/// Active writes fork-node memo rows; Shadow suppresses all persistent
200/// writes.
201#[derive(Clone, Copy, Eq, PartialEq)]
202pub(super) enum WalkMode {
203	Active,
204	Shadow,
205}
206
207/// State threaded through one walk's discovery and build phases.
208struct Walk<'a> {
209	room_id: &'a RoomId,
210	room_version: &'a RoomVersionId,
211	room_rules: RoomVersionRules,
212	create_event_id: &'a EventId,
213	mode: WalkMode,
214	max_nodes: usize,
215	top_prevs: PrevEvents,
216	class: HashMap<OwnedEventId, Class>,
217	nodes: Vec<Node>,
218	order: Vec<usize>,
219	frontier: HashMap<OwnedEventId, usize>,
220	resolved: HashMap<OwnedEventId, Arc<StateIds>>,
221	live_entries: usize,
222	peak_entries: usize,
223	forks: usize,
224	gate_drops: usize,
225	memo_hits: usize,
226	fallback: Option<Fallback>,
227	attempt: Option<WalkAttempt>,
228}
229
230/// Held outlier in the walk sub-DAG.
231struct Node {
232	pdu: PduEvent,
233	consumers: usize,
234}
235
236/// Ancestry classification from the discovery phase.
237#[derive(Clone, Copy)]
238enum Class {
239	/// Committed to the timeline with resolved state at the event.
240	Committed(ShortStateHash),
241
242	/// Uncommitted, but an eventid_resolvedstate row exists.
243	Memoized,
244
245	/// Uncommitted outlier we hold; the index into Walk::nodes.
246	Held(usize),
247}
248
249/// Why a walk gave up; every reason falls back to the federation fetch.
250#[derive(Clone, Copy)]
251enum Fallback {
252	Absent,
253	Ceiling,
254	AuthMissing,
255	AllCommitted,
256	Entries,
257	Canary,
258	CreateMismatch,
259	Unevaluable,
260	Error,
261}
262
263#[derive(Clone, Copy)]
264enum WalkOutcome {
265	Resolved,
266	Fallback(Fallback),
267	Failure,
268}
269
270/// Ceiling on simultaneously live state-map entries across one walk: the sum
271/// of the lengths of materialized maps no consumer has released yet.
272/// Exceeding it falls back to the federation fetch. Deliberately a const, not
273/// config; revisit only if operation trips it.
274const MAX_LIVE_ENTRIES: usize = 1 << 19;
275
276/// Build the state before `incoming_pdu` from events we already hold, walking
277/// locally held uncommitted ancestry down to committed or memoized ancestors
278/// with an auth gate on every folded state event. Some(map) is a complete
279/// gated build in the shape the sibling builders return; None falls back to
280/// the federation state fetch, for any reason. Err propagates only server
281/// shutdown and room-version failures.
282#[implement(super::Service)]
283pub(super) async fn state_at_incoming_local<Pdu>(
284	&self,
285	room_id: &RoomId,
286	incoming_pdu: &Pdu,
287	room_version: &RoomVersionId,
288	create_event_id: &EventId,
289	mode: WalkMode,
290) -> Result<Option<StateIds>>
291where
292	Pdu: Event,
293{
294	let top_prevs = incoming_pdu
295		.prev_events()
296		.map(ToOwned::to_owned)
297		.collect();
298
299	let services = self.services.clone();
300	let room_id = room_id.to_owned();
301	let room_version = room_version.clone();
302	let create_event_id = create_event_id.to_owned();
303	let parent = Span::current();
304	let attempt = WalkAttempt::start(self.state_local.clone());
305
306	let task = self.services.server.runtime().spawn(
307		async move {
308			services
309				.event_handler
310				.walk_task(room_id, room_version, create_event_id, mode, top_prevs, attempt)
311				.await
312		}
313		.instrument(parent),
314	);
315
316	// Abort on caller cancellation; a dropped JoinHandle only detaches.
317	let abort = task.abort_handle();
318
319	defer! {{ abort.abort(); }};
320
321	task.await.unwrap_or_else(|error| {
322		debug_warn!(
323			%error,
324			"Local state build task failed; falling back to federation fetch.",
325		);
326
327		Ok(None)
328	})
329}
330
331/// Walk body on its own task: a poll descends every combinator layer from the
332/// task root, and under /send intake, already the server's deepest stack, the
333/// walk's auth-gate subtree overflows the worker stack in debug builds.
334#[implement(super::Service)]
335#[tracing::instrument(name = "local", level = "debug", skip_all)]
336async fn walk_task(
337	&self,
338	room_id: OwnedRoomId,
339	room_version: RoomVersionId,
340	create_event_id: OwnedEventId,
341	mode: WalkMode,
342	top_prevs: PrevEvents,
343	attempt: WalkAttempt,
344) -> Result<Option<StateIds>> {
345	let max_nodes = self
346		.services
347		.server
348		.config
349		.resolve_state_locally_max;
350
351	let mut walk = Walk::new(
352		&room_id,
353		&room_version,
354		&create_event_id,
355		mode,
356		max_nodes,
357		top_prevs,
358		Some(attempt),
359	)?;
360
361	let state = self.walk_state(&mut walk).await;
362	let state = state.inspect_err(|_| {
363		walk.settle(WalkOutcome::Failure);
364	})?;
365
366	debug!(
367		visited = walk.nodes.len(),
368		forks = walk.forks,
369		gate_drops = walk.gate_drops,
370		memo_hits = walk.memo_hits,
371		live_entries_peak = walk.peak_entries,
372		outcome = walk.fallback.map_or("resolved", Fallback::name),
373		"Local state build finished.",
374	);
375
376	if let Some(fallback) = walk.fallback {
377		debug_warn!(
378			reason = fallback.name(),
379			"Local state build falling back to federation fetch.",
380		);
381	}
382
383	let (state, outcome) = match (state, walk.fallback) {
384		| (Some(state), None) => (Some(state), WalkOutcome::Resolved),
385		| (None, Some(fallback)) => (None, WalkOutcome::Fallback(fallback)),
386		| _ => {
387			debug_assert!(false, "local walk state and fallback disagree");
388			(None, WalkOutcome::Failure)
389		},
390	};
391
392	walk.settle(outcome);
393
394	Ok(state)
395}
396
397/// Read an observational snapshot of production local-build totals.
398///
399/// The values cover this process lifetime and do not reset when read.
400#[implement(super::Service)]
401#[inline]
402#[must_use]
403pub fn state_local_metrics(&self) -> StateLocalMetrics { self.state_local.snapshot() }
404
405/// Run a diagnostic walk for one stored event and describe the outcome.
406///
407/// The walk does not write authoritative state, resolved-state memos, or
408/// production counters. It may allocate short IDs and warm auth-chain caches.
409#[implement(super::Service)]
410pub async fn local_state_report(&self, event_id: &EventId) -> Result<LocalBuildReport> {
411	let pdu = self.services.timeline.get_pdu(event_id).await?;
412
413	let create_event = self
414		.services
415		.state_accessor
416		.room_state_get(pdu.room_id(), &StateEventType::RoomCreate, "")
417		.await?;
418
419	let room_version = from_create_event(&create_event)?;
420	let max_nodes = self
421		.services
422		.server
423		.config
424		.resolve_state_locally_max;
425
426	let top_prevs = pdu.prev_events().map(ToOwned::to_owned).collect();
427
428	let mut walk = Walk::new(
429		pdu.room_id(),
430		&room_version,
431		create_event.event_id(),
432		WalkMode::Shadow,
433		max_nodes,
434		top_prevs,
435		None,
436	)?;
437
438	let state = self.walk_state(&mut walk).await?;
439
440	Ok(LocalBuildReport {
441		state_len: state.map(|state| state.len()),
442		visited: walk.nodes.len(),
443		forks: walk.forks,
444		gate_drops: walk.gate_drops,
445		memo_hits: walk.memo_hits,
446		fallback: walk
447			.fallback
448			.map(|fallback| fallback.name().to_owned()),
449	})
450}
451
452/// Drive discovery then the post-order build; any abnormality sets
453/// walk.fallback and yields None.
454#[implement(super::Service)]
455async fn walk_state(&self, walk: &mut Walk<'_>) -> Result<Option<StateIds>> {
456	self.walk_discover(walk).await?;
457
458	if walk.fallback.is_some() {
459		return Ok(None);
460	}
461
462	self.walk_build(walk)
463		.boxed() // size firewall
464		.await
465}
466
467/// Classify the uncommitted ancestry below the incoming event with point
468/// reads only, emitting held nodes in post-order; every condition the build
469/// cannot survive sets walk.fallback here, before any state materializes.
470#[implement(super::Service)]
471async fn walk_discover(&self, walk: &mut Walk<'_>) -> Result {
472	let mut stack: Vec<(OwnedEventId, bool)> = walk
473		.top_prevs
474		.iter()
475		.map(|prev| (prev.clone(), false))
476		.collect();
477
478	while let Some((event_id, expanded)) = stack.pop() {
479		self.services.server.check_running()?;
480
481		if expanded {
482			// Post-order emission: every prev of this node is fully classified.
483			let Some(Class::Held(index)) = walk.class.get(&event_id).copied() else {
484				debug_assert!(false, "expanded stack entries are held nodes");
485				walk.fallback = Some(Fallback::Error);
486				return Ok(());
487			};
488
489			walk.order.push(index);
490			continue;
491		}
492
493		if walk.class.contains_key(&event_id) {
494			continue;
495		}
496
497		if let Ok(shortstatehash) = self
498			.services
499			.state
500			.pdu_shortstatehash(&event_id)
501			.await
502		{
503			walk.class
504				.insert(event_id, Class::Committed(shortstatehash));
505
506			continue;
507		}
508
509		if self
510			.db
511			.eventid_resolvedstate
512			.exists(&event_id)
513			.await
514			.is_ok()
515		{
516			walk.class.insert(event_id, Class::Memoized);
517			continue;
518		}
519
520		let Ok(pdu) = self.services.timeline.get_pdu(&event_id).await else {
521			trace!(%event_id, "Ancestor is not held locally.");
522			walk.fallback = Some(Fallback::Absent);
523			return Ok(());
524		};
525
526		if walk.nodes.len() >= walk.max_nodes {
527			walk.fallback = Some(Fallback::Ceiling);
528			return Ok(());
529		}
530
531		if pdu.prev_events().next().is_none() {
532			debug_warn!(%event_id, "Held uncommitted ancestor has no prev events.");
533			walk.fallback = Some(Fallback::Error);
534			return Ok(());
535		}
536
537		if !self.walk_auth_present(walk, &pdu).await {
538			walk.fallback = Some(Fallback::AuthMissing);
539			return Ok(());
540		}
541
542		walk.class
543			.insert(event_id.clone(), Class::Held(walk.nodes.len()));
544
545		stack.push((event_id, true));
546		stack.extend(
547			pdu.prev_events()
548				.map(|prev| (prev.to_owned(), false)),
549		);
550		walk.nodes.push(Node { pdu, consumers: 0 });
551	}
552
553	if walk.nodes.is_empty() {
554		// The sibling builders already failed the all-committed shape before
555		// the walk ran; re-resolving it would only fail again.
556		walk.fallback = Some(Fallback::AllCommitted);
557		return Ok(());
558	}
559
560	walk.count_consumers();
561
562	Ok(())
563}
564
565/// The auth gate must stay evaluable: every auth event of a held node has to
566/// be present locally before the walk commits to building through it. Hydra
567/// rooms chain the create event implied by the room id.
568#[implement(super::Service)]
569async fn walk_auth_present(&self, walk: &Walk<'_>, pdu: &PduEvent) -> bool {
570	let is_hydra = !walk
571		.room_rules
572		.event_format
573		.allow_room_create_in_auth_events;
574
575	let not_create = *pdu.kind() != TimelineEventType::RoomCreate;
576	let hydra_create_id = (not_create && is_hydra)
577		.then(|| pdu.room_id().as_event_id().ok())
578		.flatten();
579
580	pdu.auth_events()
581		.chain(hydra_create_id.as_deref())
582		.stream()
583		.all(|auth_id| self.services.timeline.pdu_exists(auth_id))
584		.await
585}
586
587/// Compute state through the walk sub-DAG in post-order, so every node's
588/// prevs resolve before it, then combine at the incoming event's own prevs.
589#[implement(super::Service)]
590async fn walk_build(&self, walk: &mut Walk<'_>) -> Result<Option<StateIds>> {
591	let order = take(&mut walk.order);
592	for index in order {
593		self.services.server.check_running()?;
594
595		if !self.walk_node(walk, index).await {
596			return Ok(None);
597		}
598	}
599
600	let top_prevs = take(&mut walk.top_prevs);
601	let state = match top_prevs.as_slice() {
602		| [prev] => self.state_after(walk, prev).await,
603		| _ => self.fork_resolve(walk, &top_prevs, None).await,
604	};
605
606	let Some(state) = state else {
607		return Ok(None);
608	};
609
610	// Mirror fetch_state's canary: the original create event must still be in
611	// the built state.
612	let create_entry = self
613		.services
614		.short
615		.get_shortstatekey(&StateEventType::RoomCreate, "")
616		.await
617		.ok()
618		.and_then(|shortstatekey| state.get(&shortstatekey))
619		.map(AsRef::as_ref);
620
621	if state.is_empty() || create_entry != Some(walk.create_event_id) {
622		walk.fallback = Some(Fallback::CreateMismatch);
623		return Ok(None);
624	}
625
626	walk.resolved.clear();
627
628	let state = Arc::try_unwrap(state).unwrap_or_else(|state| (*state).clone());
629
630	Ok(Some(state))
631}
632
633/// Resolve one held node: state-before from its prevs, its own gated fold on
634/// top, retained until the last consumer releases it.
635#[implement(super::Service)]
636async fn walk_node(&self, walk: &mut Walk<'_>, index: usize) -> bool {
637	let node = &walk.nodes[index];
638	let event_id = node.pdu.event_id().to_owned();
639	let prevs: PrevEvents = node
640		.pdu
641		.prev_events()
642		.map(ToOwned::to_owned)
643		.collect();
644
645	let before = match prevs.as_slice() {
646		| [prev] => self.state_after(walk, prev).await,
647		| _ =>
648			self.fork_resolve(walk, &prevs, Some(&event_id))
649				.await,
650	};
651
652	let Some(before) = before else {
653		return false;
654	};
655
656	let after = match walk.nodes[index].pdu.state_key() {
657		| None => Ok(before),
658		| Some(_) =>
659			self.gated_fold(
660				&walk.room_rules,
661				&mut walk.gate_drops,
662				&walk.nodes[index].pdu,
663				&before,
664			)
665			.await,
666	};
667
668	let after = after.inspect_err(|error| {
669		debug_warn!(event_id = %event_id, %error, "Auth gate could not be evaluated.");
670	});
671
672	let Ok(after) = after else {
673		walk.fallback = Some(Fallback::Unevaluable);
674		return false;
675	};
676
677	if !walk.retain(event_id, after) {
678		return false;
679	}
680
681	walk.release(&prevs);
682
683	true
684}
685
686/// State after one prev: an already-resolved node or materialized frontier
687/// entry shares its map; otherwise the frontier materializes here.
688#[implement(super::Service)]
689async fn state_after(&self, walk: &mut Walk<'_>, event_id: &EventId) -> Option<Arc<StateIds>> {
690	if let Some(state) = walk.resolved.get(event_id) {
691		return Some(state.clone());
692	}
693
694	let state = match walk.class.get(event_id).copied() {
695		| Some(Class::Committed(shortstatehash)) =>
696			self.committed_state_after(walk, event_id, shortstatehash)
697				.await,
698		| Some(Class::Memoized) => self.memoized_state_after(walk, event_id).await,
699		| Some(Class::Held(_)) | None => {
700			debug_assert!(false, "held nodes resolve before their consumers");
701			walk.fallback = Some(Fallback::Error);
702			None
703		},
704	}?;
705
706	walk.retain(event_id.to_owned(), state.clone())
707		.then_some(state)
708}
709
710/// State after a committed frontier event: its stored state plus its own key
711/// folded unguarded, exactly the degree-one builder's shape.
712///
713/// Every event holding a `shorteventid_shortstatehash` row passed spec check 5
714/// (auth against the state at its own position) as a hard reject; soft failure
715/// (spec check 6) still writes the row, so soft-failed events are valid fold
716/// inputs while positionally rejected events never gain a row.
717#[implement(super::Service)]
718async fn committed_state_after(
719	&self,
720	walk: &mut Walk<'_>,
721	event_id: &EventId,
722	shortstatehash: ShortStateHash,
723) -> Option<Arc<StateIds>> {
724	let pdu = self.services.timeline.get_pdu(event_id);
725
726	let state = self
727		.services
728		.state_accessor
729		.state_full_ids_strict(shortstatehash)
730		.try_collect::<StateIds>();
731
732	let (pdu, state) = join(pdu, state).await;
733	let Ok(mut state) = state.inspect_err(|e| {
734		debug_warn!(%event_id, %e, "Failed loading complete committed state.");
735	}) else {
736		walk.fallback = Some(Fallback::Unevaluable);
737		return None;
738	};
739
740	let Ok(pdu) = pdu.inspect_err(|e| {
741		debug_warn!(%event_id, %e, "Failed loading committed event.");
742	}) else {
743		walk.fallback = Some(Fallback::Error);
744		return None;
745	};
746
747	if let Some(state_key) = pdu.state_key() {
748		let event_type = pdu.event_type().to_cow_str().into();
749		let shortstatekey = self
750			.services
751			.short
752			.get_or_create_shortstatekey(&event_type, state_key)
753			.await;
754
755		state.insert(shortstatekey, event_id.to_owned());
756	}
757
758	Some(Arc::new(state))
759}
760
761/// State after a memoized frontier event: the memo row is its complete
762/// state-before, so its own gated fold preserves that guarantee for
763/// descendants.
764#[implement(super::Service)]
765async fn memoized_state_after(
766	&self,
767	walk: &mut Walk<'_>,
768	event_id: &EventId,
769) -> Option<Arc<StateIds>> {
770	walk.memo_hits = walk.memo_hits.saturating_add(1);
771
772	let state = self.cached_resolved_state(event_id);
773
774	let pdu = self
775		.services
776		.timeline
777		.get_pdu(event_id)
778		.inspect_err(|e| debug_warn!(%event_id, %e, "Failed loading memoized event."));
779
780	let (state, pdu) = join(state, pdu).await;
781
782	let state = match state {
783		| Ok(Some(state)) => state,
784		| Ok(None) => {
785			walk.fallback = Some(Fallback::Canary);
786			return None;
787		},
788		| Err(e) => {
789			debug_warn!(%event_id, %e, "Failed loading complete memoized state.");
790			walk.fallback = Some(Fallback::Unevaluable);
791			return None;
792		},
793	};
794
795	let Ok(pdu) = pdu else {
796		walk.fallback = Some(Fallback::Error);
797		return None;
798	};
799
800	let before = Arc::new(state);
801	if pdu.state_key().is_none() {
802		return Some(before);
803	}
804
805	let after = self
806		.gated_fold(&walk.room_rules, &mut walk.gate_drops, &pdu, &before)
807		.await;
808
809	match after {
810		| Ok(after) => Some(after),
811		| Err(error) => {
812			debug_warn!(%event_id, %error, "Memoized auth gate could not be evaluated.");
813			walk.fallback = Some(Fallback::Unevaluable);
814			None
815		},
816	}
817}
818
819/// Fold the event's own state key over its state-before, only when the
820/// position-correct auth gate passes; a rejection leaves state unchanged.
821///
822/// Evaluation failures abort the local walk so federation can rebuild a
823/// complete input state.
824#[implement(super::Service)]
825async fn gated_fold(
826	&self,
827	room_rules: &RoomVersionRules,
828	gate_drops: &mut usize,
829	pdu: &PduEvent,
830	before: &Arc<StateIds>,
831) -> Result<Arc<StateIds>> {
832	let create_shortstatekey = self
833		.services
834		.short
835		.get_shortstatekey(&StateEventType::RoomCreate, "")
836		.await?;
837
838	if !before.contains_key(&create_shortstatekey) {
839		return Err(err!(Database("State before event is missing the room create event.")));
840	}
841
842	let state_fetch = IdMapState { services: &self.services, ids: before };
843
844	if let AuthCheckOutcome::Deny(error) =
845		auth_check(room_rules, pdu, &*self.services.timeline, state_fetch).await?
846	{
847		debug!(event_id = %pdu.event_id(), %error, "Auth gate rejected fold.");
848		*gate_drops = gate_drops.saturating_add(1);
849		return Ok(before.clone());
850	}
851
852	let state_key = pdu.state_key().expect("only state events fold");
853
854	let event_type = pdu.event_type().to_cow_str().into();
855	let shortstatekey = self
856		.services
857		.short
858		.get_or_create_shortstatekey(&event_type, state_key)
859		.await;
860
861	let mut state = StateIds::clone(before);
862	state.insert(shortstatekey, pdu.event_id().to_owned());
863
864	Ok(Arc::new(state))
865}
866
867/// State before a fork node, resolving the state after each of its prevs
868/// exactly as the committed-prev fork resolves today. Fork outputs are the
869/// artifacts worth memoizing; chain nodes are cheap to re-derive.
870#[implement(super::Service)]
871async fn fork_resolve(
872	&self,
873	walk: &mut Walk<'_>,
874	prevs: &[OwnedEventId],
875	memo_event_id: Option<&EventId>,
876) -> Option<Arc<StateIds>> {
877	walk.forks = walk.forks.saturating_add(1);
878
879	// Sequential: materializing a frontier prev writes the walk's accounting.
880	let mut afters = Vec::with_capacity(prevs.len());
881	for prev in prevs {
882		afters.push(self.state_after(walk, prev).await?);
883	}
884
885	let (room_id, room_version) = (walk.room_id, walk.room_version);
886	let fork_states: Result<Vec<_>> = afters
887		.iter()
888		.stream()
889		.wide_then(async |after| {
890			let state = after
891				.iter()
892				.map(|(shortstatekey, event_id)| (*shortstatekey, event_id));
893
894			self.fork_state(state).await
895		})
896		.try_collect()
897		.await;
898
899	let Ok(fork_states) = fork_states.inspect_err(|e| {
900		debug_warn!(%e, "Failed converting complete fork state.");
901	}) else {
902		walk.fallback = Some(Fallback::Unevaluable);
903		return None;
904	};
905
906	let chain_complete = AtomicBool::new(true);
907	let auth_chains = prevs
908		.iter()
909		.zip(&afters)
910		.stream()
911		.wide_then(|(prev_event, after)| {
912			self.fork_chain_strict(
913				room_id,
914				room_version,
915				after.values().map(Borrow::borrow),
916				&chain_complete,
917			)
918			.inspect_err(move |e| {
919				debug_warn!(%prev_event, %e, "Failed loading complete fork auth chain.");
920			})
921		})
922		.ready_filter_map(Result::ok);
923
924	let resolved = self
925		.state_resolution(
926			room_id,
927			room_version,
928			fork_states.into_iter().stream(),
929			auth_chains,
930			Some(&chain_complete),
931		)
932		.await;
933
934	// Only polled chains can affect resolution. Check completeness before using
935	// its result or writing the transitively complete memo.
936	if !chain_complete.load(Ordering::Relaxed) {
937		debug_warn!("Polled fork auth chain was incomplete.");
938		walk.fallback = Some(Fallback::Unevaluable);
939		return None;
940	}
941
942	let Ok(resolved) = resolved else {
943		walk.fallback = Some(Fallback::Error);
944		return None;
945	};
946
947	let state: StateIds = resolved
948		.into_iter()
949		.stream()
950		.broad_then(async |((event_type, state_key), event_id)| {
951			self.services
952				.short
953				.get_or_create_shortstatekey(&event_type, &state_key)
954				.map(move |shortstatekey| (shortstatekey, event_id))
955				.await
956		})
957		.collect()
958		.await;
959
960	if let Some(event_id) = memo_event_id.filter(|_| walk.mode == WalkMode::Active) {
961		// Strict frontier loads and the polled-chain sentinel make this state
962		// transitively complete for later memo consumers.
963		let compressed: Arc<CompressedState> = self
964			.services
965			.state_compressor
966			.compress_state_events(
967				state
968					.iter()
969					.map(|(shortstatekey, event_id)| (shortstatekey, event_id.borrow())),
970			)
971			.collect()
972			.map(Arc::new)
973			.await;
974
975		self.cache_resolved_state(walk.room_id, event_id, compressed)
976			.await;
977	}
978
979	Some(Arc::new(state))
980}
981
982impl<'a> Walk<'a> {
983	fn new(
984		room_id: &'a RoomId,
985		room_version: &'a RoomVersionId,
986		create_event_id: &'a EventId,
987		mode: WalkMode,
988		max_nodes: usize,
989		top_prevs: PrevEvents,
990		attempt: Option<WalkAttempt>,
991	) -> Result<Self> {
992		Ok(Self {
993			room_id,
994			room_version,
995			room_rules: room_version::rules(room_version)?,
996			create_event_id,
997			mode,
998			max_nodes,
999			top_prevs,
1000			class: HashMap::new(),
1001			nodes: Vec::new(),
1002			order: Vec::new(),
1003			frontier: HashMap::new(),
1004			resolved: HashMap::new(),
1005			live_entries: 0,
1006			peak_entries: 0,
1007			forks: 0,
1008			gate_drops: 0,
1009			memo_hits: 0,
1010			fallback: None,
1011			attempt,
1012		})
1013	}
1014
1015	fn settle(&mut self, outcome: WalkOutcome) {
1016		if let Some(attempt) = self.attempt.take() {
1017			attempt.settle(outcome, self.gate_drops);
1018		}
1019	}
1020
1021	/// Consumer counts drive state-map reaping: each held node's prevs and
1022	/// the incoming event's own prevs each count one consumption.
1023	fn count_consumers(&mut self) {
1024		let mut held = vec![0_usize; self.nodes.len()];
1025
1026		let edges = self
1027			.nodes
1028			.iter()
1029			.flat_map(|node| node.pdu.prev_events())
1030			.chain(self.top_prevs.iter().map(AsRef::as_ref));
1031
1032		for prev in edges {
1033			match self.class.get(prev).copied() {
1034				| Some(Class::Held(index)) => held[index] = held[index].saturating_add(1),
1035				| Some(_) => {
1036					let consumers = self.frontier.entry(prev.to_owned()).or_default();
1037
1038					*consumers = consumers.saturating_add(1);
1039				},
1040				| None => debug_assert!(false, "every walk edge is classified"),
1041			}
1042		}
1043
1044		for (node, consumers) in self.nodes.iter_mut().zip(held) {
1045			node.consumers = consumers;
1046		}
1047	}
1048
1049	/// Retain a computed state map until its last consumer releases it; the
1050	/// running live-entry total is the walk's memory ceiling. Arc-shared maps
1051	/// count once per holder, deliberately over-counting toward the ceiling.
1052	fn retain(&mut self, event_id: OwnedEventId, state: Arc<StateIds>) -> bool {
1053		let live_entries = self.live_entries.saturating_add(state.len());
1054		if live_entries > MAX_LIVE_ENTRIES {
1055			self.fallback = Some(Fallback::Entries);
1056			return false;
1057		}
1058
1059		self.live_entries = live_entries;
1060		self.peak_entries = self.peak_entries.max(live_entries);
1061		self.resolved.insert(event_id, state);
1062
1063		true
1064	}
1065
1066	/// Release one consumption of each prev, dropping maps no consumer
1067	/// awaits.
1068	fn release(&mut self, prevs: &[OwnedEventId]) {
1069		for prev in prevs {
1070			let remaining = match self.class.get(prev).copied() {
1071				| Some(Class::Held(index)) => {
1072					let node = &mut self.nodes[index];
1073					node.consumers = node.consumers.saturating_sub(1);
1074					node.consumers
1075				},
1076				| _ => {
1077					let Some(consumers) = self.frontier.get_mut(prev) else {
1078						continue;
1079					};
1080
1081					*consumers = consumers.saturating_sub(1);
1082					*consumers
1083				},
1084			};
1085
1086			if remaining == 0
1087				&& let Some(state) = self.resolved.remove(prev)
1088			{
1089				self.live_entries = self.live_entries.saturating_sub(state.len());
1090			}
1091		}
1092	}
1093}
1094
1095impl Drop for Walk<'_> {
1096	fn drop(&mut self) { self.settle(WalkOutcome::Failure); }
1097}
1098
1099impl Fallback {
1100	fn name(self) -> &'static str {
1101		match self {
1102			| Self::Absent => "absent",
1103			| Self::Ceiling => "ceiling",
1104			| Self::AuthMissing => "auth_missing",
1105			| Self::AllCommitted => "all_committed",
1106			| Self::Entries => "entries",
1107			| Self::Canary => "canary",
1108			| Self::CreateMismatch => "create_mismatch",
1109			| Self::Unevaluable => "unevaluable",
1110			| Self::Error => "error",
1111		}
1112	}
1113}