Skip to main content

tuwunel_router/
run.rs

1#[cfg(all(feature = "systemd", target_os = "linux"))]
2use std::env::var_os;
3use std::{
4	sync::{Arc, Weak, atomic::Ordering},
5	time::Duration,
6};
7
8use futures::{FutureExt, future::join, pin_mut};
9#[cfg(all(feature = "systemd", target_os = "linux"))]
10use sd_notify::{NotifyState, notify, notify_and_unset_env, watchdog_enabled};
11use tokio::time::{MissedTickBehavior, interval};
12use tuwunel_core::{
13	Error, Result, Server, debug, debug_error, debug_info, defer, error, info, utils::BoolExt,
14	warn,
15};
16use tuwunel_service::Services;
17
18use crate::{handle::ServerHandle, serve};
19
20/// How often startup reports its progress and extends the service manager's
21/// timeout.
22///
23/// The extension asks for twice this, so one missed tick does not end the
24/// grace the service manager is holding open.
25const STARTUP_INTERVAL: Duration = Duration::from_secs(15);
26
27/// Main loop base
28#[tracing::instrument(skip_all)]
29pub(crate) async fn run(services: Arc<Services>) -> Result {
30	let server = &services.server;
31	debug!("Start");
32
33	// Install the admin command root here for now
34	tuwunel_admin::init(&services.admin);
35
36	// Execute configured startup commands.
37	services.admin.startup_execute().await?;
38
39	// Setup shutdown/signal handling
40	let handle = ServerHandle::new();
41	let sigs = server
42		.runtime()
43		.spawn(signal(server.clone(), handle.clone()));
44	#[cfg(all(feature = "systemd", target_os = "linux"))]
45	let watchdog = server.runtime().spawn(start_systemd_watchdog());
46
47	let non_listener = services
48		.config
49		.listening
50		.is_false()
51		.then_async(|| server.until_shutdown().map(Ok));
52
53	let listener = services.config.listening.then_async(|| {
54		server
55			.runtime()
56			.spawn(serve::serve(services.clone(), handle))
57			.map(|res| res.map_err(Error::from).unwrap_or_else(Err))
58	});
59
60	// Focal point
61	debug!("Running");
62	pin_mut!(listener, non_listener);
63	let res = tokio::select! {
64		res = join(&mut listener, &mut non_listener) => {
65			res.0.unwrap_or(res.1.unwrap_or(Ok(())))
66		},
67		res = services.poll() => {
68			server.until_shutdown().await;
69			handle_services_finish(server, res, listener.await)
70		},
71	};
72
73	// Join watchdog and the signal handler before we leave.
74	#[cfg(all(feature = "systemd", target_os = "linux"))]
75	{
76		watchdog.abort();
77		_ = watchdog.await;
78	};
79
80	sigs.abort();
81	_ = sigs.await;
82
83	// Remove the admin command root
84	tuwunel_admin::fini(&services.admin);
85
86	debug_info!("Finish");
87	res
88}
89
90/// Async initializations
91#[tracing::instrument(skip_all)]
92pub(crate) async fn start(server: Arc<Server>) -> Result<Arc<Services>> {
93	debug!("Starting...");
94
95	// The ticker holds the stop timeout open too, so ending it any earlier lets a
96	// stop request kill a migration mid-write.
97	let reporter = server
98		.runtime()
99		.spawn(report_startup_progress(server.clone()));
100
101	let abort = reporter.abort_handle();
102
103	defer! {{
104		abort.abort();
105	}}
106
107	let services = async move { Services::build(server).await?.start().await }.await;
108
109	reporter.abort();
110	_ = reporter.await;
111
112	let services = services?;
113
114	// The status is set here so it reads as a baseline rather than staying blank
115	// until the first reload replaces it.
116	#[cfg(all(feature = "systemd", target_os = "linux"))]
117	notify(&[NotifyState::Ready, NotifyState::Status("Running")])
118		.expect("failed to notify systemd of ready state");
119
120	debug!("Started");
121	Ok(services)
122}
123
124/// Async destructions
125#[tracing::instrument(skip_all)]
126pub(crate) async fn stop(services: Arc<Services>) -> Result {
127	debug!("Shutting down...");
128
129	#[cfg(all(feature = "systemd", target_os = "linux"))]
130	notify_systemd_shutdown(&services.server);
131
132	// Wait for all completions before dropping or we'll lose them to the module
133	// unload and explode.
134	services.stop().await;
135
136	// Check that Services and Database will drop as expected, The complex of Arc's
137	// used for various components can easily lead to references being held
138	// somewhere improperly; this can hang shutdowns.
139	debug!("Cleaning up...");
140	let db = Arc::downgrade(&services.db);
141	if let Err(services) = Arc::try_unwrap(services) {
142		debug_error!(
143			"{} dangling references to Services after shutdown",
144			Arc::strong_count(&services)
145		);
146	}
147
148	if Weak::strong_count(&db) > 0 {
149		debug_error!(
150			"{} dangling references to Database after shutdown",
151			Weak::strong_count(&db)
152		);
153	}
154
155	info!("Shutdown complete.");
156	Ok(())
157}
158
159#[cfg(all(feature = "systemd", target_os = "linux"))]
160fn notify_systemd_shutdown(server: &Server) {
161	// An in-place exec restart keeps this PID; report a reload, not an exit, so
162	// the unit stays active and NOTIFY_SOCKET survives for the next image. The
163	// watchdog stays armed while reloading, so reset it to give teardown and
164	// exec the full interval.
165	if server.is_restarting() {
166		let monotonic = NotifyState::monotonic_usec_now().expect("failed to get monotonic time");
167
168		notify(&[NotifyState::Reloading, monotonic, NotifyState::Watchdog])
169			.expect("failed to notify systemd of reloading state");
170
171		return;
172	}
173
174	// SAFETY: clears NOTIFY_SOCKET from the process environment. Safe because no
175	// other thread reads or writes that variable; this matches the previous
176	// `notify(unset_env=true, ...)` semantics from sd-notify 0.4.
177	unsafe { notify_and_unset_env(&[NotifyState::Stopping]) }
178		.expect("failed to notify systemd of stopping state");
179}
180
181#[tracing::instrument(skip_all)]
182async fn signal(server: Arc<Server>, handle: ServerHandle) {
183	server.until_shutdown().await;
184	handle_shutdown(&server, &handle);
185}
186
187fn handle_shutdown(server: &Arc<Server>, handle: &ServerHandle) {
188	let timeout = server.config.client_shutdown_timeout;
189	let timeout = Duration::from_secs(timeout);
190	debug!(
191		?timeout,
192		handle_active = ?server.metrics.requests_handle_active.load(Ordering::Relaxed),
193		"Notifying for graceful shutdown"
194	);
195
196	handle.graceful_shutdown(Some(timeout));
197}
198
199fn handle_services_finish(
200	server: &Arc<Server>,
201	result: Result,
202	listener: Option<Result>,
203) -> Result {
204	debug!("Service manager finished: {result:?}");
205
206	if server.is_running()
207		&& let Err(e) = server.shutdown()
208	{
209		error!("Failed to send shutdown signal: {e}");
210	}
211
212	if let Some(Err(e)) = listener {
213		error!("Client listener task finished with error: {e}");
214	}
215
216	result
217}
218
219#[cfg(all(feature = "systemd", target_os = "linux"))]
220#[expect(clippy::infinite_loop)]
221async fn start_systemd_watchdog() {
222	let Some(watchdog) = watchdog_enabled() else {
223		return;
224	};
225
226	let watchdog_usec = u64::try_from(watchdog.as_micros()).unwrap_or(u64::MAX);
227	let interval_usec = (watchdog_usec / 2).max(1);
228	let period = Duration::from_micros(interval_usec);
229
230	let mut ticker = interval(period);
231	ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
232	loop {
233		ticker.tick().await;
234
235		notify_systemd(&[NotifyState::Watchdog], "watchdog");
236	}
237}
238
239/// Reports the long startup phase in flight and keeps the service manager
240/// waiting for it.
241///
242/// A database migration can run for many minutes with nothing else to show for
243/// it, so every tick logs the phase, its position and how long it has been
244/// running. The same tick extends systemd's timeout, which covers the stop
245/// timeout as well as the start timeout, so ending this task early lets a stop
246/// request kill a migration mid-write.
247#[expect(clippy::infinite_loop)]
248async fn report_startup_progress(server: Arc<Server>) {
249	#[cfg(all(feature = "systemd", target_os = "linux"))]
250	let notifiable = var_os("NOTIFY_SOCKET").is_some();
251
252	let mut ticker = interval(STARTUP_INTERVAL);
253	let mut announced = false;
254
255	ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
256	loop {
257		ticker.tick().await;
258
259		let progress = server.progress.report();
260
261		#[cfg(all(feature = "systemd", target_os = "linux"))]
262		if notifiable {
263			notify_systemd_startup(progress.as_deref());
264		}
265
266		let Some(progress) = progress else {
267			continue;
268		};
269
270		if announced {
271			info!(%progress, "Database migration in progress");
272			continue;
273		}
274
275		announced = true;
276		warn!(
277			%progress,
278			"Database migration in progress. A large database can take many minutes. A stop \
279			 request is honored between steps and every step that finished is recorded, so the \
280			 migration resumes where it left off; killing the process instead can leave the \
281			 database mid-write."
282		);
283	}
284}
285
286/// Extends the service manager's timeout by another interval and reports what
287/// the server is waiting on.
288///
289/// The extension applies to whichever timeout is armed, so it holds a stop
290/// request off a migration in flight as much as it holds off the start
291/// timeout. The caller establishes that a service manager is listening.
292#[cfg(all(feature = "systemd", target_os = "linux"))]
293fn notify_systemd_startup(status: Option<&str>) {
294	let extend_usec = u32::try_from(STARTUP_INTERVAL.as_micros())
295		.unwrap_or(u32::MAX)
296		.saturating_mul(2);
297
298	notify_systemd(&[NotifyState::ExtendTimeoutUsec(extend_usec)], "startup timeout extension");
299
300	let Some(status) = status else {
301		return;
302	};
303
304	notify_systemd(&[NotifyState::Status(status)], "startup status");
305}
306
307/// Sends notification states to the service manager.
308///
309/// A notification is advisory, so a failure is logged against the name of what
310/// could not be sent and never reaches the caller.
311#[cfg(all(feature = "systemd", target_os = "linux"))]
312fn notify_systemd(states: &[NotifyState<'_>], about: &'static str) {
313	if let Err(e) = notify(states) {
314		error!(%e, %about, "failed to notify systemd");
315	}
316}