use alloc::sync::Arc;
use arc_swap::ArcSwap;
#[derive(Clone, Default, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct MutableMetadata {
pub runtime_id: String,
pub process_tags: String,
}
#[derive(Clone, Default, Debug)]
pub struct MutableMetadataHandle(Arc<ArcSwap<MutableMetadata>>);
impl MutableMetadataHandle {
pub fn load(&self) -> arc_swap::Guard<Arc<MutableMetadata>> {
self.0.load()
}
pub fn load_full(&self) -> Arc<MutableMetadata> {
self.0.load_full()
}
pub fn set_runtime_id(&self, runtime_id: String) {
self.update(|mut metadata| {
metadata.runtime_id = runtime_id.clone();
metadata
});
}
pub fn set_process_tags(&self, process_tags: String) {
self.update(|mut metadata| {
metadata.process_tags = process_tags.clone();
metadata
});
}
pub fn update<F>(&self, mut f: F)
where
F: FnMut(MutableMetadata) -> MutableMetadata,
{
self.0.rcu(|current| {
let next = (**current).clone();
f(next)
});
}
}
impl From<MutableMetadata> for MutableMetadataHandle {
fn from(value: MutableMetadata) -> Self {
MutableMetadataHandle(Arc::new(ArcSwap::from_pointee(value)))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn initial_handle() -> MutableMetadataHandle {
let handle = MutableMetadataHandle::default();
handle.set_runtime_id("rt-1".into());
handle.set_process_tags("k1:v1".into());
handle
}
#[test]
fn handle_propagates_updates_to_all_clones() {
let handle = initial_handle();
let clone = handle.clone();
let snapshot = handle.load();
assert_eq!(snapshot.runtime_id, "rt-1");
assert_eq!(snapshot.process_tags, "k1:v1");
drop(snapshot);
clone.update(|mut metadata| {
metadata.runtime_id = "rt-2".into();
metadata.process_tags = "k2:v2".into();
metadata
});
let snapshot = handle.load();
assert_eq!(snapshot.runtime_id, "rt-2");
assert_eq!(snapshot.process_tags, "k2:v2");
}
#[test]
fn field_updates_keep_the_other_field() {
let handle = initial_handle();
handle.set_runtime_id("rt-2".into());
let snapshot = handle.load();
assert_eq!(snapshot.runtime_id, "rt-2");
assert_eq!(snapshot.process_tags, "k1:v1");
drop(snapshot);
handle.set_process_tags("k2:v2".into());
let snapshot = handle.load();
assert_eq!(snapshot.runtime_id, "rt-2");
assert_eq!(snapshot.process_tags, "k2:v2");
}
#[test]
fn default_handle_yields_empty_values() {
let handle = MutableMetadataHandle::default();
let snapshot = handle.load();
assert!(snapshot.runtime_id.is_empty());
assert!(snapshot.process_tags.is_empty());
}
#[test]
fn update_modifies_the_current_snapshot() {
let handle = initial_handle();
handle.update(|mut m| {
m.runtime_id = "rt-2".into();
m
});
let snapshot = handle.load();
assert_eq!(snapshot.runtime_id, "rt-2");
assert_eq!(snapshot.process_tags, "k1:v1");
}
#[test]
fn update_retries_without_losing_concurrent_changes() {
use core::sync::atomic::{AtomicBool, Ordering};
use std::sync::Barrier;
let handle = initial_handle();
let snapshot_loaded = Barrier::new(2);
let concurrent_update_done = Barrier::new(2);
let first_attempt = AtomicBool::new(true);
std::thread::scope(|scope| {
let writer = scope.spawn(|| {
handle.update(|mut metadata| {
if first_attempt.swap(false, Ordering::SeqCst) {
snapshot_loaded.wait();
concurrent_update_done.wait();
}
metadata.runtime_id = "rt-2".into();
metadata
});
});
snapshot_loaded.wait();
handle.set_process_tags("k2:v2".into());
concurrent_update_done.wait();
writer.join().unwrap();
});
let snapshot = handle.load();
assert_eq!(snapshot.runtime_id, "rt-2");
assert_eq!(snapshot.process_tags, "k2:v2");
}
#[test]
fn load_full_returns_an_owning_arc() {
let handle = initial_handle();
let snapshot = handle.load_full();
assert_eq!(snapshot.runtime_id, "rt-1");
assert_eq!(snapshot.process_tags, "k1:v1");
}
}