Skip to main content

tuwunel_service/rooms/state/
mod.rs

1mod prune;
2
3use std::{collections::HashMap, fmt::Write, iter::once, sync::Arc};
4
5use async_trait::async_trait;
6use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, future::join_all};
7pub(crate) use prune::prune_goal;
8pub use prune::{PruneSummary, Trigger};
9use ruma::{
10	CanonicalJsonObject, EventId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId, UserId,
11	events::{AnyStrippedStateEvent, StateEventType, TimelineEventType},
12	room_version_rules::AuthorizationRules,
13	serde::Raw,
14};
15use serde_json::value::RawValue as RawJsonValue;
16use tuwunel_core::{
17	Event, PduEvent, Result, err,
18	error::inspect_debug_log,
19	implement,
20	matrix::{PduCount, RoomVersionRules, StateKey, TypeStateKey, room_version},
21	result::{AndThenRef, FlatOk},
22	smallvec::SmallVec,
23	trace,
24	utils::{
25		IterStream, MutexMap, MutexMapGuard, ReadyExt, calculate_hash,
26		mutex_map::Guard,
27		stream::{BroadbandExt, TryIgnore, WidebandExt},
28	},
29	warn,
30};
31use tuwunel_database::{Deserialized, Ignore, Interfix, Map, Txn};
32
33use crate::{
34	rooms::{
35		short::{ShortEventId, ShortStateHash, ShortStateKey},
36		state_cache::MembershipUpdate,
37		state_compressor::{CompressedState, parse_compressed_state_event},
38		state_res::{StateMap, auth_types_for_event},
39	},
40	services::OnceServices,
41};
42
43pub struct Service {
44	/// Serializes room state as the middle per-room operation.
45	///
46	/// Acquire it after federation and before timeline insertion when those
47	/// mutexes share a room. Never acquire the federation mutex while holding
48	/// this guard.
49	pub mutex: RoomMutexMap,
50	services: Arc<OnceServices>,
51	db: Data,
52}
53
54struct Data {
55	shorteventid_shortstatehash: Arc<Map>,
56	roomid_shortstatehash: Arc<Map>,
57	roomid_pduleaves: Arc<Map>,
58}
59
60type RoomMutexMap = MutexMap<OwnedRoomId, ()>;
61pub type RoomMutexGuard = MutexMapGuard<OwnedRoomId, ()>;
62type ForwardExtremities = SmallVec<[OwnedEventId; 1]>;
63
64#[async_trait]
65impl crate::Service for Service {
66	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
67		Ok(Arc::new(Self {
68			mutex: RoomMutexMap::new(),
69			services: args.services.clone(),
70			db: Data {
71				shorteventid_shortstatehash: args.db["shorteventid_shortstatehash"].clone(),
72				roomid_shortstatehash: args.db["roomid_shortstatehash"].clone(),
73				roomid_pduleaves: args.db["roomid_pduleaves"].clone(),
74			},
75		}))
76	}
77
78	async fn memory_usage(&self, out: &mut (dyn Write + Send)) -> Result {
79		let mutex = self.mutex.len();
80		writeln!(out, "- state_mutex: {mutex}")?;
81
82		Ok(())
83	}
84
85	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
86}
87
88/// Set the room to the given statehash and update caches.
89#[implement(Service)]
90#[tracing::instrument(
91	name = "force",
92	level = "debug",
93	skip_all,
94	fields(
95		count = ?self.services.globals.pending_count(),
96		%shortstatehash,
97	)
98)]
99pub async fn force_state(
100	&self,
101	room_id: &RoomId,
102	shortstatehash: u64,
103	statediffnew: Arc<CompressedState>,
104	_statediffremoved: Arc<CompressedState>,
105	state_lock: &RoomMutexGuard,
106) -> Result {
107	statediffnew
108		.iter()
109		.stream()
110		.map(|&new| parse_compressed_state_event(new).1)
111		.wide_filter_map(async |shorteventid| {
112			let event_id: OwnedEventId = self
113				.services
114				.short
115				.get_eventid_from_short(shorteventid)
116				.inspect_err(inspect_debug_log)
117				.await
118				.ok()?;
119
120			self.services
121				.timeline
122				.get_pdu(&event_id)
123				.await
124				.ok()
125		})
126		.map(Ok)
127		.try_for_each(async |pdu| match pdu.kind {
128			| TimelineEventType::RoomMember => {
129				let Some(user_id) = pdu
130					.state_key
131					.as_ref()
132					.map(UserId::parse)
133					.flat_ok()
134				else {
135					return Ok(());
136				};
137
138				let Ok(membership_event) = pdu.get_content() else {
139					return Ok(());
140				};
141
142				let count = self.services.globals.next_count();
143				self.services
144					.state_cache
145					.update_membership(MembershipUpdate {
146						room_id,
147						user_id: &user_id,
148						membership_event,
149						sender: &pdu.sender,
150						last_state: None,
151						invite_via: None,
152						update_joined_count: false,
153						count: PduCount::Normal(*count),
154					})
155					.await
156			},
157			| _ => Ok(()),
158		})
159		.boxed()
160		.await?;
161
162	self.services
163		.state_cache
164		.update_joined_count(room_id)
165		.await;
166
167	self.set_room_state(room_id, shortstatehash, state_lock);
168
169	// Forced state may change this room's cached hierarchy summary.
170	self.services.spaces.cache_evict(room_id);
171
172	Ok(())
173}
174
175/// Generates a new StateHash and associates it with the incoming event.
176///
177/// This adds all current state events (not including the incoming event)
178/// to `stateid_pduid` and adds the incoming event to `eventid_statehash`.
179#[implement(Service)]
180#[tracing::instrument(
181	name = "set",
182	level = "debug",
183	skip(self, state_ids_compressed),
184	fields(
185		count = ?self.services.globals.pending_count(),
186	)
187)]
188pub async fn set_event_state(
189	&self,
190	event_id: &EventId,
191	room_id: &RoomId,
192	state_ids_compressed: Arc<CompressedState>,
193) -> Result<ShortStateHash> {
194	const KEY_LEN: usize = size_of::<ShortEventId>();
195	const VAL_LEN: usize = size_of::<ShortStateHash>();
196
197	let shorteventid = self
198		.services
199		.short
200		.get_or_create_shorteventid(event_id)
201		.await;
202
203	let state_hash = calculate_hash(state_ids_compressed.iter().map(|s| &s[..]));
204
205	if let Ok(shortstatehash) = self
206		.services
207		.short
208		.get_shortstatehash(&state_hash)
209		.await
210	{
211		self.db
212			.shorteventid_shortstatehash
213			.aput::<KEY_LEN, VAL_LEN, _, _>(shorteventid, shortstatehash);
214
215		return Ok(shortstatehash);
216	}
217
218	let previous_shortstatehash = self.get_room_shortstatehash(room_id).await;
219	let states_parents = match previous_shortstatehash {
220		| Ok(p) =>
221			self.services
222				.state_compressor
223				.load_shortstatehash_info(p)
224				.await?,
225		| _ => Vec::new(),
226	};
227
228	let (statediffnew, statediffremoved) = if let Some(parent_stateinfo) = states_parents.last() {
229		let statediffnew: CompressedState = state_ids_compressed
230			.difference(&parent_stateinfo.full_state)
231			.copied()
232			.collect();
233
234		let statediffremoved: CompressedState = parent_stateinfo
235			.full_state
236			.difference(&state_ids_compressed)
237			.copied()
238			.collect();
239
240		(Arc::new(statediffnew), Arc::new(statediffremoved))
241	} else {
242		(state_ids_compressed, Arc::new(CompressedState::new()))
243	};
244
245	let save_statediff = |txn: &mut Txn, shortstatehash| {
246		self.services
247			.state_compressor
248			.save_state_from_diff(
249				txn,
250				shortstatehash,
251				statediffnew,
252				statediffremoved,
253				1_000_000, // high number because no state will be based on this one
254				states_parents,
255			)
256	};
257
258	let (shortstatehash, _) = self
259		.services
260		.short
261		.get_or_create_shortstatehash(&state_hash, save_statediff)
262		.await?;
263
264	self.db
265		.shorteventid_shortstatehash
266		.aput::<KEY_LEN, VAL_LEN, _, _>(shorteventid, shortstatehash);
267
268	Ok(shortstatehash)
269}
270
271/// Generates a new StateHash and associates it with the incoming event.
272///
273/// This adds all current state events (not including the incoming event)
274/// to `stateid_pduid` and adds the incoming event to `eventid_statehash`.
275/// The event's short id is allocated here if absent, which is the only
276/// allocation of it on the local append path.
277#[implement(Service)]
278#[tracing::instrument(
279	name = "set",
280	level = "debug",
281	skip(self, new_pdu),
282	fields(
283		count = ?self.services.globals.pending_count(),
284	)
285)]
286pub async fn append_to_state(&self, new_pdu: &PduEvent) -> Result<u64> {
287	const KEY_LEN: usize = size_of::<ShortEventId>();
288	const VAL_LEN: usize = size_of::<ShortStateHash>();
289
290	let shorteventid = self
291		.services
292		.short
293		.get_or_create_shorteventid(&new_pdu.event_id)
294		.await;
295
296	let previous_shortstatehash = self
297		.get_room_shortstatehash(&new_pdu.room_id)
298		.await;
299
300	if let Ok(p) = previous_shortstatehash {
301		self.db
302			.shorteventid_shortstatehash
303			.aput::<KEY_LEN, VAL_LEN, _, _>(shorteventid, p);
304	}
305
306	match &new_pdu.state_key {
307		| Some(state_key) => {
308			let states_parents = match previous_shortstatehash {
309				| Ok(p) =>
310					self.services
311						.state_compressor
312						.load_shortstatehash_info(p)
313						.await?,
314				| _ => Vec::new(),
315			};
316
317			let shortstatekey = self
318				.services
319				.short
320				.get_or_create_shortstatekey(&new_pdu.kind.to_string().into(), state_key)
321				.await;
322
323			let new = self
324				.services
325				.state_compressor
326				.compress_state_event(shortstatekey, &new_pdu.event_id)
327				.await;
328
329			let replaces = states_parents
330				.last()
331				.map(|info| {
332					info.full_state
333						.iter()
334						.find(|bytes| bytes.starts_with(&shortstatekey.to_be_bytes()))
335				})
336				.unwrap_or_default();
337
338			if Some(&new) == replaces {
339				return Ok(previous_shortstatehash.expect("must exist"));
340			}
341
342			// TODO: statehash with deterministic inputs
343			let shortstatehash = self.services.globals.next_count();
344			let mut txn = self.services.db.txn();
345
346			let mut statediffnew = CompressedState::new();
347			statediffnew.insert(new);
348
349			let mut statediffremoved = CompressedState::new();
350			if let Some(replaces) = replaces {
351				statediffremoved.insert(*replaces);
352			}
353
354			self.services
355				.state_compressor
356				.save_state_from_diff(
357					&mut txn,
358					*shortstatehash,
359					Arc::new(statediffnew),
360					Arc::new(statediffremoved),
361					2,
362					states_parents,
363				)?;
364
365			txn.execute();
366
367			Ok(*shortstatehash)
368		},
369		| _ => Ok(previous_shortstatehash.expect("first event in room must be a state event")),
370	}
371}
372
373/// Set the state hash to a new version, but does not update state_cache.
374#[implement(Service)]
375#[tracing::instrument(skip(self, _mutex_lock), level = "debug")]
376pub fn set_room_state(
377	&self,
378	room_id: &RoomId,
379	shortstatehash: u64,
380	// Take mutex guard to make sure users get the room state mutex
381	_mutex_lock: &RoomMutexGuard,
382) {
383	const BUFSIZE: usize = size_of::<u64>();
384
385	self.db
386		.roomid_shortstatehash
387		.raw_aput::<BUFSIZE, _, _>(room_id, shortstatehash);
388}
389
390/// This fetches auth events from the current state.
391#[implement(Service)]
392#[expect(clippy::too_many_arguments)]
393#[tracing::instrument(skip(self, content), level = "debug")]
394pub async fn get_auth_events(
395	&self,
396	room_id: &RoomId,
397	kind: &TimelineEventType,
398	sender: &UserId,
399	state_key: Option<&str>,
400	content: &serde_json::value::RawValue,
401	auth_rules: &AuthorizationRules,
402	include_create: bool,
403) -> Result<StateMap<PduEvent>>
404where
405	StateEventType: Send + Sync,
406	StateKey: Send + Sync,
407{
408	let Ok(shortstatehash) = self.get_room_shortstatehash(room_id).await else {
409		return Ok(StateMap::new());
410	};
411
412	let sauthevents: HashMap<ShortStateKey, TypeStateKey> =
413		auth_types_for_event(kind, sender, state_key, content, auth_rules, include_create)?
414			.into_iter()
415			.stream()
416			.broad_filter_map(async |(event_type, state_key): TypeStateKey| {
417				self.services
418					.short
419					.get_shortstatekey(&event_type, &state_key)
420					.await
421					.map(move |sstatekey| (sstatekey, (event_type, state_key)))
422					.ok()
423			})
424			.collect()
425			.await;
426
427	let (state_keys, event_ids): (Vec<_>, Vec<_>) = self
428		.services
429		.state_accessor
430		.state_full_shortids(shortstatehash)
431		.ready_filter_map(Result::ok)
432		.ready_filter_map(|(shortstatekey, shorteventid)| {
433			sauthevents
434				.get(&shortstatekey)
435				.map(move |(ty, sk)| ((ty, sk), shorteventid))
436		})
437		.unzip()
438		.await;
439
440	self.services
441		.short
442		.multi_get_eventid_from_short(event_ids.into_iter().stream())
443		.zip(state_keys.into_iter().stream())
444		.ready_filter_map(|(event_id, (ty, sk))| Some(((ty, sk), event_id.ok()?)))
445		.broad_filter_map(async |((ty, sk), event_id): ((&_, &_), OwnedEventId)| {
446			let pdu = self.services.timeline.get_pdu(&event_id).await;
447
448			Some(((ty.clone(), sk.clone()), pdu.ok()?))
449		})
450		.collect()
451		.map(Ok)
452		.await
453}
454
455#[implement(Service)]
456#[tracing::instrument(skip_all, level = "debug")]
457pub async fn summary_stripped<Pdu: Event>(&self, event: &Pdu) -> Vec<Raw<AnyStrippedStateEvent>> {
458	let cells = [
459		(&StateEventType::RoomCreate, ""),
460		(&StateEventType::RoomJoinRules, ""),
461		(&StateEventType::RoomCanonicalAlias, ""),
462		(&StateEventType::RoomName, ""),
463		(&StateEventType::RoomAvatar, ""),
464		(&StateEventType::RoomMember, event.sender().as_str()), // Add recommended events
465		(&StateEventType::RoomEncryption, ""),
466		(&StateEventType::RoomTopic, ""),
467	];
468
469	let fetches = cells.into_iter().map(|(event_type, state_key)| {
470		self.services
471			.state_accessor
472			.room_state_get(event.room_id(), event_type, state_key)
473	});
474
475	join_all(fetches)
476		.await
477		.into_iter()
478		.filter_map(Result::ok)
479		.map(Event::into_format)
480		.chain(once(event.to_format()))
481		.collect()
482}
483
484/// Like `summary_stripped`, but formats each event as a full federation PDU
485/// per the room version's event format (MSC4311). The membership `event` is
486/// formatted from its `event_json`; the recommended state cells are fetched
487/// from stored room state.
488#[implement(Service)]
489#[tracing::instrument(skip_all, level = "debug")]
490pub async fn summary_pdus<Pdu: Event>(
491	&self,
492	event: &Pdu,
493	event_json: &CanonicalJsonObject,
494	room_version: &RoomVersionId,
495) -> Vec<Box<RawJsonValue>> {
496	let cells = [
497		(&StateEventType::RoomCreate, ""),
498		(&StateEventType::RoomJoinRules, ""),
499		(&StateEventType::RoomCanonicalAlias, ""),
500		(&StateEventType::RoomName, ""),
501		(&StateEventType::RoomAvatar, ""),
502		(&StateEventType::RoomMember, event.sender().as_str()),
503		(&StateEventType::RoomEncryption, ""),
504		(&StateEventType::RoomTopic, ""),
505	];
506
507	let membership = self
508		.services
509		.federation
510		.format_pdu_into(event_json.clone(), Some(room_version))
511		.boxed() // query-depth firewall
512		.await;
513
514	cells
515		.into_iter()
516		.stream()
517		.wide_filter_map(async |(event_type, state_key)| {
518			let pdu = self
519				.services
520				.state_accessor
521				.room_state_get(event.room_id(), event_type, state_key)
522				.await
523				.ok()?;
524
525			let pdu_json = self
526				.services
527				.timeline
528				.get_pdu_json(pdu.event_id())
529				.await
530				.ok()?;
531
532			Some(
533				self.services
534					.federation
535					.format_pdu_into(pdu_json, Some(room_version))
536					.await,
537			)
538		})
539		.chain(once(membership).stream())
540		.collect()
541		.await
542}
543
544/// Returns the room's version rules
545#[implement(Service)]
546#[inline]
547pub async fn get_room_version_rules(&self, room_id: &RoomId) -> Result<RoomVersionRules> {
548	self.get_room_version(room_id)
549		.await
550		.and_then_ref(room_version::rules)
551}
552
553/// Returns the room's version.
554#[implement(Service)]
555#[tracing::instrument(
556	level = "debug"
557	skip(self),
558	ret(level = "trace"),
559)]
560pub async fn get_room_version(&self, room_id: &RoomId) -> Result<RoomVersionId> {
561	self.services
562		.state_accessor
563		.room_state_get_content(room_id, &StateEventType::RoomCreate, "")
564		.await
565		.as_ref()
566		.map(room_version::from_create_content)
567		.cloned()
568		.map_err(|e| err!(Request(NotFound("No create event found: {e:?}"))))
569}
570
571#[implement(Service)]
572#[tracing::instrument(
573	level = "debug"
574	skip(self),
575	ret(level = "trace"),
576)]
577pub async fn get_room_shortstatehash(&self, room_id: &RoomId) -> Result<ShortStateHash> {
578	self.db
579		.roomid_shortstatehash
580		.get(room_id)
581		.await
582		.deserialized()
583}
584
585/// Returns the state hash at this event.
586#[implement(Service)]
587pub async fn pdu_shortstatehash(&self, event_id: &EventId) -> Result<ShortStateHash> {
588	self.services
589		.short
590		.get_shorteventid(event_id)
591		.and_then(|shorteventid| self.get_shortstatehash(shorteventid))
592		.await
593}
594
595/// Returns the state hash at this event.
596#[implement(Service)]
597#[tracing::instrument(
598	level = "debug"
599	skip(self),
600	ret(level = "trace"),
601)]
602pub async fn get_shortstatehash(&self, shorteventid: ShortEventId) -> Result<ShortStateHash> {
603	const BUFSIZE: usize = size_of::<ShortEventId>();
604
605	self.db
606		.shorteventid_shortstatehash
607		.aqry::<BUFSIZE, _>(&shorteventid)
608		.await
609		.deserialized()
610}
611
612#[implement(Service)]
613pub(super) fn delete_room_shortstatehash(
614	&self,
615	room_id: &RoomId,
616	_mutex_lock: &Guard<OwnedRoomId, ()>,
617) -> Result {
618	self.db.roomid_shortstatehash.remove(room_id);
619
620	Ok(())
621}
622
623/// Collapses the room to a single forward extremity, keeping the one furthest
624/// along in stream order, and returns the number removed.
625#[implement(Service)]
626#[tracing::instrument(
627	level = "debug"
628	skip_all,
629	fields(%room_id),
630)]
631pub async fn collapse_forward_extremities(
632	&self,
633	room_id: &RoomId,
634	state_lock: &RoomMutexGuard,
635) -> usize {
636	let extremities: ForwardExtremities = self
637		.get_forward_extremities(room_id)
638		.map(ToOwned::to_owned)
639		.collect()
640		.await;
641
642	if extremities.len() <= 1 {
643		return 0;
644	}
645
646	let survivor = join_all(extremities.iter().map(async |event_id| {
647		self.services
648			.timeline
649			.get_pdu_count(event_id)
650			.await
651			.ok()
652			.map(|count| (count, event_id))
653	}))
654	.await
655	.into_iter()
656	.flatten()
657	.max_by_key(|(count, _)| *count)
658	.map(|(_, event_id)| event_id);
659
660	let Some(survivor) = survivor else {
661		return 0;
662	};
663
664	self.set_forward_extremities(room_id, once(&**survivor), state_lock)
665		.await;
666
667	extremities.len().saturating_sub(1)
668}
669
670#[implement(Service)]
671#[tracing::instrument(
672	level = "trace"
673	skip(self),
674)]
675pub fn get_forward_extremities<'a>(
676	&'a self,
677	room_id: &'a RoomId,
678) -> impl Stream<Item = &EventId> + Send + '_ {
679	let prefix = (room_id, Interfix);
680
681	self.db
682		.roomid_pduleaves
683		.keys_prefix(&prefix)
684		.map_ok(|(_, event_id): (Ignore, &EventId)| event_id)
685		.ignore_err()
686}
687
688#[implement(Service)]
689#[tracing::instrument(
690	level = "debug"
691	skip_all,
692	fields(%room_id),
693)]
694pub async fn set_forward_extremities<'a, I>(
695	&'a self,
696	room_id: &'a RoomId,
697	event_ids: I,
698	_state_lock: &'a RoomMutexGuard,
699) where
700	I: Iterator<Item = &'a EventId> + Send + 'a,
701{
702	let prefix = (room_id, Interfix);
703	self.db
704		.roomid_pduleaves
705		.keys_prefix_raw(&prefix)
706		.ignore_err()
707		.ready_for_each(|key| self.db.roomid_pduleaves.remove(key))
708		.await;
709
710	for event_id in event_ids {
711		let key = (room_id, event_id);
712		self.db.roomid_pduleaves.put_raw(key, event_id);
713	}
714}
715
716#[implement(Service)]
717pub(super) async fn delete_all_rooms_forward_extremities(&self, room_id: &RoomId) -> Result {
718	let prefix = (room_id, Interfix);
719
720	self.db
721		.roomid_pduleaves
722		.keys_prefix_raw(&prefix)
723		.ignore_err()
724		.ready_for_each(|key| {
725			trace!("Removing key: {key:?}");
726			self.db.roomid_pduleaves.remove(key);
727		})
728		.await;
729
730	Ok(())
731}