1use std::future::Future;
18use std::pin::Pin;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::{Arc, Mutex, Weak};
21use std::task::{Context, Poll};
22use std::time::Duration;
23use tokio::sync::watch;
25use super::Runtime;
27use crate::storage::StorageManager;
28use crate::{Application, Result};
29
30type StartupFuture<A> = Pin<Box<dyn Future<Output = std::result::Result<Runtime<A>, super::Incompatible>> + Send>>;
37type CleanupFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
38type ReconcileFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
39type InitializeFuture = Pin<Box<dyn Future<Output = std::result::Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send>>;
40type Initialize<A> = Box<dyn FnOnce(super::Handle<A>) -> InitializeFuture + Send>;
41pub(super) type Reconciliation<A> =
42 Box<dyn FnOnce(super::StateManager<A>, StorageManager<crate::storage::ErasedCatalog>) -> ReconcileFuture + Send>;
43
44pub(super) struct Initializer<A: Application> {
47 pub(super) timeout: Duration,
48 pub(super) initialize: Initialize<A>,
49}
50
51#[derive(Clone)]
52pub(super) struct Cleanup {
53 close: Arc<dyn Fn() -> CleanupFuture + Send + Sync>,
54 receipt: Weak<CleanupState>,
55}
56
57pub struct Startup<A: Application> {
58 future: Option<StartupFuture<A>>,
59 cleanup: BootstrapCleanup,
60}
61
62#[derive(Clone)]
63pub struct BootstrapCleanup {
64 shared: Arc<CleanupState>,
65}
66
67struct CleanupState {
68 retained: Mutex<Retained>,
69 status: watch::Sender<BootstrapStatus>,
70}
71
72struct Retained {
73 owner: Option<Cleanup>,
74 failure: Option<Arc<crate::Error>>,
75}
76
77pub(super) struct Boot<A: Application> {
78 pub(super) application: Option<super::launch::ApplicationCleanup<A>>,
79 pub(super) identity: Arc<()>,
80 pub(super) storage: Option<StorageManager<crate::storage::ErasedCatalog>>,
81 pub(super) runtime: Option<Runtime<A>>,
82 pub(super) writer: Option<tokio::task::JoinHandle<()>>,
83 pub(super) state: Option<super::StateManager<A>>,
84 armed: bool,
85 cleanup: BootstrapCleanup,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub enum BootstrapStatus {
92 Starting,
93 Cleaning,
94 Failed,
95 Settled,
96 HandedOff,
97}
98
99impl<A: Application> Startup<A> {
104 pub(super) fn new(builder: super::Builder<A>, storage: Option<StorageManager<crate::storage::ErasedCatalog>>) -> Self {
105 let cleanup = builder.cleanup.clone();
106 let receipt = cleanup.clone();
107 let future = Box::pin(async move {
108 let outcome = builder.boot(storage.as_ref(), receipt.clone()).await;
109 receipt.settle_unowned();
110 outcome
111 });
112 Self { future: Some(future), cleanup }
113 }
114}
115
116impl<A: Application> Future for Startup<A> {
117 type Output = std::result::Result<Runtime<A>, super::Incompatible>;
118 fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
119 let future = self.future.as_mut().expect("startup is polled before completion");
120 let outcome = future.as_mut().poll(context);
121 if outcome.is_ready() {
122 self.future.take();
123 }
124 outcome
125 }
126}
127
128impl<A: Application> Drop for Startup<A> {
129 fn drop(&mut self) {
130 self.future.take();
132 self.cleanup.settle_unowned();
133 }
134}
135
136impl BootstrapCleanup {
137 pub(super) fn new() -> Self {
138 let (status, _) = watch::channel(BootstrapStatus::Starting);
139 Self {
140 shared: Arc::new(CleanupState {
141 retained: Mutex::new(Retained { owner: None, failure: None }),
142 status,
143 }),
144 }
145 }
146
147 pub fn status(&self) -> BootstrapStatus {
148 *self.shared.status.borrow()
149 }
150
151 pub fn failure(&self) -> Option<Arc<crate::Error>> {
152 self.shared.retained.lock().expect("bootstrap cleanup lock").failure.clone()
153 }
154
155 pub async fn wait(&self) -> Result<()> {
156 let mut status = self.shared.status.subscribe();
157 loop {
158 let current = *status.borrow_and_update();
159 match current {
160 BootstrapStatus::Settled | BootstrapStatus::HandedOff => return Ok(()),
161 BootstrapStatus::Failed => {
162 let source = {
163 let retained = self.shared.retained.lock().expect("bootstrap cleanup lock");
164 if self.status() != BootstrapStatus::Failed {
165 continue;
166 }
167 retained.failure.clone().expect("failed cleanup retains its cause")
168 };
169 return Err(crate::Error::TaskFailed { domain: "bootstrap cleanup", source });
170 }
171 BootstrapStatus::Starting | BootstrapStatus::Cleaning => {}
172 }
173 status.changed().await.expect("receipt retains its status owner");
174 }
175 }
176
177 pub async fn retry(&self) -> Result<()> {
178 let owner = {
179 let mut retained = self.shared.retained.lock().expect("bootstrap cleanup lock");
180 if self.status() == BootstrapStatus::Failed {
181 let owner = retained.owner.clone().expect("failed cleanup owns resources");
182 retained.failure = None;
183 self.shared.status.send_replace(BootstrapStatus::Cleaning);
184 Some(owner)
185 } else {
186 None
187 }
188 };
189 if let Some(owner) = owner {
190 self.run(owner);
191 }
192 self.wait().await
193 }
194
195 fn run(&self, owner: Cleanup) {
196 tokio::spawn(async move {
197 let _ = owner.close().await;
198 });
199 }
200
201 pub(super) fn failed(&self, owner: Option<Cleanup>, failure: Arc<crate::Error>) {
202 let mut retained = self.shared.retained.lock().expect("bootstrap cleanup lock");
203 retained.owner = owner;
204 retained.failure = Some(failure);
205 self.shared.status.send_replace(BootstrapStatus::Failed);
206 }
207
208 fn cancelled(&self, owner: Cleanup) {
209 {
210 let mut retained = self.shared.retained.lock().expect("bootstrap cleanup lock");
211 retained.owner = Some(owner.clone());
212 self.shared.status.send_replace(BootstrapStatus::Cleaning);
213 }
214 self.run(owner);
215 }
216
217 pub(super) fn settle_unowned(&self) {
218 let retained = self.shared.retained.lock().expect("bootstrap cleanup lock");
219 if retained.owner.is_none() && self.status() == BootstrapStatus::Starting {
220 self.shared.status.send_replace(BootstrapStatus::Settled);
221 }
222 }
223}
224
225impl<A: Application> Initializer<A> {
226 pub(super) fn new<F, Fut, E>(timeout: Duration, initialize: F) -> Self
227 where
228 F: FnOnce(super::Handle<A>) -> Fut + Send + 'static,
229 Fut: Future<Output = std::result::Result<(), E>> + Send + 'static,
230 E: std::error::Error + Send + Sync + 'static,
231 {
232 Self {
233 timeout,
234 initialize: Box::new(move |handle| Box::pin(async move { initialize(handle).await.map_err(|source| Box::new(source) as _) })),
235 }
236 }
237}
238
239impl Cleanup {
240 pub(super) async fn close(&self) -> Result<()> {
241 let outcome = (self.close)().await;
242 let Some(receipt) = self.receipt.upgrade() else {
243 return outcome;
244 };
245 let mut retained = receipt.retained.lock().expect("bootstrap cleanup lock");
246 match outcome {
247 Ok(()) => {
248 retained.owner = None;
249 retained.failure = None;
250 receipt.status.send_replace(BootstrapStatus::Settled);
251 Ok(())
252 }
253 Err(error) => {
254 let source = Arc::new(error);
255 retained.failure = Some(Arc::clone(&source));
256 receipt.status.send_replace(BootstrapStatus::Failed);
257 Err(crate::Error::TaskFailed { domain: "bootstrap cleanup", source })
258 }
259 }
260 }
261}
262
263impl std::fmt::Debug for Cleanup {
264 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 formatter.debug_struct("Cleanup").finish_non_exhaustive()
266 }
267}
268
269impl<A: Application> Boot<A> {
270 pub(super) fn new(storage: Option<&StorageManager<crate::storage::ErasedCatalog>>, cleanup: BootstrapCleanup) -> Result<Self> {
271 let identity = Arc::new(());
272 if let Some(storage) = storage {
273 storage.claim_owner(identity.clone())?;
276 }
277
278 Ok(Self {
279 application: None,
280 identity,
281 storage: storage.cloned(),
282 runtime: None,
283 writer: None,
284 state: None,
285 armed: true,
286 cleanup,
287 })
288 }
289
290 pub(super) async fn acquire(&self) -> Result<()> {
291 if let Some(storage) = &self.storage {
292 storage.acquire().await?;
293 }
294 Ok(())
295 }
296
297 pub(super) async fn close(&mut self) -> Result<()> {
298 if self.runtime.is_none()
299 && let Some(application) = &self.application
300 {
301 application.settle().await.map_err(|source| crate::Error::TaskFailed { domain: "application cleanup", source })?;
302 }
303 if self.runtime.is_none() {
304 if let Some(state) = self.state.take() {
305 state.seal();
306 }
307 if let Some(writer) = self.writer.as_mut() {
308 let _ = writer.await;
309 }
310 self.writer.take();
311 }
312 let result = if let Some(runtime) = &self.runtime {
313 runtime.shutdown().await.map(|_| ())
314 } else if let Some(storage) = &self.storage {
315 storage.close().await
316 } else {
317 Ok(())
318 };
319 self.armed = false;
322 if result.is_ok() {
323 self.runtime.take();
324 }
325 result
326 }
327
328 pub(super) async fn reject(mut self, source: super::Incompatible) -> super::Incompatible {
329 match self.close().await {
330 Ok(()) => {
331 self.cleanup.shared.status.send_replace(BootstrapStatus::Settled);
332 source
333 }
334 Err(failure) => {
335 let failure = Arc::new(failure);
336 let retained = self.retain_cleanup(true);
337 if retained.is_some() {
338 self.cleanup.failed(retained.clone(), Arc::clone(&failure));
339 } else {
340 self.cleanup.shared.status.send_replace(BootstrapStatus::Settled);
341 }
342 super::Incompatible {
343 cause: super::Cause::Cleanup {
344 source: Box::new(source),
345 failure: crate::Error::TaskFailed {
346 domain: "bootstrap cleanup",
347 source: failure,
348 },
349 },
350 cleanup: retained,
351 }
352 }
353 }
354 }
355
356 pub(super) fn retain_cleanup(&mut self, failure_reported: bool) -> Option<Cleanup> {
357 let state = self.state.take();
358 let writer = self.writer.take();
359 let runtime = self.runtime.take().filter(|runtime| !failure_reported || !physically_settled(runtime));
360 let application = self.application.take();
361 let storage = self.storage.take().filter(|storage| !storage.status().closed);
362 self.armed = false;
363 if runtime.is_none() && storage.is_none() && writer.is_none() && state.is_none() && application.is_none() {
364 return None;
365 }
366 let writer = Arc::new(tokio::sync::Mutex::new(writer));
367 let runtime = runtime.map(Arc::new);
368 let failure_reported = Arc::new(AtomicBool::new(false));
369 Some(Cleanup {
370 receipt: Arc::downgrade(&self.cleanup.shared),
371 close: Arc::new(move || {
372 let state = state.clone();
373 let writer = Arc::clone(&writer);
374 let runtime = runtime.clone();
375 let storage = storage.clone();
376 let application = application.clone();
377 let failure_reported = failure_reported.clone();
378 Box::pin(async move {
379 if runtime.is_none() {
380 if let Some(application) = application {
381 application
382 .settle()
383 .await
384 .map_err(|source| crate::Error::TaskFailed { domain: "application cleanup", source })?;
385 }
386 if let Some(state) = state {
387 state.seal();
388 }
389 let mut writer = writer.lock().await;
390 if let Some(task) = writer.as_mut() {
391 let _ = task.await;
392 }
393 writer.take();
394 }
395 if let Some(runtime) = runtime {
396 match runtime.shutdown().await {
397 Err(source) if physically_settled(&runtime) => {
398 if failure_reported.swap(true, Ordering::AcqRel) {
399 Ok(())
400 } else {
401 Err(source)
402 }
403 }
404 outcome => outcome.map(|_| ()),
405 }
406 } else if let Some(storage) = storage {
407 storage.close().await
408 } else {
409 Ok(())
410 }
411 })
412 }),
413 })
414 }
415
416 pub(super) fn finish(mut self) -> Runtime<A> {
417 self.armed = false;
418 self.cleanup.shared.status.send_replace(BootstrapStatus::HandedOff);
419 self.runtime.take().expect("successful boot owns a runtime")
420 }
421}
422
423impl<A: Application> Drop for Boot<A> {
424 fn drop(&mut self) {
425 if !self.armed {
426 return;
427 }
428 if let Some(owner) = self.retain_cleanup(false) {
429 self.cleanup.cancelled(owner);
430 }
431 }
432}
433
434fn physically_settled<A: Application>(runtime: &Runtime<A>) -> bool {
439 let stopping = runtime.stopping.lock().expect("runtime transition is healthy");
440 matches!(&*stopping, super::Stopping::Closing { outcome, .. } if outcome.get().is_some())
441}
442
443