tuwunel_core/server.rs
1//! Tracks server lifecycle state and its runtime handle.
2//!
3//! The server coordinates reload, restart, and shutdown notifications. Shared
4//! services use its state to stop work promptly during teardown.
5
6#[cfg(test)]
7mod tests;
8
9use std::{
10 sync::{
11 Arc,
12 atomic::{AtomicBool, Ordering},
13 },
14 time::SystemTime,
15};
16
17use ruma::OwnedServerName;
18use tokio::{runtime, sync::broadcast};
19
20use crate::{Err, Result, config, config::Config, log::Logging, metrics::Metrics};
21
22/// Server runtime state; public portion
23pub struct Server {
24 /// Configured name of server. This is the same as the one in the config
25 /// but developers can (and should) reference this string instead.
26 pub name: OwnedServerName,
27
28 /// Server-wide configuration instance
29 pub config: config::Manager,
30
31 /// Where the configuration came from, replayed on reload.
32 pub config_sources: config::Sources,
33
34 /// Timestamp server was started; used for uptime.
35 pub started: SystemTime,
36
37 /// Reload/shutdown pending indicator; server is shutting down. This is an
38 /// observable used on shutdown and should not be modified.
39 pub stopping: AtomicBool,
40
41 /// Reload/shutdown desired indicator; when false, shutdown is desired. This
42 /// is an observable used on shutdown and modifying is not recommended.
43 pub reloading: AtomicBool,
44
45 /// Restart desired; when true, restart it desired after shutdown.
46 pub restarting: AtomicBool,
47
48 /// Set when a backup restore is claimed, which is before it runs and is not
49 /// undone if it fails, so a database reopened later in the same process
50 /// does not restore a second time. Clearing this re-arms a destructive
51 /// operation and is never correct; claim it instead.
52 pub backup_restored: AtomicBool,
53
54 /// Handle to the runtime
55 pub runtime: Option<runtime::Handle>,
56
57 /// Reload/shutdown signal
58 pub signal: broadcast::Sender<&'static str>,
59
60 /// Logging subsystem state
61 pub log: Logging,
62
63 /// Metrics subsystem state
64 pub metrics: Arc<Metrics>,
65}
66
67impl Server {
68 #[must_use]
69 /// Creates shared server lifecycle state.
70 ///
71 /// The initial configuration, source list, logging state, and metrics
72 /// become available to all services. A supplied runtime handle enables
73 /// task spawning.
74 pub fn new(
75 config: Config,
76 config_sources: config::Sources,
77 runtime: Option<&runtime::Handle>,
78 log: Logging,
79 metrics: Arc<Metrics>,
80 ) -> Self {
81 Self {
82 name: config.server_name.clone(),
83 config: config::Manager::new(config),
84 config_sources,
85 started: SystemTime::now(),
86 stopping: AtomicBool::new(false),
87 reloading: AtomicBool::new(false),
88 restarting: AtomicBool::new(false),
89 backup_restored: AtomicBool::new(false),
90 runtime: runtime.cloned(),
91 signal: broadcast::channel::<&'static str>(1).0,
92 log,
93 metrics,
94 }
95 }
96
97 /// Requests a dynamic module reload.
98 ///
99 /// The request marks the server as reloading and stopping before
100 /// broadcasting `SIGINT`. Concurrent reload or shutdown requests are
101 /// rejected.
102 pub fn reload(&self) -> Result {
103 if cfg!(any(not(tuwunel_mods), not(feature = "tuwunel_mods"))) {
104 return Err!("Reloading not enabled");
105 }
106
107 if self.reloading.swap(true, Ordering::AcqRel) {
108 return Err!("Reloading already in progress");
109 }
110
111 if self.stopping.swap(true, Ordering::AcqRel) {
112 return Err!("Shutdown already in progress");
113 }
114
115 self.signal("SIGINT").inspect_err(|_| {
116 self.stopping.store(false, Ordering::Release);
117 self.reloading.store(false, Ordering::Release);
118 })
119 }
120
121 /// Requests a process restart through the normal shutdown path.
122 ///
123 /// The restarting flag is claimed once before shutdown begins. A rejected
124 /// shutdown clears that flag so a later request may retry.
125 pub fn restart(&self) -> Result {
126 if self.restarting.swap(true, Ordering::AcqRel) {
127 return Err!("Restart already in progress");
128 }
129
130 self.shutdown().inspect_err(|_| {
131 self.restarting.store(false, Ordering::Release);
132 })
133 }
134
135 /// Requests an orderly server shutdown.
136 ///
137 /// The stopping flag is claimed once before broadcasting `SIGTERM`. A
138 /// second request is rejected while shutdown remains in progress.
139 pub fn shutdown(&self) -> Result {
140 if self.stopping.swap(true, Ordering::AcqRel) {
141 return Err!("Shutdown already in progress");
142 }
143
144 self.signal("SIGTERM").inspect_err(|_| {
145 self.stopping.store(false, Ordering::Release);
146 })
147 }
148
149 /// Claims the one-shot backup restore, reporting whether this caller is the
150 /// one to perform it.
151 #[inline]
152 pub fn claim_backup_restore(&self) -> bool {
153 !self.backup_restored.swap(true, Ordering::AcqRel)
154 }
155
156 /// Broadcasts a process-signal name to lifecycle subscribers.
157 ///
158 /// Delivery is best effort because subscribers may not yet be listening.
159 /// The method therefore succeeds even when the channel has no receivers.
160 pub fn signal(&self, sig: &'static str) -> Result {
161 self.signal.send(sig).ok();
162 Ok(())
163 }
164
165 #[inline]
166 /// Waits until the server enters its stopping state.
167 ///
168 /// Lifecycle notifications wake the loop so it can recheck the shared
169 /// state. Calling it after shutdown has begun returns immediately.
170 pub async fn until_shutdown(self: &Arc<Self>) {
171 let mut signal = self.signal.subscribe();
172 while self.is_running() {
173 signal.recv().await.ok();
174 }
175 }
176
177 #[inline]
178 /// Returns the runtime handle supplied during server construction.
179 ///
180 /// Services use this handle to spawn work on the embedding runtime. The
181 /// handle is borrowed for the lifetime of the server.
182 ///
183 /// # Panics
184 ///
185 /// Panics when the server was constructed without a runtime handle.
186 pub fn runtime(&self) -> &runtime::Handle {
187 self.runtime
188 .as_ref()
189 .expect("runtime handle available in Server")
190 }
191
192 #[inline]
193 /// Rejects new work after shutdown begins.
194 ///
195 /// A running server returns success. A stopping server returns an
196 /// interrupted I/O error wrapped in the shared error type.
197 pub fn check_running(&self) -> Result {
198 use std::{io, io::ErrorKind::Interrupted};
199
200 self.is_running()
201 .then_some(())
202 .ok_or_else(|| io::Error::new(Interrupted, "Server shutting down"))
203 .map_err(Into::into)
204 }
205
206 #[inline]
207 /// Reports whether the server still accepts work.
208 ///
209 /// Running is the inverse of the stopping state. Reload and restart
210 /// requests also transition the server through stopping.
211 pub fn is_running(&self) -> bool { !self.is_stopping() }
212
213 #[inline]
214 /// Reports whether shutdown has begun.
215 ///
216 /// The flag is set by shutdown and reload transitions. Reads are relaxed
217 /// because callers use it as a lifecycle observation rather than a
218 /// synchronization edge.
219 pub fn is_stopping(&self) -> bool { self.stopping.load(Ordering::Relaxed) }
220
221 #[inline]
222 /// Reports whether a dynamic module reload is in progress.
223 ///
224 /// Reload claims the flag before checking the stopping state.
225 /// Signal-delivery failure clears it, while rejection by an existing
226 /// shutdown can leave it set.
227 pub fn is_reloading(&self) -> bool { self.reloading.load(Ordering::Relaxed) }
228
229 #[inline]
230 /// Reports whether a process restart is in progress.
231 ///
232 /// Restart claims the flag before requesting shutdown. Failed shutdown
233 /// initiation clears it for a later attempt.
234 pub fn is_restarting(&self) -> bool { self.restarting.load(Ordering::Relaxed) }
235
236 #[inline]
237 /// Reports whether a name matches the configured local server name.
238 ///
239 /// The comparison uses the active configuration snapshot. It performs an
240 /// exact, case-sensitive string comparison.
241 pub fn is_ours(&self, name: &str) -> bool { name == self.config.server_name }
242}