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}