use std::{ cell::{Cell, RefCell}, ops::Deref, ptr, ptr::null_mut, sync::{ Arc, atomic::{AtomicPtr, Ordering}, }, }; use super::Config; use crate::{Result, implement}; /// The configuration manager is an indirection to reload the configuration for /// the server while it is running. In order to not burden or clutter the many /// callsites which query for configuration items, this object implements Deref /// for the actively loaded configuration. pub struct Manager { active: AtomicPtr, } thread_local! { static INDEX: Cell = 0.into(); static HANDLE: RefCell = const { RefCell::new([const { None }; HISTORY]) }; } type Handle = Option>; type Handles = [Handle; HISTORY]; const HISTORY: usize = 8; impl Manager { pub(crate) fn new(config: Config) -> Self { let config = Arc::new(config); Self { active: AtomicPtr::new(Arc::into_raw(config).cast_mut()), } } } impl Drop for Manager { fn drop(&mut self) { let config = self.active.swap(null_mut(), Ordering::AcqRel); // SAFETY: The active pointer was set using an Arc::into_raw(). We're obliged to // reconstitute that into Arc otherwise it will leak. unsafe { Arc::from_raw(config) }; } } impl Deref for Manager { type Target = Arc; fn deref(&self) -> &Self::Target { HANDLE.with_borrow_mut(|handle| self.load(handle)) } } /// Update the active configuration, returning prior configuration. #[implement(Manager)] #[tracing::instrument(skip_all)] pub fn update(&self, config: Config) -> Result> { let config = Arc::new(config); let new = Arc::into_raw(config); let old = self.active.swap(new.cast_mut(), Ordering::AcqRel); // SAFETY: The old active pointer was set using an Arc::into_raw(). We're // obliged to reconstitute that into Arc otherwise it will leak. Ok(unsafe { Arc::from_raw(old) }) } #[implement(Manager)] fn load(&self, handle: &mut [Option>]) -> &'static Arc { let config = self.active.load(Ordering::Acquire); // Branch taken after config reload or first access by this thread. if handle[INDEX.get()] .as_ref() .is_none_or(|handle| !ptr::eq(config, Arc::as_ptr(handle))) { INDEX.set(INDEX.get().wrapping_add(1).wrapping_rem(HISTORY)); return load_miss(handle, INDEX.get(), config); } let config: &Arc = handle[INDEX.get()] .as_ref() .expect("handle was already cached for this thread"); // SAFETY: The caller should not hold multiple references at a time directly // into Config, as a subsequent reference might invalidate the thread's cache // causing another reference to dangle. // // This is a highly unusual pattern as most config values are copied by value or // used immediately without running overlap with another value. Even if it does // actually occur somewhere, the window of danger is limited to the config being // reloaded while the reference is held and another access is made by the same // thread into a different config value. This is mitigated by creating a buffer // of old configs rather than discarding at the earliest opportunity; the odds // of this scenario are thus astronomical. unsafe { std::mem::transmute(config) } } #[tracing::instrument( name = "miss", level = "trace", skip_all, fields(%index, ?config) )] #[allow(clippy::transmute_ptr_to_ptr)] fn load_miss( handle: &mut [Option>], index: usize, config: *const Config, ) -> &'static Arc { // SAFETY: The active pointer was set prior and always remains valid. We're // reconstituting the Arc here but as a new reference, so the count is // incremented. This instance will be cached in the thread-local. let config = unsafe { Arc::increment_strong_count(config); Arc::from_raw(config) }; // SAFETY: See the note on the transmute above. The caller should not hold more // than one reference at a time directly into Config, as the second access // might invalidate the thread's cache, dangling the reference to the first. unsafe { std::mem::transmute(handle[index].insert(config)) } }