Skip to main content

polydat_core/
resource.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Dependency-inverted resource-accessor bridge.
5//!
6//! A polydat node sometimes needs a **live, host-owned resource** (the
7//! first consumer is a CQL `Session`) addressed by its configuration
8//! **fingerprint**. polydat is the dependency floor and does not depend
9//! on the host runtime, so the bridge is a type-erased trait,
10//! [`ResourceAccessor`], which the host implements over its own store.
11//!
12//! The accessor belongs to a program tree, not to the process. A
13//! [`ResourceScope`] is the tree's slot for it: a host installs an
14//! accessor when it compiles (`CompileOptions::resources`) or later, on
15//! the kernel it holds ([`crate::Kernel::resources`]). Every program of
16//! the tree shares the one scope: `for` bodies, subscopes built under a
17//! kernel of the tree, and every kernel created from or forked off one
18//! of its programs. A node reaches the scope through the
19//! [`BuildContext`](crate::dsl::factory::BuildContext) its factory
20//! receives, keeps it, and looks a resource up when it evaluates. Two
21//! trees in one process each see their own accessor.
22//!
23//! A program compiled on its own has a scope of its own. Binding one of
24//! its kernels under a parent ([`crate::kernel::bind_under`]) joins that
25//! scope to the parent's ([`ResourceScope::join`]): while the child's
26//! scope has no accessor installed, its lookups resolve through the
27//! parent's. The link is on the scope, so every kernel of the child's
28//! program shares it, and an accessor installed once at the root serves
29//! every image bound beneath it.
30//!
31//! A lookup is a synchronous read of what the host has already
32//! attached: it never blocks and never connects, and the payload is
33//! erased to `Arc<dyn Any + Send + Sync>`, which the consuming node
34//! downcasts to its own concrete handle type.
35
36use std::any::Any;
37use std::sync::atomic::{AtomicBool, Ordering};
38use std::sync::{Arc, OnceLock};
39
40/// Type-erased accessor over the host's live resource store.
41///
42/// Implemented by the host runtime (nbrs-runtime's resource pool) and
43/// installed into a program tree's [`ResourceScope`]. The trait is
44/// deliberately minimal and free of host types so polydat stays the
45/// dependency floor.
46pub trait ResourceAccessor: Send + Sync {
47    /// Synchronous lookup of an already-attached resource's accessor
48    /// payload by fingerprint `key`. Returns `None` when no live entry
49    /// matches that key (never blocks, never connects).
50    ///
51    /// The `key` is the host's stable rendering of a resource fingerprint;
52    /// a single canonical rendering is shared by whoever installs the
53    /// payload and whoever looks it up, so the string round-trips exactly.
54    fn lookup(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>>;
55}
56
57/// A program tree's slot for its host's [`ResourceAccessor`].
58///
59/// Cloning a scope yields another handle to the same slot, which is how
60/// every program and kernel of one tree shares it. An accessor is
61/// installed at most once per scope. A scope with none installed
62/// resolves a lookup through the parent it joined
63/// ([`Self::join`]), and answers `None` when it joined none, which is
64/// the norm for a program that runs without a host.
65#[derive(Clone, Default)]
66pub struct ResourceScope(Arc<Slot>);
67
68/// The shared state behind every handle to one scope.
69#[derive(Default)]
70struct Slot {
71    accessor: OnceLock<Arc<dyn ResourceAccessor>>,
72    parent: OnceLock<Link>,
73}
74
75/// A scope's delegation to the parent it joined. A link that lost a
76/// race to close a cycle is severed: it stays set, and lookups and
77/// joins treat the scope as joined to nothing.
78struct Link {
79    parent: ResourceScope,
80    live: AtomicBool,
81}
82
83/// Why [`ResourceScope::join`] left a scope joined to nothing.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub enum ScopeJoinError {
86    /// Another thread joined scopes at the same moment in a way that,
87    /// with this join, would have made a scope delegate to itself. This
88    /// join is severed and the scope delegates to nothing.
89    Cycle,
90}
91
92impl std::fmt::Display for ScopeJoinError {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        match self {
95            ScopeJoinError::Cycle => write!(
96                f,
97                "joining the child's resource scope to its parent's, concurrently with \
98                 another join, would have made a scope delegate to itself"
99            ),
100        }
101    }
102}
103
104impl std::error::Error for ScopeJoinError {}
105
106impl ResourceScope {
107    /// A scope with no accessor installed, for a new program tree.
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    /// A scope with `accessor` already installed.
113    pub fn with_accessor(accessor: Arc<dyn ResourceAccessor>) -> Self {
114        let scope = Self::new();
115        let _ = scope.0.accessor.set(accessor);
116        scope
117    }
118
119    /// Install `accessor` for every program and kernel of this tree.
120    /// A scope has one accessor for its life: when one is already
121    /// installed the call returns `accessor` back as the error and the
122    /// installed one stays. An accessor installed here takes precedence
123    /// over the parent the scope joined.
124    pub fn install(
125        &self,
126        accessor: Arc<dyn ResourceAccessor>,
127    ) -> Result<(), Arc<dyn ResourceAccessor>> {
128        self.0.accessor.set(accessor)
129    }
130
131    /// Whether an accessor is installed on this scope itself. A scope
132    /// that resolves through its parent reports `false`.
133    pub fn is_installed(&self) -> bool {
134        self.0.accessor.get().is_some()
135    }
136
137    /// The parent this scope delegates to, when it joined one.
138    pub fn parent(&self) -> Option<&ResourceScope> {
139        self.0
140            .parent
141            .get()
142            .filter(|link| link.live.load(Ordering::Acquire))
143            .map(|link| &link.parent)
144    }
145
146    /// Join this scope to `parent`, so that while no accessor is
147    /// installed here, lookups resolve through `parent`. Binding a
148    /// kernel under a parent kernel ([`crate::kernel::bind_under`])
149    /// joins the child program's scope to the parent's.
150    ///
151    /// A scope joins at most one parent, the first it is joined to, and
152    /// keeps it for its life. The join changes nothing when:
153    ///
154    /// - this scope has an accessor installed, which it keeps;
155    /// - `parent` is this scope, or already resolves through it (this
156    ///   scope is `parent`'s ancestor), so joining would make a scope
157    ///   delegate to itself;
158    /// - this scope already joined a parent, `parent` or another. The
159    ///   first join wins: a node holds its program's one scope, so a
160    ///   program whose kernels are bound under two trees resolves
161    ///   through the first. A host that binds one program under trees
162    ///   with different accessors installs one on the program itself.
163    ///
164    /// Otherwise this scope joins `parent`. The one error is a join
165    /// that raced others into a cycle ([`ScopeJoinError::Cycle`]).
166    pub fn join(&self, parent: &ResourceScope) -> Result<(), ScopeJoinError> {
167        if self.is_installed() || parent.reaches(self) {
168            return Ok(());
169        }
170        let link = Link {
171            parent: parent.clone(),
172            live: AtomicBool::new(true),
173        };
174        if self.0.parent.set(link).is_err() {
175            return Ok(());
176        }
177        // A concurrent join elsewhere may have closed a cycle through
178        // this link after the check above. The last link a cycle needs
179        // sees the whole cycle here, so severing it keeps every scope's
180        // chain acyclic.
181        if parent.reaches(self)
182            && let Some(link) = self.0.parent.get()
183        {
184            link.live.store(false, Ordering::Release);
185            return Err(ScopeJoinError::Cycle);
186        }
187        Ok(())
188    }
189
190    /// Whether `target` is this scope or one it delegates to.
191    fn reaches(&self, target: &ResourceScope) -> bool {
192        let mut scope = self;
193        loop {
194            if scope.same_scope(target) {
195                return true;
196            }
197            match scope.parent() {
198                Some(parent) => scope = parent,
199                None => return false,
200            }
201        }
202    }
203
204    /// Look up an already-attached resource's accessor payload by
205    /// fingerprint `key`: through this scope's accessor when one is
206    /// installed, and otherwise through the parent it joined.
207    ///
208    /// Returns `None` when no scope on the way has an accessor or when
209    /// the accessor that answers holds no live entry under `key`. The
210    /// lookup is synchronous and never blocks.
211    pub fn lookup(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
212        let mut scope = self;
213        loop {
214            if let Some(accessor) = scope.0.accessor.get() {
215                return accessor.lookup(key);
216            }
217            scope = scope.parent()?;
218        }
219    }
220
221    /// Whether `self` and `other` are handles to one scope.
222    pub fn same_scope(&self, other: &ResourceScope) -> bool {
223        Arc::ptr_eq(&self.0, &other.0)
224    }
225}
226
227impl std::fmt::Debug for ResourceScope {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        f.debug_struct("ResourceScope")
230            .field("installed", &self.is_installed())
231            .field("joined", &self.parent().is_some())
232            .finish()
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    struct Fixed(&'static str, u64);
241
242    impl ResourceAccessor for Fixed {
243        fn lookup(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
244            (key == self.0).then(|| Arc::new(self.1) as Arc<dyn Any + Send + Sync>)
245        }
246    }
247
248    fn read(scope: &ResourceScope) -> Option<u64> {
249        scope
250            .lookup("k")
251            .and_then(|v| v.downcast::<u64>().ok())
252            .map(|v| *v)
253    }
254
255    #[test]
256    fn an_empty_scope_answers_none() {
257        assert!(ResourceScope::new().lookup("k").is_none());
258    }
259
260    #[test]
261    fn clones_share_one_slot_and_install_once() {
262        let scope = ResourceScope::new();
263        let child = scope.clone();
264        assert!(scope.install(Arc::new(Fixed("k", 7))).is_ok());
265        assert_eq!(read(&child), Some(7));
266        assert!(scope.install(Arc::new(Fixed("k", 8))).is_err());
267        assert!(child.same_scope(&scope));
268        assert!(!ResourceScope::new().same_scope(&scope));
269    }
270
271    #[test]
272    fn a_joined_scope_resolves_through_its_parent_until_it_installs_its_own() {
273        let root = ResourceScope::new();
274        let child = ResourceScope::new();
275        let grandchild = ResourceScope::new();
276        child.join(&root).unwrap();
277        grandchild.join(&child).unwrap();
278        assert_eq!(read(&grandchild), None, "no accessor anywhere yet");
279        root.install(Arc::new(Fixed("k", 1))).ok();
280        assert_eq!(read(&grandchild), Some(1), "installed once at the root");
281        child.install(Arc::new(Fixed("k", 2))).ok();
282        assert_eq!(read(&grandchild), Some(2), "the nearest accessor answers");
283        assert_eq!(read(&root), Some(1), "a parent never reads its child's");
284    }
285
286    #[test]
287    fn a_scope_with_its_own_accessor_keeps_it() {
288        let root = ResourceScope::with_accessor(Arc::new(Fixed("k", 1)));
289        let own = ResourceScope::with_accessor(Arc::new(Fixed("k", 2)));
290        own.join(&root).unwrap();
291        assert!(own.parent().is_none());
292        assert_eq!(read(&own), Some(2));
293    }
294
295    #[test]
296    fn a_scope_never_delegates_to_itself_or_a_descendant() {
297        let root = ResourceScope::new();
298        root.join(&root).unwrap();
299        assert!(root.parent().is_none(), "joining itself changes nothing");
300        let child = ResourceScope::new();
301        child.join(&root).unwrap();
302        let grandchild = ResourceScope::new();
303        grandchild.join(&child).unwrap();
304        root.join(&grandchild).unwrap();
305        assert!(
306            root.parent().is_none(),
307            "an ancestor bound under its descendant stays a root"
308        );
309    }
310
311    #[test]
312    fn the_first_join_wins() {
313        let a = ResourceScope::with_accessor(Arc::new(Fixed("k", 1)));
314        let b = ResourceScope::with_accessor(Arc::new(Fixed("k", 2)));
315        let child = ResourceScope::new();
316        child.join(&a).unwrap();
317        child.join(&b).unwrap();
318        assert!(child.parent().is_some_and(|p| p.same_scope(&a)));
319        assert_eq!(read(&child), Some(1), "the second join changed nothing");
320    }
321
322    #[test]
323    fn concurrent_joins_never_close_a_cycle() {
324        for _ in 0..200 {
325            let scopes: Vec<ResourceScope> = (0..4).map(|_| ResourceScope::new()).collect();
326            std::thread::scope(|s| {
327                for i in 0..4 {
328                    let (from, to) = (scopes[i].clone(), scopes[(i + 1) % 4].clone());
329                    s.spawn(move || from.join(&to));
330                }
331            });
332            // Every chain ends: at most three links of four stand.
333            let live = scopes.iter().filter(|s| s.parent().is_some()).count();
334            assert!(live < 4, "a cycle of joins stood");
335            scopes[3].install(Arc::new(Fixed("k", 9))).ok();
336            for s in &scopes {
337                let _ = read(s);
338            }
339        }
340    }
341}