1use std::sync::{
2 Arc,
3 atomic::{AtomicBool, Ordering},
4};
5
6use futures::{FutureExt, future::BoxFuture};
7use tokio::sync::{mpsc, oneshot, watch};
8
9use crate::{
10 Context, Error, Plugin, PluginContext, PluginHandle, PluginStatus, Result,
11 plugin::{ActivationId, FailurePhase, PluginCommand, PluginId},
12 runtime::{DependencySnapshot, Runtime},
13 scope::ScopeInner,
14};
15
16type ApplyFn = Arc<dyn Fn(PluginContext) -> BoxFuture<'static, Result<()>> + Send + Sync>;
17
18struct Activation {
19 scope: Arc<ScopeInner>,
20 snapshot: DependencySnapshot,
21}
22
23#[derive(Default)]
24pub(crate) struct ControlPlane {
25 registrations: std::sync::Mutex<Vec<RegistrationRecord>>,
26 transition: tokio::sync::Mutex<()>,
27}
28
29#[derive(Clone)]
30struct RegistrationRecord {
31 id: PluginId,
32 name: &'static str,
33 dependencies: Vec<crate::Dependency>,
34 provides: Vec<crate::ServiceDeclaration>,
35 commands: mpsc::Sender<PluginCommand>,
36 status: watch::Receiver<PluginStatus>,
37}
38
39pub struct App {
41 runtime: Arc<Runtime>,
42 root: Arc<ScopeInner>,
43 control: Arc<ControlPlane>,
44 started: AtomicBool,
45 shutdown: AtomicBool,
46}
47
48impl App {
49 pub fn new() -> Self {
50 let runtime = Runtime::new();
51 let root = ScopeInner::new(runtime.next_id(), "application");
52 Self {
53 runtime,
54 root,
55 control: Arc::new(ControlPlane::default()),
56 started: AtomicBool::new(false),
57 shutdown: AtomicBool::new(false),
58 }
59 }
60
61 pub fn context(&self) -> Context {
62 Context::root(self.runtime.clone())
63 }
64
65 pub fn is_started(&self) -> bool {
66 self.started.load(Ordering::Acquire)
67 }
68
69 pub async fn install<P: Plugin>(&self, plugin: P, config: P::Config) -> Result<PluginHandle> {
75 let name = plugin.name();
76 let dependencies = plugin.dependencies();
77 let provides = plugin.provides();
78 let plugin = Arc::new(plugin);
79 let config = Arc::new(config);
80 let apply: ApplyFn = Arc::new(move |ctx| {
81 let plugin = plugin.clone();
82 let config = config.clone();
83 Box::pin(async move { plugin.apply(ctx, config).await })
84 });
85 self.install_apply(name, dependencies, provides, apply)
86 .await
87 }
88
89 pub async fn install_erased(
90 &self,
91 plugin: Arc<dyn crate::ErasedPlugin>,
92 config: crate::ErasedConfig,
93 ) -> Result<PluginHandle> {
94 let name = plugin.name();
95 let dependencies = plugin.dependencies();
96 let provides = plugin.provides();
97 let apply: ApplyFn = Arc::new(move |ctx| plugin.apply(ctx, config.clone()));
98 self.install_apply(name, dependencies, provides, apply)
99 .await
100 }
101
102 async fn install_apply(
103 &self,
104 name: &'static str,
105 dependencies: Vec<crate::Dependency>,
106 provides: Vec<crate::ServiceDeclaration>,
107 apply: ApplyFn,
108 ) -> Result<PluginHandle> {
109 if self.shutdown.load(Ordering::Acquire) {
110 return Err(Error::ApplicationShutdown);
111 }
112 self.ensure_acyclic(name, &dependencies, &provides)?;
113 let id = PluginId(self.runtime.next_id());
114 let initial = PluginStatus::Suspended {
115 missing: dependencies
116 .iter()
117 .filter(|dependency| dependency.required)
118 .map(|dependency| dependency.name)
119 .collect::<Vec<_>>()
120 .into(),
121 };
122 let (status_tx, status_rx) = watch::channel(initial.clone());
123 let diagnostics = Arc::new(std::sync::Mutex::new(vec![crate::PluginDiagnostic {
124 at: std::time::SystemTime::now(),
125 status: initial,
126 }]));
127 let diagnostic_log = diagnostics.clone();
128 let mut diagnostic_status = status_rx.clone();
129 tokio::spawn(async move {
130 while diagnostic_status.changed().await.is_ok() {
131 diagnostic_log
132 .lock()
133 .expect("diagnostics lock poisoned")
134 .push(crate::PluginDiagnostic {
135 at: std::time::SystemTime::now(),
136 status: diagnostic_status.borrow().clone(),
137 });
138 }
139 });
140 let (command_tx, command_rx) = mpsc::channel(16);
141 let (initialized_tx, initialized_rx) = oneshot::channel();
142 let services = self.runtime.subscribe_services();
143 let runtime = self.runtime.clone();
144 let control = self.control.clone();
145
146 tokio::spawn(run_plugin(
147 runtime,
148 control,
149 id,
150 name,
151 dependencies.clone(),
152 provides.clone(),
153 apply,
154 command_rx,
155 services,
156 status_tx,
157 initialized_tx,
158 ));
159
160 let shutdown_tx = command_tx.clone();
161 if let Err(error) = self.root.push(Box::new(move || {
162 Box::pin(async move {
163 let (reply_tx, reply_rx) = oneshot::channel();
164 if shutdown_tx
165 .send(PluginCommand::Dispose(reply_tx))
166 .await
167 .is_err()
168 {
169 return Ok(());
170 }
171 reply_rx.await.unwrap_or(Ok(()))
172 })
173 })) {
174 let (reply_tx, _) = oneshot::channel();
175 let _ = command_tx.send(PluginCommand::Dispose(reply_tx)).await;
176 return Err(error);
177 }
178
179 initialized_rx.await.map_err(|_| Error::PluginDisposed)??;
180 self.control
181 .registrations
182 .lock()
183 .expect("registration lock poisoned")
184 .push(RegistrationRecord {
185 id,
186 name,
187 dependencies,
188 provides,
189 commands: command_tx.clone(),
190 status: status_rx.clone(),
191 });
192 Ok(PluginHandle::new(
193 id,
194 name,
195 command_tx,
196 status_rx,
197 diagnostics,
198 self.control.clone(),
199 ))
200 }
201
202 fn ensure_acyclic(
203 &self,
204 name: &'static str,
205 dependencies: &[crate::Dependency],
206 provides: &[crate::ServiceDeclaration],
207 ) -> Result<()> {
208 let records = self
209 .control
210 .registrations
211 .lock()
212 .expect("registration lock poisoned");
213 let mut declarations: Vec<_> = records
214 .iter()
215 .filter(|record| !matches!(*record.status.borrow(), PluginStatus::Disposed))
216 .map(|record| {
217 (
218 record.name,
219 record.dependencies.clone(),
220 record.provides.clone(),
221 )
222 })
223 .collect();
224 declarations.push((name, dependencies.to_vec(), provides.to_vec()));
225 if topological_order(&declarations).len() != declarations.len() {
226 let names = declarations
227 .iter()
228 .map(|(name, _, _)| *name)
229 .collect::<Vec<_>>()
230 .join(" -> ");
231 return Err(Error::DependencyCycle(names));
232 }
233 Ok(())
234 }
235
236 pub async fn start(&self) -> Result<()> {
237 if self.shutdown.load(Ordering::Acquire) {
238 return Err(Error::ApplicationShutdown);
239 }
240 if !self.started.swap(true, Ordering::AcqRel) {
241 self.context().emit(crate::Ready).await?;
242 }
243 Ok(())
244 }
245
246 pub async fn shutdown(&self) -> Result<()> {
249 if self.shutdown.swap(true, Ordering::AcqRel) {
250 return Ok(());
251 }
252
253 let _transition = self.control.transition.lock().await;
254 let records = self
255 .control
256 .registrations
257 .lock()
258 .expect("registration lock poisoned")
259 .clone();
260 let declarations = records
261 .iter()
262 .map(|record| {
263 (
264 record.name,
265 record.dependencies.clone(),
266 record.provides.clone(),
267 )
268 })
269 .collect::<Vec<_>>();
270 let mut first_error = None;
271 for index in topological_order(&declarations).into_iter().rev() {
272 let record = &records[index];
273 if matches!(*record.status.borrow(), PluginStatus::Disposed) {
274 continue;
275 }
276 let (reply_tx, reply_rx) = oneshot::channel();
277 if record
278 .commands
279 .send(PluginCommand::Dispose(reply_tx))
280 .await
281 .is_ok()
282 {
283 if let Ok(Err(error)) = reply_rx.await {
284 first_error.get_or_insert(error);
285 }
286 }
287 }
288 if let Err(error) = self.root.dispose().await {
289 first_error.get_or_insert(error);
290 }
291 first_error.map_or(Ok(()), Err)
292 }
293}
294
295impl Default for App {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301#[derive(Clone, Copy)]
302enum ControlAction {
303 Reload,
304 Quiesce,
305 Resume,
306 Dispose,
307}
308
309async fn send_control(sender: &mpsc::Sender<PluginCommand>, action: ControlAction) -> Result<()> {
310 let (reply_tx, reply_rx) = oneshot::channel();
311 let command = match action {
312 ControlAction::Reload => PluginCommand::Reload(reply_tx),
313 ControlAction::Quiesce => PluginCommand::Quiesce(reply_tx),
314 ControlAction::Resume => PluginCommand::Resume(reply_tx),
315 ControlAction::Dispose => PluginCommand::Dispose(reply_tx),
316 };
317 sender
318 .send(command)
319 .await
320 .map_err(|_| Error::PluginDisposed)?;
321 reply_rx.await.map_err(|_| Error::PluginDisposed)?
322}
323
324impl ControlPlane {
325 pub(crate) async fn reload(&self, id: PluginId) -> Result<()> {
326 self.transition(id, false).await
327 }
328
329 pub(crate) async fn dispose(&self, id: PluginId) -> Result<()> {
330 self.transition(id, true).await
331 }
332
333 async fn transition(&self, id: PluginId, disposing: bool) -> Result<()> {
334 let _guard = self.transition.lock().await;
335 let records = self
336 .registrations
337 .lock()
338 .expect("registration lock poisoned")
339 .clone();
340 let target = records
341 .iter()
342 .position(|record| record.id == id)
343 .ok_or(Error::PluginDisposed)?;
344 let declarations = records
345 .iter()
346 .map(|record| {
347 (
348 record.name,
349 record.dependencies.clone(),
350 record.provides.clone(),
351 )
352 })
353 .collect::<Vec<_>>();
354 let order = topological_order(&declarations);
355 let dependents = dependent_indices(&declarations, target);
356
357 for index in order.iter().rev().copied() {
358 if dependents.contains(&index)
359 && !matches!(*records[index].status.borrow(), PluginStatus::Disposed)
360 {
361 send_control(&records[index].commands, ControlAction::Quiesce).await?;
362 }
363 }
364
365 let target_action = if disposing {
366 ControlAction::Dispose
367 } else {
368 ControlAction::Reload
369 };
370 let target_result = send_control(&records[target].commands, target_action).await;
371
372 for index in order {
373 if dependents.contains(&index)
374 && !matches!(*records[index].status.borrow(), PluginStatus::Disposed)
375 {
376 let _ = send_control(&records[index].commands, ControlAction::Resume).await;
377 }
378 }
379 target_result
380 }
381}
382
383fn graph_plan(
384 control: &ControlPlane,
385 id: PluginId,
386) -> Result<(
387 Vec<RegistrationRecord>,
388 Vec<usize>,
389 std::collections::HashSet<usize>,
390)> {
391 let records = control
392 .registrations
393 .lock()
394 .expect("registration lock poisoned")
395 .clone();
396 let target = records
397 .iter()
398 .position(|record| record.id == id)
399 .ok_or(Error::PluginDisposed)?;
400 let declarations = records
401 .iter()
402 .map(|record| {
403 (
404 record.name,
405 record.dependencies.clone(),
406 record.provides.clone(),
407 )
408 })
409 .collect::<Vec<_>>();
410 let order = topological_order(&declarations);
411 let dependents = dependent_indices(&declarations, target);
412 Ok((records, order, dependents))
413}
414
415async fn quiesce_plan(
416 records: &[RegistrationRecord],
417 order: &[usize],
418 dependents: &std::collections::HashSet<usize>,
419) -> Result<()> {
420 for index in order.iter().rev().copied() {
421 if dependents.contains(&index)
422 && !matches!(*records[index].status.borrow(), PluginStatus::Disposed)
423 {
424 send_control(&records[index].commands, ControlAction::Quiesce).await?;
425 }
426 }
427 Ok(())
428}
429
430async fn resume_plan(
431 records: &[RegistrationRecord],
432 order: &[usize],
433 dependents: &std::collections::HashSet<usize>,
434) {
435 for index in order.iter().copied() {
436 if dependents.contains(&index)
437 && !matches!(*records[index].status.borrow(), PluginStatus::Disposed)
438 {
439 let _ = send_control(&records[index].commands, ControlAction::Resume).await;
440 }
441 }
442}
443
444#[allow(clippy::too_many_arguments)]
445async fn run_plugin(
446 runtime: Arc<Runtime>,
447 control: Arc<ControlPlane>,
448 id: PluginId,
449 name: &'static str,
450 dependencies: Vec<crate::Dependency>,
451 provides: Vec<crate::ServiceDeclaration>,
452 apply: ApplyFn,
453 mut commands: mpsc::Receiver<PluginCommand>,
454 mut services: watch::Receiver<u64>,
455 status: watch::Sender<PluginStatus>,
456 initialized: oneshot::Sender<Result<()>>,
457) {
458 let mut active = None;
459 let mut failed_generations = None;
460 let mut quiesced = false;
461 let initial = reconcile(
462 &runtime,
463 id,
464 name,
465 &dependencies,
466 &apply,
467 &status,
468 &mut active,
469 &mut failed_generations,
470 false,
471 )
472 .await;
473 let _ = initialized.send(Ok(()));
474 if initial.is_err() {
475 }
477
478 loop {
479 tokio::select! {
480 biased;
481 command = commands.recv() => {
482 let Some(command) = command else { break };
483 match command {
484 PluginCommand::Reload(reply) | PluginCommand::Retry(reply) => {
485 let result = reconcile(
486 &runtime, id, name, &dependencies, &apply, &status,
487 &mut active, &mut failed_generations, true,
488 ).await;
489 let _ = reply.send(result);
490 }
491 PluginCommand::Quiesce(reply) => {
492 quiesced = true;
493 let result = dispose_activation(&runtime, id, &status, &mut active).await;
494 if result.is_ok() {
495 status.send_replace(PluginStatus::Suspended { missing: Arc::new([]) });
496 }
497 let _ = reply.send(result);
498 }
499 PluginCommand::Resume(reply) => {
500 quiesced = false;
501 let result = reconcile(
502 &runtime, id, name, &dependencies, &apply, &status,
503 &mut active, &mut failed_generations, false,
504 ).await;
505 let _ = reply.send(result);
506 }
507 PluginCommand::Dispose(reply) => {
508 let result = dispose_activation(&runtime, id, &status, &mut active).await;
509 status.send_replace(PluginStatus::Disposed);
510 let _ = reply.send(result);
511 break;
512 }
513 }
514 }
515 changed = services.changed() => {
516 if changed.is_err() { break; }
517 tokio::time::sleep(std::time::Duration::from_millis(1)).await;
518 while services.has_changed().unwrap_or(false) {
519 services.borrow_and_update();
520 }
521 if quiesced { continue; }
522 let snapshot = runtime.dependency_snapshot(&dependencies);
523 let changes_active_provider = active.as_ref().is_some_and(|activation| {
524 activation.snapshot.generations != snapshot.generations
525 || !snapshot.missing.is_empty()
526 });
527 if changes_active_provider && !provides.is_empty() {
528 let _transition = control.transition.lock().await;
529 if let Ok((records, order, dependents)) = graph_plan(&control, id) {
530 let _ = quiesce_plan(&records, &order, &dependents).await;
531 let _ = reconcile(
532 &runtime, id, name, &dependencies, &apply, &status,
533 &mut active, &mut failed_generations, false,
534 ).await;
535 resume_plan(&records, &order, &dependents).await;
536 }
537 } else {
538 let _ = reconcile(
539 &runtime, id, name, &dependencies, &apply, &status,
540 &mut active, &mut failed_generations, false,
541 ).await;
542 }
543 }
544 }
545 }
546}
547
548#[allow(clippy::too_many_arguments)]
549async fn reconcile(
550 runtime: &Arc<Runtime>,
551 plugin_id: PluginId,
552 name: &'static str,
553 dependencies: &[crate::Dependency],
554 apply: &ApplyFn,
555 status: &watch::Sender<PluginStatus>,
556 active: &mut Option<Activation>,
557 failed_generations: &mut Option<Vec<(std::any::TypeId, Option<u64>)>>,
558 force: bool,
559) -> Result<()> {
560 let mut snapshot = runtime.dependency_snapshot(dependencies);
561
562 if !snapshot.missing.is_empty() {
563 dispose_activation(runtime, plugin_id, status, active).await?;
564 *failed_generations = None;
565 status.send_replace(PluginStatus::Suspended {
566 missing: snapshot.missing,
567 });
568 return Ok(());
569 }
570
571 let unchanged = active
572 .as_ref()
573 .is_some_and(|activation| activation.snapshot.generations == snapshot.generations);
574 if unchanged && !force {
575 return Ok(());
576 }
577 if !force
578 && failed_generations
579 .as_ref()
580 .is_some_and(|failed| *failed == snapshot.generations)
581 {
582 return Ok(());
583 }
584
585 dispose_activation(runtime, plugin_id, status, active).await?;
586
587 for _ in 0..8 {
591 status.send_replace(PluginStatus::Starting {
592 revision: snapshot.revision,
593 });
594 let activation_id = ActivationId(runtime.next_id());
595 let scope = ScopeInner::new(activation_id.0, name);
596 let context = Context::for_scope(runtime.clone(), activation_id.0);
597 let plugin_context = PluginContext::new(context, scope.clone());
598
599 let applied = std::panic::AssertUnwindSafe(apply(plugin_context))
600 .catch_unwind()
601 .await;
602 let applied = match applied {
603 Ok(result) => result,
604 Err(payload) => Err(Error::panic(payload)),
605 };
606 if let Err(error) = applied {
607 let _ = scope.dispose().await;
608 *failed_generations = Some(snapshot.generations.clone());
609 status.send_replace(PluginStatus::Failed {
610 phase: FailurePhase::Apply,
611 message: error.to_string().into(),
612 revision: snapshot.revision,
613 });
614 return Err(error);
615 }
616
617 let after_apply = runtime.dependency_snapshot(dependencies);
618 if after_apply.generations != snapshot.generations || !after_apply.missing.is_empty() {
619 scope.dispose().await?;
620 if !after_apply.missing.is_empty() {
621 status.send_replace(PluginStatus::Suspended {
622 missing: after_apply.missing,
623 });
624 return Ok(());
625 }
626 snapshot = after_apply;
627 continue;
628 }
629
630 if let Err(error) = scope.commit().await {
631 let _ = scope.dispose().await;
632 *failed_generations = Some(snapshot.generations.clone());
633 status.send_replace(PluginStatus::Failed {
634 phase: FailurePhase::Apply,
635 message: error.to_string().into(),
636 revision: snapshot.revision,
637 });
638 return Err(error);
639 }
640 runtime.commit_owner(activation_id.0);
641 *failed_generations = None;
642 status.send_replace(PluginStatus::Active {
643 activation: activation_id,
644 revision: snapshot.revision,
645 });
646 *active = Some(Activation { scope, snapshot });
647 if let Err(error) = runtime
648 .emit_serial(crate::Fork {
649 plugin: plugin_id,
650 activation: activation_id,
651 })
652 .await
653 {
654 let _ = dispose_activation(runtime, plugin_id, status, active).await;
655 status.send_replace(PluginStatus::Failed {
656 phase: FailurePhase::Apply,
657 message: error.to_string().into(),
658 revision: runtime.dependency_snapshot(dependencies).revision,
659 });
660 return Err(error);
661 }
662 return Ok(());
663 }
664
665 let error = Error::cleanup("dependency snapshot did not stabilize");
666 status.send_replace(PluginStatus::Failed {
667 phase: FailurePhase::Apply,
668 message: error.to_string().into(),
669 revision: snapshot.revision,
670 });
671 Err(error)
672}
673
674async fn dispose_activation(
675 runtime: &Arc<Runtime>,
676 plugin_id: PluginId,
677 status: &watch::Sender<PluginStatus>,
678 active: &mut Option<Activation>,
679) -> Result<()> {
680 let Some(activation) = active.take() else {
681 return Ok(());
682 };
683 let activation_id = ActivationId(activation.scope.id);
684 status.send_replace(PluginStatus::Stopping {
685 activation: activation_id,
686 });
687 let event_error = runtime
688 .emit_serial(crate::Dispose {
689 plugin: plugin_id,
690 activation: activation_id,
691 })
692 .await
693 .err();
694 let cleanup_error = activation.scope.dispose().await.err();
695 if let Some(error) = event_error.or(cleanup_error) {
696 status.send_replace(PluginStatus::Failed {
697 phase: FailurePhase::Dispose,
698 message: error.to_string().into(),
699 revision: activation.snapshot.revision,
700 });
701 return Err(error);
702 }
703 Ok(())
704}
705
706fn topological_order(
707 declarations: &[(
708 &'static str,
709 Vec<crate::Dependency>,
710 Vec<crate::ServiceDeclaration>,
711 )],
712) -> Vec<usize> {
713 let count = declarations.len();
714 let mut outgoing = vec![Vec::new(); count];
715 let mut indegree = vec![0usize; count];
716
717 for (provider_index, (_, _, provided)) in declarations.iter().enumerate() {
718 for (consumer_index, (_, dependencies, _)) in declarations.iter().enumerate() {
719 let linked = provided.iter().any(|service| {
720 dependencies
721 .iter()
722 .any(|dependency| dependency.key == service.key)
723 });
724 if linked {
725 outgoing[provider_index].push(consumer_index);
726 indegree[consumer_index] += 1;
727 }
728 }
729 }
730
731 let mut ready = std::collections::VecDeque::new();
732 for (index, degree) in indegree.iter().enumerate() {
733 if *degree == 0 {
734 ready.push_back(index);
735 }
736 }
737 let mut order = Vec::with_capacity(count);
738 while let Some(index) = ready.pop_front() {
739 order.push(index);
740 for consumer in &outgoing[index] {
741 indegree[*consumer] -= 1;
742 if indegree[*consumer] == 0 {
743 ready.push_back(*consumer);
744 }
745 }
746 }
747 order
748}
749
750fn dependent_indices(
751 declarations: &[(
752 &'static str,
753 Vec<crate::Dependency>,
754 Vec<crate::ServiceDeclaration>,
755 )],
756 provider: usize,
757) -> std::collections::HashSet<usize> {
758 let mut result = std::collections::HashSet::new();
759 let mut pending = vec![provider];
760 while let Some(current) = pending.pop() {
761 let provided = &declarations[current].2;
762 for (index, (_, dependencies, _)) in declarations.iter().enumerate() {
763 if index == provider || result.contains(&index) {
764 continue;
765 }
766 if provided.iter().any(|service| {
767 dependencies
768 .iter()
769 .any(|dependency| dependency.key == service.key)
770 }) {
771 result.insert(index);
772 pending.push(index);
773 }
774 }
775 }
776 result
777}