use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use crate::indexer::{
KvIndexerMetrics, LowerTierContinuation, LowerTierIndexer, LowerTierMatchDetails, MatchDetails,
ThreadPoolIndexer, WireTieredMatchDetails,
};
use crate::protocols::{LocalBlockHash, StorageTier};
#[derive(Clone)]
pub struct LowerTierIndexers {
metrics: Option<Arc<KvIndexerMetrics>>,
num_threads: usize,
block_size: u32,
indexers: Arc<RwLock<HashMap<StorageTier, Arc<ThreadPoolIndexer<LowerTierIndexer>>>>>,
}
impl LowerTierIndexers {
pub fn new(num_threads: usize, block_size: u32) -> Self {
Self::new_with_metrics(num_threads, block_size, None)
}
pub fn new_with_metrics(
num_threads: usize,
block_size: u32,
metrics: Option<Arc<KvIndexerMetrics>>,
) -> Self {
assert!(
num_threads > 0,
"lower-tier indexer threads must be non-zero"
);
Self {
num_threads,
block_size,
metrics,
indexers: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn get_or_create(
&self,
storage_tier: StorageTier,
) -> Arc<ThreadPoolIndexer<LowerTierIndexer>> {
debug_assert!(!storage_tier.is_gpu());
if let Some(indexer) = self.indexers.read().unwrap().get(&storage_tier).cloned() {
return indexer;
}
self.indexers
.write()
.unwrap()
.entry(storage_tier)
.or_insert_with(|| {
Arc::new(ThreadPoolIndexer::new_with_metrics(
LowerTierIndexer::new(),
self.num_threads,
self.block_size,
self.metrics.clone(),
))
})
.clone()
}
pub fn all(&self) -> Vec<Arc<ThreadPoolIndexer<LowerTierIndexer>>> {
self.indexers.read().unwrap().values().cloned().collect()
}
pub fn entries(&self) -> Vec<(StorageTier, Arc<ThreadPoolIndexer<LowerTierIndexer>>)> {
self.indexers
.read()
.unwrap()
.iter()
.map(|(tier, indexer)| (*tier, indexer.clone()))
.collect()
}
pub fn get(
&self,
storage_tier: StorageTier,
) -> Option<Arc<ThreadPoolIndexer<LowerTierIndexer>>> {
self.indexers.read().unwrap().get(&storage_tier).cloned()
}
}
#[derive(Debug, Clone, Default)]
pub struct TieredMatchDetails {
pub device: MatchDetails,
pub lower_tier: HashMap<StorageTier, LowerTierMatchDetails>,
}
impl From<&TieredMatchDetails> for WireTieredMatchDetails {
fn from(d: &TieredMatchDetails) -> Self {
Self {
device: d.device.overlap_scores.clone().into(),
lower_tier: d
.lower_tier
.iter()
.map(|(tier, details)| (*tier, details.into()))
.collect(),
}
}
}
impl From<WireTieredMatchDetails> for TieredMatchDetails {
fn from(w: WireTieredMatchDetails) -> Self {
let mut lower_tier = HashMap::with_capacity(w.lower_tier.len());
for (tier, details) in w.lower_tier {
if lower_tier.insert(tier, details.into()).is_some() {
tracing::warn!(
?tier,
"Duplicate StorageTier in WireTieredMatchDetails; keeping last entry"
);
}
}
Self {
device: MatchDetails {
overlap_scores: w.device.into(),
..Default::default()
},
lower_tier,
}
}
}
pub fn lower_tier_query_order() -> [StorageTier; 3] {
[
StorageTier::HostPinned,
StorageTier::Disk,
StorageTier::External,
]
}
pub fn query_lower_tiers(
indexers: &LowerTierIndexers,
sequence: &[LocalBlockHash],
device_matches: &MatchDetails,
) -> HashMap<StorageTier, LowerTierMatchDetails> {
if indexers.indexers.read().unwrap().is_empty() {
return HashMap::new();
}
let mut continuations = LowerTierMatchDetails::default().next_continuations;
for (worker, matched_blocks) in &device_matches.overlap_scores.scores {
let Some(last_hash) = device_matches.last_matched_hashes.get(worker).copied() else {
debug_assert!(
false,
"device match result missing last matched hash for worker {worker:?}"
);
continue;
};
continuations.insert(
*worker,
LowerTierContinuation::new(*matched_blocks as usize, last_hash),
);
}
let mut lower_tier_matches = HashMap::new();
for storage_tier in lower_tier_query_order() {
let Some(indexer) = indexers.get(storage_tier) else {
continue;
};
if let Some(&first_hash) = sequence.first() {
let root_workers: Vec<_> = indexer.backend().root_workers(first_hash);
for worker in root_workers.iter() {
continuations
.entry(*worker)
.or_insert_with(|| LowerTierContinuation::from_root(0));
}
}
let tier_matches = indexer
.backend()
.query_match_details(sequence, &continuations);
let matched_workers = tier_matches.hits.values().filter(|&&hits| hits > 0).count();
tracing::debug!(
?storage_tier,
queried_workers = continuations.len(),
matched_workers,
"Queried lower-tier indexer"
);
continuations = tier_matches.next_continuations.clone();
lower_tier_matches.insert(storage_tier, tier_matches);
}
lower_tier_matches
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocols::{LocalBlockHash, OverlapScores, WorkerWithDpRank};
#[test]
fn query_lower_tiers_returns_empty_when_no_tiers_allocated() {
let indexers = LowerTierIndexers::new(1, 4);
let mut overlap_scores = OverlapScores::new();
overlap_scores
.scores
.insert(WorkerWithDpRank::new(99, 0), 3);
let device_matches = MatchDetails {
overlap_scores,
last_matched_hashes: Default::default(),
};
let sequence = vec![LocalBlockHash(1), LocalBlockHash(2)];
let result = query_lower_tiers(&indexers, &sequence, &device_matches);
assert!(result.is_empty());
}
}