Skip to main content

tuwunel_service/sending/
mod.rs

1mod data;
2mod dest;
3mod sender;
4#[cfg(test)]
5mod tests;
6
7use std::{
8	fmt::Debug,
9	hash::{DefaultHasher, Hash, Hasher},
10	io::Write,
11	iter::{once, repeat_with},
12	mem::take,
13	pin::pin,
14	sync::{Arc, Mutex as StdMutex},
15};
16
17use async_trait::async_trait;
18use futures::{FutureExt, Stream, StreamExt};
19use loole::unbounded;
20use ruma::{DeviceId, OwnedRoomId, RoomId, ServerName, UserId};
21use serde::Serialize;
22use tokio::{
23	task,
24	task::{JoinError, JoinSet},
25};
26use tuwunel_core::{
27	Result, Server, debug, debug_warn, err, error,
28	smallvec::SmallVec,
29	utils::{
30		IterStream, ReadyExt, TryReadyExt, available_parallelism, future::BoolExt,
31		math::usize_from_u64_truncated, result::LogErr,
32	},
33	warn,
34};
35
36pub use self::{
37	data::Data,
38	dest::Destination,
39	sender::{EDU_LIMIT, PDU_LIMIT},
40};
41use crate::{appservice::RegistrationInfo, rooms::timeline::RawPduId};
42
43pub struct Service {
44	pub db: Data,
45	server: Arc<Server>,
46	services: Arc<crate::services::OnceServices>,
47	channels: Vec<(loole::Sender<Msg>, loole::Receiver<Msg>)>,
48
49	// Aborted and joined when the service stops.
50	flushes: StdMutex<JoinSet<()>>,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
54struct Msg {
55	dest: Destination,
56	event: SendingEvent,
57	queue_id: Vec<u8>,
58}
59
60#[expect(clippy::module_name_repetitions)]
61#[derive(Clone, Debug, PartialEq, Eq, Hash)]
62pub enum SendingEvent {
63	Pdu(RawPduId),             // pduid
64	Edu(EduBuf),               // edu json
65	ToDevice(EduBuf),          // msc4203 to-device
66	DeviceListChanged(EduBuf), // msc3202 device list
67	/// Queue an account-wide counts-only push.
68	///
69	/// The sender recomputes the count when the row is delivered.
70	BadgeRefresh,
71	Flush, // none
72}
73
74pub type EduBuf = SmallVec<[u8; EDU_BUF_CAP]>;
75pub type EduVec = SmallVec<[EduBuf; EDU_VEC_CAP]>;
76
77const EDU_BUF_CAP: usize = 128 - 16;
78const EDU_VEC_CAP: usize = 1;
79
80// Leading bytes on queued sending values select tagged event variants. Legacy
81// PDU and EDU rows cannot collide; the badge tag stands alone.
82const TAG_TO_DEVICE: u8 = 0x01;
83const TAG_DEVICE_LIST_CHANGED: u8 = 0x02;
84const TAG_BADGE_REFRESH: u8 = 0x03;
85const TAG_PREFIX_LEN: usize = 1 + size_of::<u64>();
86
87impl SendingEvent {
88	/// Return bytes written verbatim as the queue row value.
89	///
90	/// PDUs keep their ID in the row key and flushes are not persisted. EDU
91	/// variants own `[tag][count][body]`; a badge refresh owns only its tag.
92	pub(super) fn value_bytes(&self) -> &[u8] {
93		match self {
94			| Self::Edu(bytes) | Self::ToDevice(bytes) | Self::DeviceListChanged(bytes) => bytes,
95			| Self::BadgeRefresh => &[TAG_BADGE_REFRESH],
96			| Self::Pdu(_) | Self::Flush => &[],
97		}
98	}
99}
100
101/// Wire shape of one `de.sorunome.msc2409.to_device` entry (MSC4203): the
102/// stored to-device event flattened with the recipient's identifiers. The
103/// ruma `AnyAppserviceToDeviceEvent` deliberately has no `Serialize`, so the
104/// send side writes this local struct.
105#[derive(Serialize)]
106struct AsToDeviceEvent<'a> {
107	#[serde(rename = "type")]
108	kind: &'a str,
109	sender: &'a UserId,
110	content: &'a serde_json::Value,
111	to_user_id: &'a UserId,
112	to_device_id: &'a DeviceId,
113}
114
115#[async_trait]
116impl crate::Service for Service {
117	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
118		let num_senders = num_senders(args);
119		Ok(Arc::new(Self {
120			db: Data::new(args),
121			server: args.server.clone(),
122			services: args.services.clone(),
123			channels: repeat_with(unbounded).take(num_senders).collect(),
124			flushes: JoinSet::new().into(),
125		}))
126	}
127
128	async fn worker(self: Arc<Self>) -> Result {
129		let mut senders =
130			self.channels
131				.iter()
132				.enumerate()
133				.fold(JoinSet::new(), |mut joinset, (id, _)| {
134					let self_ = self.clone();
135					let worker = self_.sender(id);
136					let worker = if self.unconstrained() {
137						task::unconstrained(worker).boxed()
138					} else {
139						worker.boxed()
140					};
141
142					let runtime = self.server.runtime();
143					let _abort = joinset.spawn_on(worker, runtime);
144					joinset
145				});
146
147		while let Some(ret) = senders.join_next_with_id().await {
148			match ret {
149				| Ok((id, _)) => {
150					debug!(?id, "sender worker finished");
151				},
152				| Err(error) => {
153					error!(id = ?error.id(), ?error, "sender worker finished");
154				},
155			}
156		}
157
158		let mut flushes = take(&mut *self.flushes.lock().expect("locked"));
159
160		flushes.abort_all();
161		while let Some(result) = flushes.join_next().await {
162			log_flush(result);
163		}
164
165		Ok(())
166	}
167
168	async fn interrupt(&self) {
169		self.flushes.lock().expect("locked").abort_all();
170
171		for (sender, _) in &self.channels {
172			if !sender.is_closed() {
173				sender.close();
174			}
175		}
176	}
177
178	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
179
180	fn unconstrained(&self) -> bool { true }
181}
182
183impl Service {
184	#[tracing::instrument(skip(self, pdu_id, user, pushkey), level = "debug")]
185	pub fn send_pdu_push(&self, pdu_id: &RawPduId, user: &UserId, pushkey: String) -> Result {
186		let dest = Destination::Push(user.to_owned(), pushkey);
187		let event = SendingEvent::Pdu(*pdu_id);
188		let _cork = self.db.db.cork();
189
190		self.queue_and_dispatch(dest, event)
191	}
192
193	/// Queue one event for delivery to `dest` and wake a sender.
194	fn queue_and_dispatch(&self, dest: Destination, event: SendingEvent) -> Result {
195		let keys = self.db.queue_requests(once((&event, &dest)));
196
197		self.dispatch(Msg {
198			dest,
199			event,
200			queue_id: keys
201				.into_iter()
202				.next()
203				.expect("request queue key"),
204		})
205	}
206
207	/// Queue a counts-only push refresh for every pusher owned by a user.
208	///
209	/// Rows are durable, coalesced, and recomputed at send time.
210	#[tracing::instrument(level = "debug", skip(self))]
211	pub async fn refresh_push_badge(&self, user_id: &UserId) -> Result {
212		self.services
213			.pusher
214			.get_pushkeys(user_id)
215			.map(Ok)
216			.ready_try_for_each(|pushkey| {
217				let dest = Destination::Push(user_id.to_owned(), pushkey.to_owned());
218
219				self.queue_and_dispatch(dest, SendingEvent::BadgeRefresh)
220			})
221			.await
222	}
223
224	#[tracing::instrument(skip(self), level = "debug")]
225	pub fn send_pdu_appservice(&self, appservice_id: String, pdu_id: RawPduId) -> Result {
226		let dest = Destination::Appservice(appservice_id);
227		let event = SendingEvent::Pdu(pdu_id);
228		let _cork = self.db.db.cork();
229
230		self.queue_and_dispatch(dest, event)
231	}
232
233	#[tracing::instrument(skip(self, room_id, pdu_id), level = "debug")]
234	pub async fn send_pdu_room(&self, room_id: &RoomId, pdu_id: &RawPduId) -> Result {
235		let servers = self
236			.services
237			.state_cache
238			.room_servers(room_id)
239			.ready_filter(|server_name| !self.services.globals.server_is_ours(server_name));
240
241		self.send_pdu_servers(servers, pdu_id).await
242	}
243
244	#[tracing::instrument(skip(self, servers, pdu_id), level = "debug")]
245	pub async fn send_pdu_servers<'a, S>(&self, servers: S, pdu_id: &RawPduId) -> Result
246	where
247		S: Stream<Item = &'a ServerName> + Send + 'a,
248	{
249		let requests = servers
250			.map(|server| {
251				(Destination::Federation(server.into()), SendingEvent::Pdu(pdu_id.to_owned()))
252			})
253			.collect::<Vec<_>>()
254			.await;
255
256		let _cork = self.db.db.cork();
257		let keys = self
258			.db
259			.queue_requests(requests.iter().map(|(o, e)| (e, o)));
260
261		for ((dest, event), queue_id) in requests.into_iter().zip(keys) {
262			self.dispatch(Msg { dest, event, queue_id })?;
263		}
264
265		Ok(())
266	}
267
268	#[tracing::instrument(skip(self, server, serialized), level = "debug")]
269	pub fn send_edu_server(&self, server: &ServerName, serialized: EduBuf) -> Result {
270		let dest = Destination::Federation(server.to_owned());
271		let event = SendingEvent::Edu(serialized);
272		let _cork = self.db.db.cork();
273
274		self.queue_and_dispatch(dest, event)
275	}
276
277	#[tracing::instrument(skip(self, room_id, serialized), level = "debug")]
278	pub async fn send_edu_room(&self, room_id: &RoomId, serialized: EduBuf) -> Result {
279		let servers = self
280			.services
281			.state_cache
282			.room_servers(room_id)
283			.ready_filter(|server_name| !self.services.globals.server_is_ours(server_name));
284
285		self.send_edu_servers(servers, serialized).await
286	}
287
288	/// Queue an EDU for delivery to a specific appservice.
289	#[tracing::instrument(skip(self, serialized), level = "debug")]
290	pub fn send_edu_appservice(&self, appservice_id: String, serialized: EduBuf) -> Result {
291		let dest = Destination::Appservice(appservice_id);
292		let event = SendingEvent::Edu(serialized);
293		let _cork = self.db.db.cork();
294
295		self.queue_and_dispatch(dest, event)
296	}
297
298	/// Sends an EDU to all appservices interested in a room.
299	/// The `serialized` data must be in `EphemeralData` format, not federation
300	/// `Edu`.
301	// Stream::filter requires FnMut returning a nameable future; an async
302	// closure capturing self does not satisfy it.
303	#[expect(closure_returning_async_block)]
304	#[tracing::instrument(skip(self, serializer), level = "debug")]
305	pub async fn send_edu_room_appservices<'a, F>(
306		&self,
307		room_id: &RoomId,
308		serializer: F,
309	) -> Result
310	where
311		F: Fn(&mut dyn Write) -> Result + Send + 'a,
312		&'a F: Send + Sync,
313	{
314		self.services
315			.appservice
316			.read()
317			.await
318			.values()
319			.stream()
320			.filter(|&appservice| async move {
321				if !appservice.registration.receive_ephemeral {
322					return false;
323				}
324
325				if appservice.rooms.is_match(room_id.as_str()) {
326					return true;
327				}
328
329				let appservice_in_room = self
330					.services
331					.state_cache
332					.appservice_in_room(room_id, appservice);
333
334				let matching_aliases = self
335					.services
336					.alias
337					.local_aliases_for_room(room_id)
338					.ready_any(|room_alias| appservice.aliases.is_match(room_alias.as_str()));
339
340				pin!(appservice_in_room)
341					.or(pin!(matching_aliases))
342					.await
343			})
344			.map(Ok)
345			.ready_try_for_each(|appservice| {
346				let mut buf = EduBuf::new();
347
348				serializer(&mut buf)?;
349				self.send_edu_appservice(appservice.registration.id.clone(), buf)
350					.log_err()
351					.ok();
352
353				Ok(())
354			})
355			.await
356	}
357
358	/// Queue stored to-device events for delivery to interested appservices
359	/// (MSC4203). `deliveries` are the concrete recipient devices already
360	/// written to the inbox (post-`AllDevices` expansion) paired with their
361	/// inbox counts, which uniquify the transaction hash.
362	#[tracing::instrument(
363		skip(self, deliveries, content),
364		level = "debug",
365		fields(
366			%target_user,
367		),
368	)]
369	pub async fn send_to_device_appservices<'a, I>(
370		&self,
371		sender: &UserId,
372		target_user: &UserId,
373		deliveries: I,
374		event_type: &str,
375		content: &serde_json::Value,
376	) -> Result
377	where
378		I: Iterator<Item = (&'a DeviceId, u64)> + Clone + Send,
379	{
380		let registrations = self.services.appservice.read().await;
381		let _cork = self.db.db.cork();
382
383		let mut payloads: Option<EduVec> = None;
384		for info in registrations.values() {
385			if !info.is_user_match(target_user) {
386				continue;
387			}
388
389			let payloads = payloads.get_or_insert_with(|| {
390				to_device_payloads(sender, target_user, deliveries.clone(), event_type, content)
391			});
392
393			for buf in &*payloads {
394				let dest = Destination::Appservice(info.registration.id.clone());
395				let event = SendingEvent::ToDevice(buf.clone());
396
397				self.queue_and_dispatch(dest, event)?;
398			}
399		}
400
401		Ok(())
402	}
403
404	/// Queue a `device_lists.changed` marker (MSC3202) for delivery to
405	/// appservices that opted into transaction extensions and are interested
406	/// in `user_id`. Called from `mark_device_key_update`, reusing the count it
407	/// already allocated so the marker uniquifies the transaction hash.
408	#[tracing::instrument(
409		skip(self),
410		level = "debug",
411		fields(
412			%user_id,
413		),
414	)]
415	pub async fn send_device_list_appservices(&self, user_id: &UserId, count: u64) -> Result {
416		let registrations = self.services.appservice.read().await;
417
418		// Hot path: no bridge opted into transaction extensions.
419		if !registrations
420			.values()
421			.any(|info| info.registration.msc3202_transaction_extensions)
422		{
423			return Ok(());
424		}
425
426		let _cork = self.db.db.cork();
427
428		let mut payload = None;
429		for info in registrations.values() {
430			if !info.registration.msc3202_transaction_extensions {
431				continue;
432			}
433
434			if !info.is_user_match(user_id) && !self.shares_device_list_room(user_id, info).await
435			{
436				continue;
437			}
438
439			let payload = payload.get_or_insert_with(|| device_list_payload(user_id, count));
440
441			let dest = Destination::Appservice(info.registration.id.clone());
442			let event = SendingEvent::DeviceListChanged(payload.clone());
443
444			self.queue_and_dispatch(dest, event)?;
445		}
446
447		Ok(())
448	}
449
450	/// Whether `user_id` shares a device-list-interesting room with `info`: a
451	/// joined room the appservice participates in that is encrypted, or any
452	/// such room when `device_key_update_encrypted_rooms_only` is off.
453	async fn shares_device_list_room(&self, user_id: &UserId, info: &RegistrationInfo) -> bool {
454		let update_all_rooms = !self
455			.services
456			.config
457			.device_key_update_encrypted_rooms_only;
458
459		self.services
460			.state_cache
461			.rooms_joined(user_id)
462			.map(ToOwned::to_owned)
463			.any(async |room_id: OwnedRoomId| {
464				(update_all_rooms
465					|| self
466						.services
467						.state_accessor
468						.is_encrypted_room(&room_id)
469						.await) && self
470					.services
471					.state_cache
472					.appservice_in_room(&room_id, info)
473					.await
474			})
475			.await
476	}
477
478	#[tracing::instrument(skip(self, servers, serialized), level = "debug")]
479	pub async fn send_edu_servers<'a, S>(&self, servers: S, serialized: EduBuf) -> Result
480	where
481		S: Stream<Item = &'a ServerName> + Send + 'a,
482	{
483		let requests = servers
484			.map(|server| {
485				(
486					Destination::Federation(server.to_owned()),
487					SendingEvent::Edu(serialized.clone()),
488				)
489			})
490			.collect::<Vec<_>>()
491			.await;
492
493		let _cork = self.db.db.cork();
494		let keys = self
495			.db
496			.queue_requests(requests.iter().map(|(o, e)| (e, o)));
497
498		for ((dest, event), queue_id) in requests.into_iter().zip(keys) {
499			self.dispatch(Msg { dest, event, queue_id })?;
500		}
501
502		Ok(())
503	}
504
505	#[tracing::instrument(skip(self, room_id), level = "debug")]
506	pub async fn flush_room(&self, room_id: &RoomId) -> Result {
507		let servers = self
508			.services
509			.state_cache
510			.room_servers(room_id)
511			.ready_filter(|server_name| !self.services.globals.server_is_ours(server_name));
512
513		self.flush_servers(servers).await
514	}
515
516	#[tracing::instrument(skip(self, servers), level = "debug")]
517	pub async fn flush_servers<'a, S>(&self, servers: S) -> Result
518	where
519		S: Stream<Item = &'a ServerName> + Send + 'a,
520	{
521		servers
522			.map(ToOwned::to_owned)
523			.map(Destination::Federation)
524			.map(Ok)
525			.ready_try_for_each(|dest| {
526				self.dispatch(Msg {
527					dest,
528					event: SendingEvent::Flush,
529					queue_id: Vec::<u8>::new(),
530				})
531			})
532			.await
533	}
534
535	#[tracing::instrument(skip(self), level = "debug")]
536	pub fn flush_appservice(&self, appservice_id: String) -> Result {
537		self.dispatch(Msg {
538			dest: Destination::Appservice(appservice_id),
539			event: SendingEvent::Flush,
540			queue_id: Vec::<u8>::new(),
541		})
542	}
543
544	/// Flushes the sender for a federation peer that has proven reachable via
545	/// inbound activity or an operator reset, but only when it was actually in
546	/// its failure bucket; reports whether it was.
547	#[tracing::instrument(
548		level = "debug",
549		skip(self),
550		fields(
551			%server,
552		),
553	)]
554	pub async fn notify_peer_alive(&self, server: &ServerName) -> bool {
555		let sad = self
556			.services
557			.federation
558			.note_peer_alive(server)
559			.await;
560
561		if sad {
562			self.dispatch(Msg {
563				dest: Destination::Federation(server.to_owned()),
564				event: SendingEvent::Flush,
565				queue_id: Vec::<u8>::new(),
566			})
567			.log_err()
568			.ok();
569		}
570
571		sad
572	}
573
574	/// Clean up queued sending event data
575	///
576	/// Used after we remove an appservice registration or a user deletes a push
577	/// key
578	#[tracing::instrument(skip(self), level = "debug")]
579	pub async fn cleanup_events(
580		&self,
581		appservice_id: Option<&str>,
582		user_id: Option<&UserId>,
583		push_key: Option<&str>,
584	) -> Result {
585		match (appservice_id, user_id, push_key) {
586			| (None, Some(user_id), Some(push_key)) =>
587				self.db
588					.delete_all_requests_for(&Destination::Push(
589						user_id.to_owned(),
590						push_key.to_owned(),
591					))
592					.await,
593			| (Some(appservice_id), None, None) =>
594				self.db
595					.delete_all_requests_for(&Destination::Appservice(appservice_id.to_owned()))
596					.await,
597			| _ => debug_warn!("cleanup_events called with too many or too few arguments"),
598		}
599
600		Ok(())
601	}
602
603	fn dispatch(&self, msg: Msg) -> Result {
604		let shard = self.shard_id(&msg.dest);
605		let sender = &self
606			.channels
607			.get(shard)
608			.expect("missing sender worker channels")
609			.0;
610
611		debug_assert!(!sender.is_full(), "channel full");
612		debug_assert!(!sender.is_closed(), "channel closed");
613		sender.send(msg).map_err(|e| err!("{e}"))
614	}
615
616	pub(super) fn shard_id(&self, dest: &Destination) -> usize {
617		if self.channels.len() <= 1 {
618			return 0;
619		}
620
621		let mut hash = DefaultHasher::default();
622		dest.hash(&mut hash);
623
624		let hash: u64 = hash.finish();
625		let hash = usize_from_u64_truncated(hash);
626
627		let chans = self.channels.len().max(1);
628		hash.overflowing_rem(chans).0
629	}
630}
631
632fn to_device_payloads<'a, I>(
633	sender: &UserId,
634	target_user: &UserId,
635	deliveries: I,
636	event_type: &str,
637	content: &serde_json::Value,
638) -> EduVec
639where
640	I: Iterator<Item = (&'a DeviceId, u64)>,
641{
642	deliveries
643		.map(|(to_device_id, count)| {
644			let mut buf = EduBuf::new();
645			buf.push(TAG_TO_DEVICE);
646			buf.extend_from_slice(&count.to_be_bytes());
647
648			let event = AsToDeviceEvent {
649				kind: event_type,
650				sender,
651				content,
652				to_user_id: target_user,
653				to_device_id,
654			};
655
656			serde_json::to_writer(&mut buf, &event)
657				.expect("to-device appservice event serializes");
658
659			buf
660		})
661		.collect()
662}
663
664fn device_list_payload(user_id: &UserId, count: u64) -> EduBuf {
665	let mut buf = EduBuf::new();
666	buf.push(TAG_DEVICE_LIST_CHANGED);
667	buf.extend_from_slice(&count.to_be_bytes());
668	buf.extend_from_slice(user_id.as_bytes());
669
670	buf
671}
672
673fn num_senders(args: &crate::Args<'_>) -> usize {
674	const MIN_SENDERS: usize = 1;
675	// Limit the number of senders to the number of workers threads or number of
676	// cores, conservatively.
677	let max_senders = args
678		.server
679		.metrics
680		.num_workers()
681		.min(available_parallelism());
682
683	// If the user doesn't override the default 0, this is intended to then default
684	// to 1 for now as multiple senders is experimental.
685	args.server
686		.config
687		.sender_workers
688		.clamp(MIN_SENDERS, max_senders)
689}
690
691fn reap_flushes(flushes: &mut JoinSet<()>) {
692	while let Some(result) = flushes.try_join_next() {
693		log_flush(result);
694	}
695}
696
697// A flush that panicked is reported here or nowhere; a cancelled one is the
698// shutdown path.
699fn log_flush(result: Result<(), JoinError>) {
700	if let Err(error) = result
701		&& error.is_panic()
702	{
703		error!(?error, "Suppressed push flush panicked");
704	}
705}