1use std::future::Future;
18use std::pin::Pin;
19use std::sync::atomic::Ordering;
20use std::sync::{Arc, OnceLock, Weak};
21use std::task::Poll;
22use std::time::Duration;
23use tokio::sync::watch;
25use tokio::task::JoinHandle;
26use super::{Handle, Participants, Runtime, STOP_GRACE, Stopping};
28use crate::storage::StorageManager;
29use crate::supervision::cleanup::{Cleanup, Finalize, Resource};
30use crate::{Application, Error, Result, state::Position};
31
32pub(super) type Outcome = std::result::Result<Position, Failure>;
39
40pub(super) struct Shutdown<A: Application> {
43 application: Option<super::launch::ApplicationCleanup<A>>,
44 handles: Handle<A>,
45 participants: Participants,
46 journal_handles: Weak<()>,
47 notices: watch::Sender<Option<String>>,
48 state: Drain,
49 outcome: Arc<OnceLock<Outcome>>,
50 progress: watch::Sender<&'static str>,
51}
52
53#[derive(Clone)]
54pub(super) struct Failure {
55 domain: &'static str,
56 source: Arc<dyn std::error::Error + Send + Sync>,
57}
58
59enum Drain {
62 Running(JoinHandle<Position>),
63 Drained(Outcome),
64}
65
66impl<A: Application> Runtime<A> {
71 pub async fn start_storage<C>(&self, storage: StorageManager<C>) -> Result<()> {
72 let storage = storage.erased();
73 let storage = {
74 let _stopping = self.stopping.lock().expect("runtime transition is healthy");
75 if self.closing.load(Ordering::Acquire) {
76 return Err(Error::Closing { domain: "runtime" });
77 }
78 if let Some(current) = self.storage.get()
79 && !current.same_instance(&storage)
80 {
81 return Err(Error::AlreadyRegistered {
82 domain: "runtime storage",
83 name: "primary".to_owned(),
84 });
85 }
86 storage.claim_owner(self.operations.resident_identity())?;
87 self.storage.get_or_init(|| storage)
88 };
89 storage.start_on::<A>(&self.operations).await
90 }
91
92 pub fn storage(&self) -> Option<&StorageManager<crate::storage::ErasedCatalog>> {
93 self.storage.get()
94 }
95
96 pub async fn shutdown_with_timeout(&self, timeout: Duration) -> Result<Position> {
97 let deadline = tokio::time::Instant::now().checked_add(timeout).filter(|_| !timeout.is_zero()).ok_or(Error::InvalidInput {
98 field: "runtime shutdown timeout",
99 reason: "must be positive and representable",
100 })?;
101 match tokio::time::timeout_at(deadline, self.shutdown()).await {
102 Ok(result) => result,
103 Err(_) => {
104 let stopping = self.stopping.lock().expect("runtime transition is healthy");
105 let phase = match &*stopping {
106 Stopping::Running(_) => "admission",
107 Stopping::Closing { progress, .. } => *progress.borrow(),
108 };
109 tracing::error!(phase, ?timeout, "runtime shutdown budget exhausted; original cleanup retained");
110 Err(Error::ShutdownTimeout { phase })
111 }
112 }
113 }
114
115 pub async fn shutdown(&self) -> Result<Position> {
116 self.closing.store(true, Ordering::Release);
117 #[cfg(feature = "grpc")]
118 self.grpc.close_admission();
119 #[cfg(all(feature = "plugin", not(target_arch = "wasm32")))]
120 self.plugin.close_admission();
121 #[cfg(not(all(feature = "plugin", not(target_arch = "wasm32"))))]
122 {
123 self.operations.close_resident_admission();
125 self.operations.close_admission();
126 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
127 {
128 self.records.stop_listeners();
129 self.streams.stop_listeners();
130 }
131 }
132 let (cleanup, completed) = {
133 let mut stopping = self.stopping.lock().expect("runtime transition is healthy");
134 if let Stopping::Running(supervisor) = &mut *stopping {
135 let outcome = Arc::new(OnceLock::new());
136 let progress = watch::channel("producers and listeners").0;
137 let original = Shutdown {
138 application: self.application.clone(),
139 handles: self.handle(),
140 participants: self.participants.clone(),
141 journal_handles: self.journal_handles.clone(),
142 notices: self.notices.clone(),
143 state: Drain::Running(supervisor.take().expect("runtime owns its supervisor")),
144 outcome: outcome.clone(),
145 progress: progress.clone(),
146 };
147 *stopping = Stopping::Closing {
148 cleanup: Arc::new(Cleanup::new(original)),
149 outcome,
150 progress,
151 };
152 }
153 let Stopping::Closing { cleanup, outcome, .. } = &*stopping else {
154 unreachable!("shutdown retains its driver before awaiting it");
155 };
156 (cleanup.clone(), outcome.clone())
157 };
158 cleanup
159 .settle()
160 .await
161 .map_err(|source| Arc::try_unwrap(source).unwrap_or_else(|source| Error::TaskFailed { domain: "runtime cleanup", source }))?;
162 outcome(completed.get().expect("physical shutdown published its outcome"))
163 }
164}
165
166impl<A: Application> Resource for Shutdown<A> {
167 fn finalize(&mut self) -> Finalize<'_> {
168 Box::pin(async move { self.finish().await.map_err(Arc::new) })
169 }
170}
171
172impl<A: Application> Shutdown<A> {
173 async fn finish(&mut self) -> Result<()> {
174 if let Drain::Running(supervisor) = &mut self.state {
175 #[cfg(all(feature = "plugin", not(target_arch = "wasm32")))]
176 {
177 self.progress.send_replace("plugins");
178 self.handles.plugin.shutdown().await?;
179 }
180 self.handles.operations.close_resident_admission();
181 self.handles.operations.close_admission();
182 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
183 {
184 self.handles.records.stop_listeners();
185 self.handles.streams.stop_listeners();
186 }
187 self.progress.send_replace("observers and listeners");
188 let observers = self.handles.operations.shutdown_observers();
189 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
190 let (observers, records, streams) =
191 tokio::join!(observers, self.handles.records.shutdown_listeners(), self.handles.streams.shutdown_listeners(),);
192 #[cfg(not(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar")))]
193 let observers = observers.await;
194 if self.handles.operations.observer_cleanup_pending() {
195 return Err(observers.err().unwrap_or(Error::NotReady { domain: "observer cleanup" }));
196 }
197 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
198 records?;
199 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
200 streams?;
201 self.progress.send_replace("application services");
202 let services = self.handles.operations.shutdown_application_services().await;
203 if self.handles.operations.service_cleanup_pending(false) {
204 return Err(services.err().unwrap_or(Error::NotReady { domain: "service cleanup" }));
205 }
206 self.progress.send_replace("operations");
208 self.handles.operations.shutdown().await?;
209 self.progress.send_replace("participants");
210 self.participants.stop().await;
211 self.progress.send_replace("application lifecycle");
212 if let Some(application) = &self.application {
213 application.settle().await.map_err(|source| {
214 Arc::try_unwrap(source).unwrap_or_else(|source| Error::TaskFailed { domain: "application lifecycle", source })
215 })?;
216 }
217 self.handles.state.seal();
218 self.progress.send_replace("journal");
219 let mut settled = joined(supervisor, &self.journal_handles, &self.notices).await;
220 if let Some(source) = self.application.as_ref().and_then(super::launch::ApplicationCleanup::failure)
221 && settled.is_ok()
222 {
223 settled = Err(Failure { domain: "application", source });
224 }
225 if let Err(source) = observers
226 && settled.is_ok()
227 {
228 settled = Err(Failure {
229 domain: "observer",
230 source: Arc::new(source),
231 });
232 }
233 if let Err(source) = services
234 && settled.is_ok()
235 {
236 settled = Err(Failure {
237 domain: "service",
238 source: Arc::new(source),
239 });
240 }
241 self.handles.streams.close();
242 self.state = Drain::Drained(settled);
243 }
244 let Drain::Drained(settled) = &self.state else {
245 unreachable!("the journal is drained before storage closes");
246 };
247 let mut settled = settled.clone();
248 if let Some(storage) = self.handles.storage() {
249 self.progress.send_replace("storage");
250 storage.close().await?;
251 }
252 self.progress.send_replace("infrastructure services");
253 let services = self.handles.operations.shutdown_residents().await;
254 if self.handles.operations.service_cleanup_pending(true) {
255 return Err(services.err().unwrap_or(Error::NotReady { domain: "service cleanup" }));
256 }
257 if let Err(source) = services
258 && settled.is_ok()
259 {
260 settled = Err(Failure {
261 domain: "service",
262 source: Arc::new(source),
263 });
264 }
265 self.outcome.set(settled).unwrap_or_else(|_| unreachable!("shutdown publishes its outcome once"));
266 Ok(())
267 }
268}
269
270impl<A: Application> Handle<A> {
271 pub fn storage(&self) -> Option<&StorageManager<crate::storage::ErasedCatalog>> {
272 self.storage.get()
273 }
274}
275
276async fn joined(supervisor: &mut JoinHandle<Position>, journal_handles: &Weak<()>, notices: &watch::Sender<Option<String>>) -> Outcome {
281 let mut grace = Box::pin(tokio::time::sleep(STOP_GRACE));
282 let joined = std::future::poll_fn(|context| {
283 if let Poll::Ready(settled) = Pin::new(&mut *supervisor).poll(context) {
284 return Poll::Ready(Some(settled));
285 }
286 if grace.as_mut().poll(context).is_ready() {
287 return Poll::Ready(None);
288 }
289
290 Poll::Pending
291 })
292 .await;
293
294 match joined {
295 Some(settled) => settled,
296 None => {
297 let live = journal_handles.strong_count();
298 notices.send_replace(Some(format!(
299 "the journal stop is still draining; {live} StateManager clone{} outstanding, none \
300 of which holds it open",
301 if live == 1 { "" } else { "s" }
302 )));
303
304 supervisor.await
305 }
306 }
307 .map_err(|source| Failure {
308 domain: "journal supervisor",
309 source: Arc::new(source),
310 })
311}
312
313fn outcome(settled: &Outcome) -> Result<Position> {
314 match settled {
315 Ok(position) => Ok(*position),
316 Err(failure) => Err(Error::TaskFailed {
317 domain: failure.domain,
318 source: failure.source.clone(),
319 }),
320 }
321}
322
323