use crate::l1_cache::{L1ActionCache, L1EntrySnapshot, LookupStats};
use crate::metadata_store::{SqlEngine, SqlMetadataStore, SqlValue};
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug, Clone, Default)]
pub struct NamespacePolicy {
pub allowed: BTreeMap<String, ()>,
}
impl NamespacePolicy {
#[must_use]
pub fn allowing<I: IntoIterator<Item = String>>(namespaces: I) -> Self {
Self {
allowed: namespaces.into_iter().map(|n| (n, ())).collect(),
}
}
#[must_use]
pub fn visible(&self, project: &str) -> bool {
self.allowed.contains_key(project)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct L2ProjectCache {
pub project: String,
pub hash_dirs: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerToolchains {
pub worker: String,
pub toolchains: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StoreCacheFacts {
pub action_entries: u64,
pub workers_with_capabilities: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CacheInventoryReport {
pub l1: Option<L1Inventory>,
pub l2_visible: Vec<L2ProjectCache>,
pub restricted_project_count: u32,
pub toolchains: Vec<WorkerToolchains>,
pub store: StoreCacheFacts,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct L1Inventory {
pub capacity: usize,
pub entries: Vec<L1EntrySnapshot>,
pub stats: LookupStats,
}
pub fn collect_l2(l2_root: &Path) -> Vec<L2ProjectCache> {
let mut out = Vec::new();
let Ok(projects) = std::fs::read_dir(l2_root) else {
return out;
};
for project in projects.flatten() {
let path = project.path();
if !path.is_dir() {
continue;
}
let mut hash_dirs = 0u32;
if let Ok(hashes) = std::fs::read_dir(&path) {
for hash in hashes.flatten() {
if hash.path().is_dir() {
hash_dirs += 1;
}
}
}
out.push(L2ProjectCache {
project: project.file_name().to_string_lossy().into_owned(),
hash_dirs,
});
}
out.sort_by(|a, b| a.project.cmp(&b.project));
out
}
fn count_first_int<E: SqlEngine>(
store: &mut SqlMetadataStore<E>,
sql: &str,
) -> Result<u64, crate::metadata_store::StoreError> {
let rows = store.engine_mut().query(sql, &[])?;
Ok(rows.first().and_then(|r| r.first()).map_or(0, |v| match v {
SqlValue::Int(n) => (*n).max(0) as u64,
_ => 0,
}))
}
fn store_facts<E: SqlEngine>(
store: &mut SqlMetadataStore<E>,
) -> Result<StoreCacheFacts, crate::metadata_store::StoreError> {
let action_entries = count_first_int(store, "SELECT COUNT(*) FROM action_entries")?;
let workers_with_capabilities = count_first_int(
store,
"SELECT COUNT(DISTINCT worker) FROM worker_capabilities",
)?;
Ok(StoreCacheFacts {
action_entries,
workers_with_capabilities,
})
}
fn worker_toolchains<E: SqlEngine>(
store: &mut SqlMetadataStore<E>,
) -> Result<Vec<WorkerToolchains>, crate::metadata_store::StoreError> {
let rows = store.engine_mut().query(
"SELECT worker, capability FROM worker_capabilities \
WHERE capability LIKE 'rustc/%' OR capability LIKE 'cargo/%' \
ORDER BY worker, capability",
&[],
)?;
let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new();
for row in &rows {
let worker = match row.first() {
Some(SqlValue::Text(t)) => t.clone(),
_ => continue,
};
let cap = match row.get(1) {
Some(SqlValue::Text(t)) => t.clone(),
_ => continue,
};
map.entry(worker).or_default().push(cap);
}
Ok(map
.into_iter()
.map(|(worker, toolchains)| WorkerToolchains { worker, toolchains })
.collect())
}
pub fn build_report<E: SqlEngine>(
store: &mut SqlMetadataStore<E>,
l1: Option<&L1ActionCache>,
l2_root: &Path,
policy: &NamespacePolicy,
) -> Result<CacheInventoryReport, crate::metadata_store::StoreError> {
let all_l2 = collect_l2(l2_root);
let mut l2_visible = Vec::new();
let mut restricted_project_count = 0u32;
for project in all_l2 {
if policy.visible(&project.project) {
l2_visible.push(project);
} else {
restricted_project_count += 1;
}
}
let l1 = l1.map(|cache| L1Inventory {
capacity: cache.capacity(),
entries: cache.snapshot(),
stats: cache.stats(),
});
Ok(CacheInventoryReport {
l1,
l2_visible,
restricted_project_count,
toolchains: worker_toolchains(store)?,
store: store_facts(store)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::l1_cache::LookupStats;
fn seed_l2(root: &Path) {
for (project, hashes) in [("proj-alpha", 3), ("proj-beta", 1), ("proj-secret", 9)] {
for h in 0..hashes {
let dir = root.join(project).join(format!("hash-{h}"));
std::fs::create_dir_all(dir).expect("seed l2");
}
}
}
fn fixture_store() -> SqlMetadataStore<crate::metadata_store::RusqliteEngine> {
SqlMetadataStore::open(crate::metadata_store::RusqliteEngine::open_in_memory().unwrap())
.expect("store")
}
#[test]
fn l2_enumeration_counts_hash_directories_per_project() {
let dir = tempfile::tempdir().unwrap();
seed_l2(dir.path());
let mut all = collect_l2(dir.path());
assert_eq!(all.len(), 3);
all.retain(|p| p.project == "proj-secret");
assert_eq!(all[0].hash_dirs, 9);
}
#[test]
fn namespace_policy_hides_existence_not_just_names() {
let dir = tempfile::tempdir().unwrap();
seed_l2(dir.path());
let mut store = fixture_store();
let policy = NamespacePolicy::allowing(["proj-alpha".to_owned(), "proj-beta".to_owned()]);
let report = build_report(&mut store, None, dir.path(), &policy).unwrap();
assert_eq!(report.l2_visible.len(), 2);
assert!(report.l2_visible.iter().all(|p| p.project != "proj-secret"));
assert_eq!(report.restricted_project_count, 1);
let nobody = NamespacePolicy::default();
let report = build_report(&mut store, None, dir.path(), &nobody).unwrap();
assert!(report.l2_visible.is_empty());
assert_eq!(report.restricted_project_count, 3);
}
#[test]
fn l1_section_reports_capacity_live_entries_and_stats() {
let dir = tempfile::tempdir().unwrap();
let mut store = fixture_store();
let mut cache = L1ActionCache::new(8, None);
let key = rabs_protocol::result_identity::TypedDigest {
algorithm: rabs_protocol::result_identity::DigestAlgorithm::Sha256V1,
domain: "rabs.action-key.sha256.v1",
bytes: [7u8; 32],
};
cache.insert(
key.clone(),
crate::metadata_store::ActionEntryRow {
action_key: key.clone(),
key_epoch: 2,
projection_epoch: 5,
},
);
let policy = NamespacePolicy::allowing(["proj-alpha".to_owned()]);
let report = build_report(&mut store, Some(&cache), dir.path(), &policy).unwrap();
let l1 = report.l1.expect("l1 section present");
assert_eq!(l1.capacity, 8);
assert_eq!(l1.entries.len(), 1);
assert_eq!(l1.entries[0].key_epoch, 2);
assert_eq!(l1.entries[0].projection_epoch, 5);
assert_eq!(l1.stats.hits, 0);
let _: Option<LookupStats> = None;
}
}