use std::any::Any;
#[cfg(feature = "test-util")]
use std::collections::HashMap;
#[cfg(feature = "test-util")]
use std::marker::PhantomData;
use std::sync::Arc;
#[cfg(feature = "test-util")]
use std::sync::Mutex;
use std::sync::Weak;
use crate::{
EntrySink,
entry::BoxEntry,
sink::{AppendOnDrop, BoxEntrySink},
};
use super::Entry;
pub trait GlobalEntrySink {
fn sink() -> BoxEntrySink;
fn append(entry: impl Entry + Send + 'static);
#[track_caller]
fn append_on_drop<E: Entry + Send + 'static>(entry: E) -> AppendOnDrop<E, BoxEntrySink>
where
Self: Sized + Clone,
{
AppendOnDrop::new(entry, Self::sink())
}
#[track_caller]
fn append_on_drop_default<E: Default + Entry + Send + 'static>() -> AppendOnDrop<E, BoxEntrySink>
where
Self: Sized + Clone,
{
Self::append_on_drop(E::default())
}
}
pub trait AttachGlobalEntrySink {
fn is_attached() -> bool {
Self::try_sink().is_some()
}
fn attach(
queue_and_handle: (
impl EntrySink<BoxEntry> + Send + Sync + 'static,
impl Any + Send + Sync,
),
) -> AttachHandle;
fn try_sink() -> Option<BoxEntrySink>;
fn try_append<E: Entry + Send + 'static>(entry: E) -> Result<(), E>;
fn register_shutdown_fn(f: ShutdownFn);
}
#[must_use = "if unused the global sink will be immediately detached and shut down"]
pub struct AttachHandle {
shutdown_registry: Option<Arc<ShutdownRegistry>>,
}
pub struct ShutdownFn(Box<dyn FnOnce() + Send>);
impl ShutdownFn {
pub fn new(f: impl FnOnce() + Send + 'static) -> Self {
Self(Box::new(f))
}
fn call(self) {
self.0();
}
}
struct ShutdownOnDrop(Option<ShutdownFn>);
impl ShutdownOnDrop {
fn new(shutdown: ShutdownFn) -> Self {
Self(Some(shutdown))
}
}
impl Drop for ShutdownOnDrop {
fn drop(&mut self) {
if let Some(shutdown) = self.0.take() {
shutdown.call();
}
}
}
pub struct ShutdownRegistry {
functions: crate::primitives::Mutex<Option<ShutdownFunctions>>,
}
struct ShutdownFunctions {
detach: ShutdownFn,
subscribers: Vec<ShutdownFn>,
}
impl ShutdownRegistry {
fn new(detach: ShutdownFn) -> Self {
Self {
functions: crate::primitives::Mutex::new(Some(ShutdownFunctions {
detach,
subscribers: Vec::new(),
})),
}
}
#[doc(hidden)]
pub fn push(&self, f: ShutdownFn) -> bool {
match self.functions.lock().unwrap().as_mut() {
Some(functions) => {
functions.subscribers.push(f);
true
}
None => false,
}
}
fn drain(&self) -> Option<ShutdownFunctions> {
self.functions.lock().unwrap().take()
}
}
#[cfg(feature = "test-util")]
#[must_use = "if unused the thread-local test sink will be immediately restored"]
pub struct ThreadLocalTestSinkGuard {
clear_fn: fn(),
_marker: PhantomData<*const ()>,
}
#[cfg(feature = "test-util")]
impl ThreadLocalTestSinkGuard {
#[doc(hidden)]
pub fn new(clear_fn: fn()) -> Self {
Self {
clear_fn,
_marker: PhantomData,
}
}
}
#[cfg(feature = "test-util")]
impl Drop for ThreadLocalTestSinkGuard {
fn drop(&mut self) {
(self.clear_fn)();
}
}
#[cfg(feature = "test-util")]
type RuntimeSinkMap = Arc<Mutex<HashMap<tokio::runtime::Id, BoxEntrySink>>>;
#[cfg(feature = "test-util")]
#[must_use = "if unused the runtime test sink will be immediately removed"]
#[derive(Debug)]
pub struct TokioRuntimeTestSinkGuard {
runtime_id: tokio::runtime::Id,
map: RuntimeSinkMap,
}
#[cfg(feature = "test-util")]
impl TokioRuntimeTestSinkGuard {
#[doc(hidden)]
pub fn new(runtime_id: tokio::runtime::Id, map: RuntimeSinkMap) -> Self {
Self { runtime_id, map }
}
}
#[cfg(feature = "test-util")]
impl Drop for TokioRuntimeTestSinkGuard {
fn drop(&mut self) {
self.map.lock().unwrap().remove(&self.runtime_id);
}
}
impl Drop for AttachHandle {
fn drop(&mut self) {
let Some(registry) = self.shutdown_registry.take() else {
return;
};
let Some(ShutdownFunctions {
detach,
subscribers,
}) = registry.drain()
else {
return;
};
let _detach = ShutdownOnDrop::new(detach);
let subscribers = subscribers
.into_iter()
.rev()
.map(ShutdownOnDrop::new)
.collect::<Vec<_>>()
.into_boxed_slice();
drop(subscribers);
}
}
impl AttachHandle {
#[doc(hidden)]
pub fn new(join: fn()) -> Self {
Self {
shutdown_registry: Some(Arc::new(ShutdownRegistry::new(ShutdownFn::new(join)))),
}
}
pub fn forget(mut self) {
if let Some(registry) = self.shutdown_registry.take() {
drop(registry.drain());
}
}
#[doc(hidden)]
pub fn shutdown_registry_weak(&self) -> Weak<ShutdownRegistry> {
self.shutdown_registry
.as_ref()
.map(Arc::downgrade)
.unwrap_or_default()
}
}
impl<Q: AttachGlobalEntrySink> GlobalEntrySink for Q {
#[track_caller]
fn sink() -> BoxEntrySink {
Q::try_sink().expect("sink must be `attach()`ed before use")
}
#[track_caller]
fn append(entry: impl Entry + Send + 'static) {
if Q::try_append(entry).is_err() {
panic!("sink must be `attach()`ed before appending")
}
}
}
#[macro_export]
macro_rules! global_entry_sink {
($(#[$attr:meta])* $name:ident) => {
$(#[$attr])*
#[derive(Debug, Clone)]
pub struct $name;
const _: () = {
use ::std::{sync::Weak, boxed::Box, option::Option::{self, Some, None}, result::Result, any::Any, marker::{Send, Sync}};
use $crate::{Entry, BoxEntry, BoxEntrySink, EntrySink, global::{AttachGlobalEntrySink, AttachHandle, ShutdownFn, ShutdownRegistry}, primitives::RwLock};
const NAME: &'static str = ::std::stringify!($name);
struct AttachedState {
sink: (BoxEntrySink, Box<dyn Send + Sync + 'static>),
shutdown_registry: Weak<ShutdownRegistry>,
}
static ATTACHED: RwLock<Option<AttachedState>> = RwLock::new(None);
$crate::__test_util! {
use ::std::cell::RefCell;
use ::std::sync::{Arc, Mutex};
use ::std::collections::HashMap;
thread_local! {
static THREAD_LOCAL_TEST_SINK: RefCell<Option<BoxEntrySink>> = const { RefCell::new(None) };
}
static RUNTIME_TEST_SINKS: ::std::sync::OnceLock<Arc<Mutex<HashMap<$crate::__tokio::runtime::Id, BoxEntrySink>>>> = ::std::sync::OnceLock::new();
fn runtime_sinks() -> &'static Arc<Mutex<HashMap<$crate::__tokio::runtime::Id, BoxEntrySink>>> {
RUNTIME_TEST_SINKS.get_or_init(|| Arc::new(Mutex::new(HashMap::new())))
}
fn get_test_sink() -> Option<BoxEntrySink> {
if let Some(sink) = THREAD_LOCAL_TEST_SINK.with(|cell| cell.borrow().clone()) {
return Some(sink);
}
if let Ok(handle) = $crate::__tokio::runtime::Handle::try_current() {
let map = runtime_sinks().lock().unwrap();
return map.get(&handle.id()).cloned();
}
None
}
#[track_caller]
fn set_test_sink(sink: Option<BoxEntrySink>) {
let should_panic = THREAD_LOCAL_TEST_SINK.with(|cell| {
let mut borrowed = cell.borrow_mut();
let should_panic = borrowed.is_some() && sink.is_some();
if !should_panic {
*borrowed = sink;
}
should_panic
});
if should_panic {
panic!("A test sink was previously installed. You can only install one test sink at a time.");
}
}
}
impl AttachGlobalEntrySink for $name {
fn attach(
(sink, handle): (impl EntrySink<BoxEntry> + Send + Sync + 'static, impl Any + Send + Sync),
) -> AttachHandle {
let mut write = ATTACHED.write().unwrap();
if write.is_some() {
drop(write); panic!("Already installed a global {NAME} sink, drop the attach handle first if intentionally attaching a new sink");
}
let sink = BoxEntrySink::new(sink);
let attach_handle = AttachHandle::new(|| {
let attached = ATTACHED.write().unwrap().take();
drop(attached);
});
*write = Some(AttachedState {
sink: (sink, Box::new(handle)),
shutdown_registry: attach_handle.shutdown_registry_weak(),
});
drop(write);
attach_handle
}
fn try_sink() -> Option<BoxEntrySink> {
$crate::__test_util! {
if let Some(test_sink) = get_test_sink() {
return Some(test_sink);
}
}
let read = ATTACHED.read().unwrap();
let attached = read.as_ref()?;
Some(attached.sink.0.clone())
}
fn try_append<E: Entry + Send + 'static>(entry: E) -> Result<(), E> {
$crate::__test_util! {
if let Some(test_sink) = get_test_sink() {
test_sink.append(entry);
return Ok(());
}
}
let read = ATTACHED.read().unwrap();
if let Some(attached) = read.as_ref() {
attached.sink.0.append(entry);
Ok(())
} else {
Err(entry)
}
}
fn register_shutdown_fn(f: ShutdownFn) {
let read = ATTACHED.read().unwrap();
let attached = read.as_ref().expect("No sink attached — call attach() before subscribing");
let registry = attached.shutdown_registry.upgrade()
.expect("AttachHandle was dropped or forgotten — cannot register shutdown functions");
if !registry.push(f) {
panic!("AttachHandle was dropped or forgotten — cannot register shutdown functions");
}
}
}
impl $name {
#[doc = $crate::__macro_doctest!()]
#[doc = $crate::__macro_doctest!()]
pub fn sink_or_discard() -> BoxEntrySink {
BoxEntrySink::lazy(<Self as $crate::global::AttachGlobalEntrySink>::try_sink)
}
}
$crate::__test_util! {
const _: () = {
impl $name {
#[doc = $crate::__macro_doctest!()]
#[doc = $crate::__macro_doctest!()]
#[track_caller]
pub fn set_test_sink(sink: BoxEntrySink) -> $crate::global::ThreadLocalTestSinkGuard {
set_test_sink(Some(sink));
$crate::global::ThreadLocalTestSinkGuard::new(|| {
set_test_sink(None);
})
}
#[doc = $crate::__macro_doctest!()]
pub fn with_test_sink<F, R>(sink: BoxEntrySink, f: F) -> R
where
F: FnOnce() -> R,
{
let _guard = Self::set_test_sink(sink);
f()
}
#[doc = $crate::__macro_doctest!()]
#[track_caller]
pub fn set_test_sink_for_tokio_runtime(handle: &$crate::__tokio::runtime::Handle, sink: BoxEntrySink) -> $crate::global::TokioRuntimeTestSinkGuard {
let runtime_id = handle.id();
let map = runtime_sinks();
let already_installed = {
let mut guard = map.lock().unwrap();
if !guard.contains_key(&runtime_id) {
guard.insert(runtime_id, sink);
false
} else {
true
}
};
if already_installed {
panic!("A test sink was already installed for this runtime. You can only install one test sink per runtime at a time.");
}
$crate::global::TokioRuntimeTestSinkGuard::new(runtime_id, map.clone())
}
#[doc = $crate::__macro_doctest!()]
#[track_caller]
pub fn set_test_sink_on_current_tokio_runtime(sink: BoxEntrySink) -> $crate::global::TokioRuntimeTestSinkGuard {
let handle = $crate::__tokio::runtime::Handle::current();
Self::set_test_sink_for_tokio_runtime(&handle, sink)
}
}
};
}
};
};
}
pub use global_entry_sink;
#[cfg(test)]
mod tests {
use crate::test_stream::TestSink;
use metrique_writer::test_util::{TestEntrySink, test_entry_sink};
use metrique_writer::{
AnyEntrySink, AttachGlobalEntrySink, AttachGlobalEntrySinkExt as _, Entry, EntrySink,
EntryWriter, GlobalEntrySink, format::FormatExt as _, sink::FlushImmediately,
};
use metrique_writer_format_emf::{Emf, EntryDimensions};
use std::{
borrow::Cow,
time::{Duration, SystemTime},
};
metrique_writer::sink::global_entry_sink! { ServiceMetrics }
struct TestEntry;
impl Entry for TestEntry {
fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
writer.timestamp(SystemTime::UNIX_EPOCH + Duration::from_secs_f64(1749475336.0157819));
writer.config(
const {
&EntryDimensions::new_static(&[Cow::Borrowed(&[Cow::Borrowed(
"Operation",
)])])
},
);
writer.value("Time", &Duration::from_millis(42));
writer.value("Operation", "MyOperation");
writer.value("StringProp", "some string value");
writer.value("BasicIntCount", &1234u64);
}
}
#[test]
fn dummy() {
let output = TestSink::default();
{
let _attached = ServiceMetrics::attach_to_stream(
Emf::all_validations("MyApp".into(), vec![vec![]]).output_to(output.clone()),
);
ServiceMetrics::append(TestEntry);
}
assert_json_diff::assert_json_eq!(
serde_json::from_str::<serde_json::Value>(&output.dump()).unwrap(),
serde_json::json!({
"_aws":{
"CloudWatchMetrics": [
{
"Namespace": "MyApp",
"Dimensions": [["Operation"]],
"Metrics": [
{"Name":"Time", "Unit":"Milliseconds"},
{"Name":"BasicIntCount"}
]
}
],
"Timestamp": 1749475336015u64,
},
"Time":42,
"BasicIntCount":1234,
"Operation":"MyOperation",
"StringProp":"some string value"
})
)
}
#[test]
fn thread_local_sink_capture_raw_data() {
use crate::test_stream::TestSink;
let thread_local_output = TestSink::default();
let formatter = Emf::all_validations("ThreadLocalApp".into(), vec![vec![]])
.output_to(thread_local_output.clone());
let sink = FlushImmediately::new_boxed(formatter);
let content = {
let _guard = ServiceMetrics::set_test_sink(sink);
ServiceMetrics::append(TestEntry);
let content = thread_local_output.dump();
assert!(content.contains("Time"));
assert!(content.contains("42"));
assert!(content.contains("ThreadLocalApp")); content
};
assert_eq!(
content,
r#"{"_aws":{"CloudWatchMetrics":[{"Namespace":"ThreadLocalApp","Dimensions":[["Operation"]],"Metrics":[{"Name":"Time","Unit":"Milliseconds"},{"Name":"BasicIntCount"}]}],"Timestamp":1749475336015},"Time":42,"BasicIntCount":1234,"Operation":"MyOperation","StringProp":"some string value"}
"#
);
}
#[test]
fn thread_local_sink_capture_entry() {
use metrique_writer::test_util::{TestEntrySink, test_entry_sink};
let TestEntrySink { inspector, sink } = test_entry_sink();
let _guard = ServiceMetrics::set_test_sink(sink);
ServiceMetrics::append(TestEntry);
assert_eq!(inspector.entries()[0].metrics["BasicIntCount"], 1234);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn runtime_sink_works_across_threads() {
use metrique_writer::test_util::{TestEntrySink, test_entry_sink};
let TestEntrySink { inspector, sink } = test_entry_sink();
let _guard = ServiceMetrics::set_test_sink_on_current_tokio_runtime(sink);
let handles: Vec<_> = (0..4)
.map(|_| {
tokio::spawn(async move {
ServiceMetrics::append(TestEntry);
})
})
.collect();
for handle in handles {
handle.await.unwrap();
}
let entries = inspector.entries();
assert_eq!(entries.len(), 4);
for entry in entries {
assert_eq!(entry.metrics["BasicIntCount"], 1234);
}
}
#[tokio::test]
async fn runtime_sink_guard_is_send_sync() {
use metrique_writer::test_util::{TestEntrySink, test_entry_sink};
let TestEntrySink { inspector, sink } = test_entry_sink();
let guard = ServiceMetrics::set_test_sink_on_current_tokio_runtime(sink);
tokio::spawn(async move {
ServiceMetrics::append(TestEntry);
drop(guard); })
.await
.unwrap();
assert_eq!(inspector.entries().len(), 1);
}
#[tokio::test]
async fn runtime_sink_cleanup_on_drop() {
use metrique_writer::test_util::{TestEntrySink, test_entry_sink};
let TestEntrySink {
inspector: inspector1,
sink: sink1,
} = test_entry_sink();
{
let _guard = ServiceMetrics::set_test_sink_on_current_tokio_runtime(sink1);
ServiceMetrics::append(TestEntry);
}
assert_eq!(inspector1.entries().len(), 1);
let TestEntrySink {
inspector: inspector2,
sink: sink2,
} = test_entry_sink();
let _guard2 = ServiceMetrics::set_test_sink_on_current_tokio_runtime(sink2);
ServiceMetrics::append(TestEntry);
assert_eq!(inspector1.entries().len(), 1);
assert_eq!(inspector2.entries().len(), 1);
}
#[test]
#[should_panic(expected = "no reactor running")]
fn runtime_sink_panics_outside_tokio() {
use metrique_writer::test_util::{TestEntrySink, test_entry_sink};
let TestEntrySink { inspector: _, sink } = test_entry_sink();
let _guard = ServiceMetrics::set_test_sink_on_current_tokio_runtime(sink);
}
#[test]
fn runtime_sink_for_runtime_works() {
use metrique_writer::test_util::{TestEntrySink, test_entry_sink};
let rt = tokio::runtime::Runtime::new().unwrap();
let TestEntrySink { inspector, sink } = test_entry_sink();
let _guard = ServiceMetrics::set_test_sink_for_tokio_runtime(&rt.handle(), sink);
rt.block_on(async {
ServiceMetrics::append(TestEntry);
});
assert_eq!(inspector.entries().len(), 1);
assert_eq!(inspector.entries()[0].metrics["BasicIntCount"], 1234);
}
#[tokio::test]
async fn runtime_sink_panics_on_double_install() {
use metrique_writer::test_util::{TestEntrySink, test_entry_sink};
use std::panic::AssertUnwindSafe;
let TestEntrySink {
inspector: _,
sink: sink1,
} = test_entry_sink();
let _guard1 = ServiceMetrics::set_test_sink_on_current_tokio_runtime(sink1);
let TestEntrySink {
inspector: _,
sink: sink2,
} = test_entry_sink();
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
ServiceMetrics::set_test_sink_on_current_tokio_runtime(sink2)
}));
assert!(result.is_err());
let panic_msg = result.unwrap_err();
if let Some(s) = panic_msg.downcast_ref::<String>() {
assert!(s.contains("A test sink was already installed for this runtime"));
} else if let Some(s) = panic_msg.downcast_ref::<&str>() {
assert!(s.contains("A test sink was already installed for this runtime"));
} else {
panic!("Unexpected panic type");
}
}
#[test]
fn with_test_sink() {
let TestEntrySink { inspector, sink } = test_entry_sink();
ServiceMetrics::with_test_sink(sink, || {
ServiceMetrics::append(TestEntry);
assert_eq!(inspector.entries()[0].metrics["BasicIntCount"], 1234);
});
}
#[test]
#[should_panic]
fn duplicate_install_panics() {
let TestEntrySink {
inspector: _outer_inspector,
sink,
} = test_entry_sink();
let _outer_guard = ServiceMetrics::set_test_sink(sink);
ServiceMetrics::append(TestEntry);
let TestEntrySink {
inspector: _inner_inspector,
sink,
} = test_entry_sink();
ServiceMetrics::append(TestEntry);
let _inner_guard = ServiceMetrics::set_test_sink(sink);
}
#[test]
fn after_guard_dropped_use_global_queue() {
let TestEntrySink {
inspector: global_inspector,
sink,
} = test_entry_sink();
let _handle = ();
let _handle = ServiceMetrics::attach((sink, _handle));
ServiceMetrics::append(TestEntry);
let TestEntrySink {
inspector: thread_local_inspector,
sink,
} = test_entry_sink();
{
let _tl = ServiceMetrics::set_test_sink(sink);
ServiceMetrics::append(TestEntry);
}
assert_eq!(global_inspector.entries().len(), 1);
ServiceMetrics::append(TestEntry);
assert_eq!(global_inspector.entries().len(), 2);
assert_eq!(thread_local_inspector.entries().len(), 1);
}
#[test]
fn sink_or_discard_without_attached_sink() {
let sink = ServiceMetrics::sink_or_discard();
sink.append(TestEntry);
}
#[test]
fn sink_or_discard_with_test_sink() {
let TestEntrySink { inspector, sink } = test_entry_sink();
let _guard = ServiceMetrics::set_test_sink(sink);
ServiceMetrics::sink_or_discard().append(TestEntry);
assert_eq!(inspector.entries().len(), 1);
assert_eq!(inspector.entries()[0].metrics["BasicIntCount"], 1234);
}
#[test]
fn sink_or_discard_append_on_drop_without_sink() {
let _metric = ServiceMetrics::sink_or_discard().append_on_drop(TestEntry);
}
#[test]
fn sink_or_discard_append_on_drop_with_test_sink() {
let TestEntrySink { inspector, sink } = test_entry_sink();
let _guard = ServiceMetrics::set_test_sink(sink);
{
let _metric = ServiceMetrics::sink_or_discard().append_on_drop(TestEntry);
}
assert_eq!(inspector.entries().len(), 1);
}
#[test]
fn sink_or_discard_resolves_lazily() {
let lazy_sink = ServiceMetrics::sink_or_discard();
let TestEntrySink { inspector, sink } = test_entry_sink();
let _guard = ServiceMetrics::set_test_sink(sink);
lazy_sink.append(TestEntry);
assert_eq!(inspector.entries().len(), 1);
assert_eq!(inspector.entries()[0].metrics["BasicIntCount"], 1234);
}
#[test]
fn sink_or_discard_append_on_drop_resolves_lazily() {
let lazy_sink = ServiceMetrics::sink_or_discard();
let metric = lazy_sink.append_on_drop(TestEntry);
let TestEntrySink { inspector, sink } = test_entry_sink();
let _guard = ServiceMetrics::set_test_sink(sink);
drop(metric);
assert_eq!(inspector.entries().len(), 1);
assert_eq!(inspector.entries()[0].metrics["BasicIntCount"], 1234);
}
#[test]
fn sink_or_discard_flush_without_sink() {
let lazy_sink = ServiceMetrics::sink_or_discard();
let mut flush = std::pin::pin!(AnyEntrySink::flush_async(&lazy_sink));
let waker = std::task::Waker::noop();
let mut cx = std::task::Context::from_waker(&waker);
assert!(flush.as_mut().poll(&mut cx).is_ready());
}
#[test]
fn sink_or_discard_discards_then_forwards() {
let lazy_sink = ServiceMetrics::sink_or_discard();
lazy_sink.append(TestEntry);
let TestEntrySink { inspector, sink } = test_entry_sink();
let _guard = ServiceMetrics::set_test_sink(sink);
lazy_sink.append(TestEntry);
assert_eq!(inspector.entries().len(), 1);
}
#[test]
fn sink_or_discard_detach_stops_forwarding() {
let lazy_sink = ServiceMetrics::sink_or_discard();
let TestEntrySink { inspector, sink } = test_entry_sink();
let guard = ServiceMetrics::set_test_sink(sink);
lazy_sink.append(TestEntry);
assert_eq!(inspector.entries().len(), 1);
drop(guard);
lazy_sink.append(TestEntry);
assert_eq!(inspector.entries().len(), 1);
}
}
#[cfg(test)]
mod shutdown_registry_tests {
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use metrique_writer::sink::AttachGlobalEntrySink;
use metrique_writer::test_util::{TestEntrySink, test_entry_sink};
use metrique_writer::ShutdownFn;
#[test]
fn shutdown_fn_runs_on_drop() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let called = Arc::new(AtomicBool::new(false));
let called2 = called.clone();
let handle = Sink::attach((sink, ()));
Sink::register_shutdown_fn(ShutdownFn::new(move || {
called2.store(true, Ordering::SeqCst);
}));
assert!(!called.load(Ordering::SeqCst));
drop(handle);
assert!(called.load(Ordering::SeqCst));
}
#[test]
fn shutdown_fns_run_before_sink_detach() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let sink_was_attached_during_shutdown = Arc::new(AtomicBool::new(false));
let flag = sink_was_attached_during_shutdown.clone();
let handle = Sink::attach((sink, ()));
Sink::register_shutdown_fn(ShutdownFn::new(move || {
flag.store(Sink::try_sink().is_some(), Ordering::SeqCst);
}));
drop(handle);
assert!(
sink_was_attached_during_shutdown.load(Ordering::SeqCst),
"subscriber shutdown fn should run while sink is still attached"
);
assert!(Sink::try_sink().is_none());
}
#[test]
fn forget_prevents_shutdown_fns_from_running() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let called = Arc::new(AtomicBool::new(false));
let called2 = called.clone();
let handle = Sink::attach((sink, ()));
Sink::register_shutdown_fn(ShutdownFn::new(move || {
called2.store(true, Ordering::SeqCst);
}));
handle.forget();
assert!(!called.load(Ordering::SeqCst));
}
#[test]
fn forget_keeps_sink_attached() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let handle = Sink::attach((sink, ()));
handle.forget();
assert!(Sink::try_sink().is_some());
}
#[test]
#[should_panic(expected = "No sink attached")]
fn register_without_attach_panics() {
metrique_writer::sink::global_entry_sink! { Sink }
Sink::register_shutdown_fn(ShutdownFn::new(|| {}));
}
#[test]
#[should_panic(expected = "dropped or forgotten")]
fn register_after_forget_panics() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let handle = Sink::attach((sink, ()));
handle.forget();
Sink::register_shutdown_fn(ShutdownFn::new(|| {}));
}
#[test]
fn can_reattach_after_drop() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let called = Arc::new(AtomicUsize::new(0));
{
let handle = Sink::attach((sink, ()));
let called2 = called.clone();
Sink::register_shutdown_fn(ShutdownFn::new(move || {
called2.fetch_add(1, Ordering::SeqCst);
}));
drop(handle);
}
assert_eq!(called.load(Ordering::SeqCst), 1);
let TestEntrySink { sink, .. } = test_entry_sink();
{
let handle = Sink::attach((sink, ()));
let called2 = called.clone();
Sink::register_shutdown_fn(ShutdownFn::new(move || {
called2.fetch_add(1, Ordering::SeqCst);
}));
drop(handle);
}
assert_eq!(called.load(Ordering::SeqCst), 2);
}
#[test]
fn shutdown_fns_run_in_lifo_order() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let order = Arc::new(Mutex::new(Vec::new()));
let handle = Sink::attach((sink, ()));
for i in 1..=3 {
let order = order.clone();
Sink::register_shutdown_fn(ShutdownFn::new(move || {
order.lock().unwrap().push(i);
}));
}
drop(handle);
assert_eq!(*order.lock().unwrap(), vec![3, 2, 1]);
}
#[test]
fn drop_does_not_panic_with_outstanding_strong_ref() {
let handle = super::AttachHandle::new(|| {});
let extra_strong_ref = handle
.shutdown_registry_weak()
.upgrade()
.expect("new handle must own its shutdown registry");
drop(handle); drop(extra_strong_ref);
}
#[test]
fn push_after_drain_starts_is_rejected_and_never_runs() {
let ran = Arc::new(AtomicBool::new(false));
let registry = super::ShutdownRegistry::new(super::ShutdownFn::new(|| {}));
let drained = registry.drain().expect("registry should still be open");
assert!(
drained.subscribers.is_empty(),
"registry should initially contain no subscribers"
);
let ran2 = ran.clone();
let accepted = registry.push(super::ShutdownFn::new(move || {
ran2.store(true, Ordering::SeqCst);
}));
assert!(!accepted, "push after drain has started must be rejected");
assert!(
registry.drain().is_none(),
"a rejected push must never be enqueued"
);
assert!(!ran.load(Ordering::SeqCst));
}
#[test]
fn register_during_shutdown_produces_dropped_or_forgotten_panic() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let handle = Sink::attach((sink, ()));
Sink::register_shutdown_fn(ShutdownFn::new(|| {
let panic = std::panic::catch_unwind(|| {
Sink::register_shutdown_fn(ShutdownFn::new(|| {}));
})
.expect_err("registration after shutdown starts must panic");
let message = panic
.downcast_ref::<&str>()
.copied()
.or_else(|| panic.downcast_ref::<String>().map(String::as_str))
.unwrap_or_default();
assert!(
message.contains("AttachHandle was dropped or forgotten"),
"unexpected panic: {message}"
);
}));
drop(handle);
assert!(Sink::try_sink().is_none());
}
#[test]
#[should_panic(expected = "No sink attached")]
fn register_after_full_drop_panics() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let handle = Sink::attach((sink, ()));
drop(handle);
Sink::register_shutdown_fn(ShutdownFn::new(|| {}));
}
#[test]
fn remaining_shutdown_fns_run_and_sink_detaches_after_a_shutdown_fn_panics() {
metrique_writer::sink::global_entry_sink! { Sink }
let TestEntrySink { sink, .. } = test_entry_sink();
let order = Arc::new(Mutex::new(Vec::new()));
let handle = Sink::attach((sink, ()));
let order1 = order.clone();
Sink::register_shutdown_fn(ShutdownFn::new(move || {
order1.lock().unwrap().push(1);
}));
let order2 = order.clone();
Sink::register_shutdown_fn(ShutdownFn::new(move || {
order2.lock().unwrap().push(2);
panic!("boom");
}));
let order3 = order.clone();
Sink::register_shutdown_fn(ShutdownFn::new(move || {
order3.lock().unwrap().push(3);
}));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(handle)));
assert!(result.is_err(), "the subscriber panic must propagate");
assert_eq!(*order.lock().unwrap(), vec![3, 2, 1]);
assert!(
Sink::try_sink().is_none(),
"sink must be detached even though a shutdown fn panicked"
);
let TestEntrySink { sink, .. } = test_entry_sink();
let _handle2 = Sink::attach((sink, ()));
}
}
#[cfg(all(test, shuttle, feature = "_shuttle"))]
mod shuttle_tests {
use shuttle::sync::Mutex;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::{AttachHandle, ShutdownFn};
use crate::shuttle_test;
shuttle_test! {
num_iters = 2_000, depth = 3;
fn concurrent_register_and_drop() {
const REGISTRARS: usize = 2;
let ran = Arc::new(AtomicUsize::new(0));
let rejected = Arc::new(AtomicUsize::new(0));
let handle = AttachHandle::new(|| {});
let registry = handle
.shutdown_registry_weak()
.upgrade()
.expect("new handle must own its shutdown registry");
let registrars: Vec<_> = (0..REGISTRARS)
.map(|_| {
let registry = registry.clone();
let ran = ran.clone();
let rejected = rejected.clone();
shuttle::thread::spawn(move || {
let accepted = registry.push(ShutdownFn::new(move || {
ran.fetch_add(1, Ordering::SeqCst);
}));
if !accepted {
rejected.fetch_add(1, Ordering::SeqCst);
}
})
})
.collect();
drop(handle);
for registrar in registrars {
registrar.join().unwrap();
}
assert_eq!(
ran.load(Ordering::SeqCst) + rejected.load(Ordering::SeqCst),
REGISTRARS,
"every racing registration must be accounted for exactly once (ran xor rejected)"
);
}
}
shuttle_test! {
num_iters = 2_000, depth = 3;
fn concurrent_register_and_forget() {
let ran = Arc::new(AtomicUsize::new(0));
let handle = AttachHandle::new(|| {});
let registry = handle
.shutdown_registry_weak()
.upgrade()
.expect("new handle must own its shutdown registry");
let ran2 = ran.clone();
let registrar = shuttle::thread::spawn(move || {
registry.push(ShutdownFn::new(move || {
ran2.fetch_add(1, Ordering::SeqCst);
}));
});
shuttle::thread::yield_now();
handle.forget();
registrar.join().unwrap();
assert_eq!(
ran.load(Ordering::SeqCst),
0,
"a fn registered around forget() must never run"
);
}
}
shuttle_test! {
num_iters = 2_000, depth = 3;
fn concurrent_registrars_race_push() {
const REGISTRARS: usize = 2;
let ran = Arc::new(AtomicUsize::new(0));
let handle = AttachHandle::new(|| {});
let registry = handle
.shutdown_registry_weak()
.upgrade()
.expect("new handle must own its shutdown registry");
let registrars: Vec<_> = (0..REGISTRARS)
.map(|_| {
let registry = registry.clone();
let ran = ran.clone();
shuttle::thread::spawn(move || {
registry.push(ShutdownFn::new(move || {
ran.fetch_add(1, Ordering::SeqCst);
}));
})
})
.collect();
for registrar in registrars {
registrar.join().unwrap();
}
drop(handle);
assert_eq!(
ran.load(Ordering::SeqCst),
REGISTRARS,
"every concurrently-registered fn must run exactly once"
);
}
}
shuttle_test! {
num_iters = 2_000, depth = 3;
fn concurrent_registrars_preserve_lifo_order() {
const REGISTRARS: u32 = 2;
let push_order = Arc::new(Mutex::new(Vec::new()));
let run_order = Arc::new(Mutex::new(Vec::new()));
let handle = AttachHandle::new(|| {});
let registry = handle
.shutdown_registry_weak()
.upgrade()
.expect("new handle must own its shutdown registry");
let registrars: Vec<_> = (0..REGISTRARS)
.map(|i| {
let registry = registry.clone();
let push_order = push_order.clone();
let run_order = run_order.clone();
shuttle::thread::spawn(move || {
let mut push_order = push_order.lock().unwrap();
registry.push(ShutdownFn::new(move || {
run_order.lock().unwrap().push(i);
}));
push_order.push(i);
})
})
.collect();
for registrar in registrars {
registrar.join().unwrap();
}
drop(handle);
let expected_run_order: Vec<_> = push_order.lock().unwrap().iter().rev().copied().collect();
assert_eq!(
*run_order.lock().unwrap(),
expected_run_order,
"shutdown fns must run in exact reverse of push order, regardless of how concurrent registration interleaves"
);
}
}
static SERIALIZE_PCT_AND_DETERMINISM: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn attach_race_never_observes_sink_without_registry() {
metrique_writer::sink::global_entry_sink! { Sink }
use metrique_writer::{AttachGlobalEntrySink, ShutdownFn as WriterShutdownFn};
let attacher = shuttle::thread::spawn(|| {
Sink::attach((metrique_writer::sink::DevNullSink::new(), ()))
});
let racer = shuttle::thread::spawn(|| {
if Sink::try_sink().is_none() {
return None;
}
Some(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|| {
Sink::register_shutdown_fn(WriterShutdownFn::new(|| {}));
},
)))
});
let handle = attacher.join().unwrap();
let racer_result = racer.join().unwrap();
drop(handle);
if let Some(Err(payload)) = racer_result {
std::panic::resume_unwind(payload);
}
}
fn run_serialized(f: impl FnOnce()) {
let _guard = SERIALIZE_PCT_AND_DETERMINISM
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
f();
}
#[test]
fn attach_race_never_observes_sink_without_registry_pct() {
run_serialized(|| {
shuttle::check_pct(attach_race_never_observes_sink_without_registry, 5_000, 3)
});
}
#[test]
fn attach_race_never_observes_sink_without_registry_determinism() {
run_serialized(|| {
shuttle::check_uncontrolled_nondeterminism(
attach_race_never_observes_sink_without_registry,
5_000,
)
});
}
}
#[doc(hidden)]
#[macro_export]
#[cfg(feature = "test-util")]
macro_rules! __test_util {
($($tt:tt)*) => { $($tt)* };
}
#[doc(hidden)]
#[macro_export]
#[cfg(not(feature = "test-util"))]
macro_rules! __test_util {
($($tt:tt)*) => {};
}
#[doc(hidden)]
#[macro_export]
#[cfg(feature = "private-test-util")]
macro_rules! __macro_doctest {
() => {
"```rust"
};
}
#[doc(hidden)]
#[macro_export]
#[cfg(not(feature = "private-test-util"))]
macro_rules! __macro_doctest {
() => {
"```rust,ignore"
};
}