Skip to main content

tuwunel_core/config/
manager.rs

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