Skip to main content

tuwunel_service/sending/sender/
mod.rs

1mod dispatch;
2mod netburst;
3mod response;
4mod select;
5mod split;
6#[cfg(test)]
7mod tests;
8mod wake;
9
10use std::{
11	cmp::Reverse,
12	collections::{BinaryHeap, HashMap},
13	sync::Arc,
14	time::{Duration, Instant},
15};
16
17use futures::{FutureExt, StreamExt, stream::FuturesUnordered};
18use tokio::{
19	select,
20	time::{Instant as TokioInstant, sleep_until},
21};
22use tuwunel_core::{
23	Result, implement,
24	smallvec::{SmallVec, smallvec},
25	trace,
26	utils::BoolExt,
27};
28
29use self::{
30	dispatch::{Completion, SendingFuture},
31	select::Selection,
32	split::Split,
33	wake::{arm_park_wake, arm_wake, is_armed},
34};
35use super::{Destination, Msg, SendingEvent, Service, data::QueueItem};
36
37/// In-flight bookkeeping for one `Destination`.
38///
39/// Federation uses the peer gate when refused and the sender curve otherwise.
40/// Appservice and push retain their own destination status across retries.
41#[derive(Debug)]
42enum TransactionStatus {
43	/// A durable active generation awaiting its first dispatch after restart.
44	Pending,
45
46	/// A transaction is in flight after this many consecutive failures.
47	Running {
48		tries: u32,
49	},
50
51	/// As `Running`, with a forced retry requested while it runs.
52	RunningForceRetry {
53		tries: u32,
54	},
55
56	/// Push backoff: consecutive failures and the time of the last one.
57	Failed {
58		tries: u32,
59		last: Instant,
60	},
61
62	/// Retry state after this many consecutive failures.
63	///
64	/// For push the retry is already in flight; for other destinations the
65	/// batch waits for its replay.
66	Retrying {
67		tries: u32,
68	},
69
70	/// As `Retrying`, while a rejected transaction's rooms are sent apart.
71	Splitting {
72		tries: u32,
73		split: Split,
74	},
75}
76
77#[derive(Clone, Copy)]
78enum RetryAction {
79	None,
80	Force,
81}
82
83type SendingFutures<'a> = FuturesUnordered<SendingFuture<'a>>;
84type TransactionStatuses = HashMap<Destination, TransactionStatus>;
85
86/// The queue items one request brings to a selection.
87///
88/// A request carries one item; a badge wake dequeues up to `DEQUEUE_LIMIT`.
89type NewEvents = SmallVec<[QueueItem; 1]>;
90
91/// Per-worker retry timer keyed by earliest-retry deadline and destination.
92///
93/// Every federation, appservice, or push failure arms an entry. Traffic-triggered retries
94/// can leave stale entries, which skip an in-flight transaction or replay a
95/// waiting one. Multiple entries may therefore remain for one destination.
96type WakeQueue = BinaryHeap<Reverse<(TokioInstant, Destination)>>;
97
98const DEQUEUE_LIMIT: usize = 48;
99
100/// Most PDUs one federation transaction may carry.
101///
102/// The spec caps a `/send` body at this many PDUs; inbound bodies past it are
103/// rejected and outbound composition stays under it.
104pub const PDU_LIMIT: usize = 50;
105
106/// Most EDUs one federation transaction may carry.
107///
108/// The spec caps a `/send` body at this many EDUs; inbound bodies past it are
109/// rejected and outbound composition stays under it.
110pub const EDU_LIMIT: usize = 100;
111
112#[implement(Service)]
113#[tracing::instrument(skip(self), level = "debug")]
114pub(super) async fn sender(self: Arc<Self>, id: usize) -> Result {
115	// The worker's state, threaded as &mut through every phase.
116	let mut statuses = TransactionStatuses::new();
117	let mut futures = SendingFutures::new();
118	let mut wakes = WakeQueue::new();
119
120	self.startup_netburst(id, &mut futures, &mut statuses, &mut wakes)
121		.boxed() // size firewall
122		.await;
123
124	self.work_loop(id, &mut futures, &mut statuses, &mut wakes)
125		.await;
126
127	if !futures.is_empty() {
128		self.finish_responses(&mut futures)
129			.boxed() // size firewall
130			.await;
131	}
132
133	Ok(())
134}
135
136#[implement(Service)]
137#[tracing::instrument(
138	name = "work",
139	level = "trace",
140	skip_all,
141	fields(
142		futures = %futures.len(),
143		statuses = %statuses.len(),
144	),
145)]
146async fn work_loop<'a>(
147	&'a self,
148	id: usize,
149	futures: &mut SendingFutures<'a>,
150	statuses: &mut TransactionStatuses,
151	wakes: &mut WakeQueue,
152) {
153	let receiver = &self
154		.channels
155		.get(id)
156		.expect("Missing channel for sender worker")
157		.1;
158
159	while !receiver.is_closed() {
160		let next_due = wakes
161			.peek()
162			.map_or_else(TokioInstant::now, |Reverse((instant, _))| *instant);
163
164		select! {
165			Some(response) = futures.next() => {
166				self.handle_response(response, futures, statuses, wakes).await;
167			},
168			request = receiver.recv_async() => match request {
169				Ok(request) => self.handle_request(request, futures, statuses, wakes).await,
170				Err(_) => return,
171			},
172			() = sleep_until(next_due), if !wakes.is_empty() => {
173				self.drain_due_wakes(futures, statuses, wakes).await;
174			},
175		}
176	}
177}
178
179#[implement(Service)]
180#[tracing::instrument(name = "request", level = "debug", skip_all)]
181async fn handle_request<'a>(
182	&'a self,
183	msg: Msg,
184	futures: &mut SendingFutures<'a>,
185	statuses: &mut TransactionStatuses,
186	wakes: &mut WakeQueue,
187) {
188	let synthetic_badge =
189		msg.queue_id.is_empty() && matches!(&msg.event, SendingEvent::BadgeRefresh);
190
191	let new_events = match (synthetic_badge, statuses.contains_key(&msg.dest)) {
192		| (false, _) => smallvec![(msg.queue_id, msg.event)],
193		| (true, true) => NewEvents::new(),
194		| (true, false) =>
195			self.db
196				.queued_requests(&msg.dest)
197				.take(DEQUEUE_LIMIT)
198				.collect()
199				.await,
200	};
201
202	if let Ok(selection) = self
203		.select_events(&msg.dest, new_events, statuses)
204		.await
205	{
206		self.schedule_events(msg.dest, selection, futures, statuses, wakes);
207	}
208}
209
210#[implement(Service)]
211#[expect(
212	clippy::needless_pass_by_ref_mut,
213	reason = "mutable reference avoids requiring SendingFutures to be Sync"
214)]
215fn schedule_events<'a>(
216	&'a self,
217	dest: Destination,
218	selection: Selection,
219	futures: &mut SendingFutures<'a>,
220	statuses: &mut TransactionStatuses,
221	wakes: &mut WakeQueue,
222) {
223	match selection {
224		| Selection::Events(items) if items.is_empty() => {
225			statuses.remove(&dest);
226		},
227		| Selection::Events(items) => futures.push(self.send_events(dest, items, None)),
228		| Selection::Slice(items, split) =>
229			futures.push(self.send_events(dest, items, Some(split))),
230		| Selection::Parked { until } => {
231			statuses.remove(&dest);
232			arm_park_wake(wakes, dest, until);
233		},
234		| Selection::Refused { earliest_retry } if is_armed(wakes, &dest).is_false() =>
235			arm_wake(wakes, dest, earliest_retry),
236		| Selection::Refused { .. } | Selection::Busy => {},
237	}
238}
239
240#[implement(Service)]
241#[tracing::instrument(
242	name = "finish",
243	level = "info",
244	skip_all,
245	fields(
246		futures = %futures.len(),
247	),
248)]
249async fn finish_responses<'a>(&'a self, futures: &mut SendingFutures<'a>) {
250	let timeout = Duration::from_secs(self.server.config.sender_shutdown_timeout);
251	let now = TokioInstant::now();
252	let deadline = now.checked_add(timeout).unwrap_or(now);
253
254	loop {
255		trace!(remaining = futures.len(), "Waiting for requests to complete");
256		select! {
257			() = sleep_until(deadline) => return,
258			response = futures.next() => match response {
259				Some(Completion { result: Ok(_), keys, .. }) =>
260					self.db.delete_active_requests(&keys),
261				Some(_) => {},
262				None => return,
263			},
264		}
265	}
266}