#![forbid(unsafe_code)]
#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Default)]
pub struct Snapshot {
sections: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
registered: Vec<(TypeId, &'static str)>,
}
impl Snapshot {
#[must_use]
pub fn get<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
self.sections
.get(&TypeId::of::<T>())
.cloned()
.and_then(|section| section.downcast::<T>().ok())
}
pub fn require<T: Any + Send + Sync>(&self) -> Result<Arc<T>, NotInScope> {
match self.get::<T>() {
Some(section) => Ok(section),
None => {
let id = TypeId::of::<T>();
let name = std::any::type_name::<T>();
if self.registered.iter().any(|(known, _)| *known == id) {
Err(NotInScope::NotLoaded(name))
} else {
Err(NotInScope::NotListed(name))
}
}
}
}
#[must_use]
pub fn len(&self) -> usize {
self.sections.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.sections.is_empty()
}
#[must_use]
pub fn names(&self) -> Vec<&'static str> {
self.registered.iter().map(|(_, name)| *name).collect()
}
#[must_use]
pub fn merged_with(mut self, inner: Self) -> Self {
for (id, name) in inner.registered {
if !self.registered.iter().any(|(known, _)| *known == id) {
self.registered.push((id, name));
}
}
self.sections.extend(inner.sections);
self
}
}
impl fmt::Debug for Snapshot {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Snapshot")
.field("sections", &self.names())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotInScope {
NotListed(&'static str),
NotLoaded(&'static str),
}
impl NotInScope {
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::NotListed(name) | Self::NotLoaded(name) => name,
}
}
}
impl fmt::Display for NotInScope {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotListed(name) => write!(
formatter,
"`{name}` is not one of this request's sections; add it where the \
layer is built, with `sections![.., {name}]`"
),
Self::NotLoaded(name) => write!(
formatter,
"`{name}` is one of this request's sections but nothing had loaded \
it when the request began; call `init()` before serving"
),
}
}
}
impl std::error::Error for NotInScope {}
#[derive(Default)]
pub struct Sections {
readers: Vec<Registered>,
}
struct Registered {
id: TypeId,
name: &'static str,
read: Reader,
generation: Option<Generation>,
}
type Reader = Box<dyn Fn() -> Option<Arc<dyn Any + Send + Sync>> + Send + Sync>;
type Generation = Box<dyn Fn() -> u64 + Send + Sync>;
const ATTEMPTS: usize = 8;
impl Sections {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn section<T, F>(self, read: F) -> Self
where
T: Any + Send + Sync,
F: Fn() -> Option<Arc<T>> + Send + Sync + 'static,
{
self.push::<T>(read, None)
}
#[must_use]
pub fn section_with_generation<T, F, G>(self, read: F, generation: G) -> Self
where
T: Any + Send + Sync,
F: Fn() -> Option<Arc<T>> + Send + Sync + 'static,
G: Fn() -> u64 + Send + Sync + 'static,
{
self.push::<T>(read, Some(Box::new(generation)))
}
fn push<T>(
mut self,
read: impl Fn() -> Option<Arc<T>> + Send + Sync + 'static,
generation: Option<Generation>,
) -> Self
where
T: Any + Send + Sync,
{
let id = TypeId::of::<T>();
self.readers.retain(|existing| existing.id != id);
self.readers.push(Registered {
id,
name: std::any::type_name::<T>(),
read: Box::new(move || read().map(|section| section as Arc<dyn Any + Send + Sync>)),
generation,
});
self
}
#[must_use]
pub fn is_consistent(&self) -> bool {
self.readers
.iter()
.all(|section| section.generation.is_some())
}
#[must_use]
pub fn take(&self) -> Snapshot {
if self.readers.len() < 2 || !self.is_consistent() {
return self.read_once();
}
for _ in 0..ATTEMPTS {
let before = self.generations();
let snapshot = self.read_once();
if self.generations() == before {
return snapshot;
}
}
self.read_once()
}
fn read_once(&self) -> Snapshot {
let mut sections = HashMap::with_capacity(self.readers.len());
let mut registered = Vec::with_capacity(self.readers.len());
for section in &self.readers {
registered.push((section.id, section.name));
if let Some(value) = (section.read)() {
sections.insert(section.id, value);
}
}
Snapshot {
sections,
registered,
}
}
fn generations(&self) -> Vec<u64> {
self.readers
.iter()
.map(|section| section.generation.as_ref().map_or(0, |read| read()))
.collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.readers.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.readers.is_empty()
}
#[must_use]
pub fn names(&self) -> Vec<&'static str> {
self.readers.iter().map(|section| section.name).collect()
}
}
impl fmt::Debug for Sections {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Sections")
.field("sections", &self.names())
.finish()
}
}
#[macro_export]
macro_rules! sections {
() => {
$crate::Sections::new()
};
($($section:ty),+ $(,)?) => {{
$crate::Sections::new()
$(.section_with_generation(
|| <$section>::try_current(),
|| <$section>::generation(),
))+
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
struct Server {
port: u16,
}
#[derive(Debug, PartialEq)]
struct Features {
cache: bool,
}
#[derive(Debug)]
struct NeverLoaded;
fn server(port: u16) -> impl Fn() -> Option<Arc<Server>> + Send + Sync {
move || Some(Arc::new(Server { port }))
}
#[test]
fn a_snapshot_answers_every_section_it_took() {
let snapshot = Sections::new()
.section(server(8080))
.section(|| Some(Arc::new(Features { cache: true })))
.take();
assert_eq!(snapshot.len(), 2);
assert_eq!(snapshot.get::<Server>().unwrap().port, 8080);
assert!(snapshot.get::<Features>().unwrap().cache);
}
#[test]
fn two_reads_of_one_snapshot_are_the_same_arc() {
let snapshot = Sections::new().section(server(8080)).take();
let first = snapshot.get::<Server>().unwrap();
let second = snapshot.get::<Server>().unwrap();
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn a_snapshot_does_not_move_when_the_source_does() {
use std::sync::atomic::{AtomicU16, Ordering};
static PORT: AtomicU16 = AtomicU16::new(8080);
let sections = Sections::new().section(|| {
Some(Arc::new(Server {
port: PORT.load(Ordering::Relaxed),
}))
});
let taken = sections.take();
PORT.store(9090, Ordering::Relaxed);
assert_eq!(taken.get::<Server>().unwrap().port, 8080);
assert_eq!(sections.take().get::<Server>().unwrap().port, 9090);
}
#[test]
fn a_section_that_never_loaded_is_named_as_such() {
let snapshot = Sections::new()
.section(server(1))
.section(|| None::<Arc<NeverLoaded>>)
.take();
assert_eq!(snapshot.len(), 1, "the unloaded one is not in the map");
assert_eq!(snapshot.names().len(), 2, "but it is still registered");
match snapshot.require::<NeverLoaded>() {
Err(NotInScope::NotLoaded(name)) => assert!(name.ends_with("NeverLoaded")),
other => panic!("expected NotLoaded, got {other:?}"),
}
}
#[test]
fn two_types_with_the_same_name_are_told_apart() {
mod other {
#[derive(Debug)]
pub struct Server;
}
let snapshot = Sections::new()
.section(server(8080))
.section(|| Some(Arc::new(other::Server)))
.take();
assert_eq!(snapshot.get::<Server>().unwrap().port, 8080);
assert!(snapshot.get::<other::Server>().is_some());
assert_eq!(snapshot.len(), 2, "one name, two sections");
}
#[test]
fn a_section_nobody_registered_is_a_different_error() {
let snapshot = Sections::new().section(server(1)).take();
match snapshot.require::<Features>() {
Err(NotInScope::NotListed(name)) => assert!(name.ends_with("Features")),
other => panic!("expected NotListed, got {other:?}"),
}
}
#[test]
fn the_two_errors_say_what_to_do_about_them() {
let listed = NotInScope::NotLoaded("Server").to_string();
let missing = NotInScope::NotListed("Server").to_string();
assert!(listed.contains("init()"), "{listed}");
assert!(missing.contains("sections!"), "{missing}");
}
#[test]
fn registering_a_type_twice_keeps_the_last_reader() {
let snapshot = Sections::new().section(server(1)).section(server(2)).take();
assert_eq!(snapshot.len(), 1);
assert_eq!(snapshot.get::<Server>().unwrap().port, 2);
}
#[test]
fn debug_prints_the_names_and_not_the_values() {
let snapshot = Sections::new().section(server(5432)).take();
let rendered = format!("{snapshot:?}");
assert!(rendered.contains("Server"), "{rendered}");
assert!(
!rendered.contains("5432"),
"a value reached Debug: {rendered}"
);
}
#[test]
fn take_refuses_a_snapshot_that_straddled_a_reload() {
use std::sync::atomic::{AtomicU64, Ordering};
static A: AtomicU64 = AtomicU64::new(1);
static B: AtomicU64 = AtomicU64::new(1);
static DISTURB: AtomicU64 = AtomicU64::new(3);
let sections = Sections::new()
.section_with_generation(
|| {
if DISTURB.load(Ordering::SeqCst) > 0 {
DISTURB.fetch_sub(1, Ordering::SeqCst);
A.fetch_add(1, Ordering::SeqCst);
B.fetch_add(1, Ordering::SeqCst);
}
Some(Arc::new(Server {
port: A.load(Ordering::SeqCst) as u16,
}))
},
|| A.load(Ordering::SeqCst),
)
.section_with_generation(
|| {
Some(Arc::new(Features {
cache: B.load(Ordering::SeqCst) % 2 == 0,
}))
},
|| B.load(Ordering::SeqCst),
);
let snapshot = sections.take();
let port = u64::from(snapshot.get::<Server>().unwrap().port);
let cache = snapshot.get::<Features>().unwrap().cache;
assert_eq!(
cache,
port % 2 == 0,
"the snapshot mixed generations: port={port}, cache={cache}"
);
assert_eq!(DISTURB.load(Ordering::SeqCst), 0, "it should have retried");
}
#[test]
fn a_list_that_cannot_report_generations_still_reads() {
let sections = Sections::new().section(server(8080));
assert!(!sections.is_consistent());
assert_eq!(sections.take().get::<Server>().unwrap().port, 8080);
}
#[test]
fn a_snapshot_crosses_threads() {
let snapshot = Sections::new().section(server(8080)).take();
let moved = std::thread::spawn(move || snapshot.get::<Server>().unwrap().port);
assert_eq!(moved.join().unwrap(), 8080);
}
}