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}