Skip to main content

toolkit/plugins/
mod.rs

1use std::future::Future;
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5use tokio::sync::Mutex;
6
7use crate::gts::PluginV1;
8
9/// A resettable, allocation-friendly selector for GTS plugin instance IDs.
10///
11/// Uses a single-flight pattern to ensure that the resolve function is called
12/// at most once even under concurrent callers. The selected instance ID is
13/// cached as `Arc<str>` to avoid allocations on the happy path.
14pub struct GtsPluginSelector {
15    /// Cached selected instance ID (sync lock for fast access and sync reset).
16    cached: RwLock<Option<Arc<str>>>,
17    /// Mutex to ensure single-flight resolution.
18    resolve_lock: Mutex<()>,
19}
20
21impl Default for GtsPluginSelector {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl GtsPluginSelector {
28    #[must_use]
29    pub fn new() -> Self {
30        Self {
31            cached: RwLock::new(None),
32            resolve_lock: Mutex::new(()),
33        }
34    }
35
36    /// Create a selector with `value` already cached, skipping resolution entirely.
37    ///
38    /// Useful in tests to pre-warm the selector with a known instance ID or
39    /// an empty-string sentinel (meaning "no plugin configured").
40    #[must_use]
41    pub fn pre_cached(value: String) -> Self {
42        Self {
43            cached: RwLock::new(Some(Arc::from(value))),
44            resolve_lock: Mutex::new(()),
45        }
46    }
47
48    /// Returns the cached instance ID, or resolves it using the provided function.
49    ///
50    /// Uses a single-flight pattern: even under concurrent callers, the resolve
51    /// function is called at most once. Returns `Arc<str>` to avoid allocations
52    /// on the happy path.
53    /// # Errors
54    ///
55    /// Returns `Err(E)` if the provided `resolve` future fails.
56    pub async fn get_or_init<F, Fut, E>(&self, resolve: F) -> Result<Arc<str>, E>
57    where
58        F: FnOnce() -> Fut,
59        Fut: Future<Output = Result<String, E>>,
60    {
61        // Fast path: check if already cached (sync lock, no await)
62        {
63            let guard = self.cached.read();
64            if let Some(ref id) = *guard {
65                return Ok(Arc::clone(id));
66            }
67        }
68
69        // Slow path: acquire resolve lock for single-flight
70        let _resolve_guard = self.resolve_lock.lock().await;
71
72        // Re-check after acquiring resolve lock (another caller may have resolved)
73        {
74            let guard = self.cached.read();
75            if let Some(ref id) = *guard {
76                return Ok(Arc::clone(id));
77            }
78        }
79
80        // Resolve and cache
81        let id_string = resolve().await?;
82        let id: Arc<str> = id_string.into();
83
84        {
85            let mut guard = self.cached.write();
86            *guard = Some(Arc::clone(&id));
87        }
88
89        Ok(id)
90    }
91
92    /// Clears the cached selected instance ID.
93    ///
94    /// Returns `true` if there was a cached value, `false` otherwise.
95    pub async fn reset(&self) -> bool {
96        let _resolve_guard = self.resolve_lock.lock().await;
97        let mut guard = self.cached.write();
98        guard.take().is_some()
99    }
100}
101
102/// Error returned by [`choose_plugin_instance`].
103#[derive(Debug, thiserror::Error)]
104pub enum ChoosePluginError {
105    /// Failed to deserialize a plugin instance's content.
106    #[error("invalid plugin instance content for '{gts_id}': {reason}")]
107    InvalidPluginInstance {
108        /// GTS ID of the malformed instance.
109        gts_id: String,
110        /// Human-readable reason.
111        reason: String,
112    },
113
114    /// No plugin instance matched the requested vendor.
115    #[error("no plugin instances found for type '{type_id}', vendor '{vendor}'")]
116    PluginNotFound {
117        /// GTS Type Identifier of the plugin type being resolved.
118        type_id: String,
119        /// The vendor that was requested.
120        vendor: String,
121    },
122}
123
124/// Selects the best plugin instance for the given vendor.
125///
126/// Accepts an iterator of `(gts_id, content)` pairs — typically
127/// produced from `types_registry_sdk::GtsEntity`:
128///
129/// ```ignore
130/// choose_plugin_instance::<MyPluginSpecV1>(
131///     &self.vendor,
132///     instances.iter().map(|e| (e.gts_id.as_str(), &e.content)),
133/// )
134/// ```
135///
136/// Deserializes each entry as `PluginV1<P>`, filters by
137/// `vendor`, and returns the `gts_id` of the instance with the
138/// **lowest** priority value.
139///
140/// # Type Parameters
141///
142/// - `P` — The plugin-specific properties struct (e.g.
143///   `AuthNResolverPluginSpecV1`). Must be `DeserializeOwned`.
144///
145/// # Errors
146///
147/// - [`ChoosePluginError::InvalidPluginInstance`] if deserialization fails
148///   or the `content.id` doesn't match `gts_id`.
149/// - [`ChoosePluginError::PluginNotFound`] if no instance matches the vendor.
150pub fn choose_plugin_instance<'a, P>(
151    vendor: &str,
152    instances: impl IntoIterator<Item = (&'a str, &'a serde_json::Value)>,
153) -> Result<String, ChoosePluginError>
154where
155    P: for<'de> gts::GtsDeserialize<'de> + gts::GtsSchema,
156{
157    let mut best: Option<(&str, i16)> = None;
158    let mut count: usize = 0;
159
160    for (gts_id, content_val) in instances {
161        count += 1;
162        let content: PluginV1<P> = serde_json::from_value(content_val.clone()).map_err(|e| {
163            tracing::error!(
164                gts_id = %gts_id,
165                error = %e,
166                "Failed to deserialize plugin instance content"
167            );
168            ChoosePluginError::InvalidPluginInstance {
169                gts_id: gts_id.to_owned(),
170                reason: e.to_string(),
171            }
172        })?;
173
174        if content.id != gts_id {
175            return Err(ChoosePluginError::InvalidPluginInstance {
176                gts_id: gts_id.to_owned(),
177                reason: format!(
178                    "content.id mismatch: expected {:?}, got {:?}",
179                    gts_id, content.id
180                ),
181            });
182        }
183
184        if content.vendor != vendor {
185            continue;
186        }
187
188        match &best {
189            None => best = Some((gts_id, content.priority)),
190            Some((_, cur_priority)) => {
191                if content.priority < *cur_priority {
192                    best = Some((gts_id, content.priority));
193                }
194            }
195        }
196    }
197
198    tracing::debug!(vendor, instance_count = count, "choose_plugin_instance");
199
200    best.map(|(gts_id, _)| gts_id.to_owned())
201        .ok_or_else(|| ChoosePluginError::PluginNotFound {
202            type_id: P::TYPE_ID.to_owned(),
203            vendor: vendor.to_owned(),
204        })
205}
206
207#[cfg(test)]
208#[cfg_attr(coverage_nightly, coverage(off))]
209mod tests {
210    use super::*;
211    use std::sync::Arc;
212    use std::sync::atomic::{AtomicUsize, Ordering};
213
214    #[tokio::test]
215    async fn resolve_called_once_returns_same_str() {
216        let selector = GtsPluginSelector::new();
217        let calls = Arc::new(AtomicUsize::new(0));
218
219        let calls_a = calls.clone();
220        let id_a = selector
221            .get_or_init(|| async move {
222                calls_a.fetch_add(1, Ordering::SeqCst);
223                Ok::<_, std::convert::Infallible>(
224                    "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.a.test.plugin.v1"
225                        .to_owned(),
226                )
227            })
228            .await
229            .unwrap();
230
231        let calls_b = calls.clone();
232        let id_b = selector
233            .get_or_init(|| async move {
234                calls_b.fetch_add(1, Ordering::SeqCst);
235                Ok::<_, std::convert::Infallible>(
236                    "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.b.test.plugin.v1"
237                        .to_owned(),
238                )
239            })
240            .await
241            .unwrap();
242
243        assert_eq!(id_a, id_b);
244        assert_eq!(calls.load(Ordering::SeqCst), 1);
245    }
246
247    #[tokio::test]
248    async fn reset_triggers_reselection() {
249        let selector = GtsPluginSelector::new();
250        let calls = Arc::new(AtomicUsize::new(0));
251
252        let calls_a = calls.clone();
253        let id_a = selector
254            .get_or_init(|| async move {
255                calls_a.fetch_add(1, Ordering::SeqCst);
256                Ok::<_, std::convert::Infallible>(
257                    "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.a.test.plugin.v1"
258                        .to_owned(),
259                )
260            })
261            .await;
262        assert_eq!(
263            &*id_a.unwrap(),
264            "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.a.test.plugin.v1"
265        );
266        assert_eq!(calls.load(Ordering::SeqCst), 1);
267        assert!(selector.reset().await);
268
269        let calls_b = calls.clone();
270        let id_b = selector
271            .get_or_init(|| async move {
272                calls_b.fetch_add(1, Ordering::SeqCst);
273                Ok::<_, std::convert::Infallible>(
274                    "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.b.test.plugin.v1"
275                        .to_owned(),
276                )
277            })
278            .await;
279        assert_eq!(
280            &*id_b.unwrap(),
281            "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.b.test.plugin.v1"
282        );
283        assert_eq!(calls.load(Ordering::SeqCst), 2);
284    }
285
286    #[tokio::test]
287    async fn concurrent_get_or_init_resolves_once() {
288        let selector = Arc::new(GtsPluginSelector::new());
289        let calls = Arc::new(AtomicUsize::new(0));
290
291        let mut handles = Vec::new();
292        for _ in 0..10 {
293            let selector = Arc::clone(&selector);
294            let calls = Arc::clone(&calls);
295            handles.push(tokio::spawn(async move {
296                selector
297                    .get_or_init(|| async {
298                        // Small delay to increase chance of concurrent access
299                        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
300                        calls.fetch_add(1, Ordering::SeqCst);
301                        Ok::<_, std::convert::Infallible>(
302                            "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.concurrent.test.plugin.v1"
303                                .to_owned(),
304                        )
305                    })
306                    .await
307            }));
308        }
309
310        // Await each handle in a loop (no futures_util dependency)
311        let mut results = Vec::new();
312        for handle in handles {
313            results.push(handle.await.unwrap().unwrap());
314        }
315
316        // All results should be the same
317        for id in &results {
318            assert_eq!(
319                &**id,
320                "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.concurrent.test.plugin.v1"
321            );
322        }
323
324        // Resolve should have been called exactly once
325        assert_eq!(calls.load(Ordering::SeqCst), 1);
326    }
327}