Skip to main content

tuwunel_core/config/
manager.rs

1//! Maintains the active reloadable configuration.
2//!
3//! [`Manager`] exposes the current [`Config`] through `Deref` and atomically
4//! replaces it on reload. It also manages the lifetime of configurations still
5//! visible to readers.
6
7use std::{
8	cell::{Cell, RefCell},
9	ops::Deref,
10	ptr,
11	ptr::null_mut,
12	sync::{
13		Arc,
14		atomic::{AtomicPtr, Ordering},
15	},
16};
17
18use super::Config;
19use crate::{Result, implement};
20
21/// Provides transparent access to the server's reloadable configuration.
22///
23/// `Deref` exposes the active [`Config`], so callers can read configuration
24/// values without handling the indirection required for reloads.
25pub struct Manager {
26	active: AtomicPtr<Config>,
27}
28
29thread_local! {
30	static INDEX: Cell<usize> = const { Cell::new(0_usize) };
31	static HANDLE: RefCell<Handles> = const {
32		RefCell::new([const { None }; HISTORY])
33	};
34}
35
36type Handle = Option<Arc<Config>>;
37type Handles = [Handle; HISTORY];
38
39const HISTORY: usize = 8;
40
41impl Manager {
42	pub(crate) fn new(config: Config) -> Self {
43		let config = Arc::new(config);
44		Self {
45			active: AtomicPtr::new(Arc::into_raw(config).cast_mut()),
46		}
47	}
48}
49
50impl Drop for Manager {
51	fn drop(&mut self) {
52		let config = self.active.swap(null_mut(), Ordering::AcqRel);
53
54		// SAFETY: The active pointer was set using an Arc::into_raw(). We're obliged to
55		// reconstitute that into Arc otherwise it will leak.
56		unsafe { Arc::from_raw(config) };
57	}
58}
59
60impl Deref for Manager {
61	type Target = Arc<Config>;
62
63	fn deref(&self) -> &Self::Target { HANDLE.with_borrow_mut(|handle| self.load(handle)) }
64}
65
66/// Update the active configuration, returning prior configuration.
67#[implement(Manager)]
68#[tracing::instrument(skip_all)]
69pub fn update(&self, config: Config) -> Result<Arc<Config>> {
70	let config = Arc::new(config);
71	let new = Arc::into_raw(config);
72	let old = self.active.swap(new.cast_mut(), Ordering::AcqRel);
73
74	// SAFETY: The old active pointer was set using an Arc::into_raw(). We're
75	// obliged to reconstitute that into Arc otherwise it will leak.
76	Ok(unsafe { Arc::from_raw(old) })
77}
78
79#[implement(Manager)]
80fn load(&self, handle: &mut [Option<Arc<Config>>]) -> &'static Arc<Config> {
81	let config = self.active.load(Ordering::Acquire);
82
83	// Branch taken after config reload or first access by this thread.
84	if handle[INDEX.get()]
85		.as_ref()
86		.is_none_or(|handle| !ptr::eq(config, Arc::as_ptr(handle)))
87	{
88		INDEX.set(INDEX.get().wrapping_add(1).wrapping_rem(HISTORY));
89		return load_miss(handle, INDEX.get(), config);
90	}
91
92	let config: &Arc<Config> = handle[INDEX.get()]
93		.as_ref()
94		.expect("handle was already cached for this thread");
95
96	// SAFETY: The caller should not hold multiple references at a time directly
97	// into Config, as a subsequent reference might invalidate the thread's cache
98	// causing another reference to dangle.
99	//
100	// This is a highly unusual pattern as most config values are copied by value or
101	// used immediately without running overlap with another value. Even if it does
102	// actually occur somewhere, the window of danger is limited to the config being
103	// reloaded while the reference is held and another access is made by the same
104	// thread into a different config value. This is mitigated by creating a buffer
105	// of old configs rather than discarding at the earliest opportunity; the odds
106	// of this scenario are thus astronomical.
107	unsafe { std::mem::transmute(config) }
108}
109
110#[tracing::instrument(
111	name = "miss",
112	level = "trace",
113	skip_all,
114	fields(%index, ?config)
115)]
116#[expect(clippy::transmute_ptr_to_ptr)]
117fn load_miss(
118	handle: &mut [Option<Arc<Config>>],
119	index: usize,
120	config: *const Config,
121) -> &'static Arc<Config> {
122	// SAFETY: The active pointer was set prior and always remains valid. The
123	// count is incremented for the new reference reconstituted below.
124	unsafe { Arc::increment_strong_count(config) };
125
126	// SAFETY: Reconstitutes the Arc against the increment above. This instance
127	// will be cached in the thread-local.
128	let config = unsafe { Arc::from_raw(config) };
129
130	// SAFETY: See the note on the transmute above. The caller should not hold more
131	// than one reference at a time directly into Config, as the second access
132	// might invalidate the thread's cache, dangling the reference to the first.
133	unsafe { std::mem::transmute(handle[index].insert(config)) }
134}