Skip to main content

harn_vm/connectors/
active_clients.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::sync::Arc;
4
5use async_trait::async_trait;
6
7use super::{ClientError, ConnectorClient, ProviderId};
8
9/// Execution-owned fallback for connector clients that are expensive to
10/// construct until a script actually names their provider.
11#[async_trait]
12pub trait ConnectorClientResolver: Send + Sync {
13    async fn resolve(
14        &self,
15        provider: &str,
16    ) -> Result<Option<Arc<dyn ConnectorClient>>, ClientError>;
17}
18
19/// Connector projection carried by a VM tree rather than ambient thread
20/// state. Child VMs share the same resolver and therefore the same lazy cache
21/// even when Tokio migrates their tasks between worker threads.
22#[derive(Clone, Default)]
23pub struct VmConnectorClients {
24    clients: Arc<BTreeMap<String, Arc<dyn ConnectorClient>>>,
25    resolver: Option<Arc<dyn ConnectorClientResolver>>,
26}
27
28impl VmConnectorClients {
29    pub fn new(
30        clients: BTreeMap<ProviderId, Arc<dyn ConnectorClient>>,
31        resolver: Option<Arc<dyn ConnectorClientResolver>>,
32    ) -> Self {
33        Self {
34            clients: Arc::new(client_map(clients)),
35            resolver,
36        }
37    }
38
39    pub async fn resolve(
40        &self,
41        provider: &str,
42    ) -> Result<Option<Arc<dyn ConnectorClient>>, ClientError> {
43        // Project declarations can intentionally replace a built-in provider,
44        // so the execution-owned resolver gets first refusal. Falling back to
45        // the core map first would silently select the wrong implementation.
46        if let Some(resolver) = self.resolver.as_ref() {
47            if let Some(client) = resolver.resolve(provider).await? {
48                return Ok(Some(client));
49            }
50        }
51        Ok(self.clients.get(provider).cloned())
52    }
53}
54
55thread_local! {
56    static ACTIVE_CONNECTOR_CLIENTS: RefCell<BTreeMap<String, Arc<dyn ConnectorClient>>> =
57        RefCell::new(BTreeMap::new());
58}
59
60pub fn install_active_connector_clients(clients: BTreeMap<ProviderId, Arc<dyn ConnectorClient>>) {
61    ACTIVE_CONNECTOR_CLIENTS.with(|slot| *slot.borrow_mut() = client_map(clients));
62}
63
64/// Keep one connector client map active for the lifetime of the returned guard.
65///
66/// Leaving a nested runtime restores the host's prior connector projection.
67pub fn scope_active_connector_clients(
68    clients: BTreeMap<ProviderId, Arc<dyn ConnectorClient>>,
69) -> ActiveConnectorClientsGuard {
70    let previous = ACTIVE_CONNECTOR_CLIENTS
71        .with(|slot| std::mem::replace(&mut *slot.borrow_mut(), client_map(clients)));
72    ActiveConnectorClientsGuard { previous }
73}
74
75pub struct ActiveConnectorClientsGuard {
76    previous: BTreeMap<String, Arc<dyn ConnectorClient>>,
77}
78
79impl Drop for ActiveConnectorClientsGuard {
80    fn drop(&mut self) {
81        ACTIVE_CONNECTOR_CLIENTS.with(|slot| {
82            *slot.borrow_mut() = std::mem::take(&mut self.previous);
83        });
84    }
85}
86
87pub fn active_connector_client(provider: &str) -> Option<Arc<dyn ConnectorClient>> {
88    ACTIVE_CONNECTOR_CLIENTS.with(|slot| slot.borrow().get(provider).cloned())
89}
90
91pub fn clear_active_connector_clients() {
92    ACTIVE_CONNECTOR_CLIENTS.with(|slot| slot.borrow_mut().clear());
93}
94
95fn client_map(
96    clients: BTreeMap<ProviderId, Arc<dyn ConnectorClient>>,
97) -> BTreeMap<String, Arc<dyn ConnectorClient>> {
98    clients
99        .into_iter()
100        .map(|(provider, client)| (provider.as_str().to_string(), client))
101        .collect()
102}
103
104#[cfg(test)]
105mod tests {
106    use std::sync::atomic::{AtomicUsize, Ordering};
107
108    use async_trait::async_trait;
109    use serde_json::Value as JsonValue;
110
111    use super::*;
112    use crate::connectors::ClientError;
113
114    struct NamedClient(&'static str);
115
116    #[async_trait]
117    impl ConnectorClient for NamedClient {
118        async fn call(&self, _method: &str, _args: JsonValue) -> Result<JsonValue, ClientError> {
119            Ok(JsonValue::String(self.0.to_string()))
120        }
121    }
122
123    struct OnceResolver {
124        initializations: AtomicUsize,
125        clients: tokio::sync::OnceCell<BTreeMap<ProviderId, Arc<dyn ConnectorClient>>>,
126    }
127
128    #[async_trait]
129    impl ConnectorClientResolver for OnceResolver {
130        async fn resolve(
131            &self,
132            provider: &str,
133        ) -> Result<Option<Arc<dyn ConnectorClient>>, ClientError> {
134            let clients = self
135                .clients
136                .get_or_init(|| async {
137                    self.initializations.fetch_add(1, Ordering::SeqCst);
138                    tokio::task::yield_now().await;
139                    BTreeMap::from([(
140                        ProviderId::from("core"),
141                        Arc::new(NamedClient("project")) as Arc<dyn ConnectorClient>,
142                    )])
143                })
144                .await;
145            Ok(clients.get(&ProviderId::from(provider)).cloned())
146        }
147    }
148
149    #[test]
150    fn nested_client_scope_restores_the_host_projection() {
151        clear_active_connector_clients();
152        install_active_connector_clients(BTreeMap::from([(
153            ProviderId::from("outer"),
154            Arc::new(NamedClient("outer")) as Arc<dyn ConnectorClient>,
155        )]));
156
157        {
158            let _inner = scope_active_connector_clients(BTreeMap::from([(
159                ProviderId::from("inner"),
160                Arc::new(NamedClient("inner")) as Arc<dyn ConnectorClient>,
161            )]));
162            assert!(active_connector_client("inner").is_some());
163            assert!(active_connector_client("outer").is_none());
164        }
165
166        assert!(active_connector_client("outer").is_some());
167        assert!(active_connector_client("inner").is_none());
168        clear_active_connector_clients();
169    }
170
171    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
172    async fn vm_resolver_is_once_only_across_tasks_and_overrides_core_clients() {
173        let resolver = Arc::new(OnceResolver {
174            initializations: AtomicUsize::new(0),
175            clients: tokio::sync::OnceCell::new(),
176        });
177        let clients = VmConnectorClients::new(
178            BTreeMap::from([(
179                ProviderId::from("core"),
180                Arc::new(NamedClient("builtin")) as Arc<dyn ConnectorClient>,
181            )]),
182            Some(resolver.clone()),
183        );
184
185        let tasks = (0..8)
186            .map(|_| {
187                let clients = clients.clone();
188                tokio::spawn(async move {
189                    let client = clients
190                        .resolve("core")
191                        .await
192                        .expect("resolve")
193                        .expect("project override");
194                    client.call("ping", JsonValue::Null).await.expect("call")
195                })
196            })
197            .collect::<Vec<_>>();
198        for task in tasks {
199            assert_eq!(
200                task.await.expect("task"),
201                JsonValue::String("project".to_string())
202            );
203        }
204        assert_eq!(resolver.initializations.load(Ordering::SeqCst), 1);
205    }
206}