Skip to main content

cordis_core/
plugin.rs

1use std::{
2    future::Future,
3    sync::{Arc, Mutex},
4    time::SystemTime,
5};
6
7use futures::future::BoxFuture;
8use tokio::sync::{mpsc, oneshot, watch};
9
10use crate::{Dependency, Error, PluginContext, Result};
11
12/// Strongly typed plugin API. Implementations can use native `async fn` on
13/// Rust 1.85+; no proc-macro is required.
14pub trait Plugin: Send + Sync + 'static {
15    type Config: Send + Sync + 'static;
16
17    fn name(&self) -> &'static str;
18
19    fn dependencies(&self) -> Vec<Dependency> {
20        Vec::new()
21    }
22
23    fn provides(&self) -> Vec<crate::ServiceDeclaration> {
24        Vec::new()
25    }
26
27    fn apply(
28        &self,
29        ctx: PluginContext,
30        config: Arc<Self::Config>,
31    ) -> impl Future<Output = Result<()>> + Send;
32}
33
34pub type ErasedConfig = Arc<dyn std::any::Any + Send + Sync>;
35
36/// Object-safe boundary for configuration-driven registries. Serialization is
37/// deliberately left to the framework using this crate.
38pub trait ErasedPlugin: Send + Sync + 'static {
39    fn name(&self) -> &'static str;
40
41    fn dependencies(&self) -> Vec<Dependency> {
42        Vec::new()
43    }
44
45    fn provides(&self) -> Vec<crate::ServiceDeclaration> {
46        Vec::new()
47    }
48
49    fn apply(&self, ctx: PluginContext, config: ErasedConfig) -> BoxFuture<'static, Result<()>>;
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
53pub struct PluginId(pub(crate) u64);
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub struct ActivationId(pub(crate) u64);
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum FailurePhase {
60    Apply,
61    Dispose,
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum PluginStatus {
67    Suspended {
68        missing: Arc<[&'static str]>,
69    },
70    Starting {
71        revision: u64,
72    },
73    Active {
74        activation: ActivationId,
75        revision: u64,
76    },
77    Stopping {
78        activation: ActivationId,
79    },
80    Failed {
81        phase: FailurePhase,
82        message: Arc<str>,
83        revision: u64,
84    },
85    Disposed,
86}
87
88#[derive(Clone, Debug)]
89pub struct PluginDiagnostic {
90    pub at: SystemTime,
91    pub status: PluginStatus,
92}
93
94pub(crate) enum PluginCommand {
95    Reload(oneshot::Sender<Result<()>>),
96    Retry(oneshot::Sender<Result<()>>),
97    Quiesce(oneshot::Sender<Result<()>>),
98    Resume(oneshot::Sender<Result<()>>),
99    Dispose(oneshot::Sender<Result<()>>),
100}
101
102/// Persistent plugin registration. Its activation may be repeatedly created
103/// and disposed as dependency generations change.
104#[must_use = "keep the handle to inspect status or call dispose().await"]
105#[derive(Clone)]
106pub struct PluginHandle {
107    id: PluginId,
108    name: &'static str,
109    commands: mpsc::Sender<PluginCommand>,
110    status: watch::Receiver<PluginStatus>,
111    diagnostics: Arc<Mutex<Vec<PluginDiagnostic>>>,
112    control: Arc<crate::app::ControlPlane>,
113    dispose_lock: Arc<tokio::sync::Mutex<()>>,
114    dispose_result: Arc<Mutex<Option<Option<Arc<str>>>>>,
115}
116
117impl PluginHandle {
118    pub(crate) fn new(
119        id: PluginId,
120        name: &'static str,
121        commands: mpsc::Sender<PluginCommand>,
122        status: watch::Receiver<PluginStatus>,
123        diagnostics: Arc<Mutex<Vec<PluginDiagnostic>>>,
124        control: Arc<crate::app::ControlPlane>,
125    ) -> Self {
126        Self {
127            id,
128            name,
129            commands,
130            status,
131            diagnostics,
132            control,
133            dispose_lock: Arc::new(tokio::sync::Mutex::new(())),
134            dispose_result: Arc::new(Mutex::new(None)),
135        }
136    }
137
138    pub fn id(&self) -> PluginId {
139        self.id
140    }
141
142    pub fn name(&self) -> &'static str {
143        self.name
144    }
145
146    pub fn status(&self) -> PluginStatus {
147        self.status.borrow().clone()
148    }
149
150    pub fn subscribe(&self) -> watch::Receiver<PluginStatus> {
151        self.status.clone()
152    }
153
154    pub fn diagnostics(&self) -> Vec<PluginDiagnostic> {
155        self.diagnostics
156            .lock()
157            .expect("diagnostics lock poisoned")
158            .clone()
159    }
160
161    pub async fn wait_active(&self) -> Result<ActivationId> {
162        let mut status = self.status.clone();
163        loop {
164            let current = status.borrow().clone();
165            match current {
166                PluginStatus::Active { activation, .. } => return Ok(activation),
167                PluginStatus::Failed { message, .. } => {
168                    return Err(Error::PluginFailed(message.to_string()));
169                }
170                PluginStatus::Disposed => return Err(Error::PluginDisposed),
171                _ => {}
172            }
173            status.changed().await.map_err(|_| Error::PluginDisposed)?;
174        }
175    }
176
177    pub async fn reload(&self) -> Result<()> {
178        self.control.reload(self.id).await
179    }
180
181    pub async fn retry(&self) -> Result<()> {
182        self.request(PluginCommand::Retry).await
183    }
184
185    async fn request(
186        &self,
187        make: impl FnOnce(oneshot::Sender<Result<()>>) -> PluginCommand,
188    ) -> Result<()> {
189        let (tx, rx) = oneshot::channel();
190        self.commands
191            .send(make(tx))
192            .await
193            .map_err(|_| Error::PluginDisposed)?;
194        rx.await.map_err(|_| Error::PluginDisposed)?
195    }
196
197    pub async fn dispose(&self) -> Result<()> {
198        let _guard = self.dispose_lock.lock().await;
199        if let Some(result) = self
200            .dispose_result
201            .lock()
202            .expect("dispose result lock poisoned")
203            .clone()
204        {
205            return result.map_or(Ok(()), |message| Err(Error::cleanup(message)));
206        }
207
208        if matches!(self.status(), PluginStatus::Disposed) {
209            *self
210                .dispose_result
211                .lock()
212                .expect("dispose result lock poisoned") = Some(None);
213            return Ok(());
214        }
215
216        let result = self.control.dispose(self.id).await;
217        *self
218            .dispose_result
219            .lock()
220            .expect("dispose result lock poisoned") = Some(
221            result
222                .as_ref()
223                .err()
224                .map(|error| Arc::<str>::from(error.to_string())),
225        );
226        result
227    }
228}
229
230/// Backwards-compatible name for the persistent plugin registration.
231pub type PluginScope = PluginHandle;