use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use parking_lot::RwLock;
use thiserror::Error;
pub const BASE_SUFFIX: &str = "_base";
pub type Owner = (u64, Option<String>);
type Key = (String, String, String, String, String, String);
#[allow(clippy::too_many_arguments)]
fn make_key(
namespace: &str,
component: &str,
endpoint: &str,
slug: &str,
suffix: &str,
filename: &str,
) -> Key {
(
namespace.to_string(),
component.to_string(),
endpoint.to_string(),
slug.to_string(),
suffix.to_string(),
filename.to_string(),
)
}
#[derive(Debug, Error)]
#[error("metadata-registry collision on key {key:?}: prior_owner={prior:?}, new_owner={new:?}")]
pub struct CollisionError {
pub key: Key,
pub prior: Owner,
pub new: Owner,
}
#[derive(Clone, Debug, Default)]
pub struct MetadataArtifactRegistry {
entries: Arc<RwLock<HashMap<Key, (PathBuf, Owner)>>>,
}
impl MetadataArtifactRegistry {
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::too_many_arguments)]
pub fn register(
&self,
owner: &Owner,
namespace: &str,
component: &str,
endpoint: &str,
slug: &str,
suffix: &str,
filename: &str,
path: PathBuf,
) -> Result<(), Box<CollisionError>> {
let key = make_key(namespace, component, endpoint, slug, suffix, filename);
let mut entries = self.entries.write();
if let Some((_, prior)) = entries.get(&key)
&& prior != owner
{
return Err(Box::new(CollisionError {
key,
prior: prior.clone(),
new: owner.clone(),
}));
}
entries.insert(key, (path, owner.clone()));
tracing::debug!(
namespace,
component,
endpoint,
slug,
suffix,
filename,
"registered metadata artifact",
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn get(
&self,
namespace: &str,
component: &str,
endpoint: &str,
slug: &str,
suffix: &str,
filename: &str,
) -> Option<PathBuf> {
let key = make_key(namespace, component, endpoint, slug, suffix, filename);
self.entries.read().get(&key).map(|(p, _)| p.clone())
}
pub fn unregister_for_owner(&self, owner: &Owner) {
self.entries.write().retain(|_, (_, o)| o != owner);
}
pub fn len(&self) -> usize {
self.entries.read().len()
}
pub fn is_empty(&self) -> bool {
self.entries.read().is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> Owner {
(1, None)
}
fn lora(slug: &str) -> Owner {
(1, Some(slug.to_string()))
}
#[test]
fn register_get_roundtrip() {
let reg = MetadataArtifactRegistry::new();
let p = PathBuf::from("/tmp/tokenizer.json");
reg.register(
&base(),
"ns",
"comp",
"ep",
"llama-3-8b",
"_base",
"tokenizer.json",
p.clone(),
)
.unwrap();
assert_eq!(
reg.get("ns", "comp", "ep", "llama-3-8b", "_base", "tokenizer.json"),
Some(p)
);
assert!(
reg.get("ns", "comp", "ep", "llama-3-8b", "_base", "missing.json")
.is_none()
);
assert!(
reg.get(
"ns",
"comp",
"ep",
"llama-3-8b",
"lora-v1",
"tokenizer.json"
)
.is_none()
);
}
#[test]
fn unregister_for_owner_clears_only_that_owner() {
let reg = MetadataArtifactRegistry::new();
let lora_owner = lora("lora-v1");
reg.register(
&base(),
"ns",
"comp",
"ep",
"m",
"_base",
"config.json",
PathBuf::from("/m/c"),
)
.unwrap();
reg.register(
&base(),
"ns",
"comp",
"ep",
"m",
"_base",
"tokenizer.json",
PathBuf::from("/m/t"),
)
.unwrap();
reg.register(
&lora_owner,
"ns",
"comp",
"ep",
"m",
"lora-v1",
"adapter.json",
PathBuf::from("/m/a"),
)
.unwrap();
reg.unregister_for_owner(&lora_owner);
assert!(
reg.get("ns", "comp", "ep", "m", "lora-v1", "adapter.json")
.is_none()
);
assert_eq!(
reg.get("ns", "comp", "ep", "m", "_base", "config.json"),
Some(PathBuf::from("/m/c"))
);
reg.unregister_for_owner(&lora_owner);
assert_eq!(reg.len(), 2);
}
#[test]
fn register_returns_err_on_owner_collision() {
let reg = MetadataArtifactRegistry::new();
let owner_a = (1, None);
let owner_b = (2, None);
reg.register(
&owner_a,
"ns",
"comp",
"ep",
"m",
"_base",
"config.json",
PathBuf::from("/a"),
)
.unwrap();
let err = reg
.register(
&owner_b,
"ns",
"comp",
"ep",
"m",
"_base",
"config.json",
PathBuf::from("/b"),
)
.unwrap_err();
assert_eq!(err.prior, owner_a);
assert_eq!(err.new, owner_b);
}
#[test]
fn register_same_owner_updates_path() {
let reg = MetadataArtifactRegistry::new();
reg.register(
&base(),
"ns",
"comp",
"ep",
"m",
"_base",
"config.json",
PathBuf::from("/a"),
)
.unwrap();
reg.register(
&base(),
"ns",
"comp",
"ep",
"m",
"_base",
"config.json",
PathBuf::from("/b"),
)
.unwrap();
assert_eq!(
reg.get("ns", "comp", "ep", "m", "_base", "config.json"),
Some(PathBuf::from("/b"))
);
}
#[test]
fn different_endpoints_coexist() {
let reg = MetadataArtifactRegistry::new();
let owner_a = (1, None);
let owner_b = (2, None);
reg.register(
&owner_a,
"ns",
"comp",
"ep-a",
"m",
"_base",
"config.json",
PathBuf::from("/a"),
)
.unwrap();
reg.register(
&owner_b,
"ns",
"comp",
"ep-b",
"m",
"_base",
"config.json",
PathBuf::from("/b"),
)
.unwrap();
assert_eq!(
reg.get("ns", "comp", "ep-a", "m", "_base", "config.json"),
Some(PathBuf::from("/a"))
);
assert_eq!(
reg.get("ns", "comp", "ep-b", "m", "_base", "config.json"),
Some(PathBuf::from("/b"))
);
}
}