Skip to main content

tuwunel_service/sending/sender/dispatch/push/
mod.rs

1mod suppressed;
2
3use futures::{FutureExt, TryFutureExt, future::try_join3};
4use ruma::{OwnedUserId, api::error::ErrorKind};
5use tuwunel_core::{
6	Error, Event, debug, error,
7	error::error_chain,
8	extract_variant, implement,
9	smallvec::SmallVec,
10	utils::{BoolExt, IterStream, ReadyExt, stream::WidebandExt},
11	warn,
12};
13
14use super::SendingResult;
15use crate::{
16	rooms::timeline::RawPduId,
17	sending::{Destination, SendingEvent, Service},
18};
19
20type FailedIds = SmallVec<[RawPduId; 1]>;
21
22// Notices in flight to one push gateway at once.
23pub(super) const PUSH_WIDTH: usize = 4;
24
25/// The PDUs a push transaction failed to deliver, with the first error.
26///
27/// Permanent errors are dropped before they reach here; the retained IDs keep
28/// their active rows for the retry.
29#[derive(Default)]
30struct Failures {
31	ids: FailedIds,
32	error: Option<Error>,
33}
34
35#[implement(Service)]
36#[tracing::instrument(
37	name = "push",
38	level = "info",
39	skip(self, events),
40	fields(
41		events = events.len(),
42	),
43)]
44pub(super) async fn send_events_dest_push(
45	&self,
46	user_id: OwnedUserId,
47	pushkey: String,
48	events: Vec<SendingEvent>,
49) -> SendingResult {
50	let has_pdu = events
51		.iter()
52		.any(|event| matches!(event, SendingEvent::Pdu(_)));
53
54	let destination = || Destination::Push(user_id.clone(), pushkey.clone());
55	let suppressed = self.pushing_suppressed(&user_id).map(Ok);
56	let pusher = self
57		.services
58		.pusher
59		.get_pusher(&user_id, &pushkey)
60		.map(|result| match result {
61			| Ok(pusher) => Ok(Some(pusher)),
62			| Err(error) if error.is_not_found() => {
63				error!(%user_id, %pushkey, "Pusher disappeared before delivery");
64
65				Ok(None)
66			},
67			| Err(error) => Err((destination(), error)),
68		});
69
70	let ruleset = has_pdu
71		.then_async(|| self.services.pusher.ruleset(&user_id))
72		.map(Ok);
73
74	let (pusher, ruleset, suppressed) = try_join3(pusher, ruleset, suppressed).await?;
75
76	let Some(pusher) = pusher else {
77		return Ok(Destination::Push(user_id, pushkey));
78	};
79
80	// Reconciliation, not an alert: a suppressed drop strands a stale badge.
81	if events.contains(&SendingEvent::BadgeRefresh) {
82		let result = self
83			.services
84			.pusher
85			.send_badge_notice(&user_id, &pusher)
86			.await;
87
88		match result {
89			| Ok(()) => (),
90			| Err(error) if is_permanent_error(&error) => warn!(
91				%user_id,
92				%pushkey,
93				chain = %error_chain(&error),
94				"Dropping a badge push with a permanent local error",
95			),
96			| Err(error) => return Err((destination(), error)),
97		}
98	}
99
100	if suppressed {
101		let queued = self
102			.enqueue_suppressed_push_events(&user_id, &pushkey, &events)
103			.await;
104
105		debug!(
106			%user_id,
107			%pushkey,
108			queued,
109			events = events.len(),
110			"Push suppressed; queued events"
111		);
112
113		return Ok(Destination::Push(user_id, pushkey));
114	}
115
116	self.schedule_flush_suppressed_for_pushkey(
117		user_id.clone(),
118		pushkey.clone(),
119		"non-suppressed push",
120	);
121
122	let Some(ruleset) = ruleset else {
123		return Ok(Destination::Push(user_id, pushkey));
124	};
125
126	let pdu_ids = || {
127		events
128			.iter()
129			.filter_map(|event| extract_variant!(event, SendingEvent::Pdu))
130	};
131
132	let failures = pdu_ids()
133		.stream()
134		.wide_filter_map(async |pdu_id| {
135			self.services
136				.timeline
137				.get_pdu_from_id(pdu_id)
138				.map_ok(|pdu| (*pdu_id, pdu))
139				.await
140				.ok()
141		})
142		.ready_filter(|(_, pdu)| !pdu.is_redacted())
143		.widen_then(Some(PUSH_WIDTH), async |(pdu_id, pdu)| {
144			let result = self
145				.services
146				.pusher
147				.send_push_notice(&user_id, &pusher, &ruleset, &pdu)
148				.await;
149
150			(pdu_id, result)
151		})
152		.ready_fold(Failures::default(), |failures, (pdu_id, result)| match result {
153			| Ok(()) => failures,
154			| Err(error) if is_permanent_error(&error) => {
155				warn!(
156					%user_id,
157					%pushkey,
158					?pdu_id,
159					chain = %error_chain(&error),
160					"Dropping a push with a permanent local error",
161				);
162
163				failures
164			},
165			| Err(error) => failures.retain(pdu_id, error),
166		})
167		.await;
168
169	let Failures { ids, error: Some(error) } = failures else {
170		return Ok(Destination::Push(user_id, pushkey));
171	};
172
173	let destination = Destination::Push(user_id, pushkey);
174
175	pdu_ids()
176		.filter(|pdu_id| !ids.contains(*pdu_id))
177		.for_each(|pdu_id| {
178			self.db
179				.delete_active_request(&destination.event_key(pdu_id));
180		});
181
182	Err((destination, error))
183}
184
185impl Failures {
186	fn retain(mut self, pdu_id: RawPduId, error: Error) -> Self {
187		self.ids.push(pdu_id);
188		self.error = self.error.or(Some(error));
189
190		self
191	}
192}
193
194#[inline]
195fn is_permanent_error(error: &Error) -> bool {
196	matches!(error, Error::Request(ErrorKind::InvalidParam, ..))
197}