Skip to main content

vortex_session/
session.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! The [`VortexSession`] container.
5//!
6//! A [`VortexSession`] is a type-map of [`VortexSessionVar`]s keyed by [`TypeId`]. It is backed by
7//! an [`ArcSwapMap`], giving lock-free reads and copy-on-write writes:
8//!
9//! * **Reads** ([`SessionExt::get`], [`SessionExt::get_opt`]) load the current snapshot
10//!   without taking any lock and hand back a [`SessionGuard`] that derefs to the variable. Because a
11//!   read never takes a lock, it can never deadlock, and because it never holds a lock across the
12//!   returned reference there is no reader/writer contention.
13//!
14//! * **Writes** ([`VortexSession::with_some`], [`SessionExt::get`] on a missing default) are
15//!   copy-on-write: the map is cloned, the change applied to the private copy, and the new map
16//!   atomically published. The value is constructed *before* the map is updated, so no user code
17//!   (in particular, no `Default::default` implementation) ever runs while a lock is held. This is
18//!   the key difference from the previous `DashMap`-backed session, where
19//!   `entry().or_insert_with(f)` ran `f` while holding the shard's write lock and could deadlock if
20//!   `f` re-entered the session.
21//!
22//! A modified session is produced by mutating it **in place**: [`VortexSession::with_some`] — and
23//! the configuration `with_*` helpers built on it — apply their change copy-on-write to the shared
24//! backing cell. Clones of a session share that cell, so a variable registered through one clone
25//! (or one `with_*` call) is visible to all of them. This is what late plugin/encoding registration
26//! relies on.
27//!
28//! To build a session from scratch, start from [`VortexSession::empty`] and chain the `with_*`
29//! helpers. Each [`empty`](VortexSession::empty) creates its own backing cell, so a session built
30//! this way is independent of any other.
31
32use std::any::TypeId;
33use std::any::type_name;
34use std::fmt::Debug;
35use std::fmt::Formatter;
36use std::hash::BuildHasherDefault;
37use std::marker::PhantomData;
38use std::ops::Deref;
39use std::ops::DerefMut;
40use std::sync::Arc;
41
42use arc_swap::Guard;
43use vortex_error::VortexExpect;
44use vortex_error::vortex_panic;
45use vortex_utils::aliases::hash_map::HashMap;
46
47use crate::ArcSwapMap;
48use crate::IdHasher;
49use crate::SessionExt;
50use crate::SessionVar;
51use crate::UnknownPluginPolicy;
52
53/// A [`SessionVar`] that can be stored in a [`VortexSession`].
54///
55/// This trait is implemented automatically for every [`SessionVar`] that is also [`Clone`], so
56/// types opt in by implementing [`Clone`] rather than implementing this trait directly. The trait
57/// itself stays object-safe (so it can be stored as `Arc<dyn VortexSessionVar>`); the [`Clone`]
58/// bound lives on the blanket impl rather than the trait.
59///
60/// Requiring [`Clone`] lets the configuration `with_*` helpers read a variable, modify a copy, and
61/// re-insert the result.
62pub trait VortexSessionVar: SessionVar {}
63
64impl<V: SessionVar + Clone> VortexSessionVar for V {}
65
66/// The hasher for the session type-map; [`TypeId`]s are already hashes.
67type IdHashBuilder = BuildHasherDefault<IdHasher>;
68
69/// The immutable type-map backing a published [`VortexSession`] snapshot.
70type SessionVars = HashMap<TypeId, Arc<dyn VortexSessionVar>, IdHashBuilder>;
71
72/// The shared, copy-on-write store that publishes [`SessionVars`] snapshots.
73type SharedSessionVars = ArcSwapMap<TypeId, Arc<dyn VortexSessionVar>, IdHashBuilder>;
74
75/// A reference to a session variable of type `V`, returned by [`SessionExt::get`] and
76/// [`SessionExt::get_opt`].
77///
78/// It borrows the session's current snapshot through an [`arc_swap::Guard`], so reads never take a
79/// lock or a full [`Arc`] clone. The guard is tied to the session borrow it was read from, so it is
80/// meant to be used on the stack rather than stored in a long-lived data structure (holding it pins
81/// an internal arc-swap slot, which can contend with concurrent writers). `SessionGuard` derefs to
82/// `V`, so it can be used wherever a `&V` is expected.
83pub struct SessionGuard<'a, V> {
84    snapshot: Guard<Arc<SessionVars>>,
85    _session: PhantomData<&'a VortexSession>,
86    _marker: PhantomData<fn() -> V>,
87}
88
89impl<V: VortexSessionVar> Deref for SessionGuard<'_, V> {
90    type Target = V;
91
92    fn deref(&self) -> &V {
93        // The constructor of `SessionGuard` guarantees the variable is present in `snapshot`.
94        self.snapshot
95            .get(&TypeId::of::<V>())
96            .vortex_expect("SessionGuard invariant: variable present in snapshot")
97            .as_any()
98            .downcast_ref::<V>()
99            .vortex_expect("Type mismatch - this is a bug")
100    }
101}
102
103impl<V: VortexSessionVar> Debug for SessionGuard<'_, V> {
104    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105        Debug::fmt(&**self, f)
106    }
107}
108
109/// A copy-on-write mutable handle to a session variable of type `V`, returned by
110/// [`SessionExt::get_mut`].
111///
112/// It holds a private clone of the variable and exposes it through [`DerefMut`]. When the handle is
113/// dropped, the (possibly mutated) value is re-inserted into the
114/// session, replacing the previous one. Because the session stores each variable behind a shared
115/// [`Arc`] observed by every clone, handing out a plain `&mut` to the stored value would be unsound;
116/// this guard provides mutable access by cloning on read and publishing on drop instead.
117pub struct SessionMut<'a, V: VortexSessionVar> {
118    session: &'a VortexSession,
119    // `Some` for the whole lifetime of the guard; taken in `drop` to move it back into the session.
120    value: Option<V>,
121}
122
123impl<V: VortexSessionVar> Deref for SessionMut<'_, V> {
124    type Target = V;
125
126    fn deref(&self) -> &V {
127        self.value
128            .as_ref()
129            .vortex_expect("SessionMut invariant: value present until drop")
130    }
131}
132
133impl<V: VortexSessionVar> DerefMut for SessionMut<'_, V> {
134    fn deref_mut(&mut self) -> &mut V {
135        self.value
136            .as_mut()
137            .vortex_expect("SessionMut invariant: value present until drop")
138    }
139}
140
141impl<V: VortexSessionVar> Drop for SessionMut<'_, V> {
142    fn drop(&mut self) {
143        if let Some(value) = self.value.take() {
144            self.session.register(value);
145        }
146    }
147}
148
149impl<V: VortexSessionVar> Debug for SessionMut<'_, V> {
150    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151        Debug::fmt(&**self, f)
152    }
153}
154
155/// A Vortex session encapsulates the set of extensible arrays, layouts, compute functions,
156/// dtypes, etc. that are available for use in a given context.
157///
158/// It is also the entry-point passed to dynamic libraries to initialize Vortex plugins.
159///
160/// Cloning a session is cheap and shares the backing store: a variable registered through one
161/// clone (via [`VortexSession::with_some`] or one of the `with_*` helpers) is observed by all
162/// clones. To build an *independent* session, start from [`VortexSession::empty`].
163#[derive(Clone)]
164pub struct VortexSession(SharedSessionVars);
165
166impl Debug for VortexSession {
167    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
168        self.0
169            .read(|vars| f.debug_tuple("VortexSession").field(vars).finish())
170    }
171}
172
173impl VortexSession {
174    /// Create a new [`VortexSession`] with no session state.
175    pub fn empty() -> Self {
176        Self(SharedSessionVars::default())
177    }
178
179    /// Inserts `V::default()` if no variable of type `V` is present yet, copy-on-write.
180    ///
181    /// The default is constructed *before* the map is updated, so `V::default()` never runs under a
182    /// lock and is never run more than once — it may therefore freely re-enter the session (read or
183    /// even register other variables) without risk of deadlock. If a concurrent writer published a
184    /// value first, that value is kept and the prebuilt default is simply dropped.
185    fn insert_default<V: VortexSessionVar + Default>(&self) {
186        let default: Arc<dyn VortexSessionVar> = Arc::new(V::default());
187        self.0.insert_if_absent(TypeId::of::<V>(), default);
188    }
189
190    /// Inserts a session variable of type `V`, replacing any existing variable of that type.
191    ///
192    /// This is the copy-on-write insert primitive behind [`with_some`](Self::with_some) and
193    /// [`get_mut`](SessionExt::get_mut); it is not public, so a variable can only enter the type-map
194    /// through those (or through a default inserted by [`get`](SessionExt::get)). The mutation is
195    /// applied in place to the shared backing store, so it is visible through every clone.
196    pub fn register<V: VortexSessionVar>(&self, var: V) {
197        self.0.insert(TypeId::of::<V>(), Arc::new(var));
198    }
199
200    /// Inserts a new session variable of type `V` with its default value, mutating this session in
201    /// place and returning it for chaining.
202    ///
203    /// The change is applied copy-on-write to the shared backing store, so it is observed through
204    /// every clone of this session.
205    ///
206    /// # Panics
207    ///
208    /// If a variable of that type already exists.
209    pub fn with<V: VortexSessionVar + Default>(self) -> Self {
210        self.with_some(V::default())
211    }
212
213    /// Inserts a new session variable of type `V`, mutating this session in place and returning it
214    /// for chaining.
215    ///
216    /// The change is applied copy-on-write to the shared backing store, so it is observed through
217    /// every clone of this session.
218    ///
219    /// # Panics
220    ///
221    /// If a variable of that type already exists.
222    pub fn with_some<V: VortexSessionVar>(self, var: V) -> Self {
223        if self.get_opt::<V>().is_some() {
224            vortex_panic!(
225                "Session variable of type {} already exists",
226                type_name::<V>()
227            );
228        }
229        self.register(var);
230        self
231    }
232
233    /// Returns whether unknown plugins should deserialize as foreign placeholders.
234    pub fn allows_unknown(&self) -> bool {
235        self.get_opt::<UnknownPluginPolicy>()
236            .is_some_and(|p| p.allow_unknown)
237    }
238
239    /// Allow deserializing unknown plugin IDs as non-executable foreign placeholders.
240    ///
241    /// Mutates this session in place and returns it for chaining.
242    pub fn allow_unknown(&self) {
243        self.get_mut::<UnknownPluginPolicy>().allow_unknown = true;
244    }
245}
246
247impl SessionExt for VortexSession {
248    fn session(&self) -> VortexSession {
249        self.clone()
250    }
251
252    fn get<V: VortexSessionVar + Default>(&self) -> SessionGuard<'_, V> {
253        if self.get_opt::<V>().is_none() {
254            self.insert_default::<V>();
255        }
256        self.get_opt::<V>()
257            .vortex_expect("variable was just inserted")
258    }
259
260    fn get_opt<V: VortexSessionVar>(&self) -> Option<SessionGuard<'_, V>> {
261        let snapshot = self.0.load();
262        snapshot
263            .contains_key(&TypeId::of::<V>())
264            .then(|| SessionGuard {
265                snapshot,
266                _session: PhantomData,
267                _marker: PhantomData,
268            })
269    }
270
271    fn get_mut<V: VortexSessionVar + Default + Clone>(&self) -> SessionMut<'_, V> {
272        let value = (*self.get::<V>()).clone();
273        SessionMut {
274            session: self,
275            value: Some(value),
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use std::any::Any;
283
284    use super::VortexSession;
285    use crate::SessionExt;
286    use crate::SessionVar;
287
288    #[derive(Clone, Debug, Default, PartialEq, Eq)]
289    struct Counter {
290        count: u32,
291    }
292
293    impl SessionVar for Counter {
294        fn as_any(&self) -> &dyn Any {
295            self
296        }
297
298        fn as_any_mut(&mut self) -> &mut dyn Any {
299            self
300        }
301    }
302
303    #[derive(Clone, Debug, Default)]
304    struct Other;
305
306    impl SessionVar for Other {
307        fn as_any(&self) -> &dyn Any {
308            self
309        }
310
311        fn as_any_mut(&mut self) -> &mut dyn Any {
312            self
313        }
314    }
315
316    thread_local! {
317        /// Lets `Reentrant::default` reach back into the session it is being inserted into.
318        static REENTRANT_SESSION: std::cell::RefCell<Option<VortexSession>> =
319            const { std::cell::RefCell::new(None) };
320    }
321
322    #[derive(Clone, Debug)]
323    struct Reentrant {
324        inner: u32,
325    }
326
327    impl Default for Reentrant {
328        fn default() -> Self {
329            // Re-enter the *same* session while this default is being constructed, itself
330            // triggering another default insertion. This is only sound because the default is
331            // built outside the `rcu` closure (no lock held); if it ran under the closure this
332            // would deadlock or recurse forever.
333            REENTRANT_SESSION.with(|s| {
334                if let Some(session) = s.borrow().as_ref() {
335                    drop(session.get::<Counter>());
336                }
337            });
338            Reentrant { inner: 7 }
339        }
340    }
341
342    impl SessionVar for Reentrant {
343        fn as_any(&self) -> &dyn Any {
344            self
345        }
346
347        fn as_any_mut(&mut self) -> &mut dyn Any {
348            self
349        }
350    }
351
352    #[test]
353    fn with_some_round_trip() {
354        let session = VortexSession::empty().with_some(Counter { count: 1 });
355
356        assert_eq!(*session.get::<Counter>(), Counter { count: 1 });
357        assert!(session.get_opt::<Other>().is_none());
358    }
359
360    #[test]
361    fn get_inserts_default() {
362        let session = VortexSession::empty();
363        assert!(session.get_opt::<Counter>().is_none());
364
365        assert_eq!(session.get::<Counter>().count, 0);
366        // The default was published, so it is now observable.
367        assert!(session.get_opt::<Counter>().is_some());
368    }
369
370    #[test]
371    fn register_is_visible_through_clones() {
372        let session = VortexSession::empty();
373        let clone = session.clone();
374
375        session.register(Counter { count: 7 });
376
377        // Registration mutates the shared backing store.
378        assert_eq!(clone.get::<Counter>().count, 7);
379    }
380
381    #[test]
382    fn with_some_mutates_shared_store() {
383        let session = VortexSession::empty();
384        let clone = session.clone();
385
386        let configured = session.with_some(Counter { count: 5 });
387        assert_eq!(configured.get::<Counter>().count, 5);
388
389        // `with_some` mutates the shared backing store in place, so the clone observes it too.
390        assert_eq!(clone.get::<Counter>().count, 5);
391    }
392
393    #[test]
394    fn allow_unknown_mutates_shared_store() {
395        let session = VortexSession::empty();
396        let clone = session.clone();
397        assert!(!clone.allows_unknown());
398
399        session.allow_unknown();
400
401        // The flag is flipped on the shared backing store.
402        assert!(clone.allows_unknown());
403    }
404
405    #[test]
406    fn empty_sessions_are_independent() {
407        // Each `empty()` creates its own backing cell, so separately built sessions do not share
408        // state.
409        let session = VortexSession::empty().with_some(Counter { count: 1 });
410        let other = VortexSession::empty().with_some(Counter { count: 2 });
411
412        session.register(Counter { count: 9 });
413        assert_eq!(session.get::<Counter>().count, 9);
414        assert_eq!(other.get::<Counter>().count, 2);
415    }
416
417    #[test]
418    #[should_panic(expected = "already exists")]
419    fn with_some_duplicate_panics() {
420        VortexSession::empty()
421            .with::<Counter>()
422            .with_some(Counter { count: 1 });
423    }
424
425    #[test]
426    fn allow_unknown_flag_is_opt_in() {
427        let session = VortexSession::empty();
428        assert!(!session.allows_unknown());
429
430        session.allow_unknown();
431        assert!(session.allows_unknown());
432    }
433
434    #[test]
435    fn get_opt_does_not_insert_a_default() {
436        let session = VortexSession::empty();
437
438        // Unlike `get`, `get_opt` is a pure read and never publishes a default.
439        assert!(session.get_opt::<Counter>().is_none());
440        assert!(session.get_opt::<Counter>().is_none());
441    }
442
443    #[test]
444    fn inserting_a_default_while_holding_a_guard_succeeds() {
445        let session = VortexSession::empty().with_some(Counter { count: 5 });
446
447        // Hold a read guard for one variable...
448        let counter = session.get::<Counter>();
449
450        // ...then read a *missing* variable, which inserts its default copy-on-write via `rcu`
451        // while `counter` is still alive. A `SessionGuard` is a lock-free snapshot, not a lock, so
452        // the write proceeds without waiting on the outstanding guard rather than deadlocking.
453        let other = session.get::<Other>();
454
455        // The held guard still observes the snapshot it was read from (which arc-swap keeps alive
456        // for the guard's lifetime), and the freshly read guard sees the just-inserted default.
457        assert_eq!(counter.count, 5);
458        let _: &Other = &other;
459
460        // The inserted default is also observable through a subsequent independent read.
461        assert!(session.get_opt::<Other>().is_some());
462    }
463
464    #[test]
465    fn a_held_guard_keeps_observing_its_own_snapshot_after_a_write() {
466        let session = VortexSession::empty().with_some(Counter { count: 1 });
467
468        let held = session.get::<Counter>();
469        session.register(Counter { count: 2 });
470
471        // The held guard pins the snapshot it was read from, so it still sees the old value, while
472        // a fresh read sees the newly published one.
473        assert_eq!(held.count, 1);
474        assert_eq!(session.get::<Counter>().count, 2);
475    }
476
477    #[test]
478    fn default_insertion_may_reenter_the_session_without_deadlocking() {
479        let session = VortexSession::empty();
480        REENTRANT_SESSION.with(|s| *s.borrow_mut() = Some(session.clone()));
481
482        // `get::<Reentrant>` inserts a default; building that default re-enters the same session
483        // via `get::<Counter>` (another default insertion). Because each default is constructed
484        // outside the `rcu` closure, both inserts complete instead of deadlocking.
485        assert_eq!(session.get::<Reentrant>().inner, 7);
486        assert!(session.get_opt::<Reentrant>().is_some());
487        assert!(session.get_opt::<Counter>().is_some());
488
489        REENTRANT_SESSION.with(|s| *s.borrow_mut() = None);
490    }
491
492    #[test]
493    fn get_mut_publishes_on_drop() {
494        let session = VortexSession::empty();
495        session.register(Counter { count: 1 });
496
497        session.get_mut::<Counter>().count = 42;
498
499        assert_eq!(session.get::<Counter>().count, 42);
500    }
501
502    #[test]
503    fn get_mut_inserts_default_then_mutates() {
504        let session = VortexSession::empty();
505        assert!(session.get_opt::<Counter>().is_none());
506
507        session.get_mut::<Counter>().count += 5;
508
509        assert_eq!(session.get::<Counter>().count, 5);
510    }
511
512    #[test]
513    fn get_mut_mutation_is_visible_through_clones() {
514        let session = VortexSession::empty().with_some(Counter { count: 1 });
515        let clone = session.clone();
516
517        session.get_mut::<Counter>().count = 9;
518
519        // The mutated value is published copy-on-write to the shared backing store.
520        assert_eq!(clone.get::<Counter>().count, 9);
521    }
522}