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