Skip to main content

behest_runtime/
extension.rs

1//! [`ExtensionPoint<T>`]: a typed, name-indexed, hot-swappable collection.
2//!
3//! In `behest`'s composable model, every pluggable category of runtime
4//! element — chat providers, embedding providers, tools, context adapters,
5//! stores, publishers, transports — is exposed as an
6//! [`ExtensionPoint<T>`]. Operators compose a runtime by registering
7//! implementations by name, and can replace any registered instance at
8//! runtime.
9//!
10//! # Design
11//!
12//! - **Name-indexed**: every entry is keyed by a stable string. The same
13//!   name space is shared with the component registry, so config files can
14//!   reference extensions by name (e.g. `"primary"`, `"fallback-eu"`).
15//! - **Clonable**: the inner state is wrapped in an [`Arc`], so cloning an
16//!   `ExtensionPoint` is cheap. A clone observes every registration
17//!   performed on the original.
18//! - **Hot-swappable**: [`ExtensionPoint::replace`] atomically swaps the
19//!   stored `Arc<T>` and returns the previous one. Callers holding the old
20//!   `Arc` continue to use it; new `get` calls return the new instance.
21//! - **In-use detection**: [`ExtensionPoint::unregister`] refuses to drop
22//!   an entry whose strong count is above the registry's reference (one
23//!   reference for the storage slot). This catches the common bug of
24//!   removing a provider that is still serving a run.
25//!
26//! # Example
27//!
28//! ```rust
29//! use std::sync::Arc;
30//! use behest_runtime::extension::ExtensionPoint;
31//!
32//! let ep: ExtensionPoint<String> = ExtensionPoint::new();
33//! ep.register("greeting", Arc::new("hello".to_string())).unwrap();
34//! assert_eq!(ep.get("greeting").map(|s| (*s).clone()), Some("hello".to_string()));
35//! ```
36
37#![allow(clippy::pedantic)]
38use std::collections::HashMap;
39use std::sync::{Arc, RwLock};
40
41use thiserror::Error;
42
43/// Errors raised by [`ExtensionPoint`] operations.
44#[derive(Debug, Error)]
45#[non_exhaustive]
46pub enum ExtensionError {
47    /// Tried to register a name that is already in use.
48    #[error("extension `{name}` is already registered")]
49    AlreadyRegistered {
50        /// The conflicting name.
51        name: String,
52    },
53    /// Tried to unregister or replace a name that is not present.
54    #[error("extension `{name}` not found")]
55    NotFound {
56        /// The missing name.
57        name: String,
58    },
59    /// Tried to unregister a name whose strong count is greater than
60    /// one (i.e. external callers are still holding the `Arc<T>`).
61    #[error("extension `{name}` has {strong_count} live references and cannot be removed")]
62    InUse {
63        /// The contended name.
64        name: String,
65        /// Number of strong references observed at the time of the call.
66        strong_count: usize,
67    },
68    /// Internal lock acquisition failed.
69    #[error("extension registry lock poisoned")]
70    LockPoisoned,
71}
72
73/// Typed, name-indexed collection of `Arc<T>`.
74///
75/// Cheap to clone; clones share the same underlying entries.
76pub struct ExtensionPoint<T: ?Sized> {
77    inner: Arc<ExtensionInner<T>>,
78}
79
80struct ExtensionInner<T: ?Sized> {
81    entries: RwLock<HashMap<String, Arc<T>>>,
82}
83
84impl<T: ?Sized> Default for ExtensionPoint<T> {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl<T: ?Sized> Clone for ExtensionPoint<T> {
91    fn clone(&self) -> Self {
92        Self {
93            inner: self.inner.clone(),
94        }
95    }
96}
97
98impl<T: ?Sized> ExtensionPoint<T> {
99    /// Construct an empty extension point.
100    #[must_use]
101    pub fn new() -> Self {
102        Self {
103            inner: Arc::new(ExtensionInner {
104                entries: RwLock::new(HashMap::new()),
105            }),
106        }
107    }
108
109    /// Number of registered entries.
110    #[must_use]
111    pub fn len(&self) -> usize {
112        self.read().map(|m| m.len()).unwrap_or_default()
113    }
114
115    /// Returns `true` if the extension point has no entries.
116    #[must_use]
117    pub fn is_empty(&self) -> bool {
118        self.len() == 0
119    }
120
121    /// Returns a sorted list of registered names. Sorting makes the result
122    /// stable for snapshot tests and log output.
123    #[must_use]
124    pub fn names(&self) -> Vec<String> {
125        let mut names = self
126            .read()
127            .map(|m| m.keys().cloned().collect::<Vec<_>>())
128            .unwrap_or_default();
129        names.sort_unstable();
130        names
131    }
132
133    /// Take a consistent snapshot of `(name, Arc<T>)` pairs.
134    #[must_use]
135    pub fn snapshot(&self) -> Vec<(String, Arc<T>)> {
136        self.read()
137            .map(|m| {
138                let mut entries: Vec<(String, Arc<T>)> =
139                    m.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
140                entries.sort_by(|a, b| a.0.cmp(&b.0));
141                entries
142            })
143            .unwrap_or_default()
144    }
145
146    fn read(
147        &self,
148    ) -> Result<std::sync::RwLockReadGuard<'_, HashMap<String, Arc<T>>>, ExtensionError> {
149        self.inner
150            .entries
151            .read()
152            .map_err(|_| ExtensionError::LockPoisoned)
153    }
154
155    fn write(
156        &self,
157    ) -> Result<std::sync::RwLockWriteGuard<'_, HashMap<String, Arc<T>>>, ExtensionError> {
158        self.inner
159            .entries
160            .write()
161            .map_err(|_| ExtensionError::LockPoisoned)
162    }
163}
164
165impl<T: ?Sized + Send + Sync + 'static> ExtensionPoint<T> {
166    /// Register a new entry. Errors if the name is already in use.
167    ///
168    /// # Errors
169    /// - [`ExtensionError::AlreadyRegistered`] if the name is taken.
170    /// - [`ExtensionError::LockPoisoned`] if the internal lock was
171    ///   poisoned by a panic.
172    pub fn register(&self, name: impl Into<String>, value: Arc<T>) -> Result<(), ExtensionError> {
173        let name = name.into();
174        let mut map = self.write()?;
175        if map.contains_key(&name) {
176            return Err(ExtensionError::AlreadyRegistered { name });
177        }
178        map.insert(name, value);
179        Ok(())
180    }
181
182    /// Register a new entry, replacing any existing one with the same
183    /// name. Returns the previous `Arc<T>`, or `None` if there was none.
184    pub fn register_or_replace(&self, name: impl Into<String>, value: Arc<T>) -> Option<Arc<T>> {
185        let name = name.into();
186        self.write().ok().and_then(|mut m| m.insert(name, value))
187    }
188
189    /// Remove an entry. Returns the removed `Arc<T>`, or `None` if the
190    /// name was not present.
191    ///
192    /// Refuses to remove an entry whose strong count is greater than one
193    /// (i.e. external callers still hold a reference).
194    pub fn unregister(&self, name: &str) -> Result<Option<Arc<T>>, ExtensionError> {
195        let map = self.read()?;
196        if let Some(existing) = map.get(name)
197            && Arc::strong_count(existing) > 1
198        {
199            return Err(ExtensionError::InUse {
200                name: name.to_string(),
201                strong_count: Arc::strong_count(existing),
202            });
203        }
204        drop(map);
205        Ok(self.write()?.remove(name))
206    }
207
208    /// Atomically replace an entry. Returns the previous `Arc<T>`, or
209    /// [`ExtensionError::NotFound`] if the name was not present.
210    ///
211    /// After this call, new [`ExtensionPoint::get`] calls return `new`.
212    /// Callers that already hold the old `Arc<T>` continue to operate on
213    /// the old instance until they drop it.
214    ///
215    /// # Errors
216    /// - [`ExtensionError::NotFound`] if the name was not present.
217    pub fn replace(&self, name: &str, new: Arc<T>) -> Result<Arc<T>, ExtensionError> {
218        let mut map = self.write()?;
219        let previous = map.remove(name).ok_or_else(|| ExtensionError::NotFound {
220            name: name.to_string(),
221        })?;
222        map.insert(name.to_string(), new);
223        Ok(previous)
224    }
225
226    /// Look up an entry by name.
227    #[must_use]
228    pub fn get(&self, name: &str) -> Option<Arc<T>> {
229        self.read().ok().and_then(|m| m.get(name).cloned())
230    }
231
232    /// Look up an entry by name, returning [`ExtensionError::NotFound`]
233    /// if missing.
234    pub fn get_required(&self, name: &str) -> Result<Arc<T>, ExtensionError> {
235        self.get(name).ok_or_else(|| ExtensionError::NotFound {
236            name: name.to_string(),
237        })
238    }
239
240    /// Returns `true` if the given name is registered.
241    #[must_use]
242    pub fn contains(&self, name: &str) -> bool {
243        self.read().map(|m| m.contains_key(name)).unwrap_or(false)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn register_and_get_round_trip() {
253        let ep: ExtensionPoint<String> = ExtensionPoint::new();
254        ep.register("greeting", Arc::new("hello".to_string()))
255            .unwrap_or_else(|e| panic!("{e}"));
256        assert_eq!(
257            ep.get("greeting").map(|s| (*s).clone()),
258            Some("hello".to_string())
259        );
260    }
261
262    #[test]
263    fn register_rejects_duplicate_names() {
264        let ep: ExtensionPoint<String> = ExtensionPoint::new();
265        ep.register("a", Arc::new("first".to_string()))
266            .unwrap_or_else(|e| panic!("{e}"));
267        let err = match ep.register("a", Arc::new("second".to_string())) {
268            Ok(_) => panic!("expected Err, got Ok"),
269            Err(e) => e,
270        };
271        assert!(matches!(err, ExtensionError::AlreadyRegistered { .. }));
272    }
273
274    #[test]
275    fn register_or_replace_swallows_duplicates() {
276        let ep: ExtensionPoint<String> = ExtensionPoint::new();
277        let prev = ep.register_or_replace("a", Arc::new("first".to_string()));
278        assert!(prev.is_none());
279        let prev = ep.register_or_replace("a", Arc::new("second".to_string()));
280        assert_eq!(prev.map(|s| (*s).clone()), Some("first".to_string()));
281        assert_eq!(
282            ep.get("a").map(|s| (*s).clone()),
283            Some("second".to_string())
284        );
285    }
286
287    #[test]
288    fn replace_returns_previous_value() {
289        let ep: ExtensionPoint<String> = ExtensionPoint::new();
290        ep.register("a", Arc::new("v1".to_string()))
291            .unwrap_or_else(|e| panic!("{e}"));
292        let prev = ep
293            .replace("a", Arc::new("v2".to_string()))
294            .unwrap_or_else(|e| panic!("{e}"));
295        assert_eq!((*prev).clone(), "v1");
296        assert_eq!(ep.get("a").map(|s| (*s).clone()), Some("v2".to_string()));
297    }
298
299    #[test]
300    fn replace_missing_returns_not_found() {
301        let ep: ExtensionPoint<String> = ExtensionPoint::new();
302        let err = match ep.replace("missing", Arc::new("v".to_string())) {
303            Ok(_) => panic!("expected Err, got Ok"),
304            Err(e) => e,
305        };
306        assert!(matches!(err, ExtensionError::NotFound { .. }));
307    }
308
309    #[test]
310    fn unregister_refuses_in_use_entry() {
311        let ep: ExtensionPoint<String> = ExtensionPoint::new();
312        ep.register("a", Arc::new("v".to_string()))
313            .unwrap_or_else(|e| panic!("{e}"));
314        let _hold = match ep.get("a") {
315            Some(v) => v,
316            None => panic!("expected Some"),
317        };
318        let err = match ep.unregister("a") {
319            Ok(_) => panic!("expected Err, got Ok"),
320            Err(e) => e,
321        };
322        assert!(matches!(err, ExtensionError::InUse { .. }));
323    }
324
325    #[test]
326    fn unregister_drops_when_only_registry_holds() {
327        let ep: ExtensionPoint<String> = ExtensionPoint::new();
328        ep.register("a", Arc::new("v".to_string()))
329            .unwrap_or_else(|e| panic!("{e}"));
330        let removed = ep.unregister("a").unwrap_or_else(|e| panic!("{e}"));
331        assert_eq!(removed.map(|s| (*s).clone()), Some("v".to_string()));
332        assert!(ep.get("a").is_none());
333    }
334
335    #[test]
336    fn clone_shares_state() {
337        let a: ExtensionPoint<String> = ExtensionPoint::new();
338        let b = a.clone();
339        a.register("shared", Arc::new("x".to_string()))
340            .unwrap_or_else(|e| panic!("{e}"));
341        assert!(b.contains("shared"));
342    }
343
344    #[test]
345    fn snapshot_is_sorted_and_complete() {
346        let ep: ExtensionPoint<String> = ExtensionPoint::new();
347        ep.register("b", Arc::new("B".to_string()))
348            .unwrap_or_else(|e| panic!("{e}"));
349        ep.register("a", Arc::new("A".to_string()))
350            .unwrap_or_else(|e| panic!("{e}"));
351        let snap = ep.snapshot();
352        assert_eq!(snap.len(), 2);
353        assert_eq!(snap[0].0, "a");
354        assert_eq!(snap[1].0, "b");
355    }
356}