Skip to main content

cordis_core/
app.rs

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
39/// Root runtime and owner of every persistent plugin registration.
40pub 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    /// Registers a plugin and performs its first reconciliation.
70    ///
71    /// Missing required services produce a suspended handle, not an error.
72    /// The same registration is automatically reactivated when declared
73    /// service generations appear, disappear, or change.
74    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    /// Disposes consumers before their declared providers, independent of
247    /// installation order, then clears application-owned resources.
248    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        // The status channel contains the actionable failure.
476    }
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    // A dependency may change while apply is running. Discard stale staging
588    // activations and retry against the latest snapshot, with a small bound to
589    // avoid pathological self-triggered loops.
590    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}