use super::MetricsError;
use super::labels::ComponentLabels;
use std::collections::HashSet;
use std::sync::{LazyLock, Mutex, PoisonError};
static CLAIMS: LazyLock<Mutex<HashSet<String>>> = LazyLock::new(|| Mutex::new(HashSet::new()));
#[derive(Debug)]
pub(crate) struct SeriesClaim {
key: String,
}
impl SeriesClaim {
pub(crate) fn try_claim(key: String) -> Result<Self, MetricsError> {
if Self::insert(&key) {
Ok(SeriesClaim { key })
} else {
Err(MetricsError::DuplicateSeries(key))
}
}
pub(crate) fn claim_or_shadow(key: String) -> Option<Self> {
if Self::insert(&key) {
return Some(SeriesClaim { key });
}
tracing::error!(
series = %key,
"another live handle set already owns this metric series; this one \
will keep counting (counters sum) but publish no gauges, so the \
owner's readings stay truthful. Two pipelines or two components \
sharing a name in one process is the usual cause."
);
None
}
fn insert(key: &str) -> bool {
Self::registry().insert(key.to_owned())
}
fn registry() -> std::sync::MutexGuard<'static, HashSet<String>> {
CLAIMS.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl Drop for SeriesClaim {
fn drop(&mut self) {
Self::registry().remove(&self.key);
}
}
pub(crate) fn series_key(root: &str, labels: &ComponentLabels, extra: &str) -> String {
format!(
"spate_{root}_|{}|{}|{}|{extra}",
labels.pipeline, labels.component, labels.component_type
)
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
fn labels(component: &str) -> ComponentLabels {
ComponentLabels::new("orders", component.to_owned(), "kafka")
}
#[test]
fn the_first_claim_wins_and_the_second_shadows() {
let key = series_key("sink", &labels("first-wins"), "shard=0");
let owner = SeriesClaim::try_claim(key.clone()).expect("free series");
assert!(
SeriesClaim::claim_or_shadow(key.clone()).is_none(),
"a second claim on a live series must shadow"
);
assert!(matches!(
SeriesClaim::try_claim(key.clone()),
Err(MetricsError::DuplicateSeries(_))
));
drop(owner);
}
#[test]
fn dropping_the_owner_frees_the_key() {
let key = series_key("sink", &labels("drop-frees"), "shard=0");
drop(SeriesClaim::try_claim(key.clone()).expect("free series"));
let second = SeriesClaim::try_claim(key.clone()).expect("freed by the drop");
drop(second);
}
#[test]
fn keys_separate_stages_shards_and_components() {
let a = series_key("sink", &labels("keys"), "shard=0");
for other in [
series_key("source", &labels("keys"), "shard=0"),
series_key("sink", &labels("keys"), "shard=1"),
series_key("sink", &labels("keys-other"), "shard=0"),
] {
assert_ne!(a, other);
}
}
}