Skip to main content

tuwunel_service/rooms/timeline/
backfill.rs

1//! Retrieves older room history from federation and inserts validated events.
2//!
3//! Candidate servers are selected from trusted and room-related origins, while
4//! remote timestamp claims are checked against the ingested event. Backfilled
5//! events receive negative stream counts so they sort before normal history.
6
7use std::{collections::HashSet, iter::once, num::NonZeroUsize};
8
9use futures::{
10	FutureExt, StreamExt, TryFutureExt,
11	future::{join, try_join, try_join4},
12};
13use rand::seq::SliceRandom;
14use ruma::{
15	CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, ServerName,
16	api::Direction, events::TimelineEventType,
17};
18use serde::Deserialize;
19use serde_json::value::RawValue as RawJsonValue;
20use tuwunel_core::{
21	Err, Result, at, debug, debug_warn, implement, is_false,
22	matrix::{
23		PduEvent,
24		event::Event,
25		pdu::{PduCount, PduId, RawPduId},
26	},
27	utils::{
28		BoolExt, IterStream, ReadyExt,
29		future::{BoolExt as FutureBoolExt, TryExtExt},
30	},
31	validated, warn,
32};
33use tuwunel_database::Json;
34
35use super::{ExtractBody, bias_count};
36use crate::{
37	federation::Candidates,
38	fetcher::{Op, Opts},
39	rooms::state_accessor::plain_text_topic,
40};
41
42/// Events requested per backfill batch.
43const BACKFILL_LIMIT: NonZeroUsize = NonZeroUsize::new(100).unwrap();
44
45const BACKFILL_ATTEMPT_LIMIT: NonZeroUsize = NonZeroUsize::new(5).unwrap();
46
47const BACKFILL_BATCH_ATTEMPTS: usize = 3;
48
49/// The `event_id` and timestamp parsed back out of an [`Op::TimestampToEvent`]
50/// fetch outcome.
51#[derive(Deserialize)]
52struct TimestampHit {
53	event_id: OwnedEventId,
54	origin_server_ts: MilliSecondsSinceUnixEpoch,
55}
56
57/// Attempts federation backfill when a request reaches local history's edge.
58///
59/// Backfill is skipped after the create event and for effectively empty rooms
60/// that are not world-readable. No candidate, a remote fetch failure, or an
61/// empty accepted chunk is treated as a successful best-effort outcome.
62#[implement(super::Service)]
63#[tracing::instrument(name = "backfill", level = "debug", skip(self))]
64pub async fn backfill_if_required(&self, room_id: &RoomId, from: PduCount) -> Result {
65	let (first_pdu_count, first_pdu) = self.first_item_in_room(room_id).await?;
66
67	if first_pdu_count < from {
68		return Ok(());
69	}
70
71	// No backfill required, reached the end.
72	if *first_pdu.event_type() == TimelineEventType::RoomCreate {
73		return Ok(());
74	}
75
76	let empty_room = self
77		.services
78		.state_cache
79		.room_joined_count(room_id)
80		.map_ok_or(true, |count| count <= 1);
81
82	let not_world_readable = self
83		.services
84		.state_accessor
85		.is_world_readable(room_id)
86		.map(is_false!());
87
88	// Room is empty (1 user or none), there is no one that can backfill
89	if empty_room.and(not_world_readable).await {
90		return Ok(());
91	}
92
93	let mut eligible = self.backfill_candidates(room_id).await;
94
95	let no_backfill = || {
96		warn!(%room_id, "No servers could backfill, but backfill was needed");
97		Ok(())
98	};
99
100	// Empty here, rather than deferring to the fetcher, keeps backfill scoped to
101	// the authoritative servers; the fetcher would otherwise fall back to the
102	// room's whole population.
103	if eligible.is_empty() {
104		return no_backfill();
105	}
106
107	for _ in 0..BACKFILL_BATCH_ATTEMPTS {
108		let opts = Opts::new(Op::Backfill, room_id.to_owned())
109			.event_id(first_pdu.event_id().to_owned())
110			.candidates(eligible.iter().cloned())
111			.attempt_limit(BACKFILL_ATTEMPT_LIMIT)
112			.backfill_limit(BACKFILL_LIMIT);
113
114		let Ok(outcome) = self.services.fetcher.fetch(opts).await else {
115			return no_backfill();
116		};
117
118		let pdus: Vec<Box<RawJsonValue>> = serde_json::from_slice(&outcome.bytes)?;
119		let batch_size = pdus.len();
120		let prepended = pdus
121			.into_iter()
122			.stream()
123			.fold(0_usize, async |prepended, pdu| {
124				let inserted = self
125					.backfill_pdu(room_id, &outcome.origin, pdu)
126					.await
127					.inspect_err(|e| debug_warn!(%room_id, %e, "Failed to add backfilled pdu"))
128					.unwrap_or(false);
129
130				prepended.saturating_add(usize::from(inserted))
131			})
132			.await;
133
134		debug!(
135			%room_id,
136			origin = %outcome.origin,
137			batch_size,
138			prepended,
139			"Processed backfill response",
140		);
141
142		if prepended > 0 {
143			return Ok(());
144		}
145
146		eligible.retain(|server| server != &outcome.origin);
147		if eligible.is_empty() {
148			break;
149		}
150	}
151
152	warn!(%room_id, "Backfill was required but prepended no events");
153	Ok(())
154}
155
156#[implement(super::Service)]
157async fn backfill_candidates(&self, room_id: &RoomId) -> Candidates {
158	let canonical_alias = self
159		.services
160		.state_accessor
161		.get_canonical_alias(room_id);
162
163	let power_levels = self
164		.services
165		.state_accessor
166		.get_power_levels(room_id);
167
168	let (canonical_alias, power_levels) = join(canonical_alias, power_levels).await;
169
170	let power_servers = power_levels
171		.iter()
172		.flat_map(|power| {
173			power
174				.rules
175				.privileged_creators
176				.iter()
177				.flat_map(|creators| creators.iter())
178		})
179		.chain(power_levels.iter().flat_map(|power| {
180			power
181				.users
182				.iter()
183				.filter_map(|(user_id, level)| level.gt(&power.users_default).then_some(user_id))
184		}))
185		.filter_map(|user_id| {
186			self.services
187				.globals
188				.user_is_local(user_id)
189				.is_false()
190				.then_some(user_id.server_name())
191		})
192		.collect::<HashSet<_>>();
193
194	let power_servers = {
195		let mut vec: Vec<_> = power_servers
196			.into_iter()
197			.map(ToOwned::to_owned)
198			.collect();
199
200		vec.shuffle(&mut rand::rng());
201		vec.into_iter().stream()
202	};
203
204	let canonical_room_alias_server = once(canonical_alias)
205		.filter_map(Result::ok)
206		.map(|alias| alias.server_name().to_owned())
207		.stream();
208
209	let trusted_servers = self
210		.services
211		.server
212		.config
213		.trusted_servers
214		.iter()
215		.map(ToOwned::to_owned)
216		.stream();
217
218	power_servers
219		.chain(canonical_room_alias_server)
220		.chain(trusted_servers)
221		.ready_filter(|server_name| !self.services.globals.server_is_ours(server_name))
222		.filter_map(async |server_name| {
223			self.services
224				.state_cache
225				.server_in_room(&server_name, room_id)
226				.await
227				.then_some(server_name)
228		})
229		.collect()
230		.await
231}
232
233#[implement(super::Service)]
234/// Finds the nearest event to a timestamp with a federation fallback.
235///
236/// The local answer is retained unless a closer remote claim can be ingested.
237/// The ingested event must belong to this room and its actual timestamp must
238/// remain on the requested side of the query.
239pub async fn get_event_id_near_ts_with_fallback(
240	&self,
241	room_id: &RoomId,
242	ts: MilliSecondsSinceUnixEpoch,
243	dir: Direction,
244) -> Result<(MilliSecondsSinceUnixEpoch, OwnedEventId)> {
245	let local = self.get_event_id_near_ts(room_id, ts, dir).await;
246
247	let federate = match &local {
248		| Err(_) => true,
249		| Ok((_, event_id)) =>
250			dir == Direction::Forward && self.is_start_edge_hit(room_id, event_id).await,
251	};
252
253	if !federate {
254		return local;
255	}
256
257	let candidates = self.backfill_candidates(room_id).await;
258
259	if candidates.is_empty() {
260		return local;
261	}
262
263	let opts = Opts::new(Op::TimestampToEvent, room_id.to_owned())
264		.ts(ts)
265		.dir(dir)
266		.candidates(candidates)
267		.checks(false);
268
269	let Ok(outcome) = self.services.fetcher.fetch(opts).await else {
270		return local;
271	};
272
273	let Ok(TimestampHit { event_id, origin_server_ts }) = serde_json::from_slice(&outcome.bytes)
274	else {
275		return local;
276	};
277
278	if let Ok((local_ts, local_id)) = &local
279		&& !nearer(dir, origin_server_ts, *local_ts)
280	{
281		return Ok((*local_ts, local_id.clone()));
282	}
283
284	// Fail closed: an un-ingested event can't be visibility-checked, so keep local.
285	let Ok(pdu) = self
286		.backfill_event(room_id, &event_id, &outcome.origin)
287		.inspect_err(|e| debug_warn!(%room_id, error = ?e, "timestamp fallback backfill failed"))
288		.await
289	else {
290		return local;
291	};
292
293	let actual_ts = pdu.origin_server_ts();
294	let matches_direction = match dir {
295		| Direction::Forward => actual_ts >= ts,
296		| Direction::Backward => actual_ts <= ts,
297	};
298
299	if actual_ts != origin_server_ts || !matches_direction {
300		debug_warn!(
301			%room_id,
302			%event_id,
303			?dir,
304			?ts,
305			?origin_server_ts,
306			?actual_ts,
307			"timestamp fallback claim was inconsistent with the ingested event"
308		);
309		return local;
310	}
311
312	Ok((actual_ts, event_id))
313}
314
315#[implement(super::Service)]
316async fn is_start_edge_hit(&self, room_id: &RoomId, event_id: &EventId) -> bool {
317	self.first_item_in_room(room_id)
318		.await
319		.is_ok_and(|(_, first)| {
320			*first.event_type() != TimelineEventType::RoomCreate && first.event_id() == event_id
321		})
322}
323
324fn nearer(dir: Direction, a: MilliSecondsSinceUnixEpoch, b: MilliSecondsSinceUnixEpoch) -> bool {
325	match dir {
326		| Direction::Forward => a < b,
327		| Direction::Backward => a > b,
328	}
329}
330
331#[implement(super::Service)]
332async fn backfill_event(
333	&self,
334	room_id: &RoomId,
335	event_id: &EventId,
336	origin: &ServerName,
337) -> Result<PduEvent> {
338	let opts = Opts::new(Op::Backfill, room_id.to_owned())
339		.event_id(event_id.to_owned())
340		.candidates([origin.to_owned()])
341		.backfill_limit(BACKFILL_LIMIT);
342
343	let outcome = self.services.fetcher.fetch(opts).await?;
344
345	let pdus: Vec<Box<RawJsonValue>> = serde_json::from_slice(&outcome.bytes)?;
346
347	let ingestion = pdus
348		.into_iter()
349		.stream()
350		.fold(Ok(()), async |prior, pdu| {
351			let current = self
352				.backfill_pdu(room_id, &outcome.origin, pdu)
353				.map_ok(|_| ())
354				.inspect_err(
355					|e| debug_warn!(%room_id, error = ?e, "Failed to add backfilled pdu"),
356				)
357				.await;
358
359			prior.and(current)
360		})
361		.await;
362
363	match self.get_pdu_count(event_id).await {
364		| Err(error) => ingestion.and(Err(error)),
365		| Ok(_) => {
366			let pdu = self.get_pdu(event_id).await?;
367
368			if pdu.room_id() != room_id {
369				return Err!(Request(NotFound(
370					"Timestamp fallback target belongs to another room."
371				)));
372			}
373
374			Ok(pdu)
375		},
376	}
377}
378
379/// Fetches one remote event and persists it through the backfill path.
380///
381/// Fetcher checks are disabled only for retrieval; `backfill_pdu`
382/// performs signature, hash, and authorization validation. An event already
383/// accepted completes successfully without another insertion, while a stored
384/// outlier is still validated and promoted.
385#[implement(super::Service)]
386#[tracing::instrument(skip(self), level = "debug")]
387pub async fn fetch_remote_event(&self, room_id: &RoomId, event_id: &EventId) -> Result {
388	let opts = Opts::new(Op::Event, room_id.to_owned())
389		.event_id(event_id.to_owned())
390		.checks(false);
391
392	let outcome = self.services.fetcher.fetch(opts).await?;
393
394	let pdu: Box<RawJsonValue> = serde_json::from_slice(&outcome.bytes)?;
395
396	self.backfill_pdu(room_id, &outcome.origin, pdu)
397		.await?;
398
399	Ok(())
400}
401
402/// Validates and inserts one remotely supplied event as backfilled history.
403///
404/// An already accepted duplicate returns `false` without insertion. A new event
405/// or stored outlier receives a negative backfill count, moves to accepted
406/// storage, updates its timestamp and ID indexes, and adds searchable message
407/// or topic content.
408#[implement(super::Service)]
409#[tracing::instrument(skip(self, pdu), level = "debug")]
410pub async fn backfill_pdu(
411	&self,
412	room_id: &RoomId,
413	origin: &ServerName,
414	pdu: Box<RawJsonValue>,
415) -> Result<bool> {
416	let parsed = self
417		.services
418		.event_handler
419		.parse_incoming_pdu(&pdu);
420
421	// Lock so we cannot backfill the same pdu twice at the same time
422	let mutex_lock = self
423		.services
424		.event_handler
425		.mutex_federation
426		.lock(room_id)
427		.map(Ok);
428
429	let ((_, event_id, value), mutex_lock) = try_join(parsed, mutex_lock).await?;
430
431	let existed = self
432		.services
433		.event_handler
434		.handle_incoming_pdu(origin, room_id, &event_id, value, false)
435		.await?
436		.map(at!(1))
437		.is_some_and(is_false!());
438
439	// Bail if the PDU already exists; a duplicate insertion is not good.
440	if existed {
441		return Ok(false);
442	}
443
444	let pdu = self.get_pdu(&event_id);
445
446	let value = self.get_pdu_json(&event_id);
447
448	let shortroomid = self.services.short.get_shortroomid(room_id);
449
450	let insert_lock = self.mutex_insert.lock(room_id).map(Ok);
451
452	let (pdu, value, shortroomid, insert_lock) =
453		try_join4(pdu, value, shortroomid, insert_lock).await?;
454
455	// A pdu_id is not returned from handle_incoming_pdu() when accepting a new
456	// event on this codepath. The pdu_id is instead created here in ℤ−
457	let count = self.services.globals.next_count();
458	let count: i64 = (*count).try_into()?;
459	let pdu_id: RawPduId = PduId {
460		shortroomid,
461		count: PduCount::Backfilled(validated!(0 - count)),
462	}
463	.into();
464
465	// Insert pdu
466	self.prepend_backfill_pdu(
467		&pdu_id,
468		room_id,
469		&event_id,
470		u64::from(pdu.origin_server_ts),
471		&value,
472	);
473	drop(insert_lock);
474
475	match pdu.kind {
476		| TimelineEventType::RoomMessage => {
477			if let Ok(ExtractBody { body: Some(body) }) = pdu.get_content() {
478				self.services
479					.search
480					.index_pdu(shortroomid, &pdu_id, &body);
481			}
482		},
483		| TimelineEventType::RoomTopic =>
484			if let Some(topic) = pdu.get_content().ok().and_then(plain_text_topic) {
485				self.services
486					.search
487					.index_pdu(shortroomid, &pdu_id, &topic);
488			},
489		| _ => {},
490	}
491
492	drop(mutex_lock);
493
494	debug!("Prepended backfill pdu");
495	Ok(true)
496}
497
498#[implement(super::Service)]
499fn prepend_backfill_pdu(
500	&self,
501	pdu_id: &RawPduId,
502	room_id: &RoomId,
503	event_id: &EventId,
504	origin_server_ts: u64,
505	json: &CanonicalJsonObject,
506) {
507	let mut txn = self.db.db.txn();
508
509	txn.raw_put(&self.db.pduid_pdu, pdu_id, Json(json));
510	txn.insert_raw(&self.db.eventid_pduid, event_id, pdu_id);
511	txn.del_raw(&self.db.eventid_outlierpdu, event_id);
512
513	let count_key = bias_count(pdu_id.count());
514	let key = (room_id, origin_server_ts, count_key);
515	txn.put_raw(&self.db.roomid_tscount_pducount, key, pdu_id.count());
516
517	txn.execute();
518}