use std::collections::HashMap;
use std::ops::Range;
use std::sync::Arc;
use std::time::Instant;
use anyhow::{Result, anyhow};
use bytes::Bytes;
use futures::future::join_all;
use futures::stream::{self, FuturesUnordered, StreamExt};
use micromegas_tracing::prelude::*;
use micromegas_tracing::property_set::PropertySet;
use object_store::{ObjectStoreExt, path::Path};
use crate::backend::FillHint;
use crate::blocks::{block_byte_range, coalesce_runs};
use crate::metric_tags;
use super::RangeCache;
use super::scheduler::{
BatchState, FulfillGuard, InFlight, Ownership, Priority, acquire_run_permit,
effective_priority, reconstruct_shared_error,
};
const BACKEND_PROBE_CONCURRENCY: usize = 16;
struct ProbeOutcome {
hits: HashMap<u64, Bytes>,
missing: Vec<u64>,
}
struct RegisteredBlocks {
owned: Vec<u64>,
entries: HashMap<u64, Arc<InFlight>>,
batch: Option<Arc<BatchState>>,
}
struct CoalescedRun {
range: Range<u64>,
entries: Vec<Arc<InFlight>>,
keys: Vec<String>,
}
impl RangeCache {
pub(super) async fn fetch_blocks(
&self,
key: &str,
file_size: u64,
indices: &[u64],
prio: Priority,
) -> Result<HashMap<u64, Bytes>> {
if indices.is_empty() {
return Ok(HashMap::new());
}
let prefix_tags = self.classify_tags(key);
let block_tag = prefix_tags.prefix;
let (run_demand_tag, run_prefetch_tag) =
(prefix_tags.prefix_demand, prefix_tags.prefix_prefetch);
let ProbeOutcome { hits, missing } = self
.probe_blocks(key, file_size, indices, prio, block_tag)
.await;
if missing.is_empty() {
return Ok(hits);
}
let RegisteredBlocks {
owned,
entries,
batch: _batch,
} = self.register_missing(key, &missing, prio);
let hint = match prio {
Priority::Demand => FillHint::Demand,
Priority::Prefetch => FillHint::Prefetch,
};
for range in coalesce_runs(&owned, self.block_size, self.max_coalesced_get_bytes) {
let run_entries: Vec<Arc<InFlight>> = (range.start..range.end)
.map(|idx| entries.get(&idx).expect("owned entry present").clone())
.collect();
let run_keys: Vec<String> = (range.start..range.end)
.map(|idx| format!("blk:{}:{key}:{idx}", self.ns))
.collect();
let run = CoalescedRun {
range,
entries: run_entries,
keys: run_keys,
};
self.spawn_run_fetch(key, file_size, run, hint, run_demand_tag, run_prefetch_tag);
}
if prio == Priority::Prefetch {
join_prefetch(entries).await?;
return Ok(HashMap::new());
}
join_demand(entries, &missing, hits).await
}
async fn probe_blocks(
&self,
key: &str,
file_size: u64,
indices: &[u64],
prio: Priority,
block_tag: &'static PropertySet,
) -> ProbeOutcome {
let mut hits: HashMap<u64, Bytes> = HashMap::new();
let mut missing: Vec<u64> = Vec::new();
{
let probe_futs: Vec<_> = indices
.iter()
.map(|&idx| {
let block_key = format!("blk:{}:{key}:{idx}", self.ns);
let backend = self.backend.clone();
async move {
imetric!("range_cache_block_request", "count", block_tag, 1_u64);
let probe =
instrument_named!(backend.get(&block_key), "range_cache_backend_read")
.await;
(idx, probe)
}
})
.collect();
let mut probes = stream::iter(probe_futs).buffer_unordered(BACKEND_PROBE_CONCURRENCY);
while let Some((idx, probe)) = probes.next().await {
match probe {
Some(data) => {
let expected_range = block_byte_range(idx, self.block_size, file_size);
let expected_len = expected_range.end - expected_range.start;
if data.len() as u64 != expected_len {
imetric!("range_cache_block_len_mismatch", "count", 1_u64);
warn!(
"range_cache block length mismatch key={key} idx={idx} \
expected={expected_len} got={}",
data.len()
);
missing.push(idx);
continue;
}
imetric!("range_cache_block_backend_hit", "count", block_tag, 1_u64);
if prio == Priority::Demand {
hits.insert(idx, data);
}
}
None => missing.push(idx),
}
}
}
missing.sort_unstable();
missing.dedup();
ProbeOutcome { hits, missing }
}
fn register_missing(&self, key: &str, missing: &[u64], prio: Priority) -> RegisteredBlocks {
let batch = if prio == Priority::Prefetch {
Some(Arc::new(BatchState::new(missing.len())))
} else {
None
};
let mut owned: Vec<u64> = Vec::new();
let mut entries: HashMap<u64, Arc<InFlight>> = HashMap::with_capacity(missing.len());
for &idx in missing {
let block_key = format!("blk:{}:{key}:{idx}", self.ns);
match self.scheduler.own_or_join(block_key, prio, batch.clone()) {
Ownership::Owner(entry) => {
owned.push(idx);
entries.insert(idx, entry);
}
Ownership::Joiner(entry) => {
entries.insert(idx, entry);
}
}
}
RegisteredBlocks {
owned,
entries,
batch,
}
}
fn spawn_run_fetch(
&self,
key: &str,
file_size: u64,
run: CoalescedRun,
hint: FillHint,
run_demand_tag: &'static PropertySet,
run_prefetch_tag: &'static PropertySet,
) {
let cache = self.clone();
let block_size = self.block_size;
let key_owned = key.to_string();
tokio::spawn(async move {
let guard = FulfillGuard::new(
cache.scheduler.clone(),
run.keys
.iter()
.cloned()
.zip(run.entries.iter().cloned())
.collect(),
);
let permit_wait_start = Instant::now();
let permit = instrument_named!(
acquire_run_permit(&cache.scheduler, &run.entries),
"range_cache_fetch_permit_wait"
)
.await;
let class = effective_priority(&run.entries).class_label();
let run_class_tag = if class == metric_tags::CLASS_DEMAND {
run_demand_tag
} else {
run_prefetch_tag
};
fmetric!(
"range_cache_fetch_permit_wait_ms",
"ms",
metric_tags::class_tags(class),
permit_wait_start.elapsed().as_secs_f64() * 1000.0
);
let byte_start = run.range.start * block_size;
let byte_end = block_byte_range(run.range.end - 1, block_size, file_size).end;
let path = Path::from(key_owned.as_str());
let origin_get_start = Instant::now();
let outcome = instrument_named!(
cache.origin.get_range(&path, byte_start..byte_end),
"range_cache_origin_get"
)
.await;
fmetric!(
"range_cache_origin_get_ms",
"ms",
metric_tags::class_tags(class),
origin_get_start.elapsed().as_secs_f64() * 1000.0
);
drop(permit);
match outcome {
Ok(data) => {
cache
.fulfill_run_success(
&run,
data,
byte_start..byte_end,
&key_owned,
run_class_tag,
hint,
)
.await;
}
Err(e) => {
let shared: Arc<anyhow::Error> = Arc::new(anyhow::Error::from(e));
for entry in &run.entries {
entry.fulfill(Err(shared.clone()));
}
}
}
for k in &run.keys {
cache.scheduler.remove_entry(k);
}
guard.disarm();
});
}
async fn fulfill_run_success(
&self,
run: &CoalescedRun,
data: Bytes,
byte_range: Range<u64>,
key_owned: &str,
run_class_tag: &'static PropertySet,
hint: FillHint,
) {
let block_size = self.block_size;
let range = &run.range;
let run_len = (range.end - range.start) as usize;
let expected_run_bytes = byte_range.end - byte_range.start;
if data.len() as u64 != expected_run_bytes {
imetric!("range_cache_origin_run_len_mismatch", "count", 1_u64);
warn!(
"range_cache origin fetch length mismatch key={key_owned} \
run={range:?} expected={expected_run_bytes} got={}",
data.len()
);
let shared: Arc<anyhow::Error> = Arc::new(anyhow!(
"origin object changed size mid-fetch: key={key_owned} \
run={range:?} expected {expected_run_bytes} bytes, got {} bytes",
data.len()
));
for entry in &run.entries {
entry.fulfill(Err(shared.clone()));
}
} else {
imetric!(
"range_cache_origin_block_fetch",
"count",
run_class_tag,
run_len as u64
);
imetric!(
"range_cache_origin_block_bytes",
"bytes",
run_class_tag,
data.len() as u64
);
debug!(
"range_cache origin fetch key={key_owned} run={range:?} bytes={}",
data.len()
);
for i in 0..run_len {
let offset = i as u64 * block_size;
let local_start = offset as usize;
let local_end = (offset + block_size).min(data.len() as u64) as usize;
let chunk = data.slice(local_start..local_end);
self.backend
.put(run.keys[i].clone(), chunk.clone(), hint)
.await;
run.entries[i].fulfill(Ok(chunk));
}
}
}
}
async fn join_demand(
entries: HashMap<u64, Arc<InFlight>>,
missing: &[u64],
mut hits: HashMap<u64, Bytes>,
) -> Result<HashMap<u64, Bytes>> {
let joined = join_all(missing.iter().map(|idx| {
let entry = entries
.get(idx)
.expect("entry present for every missing index")
.clone();
let idx = *idx;
async move { (idx, entry.join().await) }
}))
.await;
for (idx, r) in joined {
let data = r.map_err(|e| reconstruct_shared_error(&e))?;
hits.insert(idx, data);
}
Ok(hits)
}
async fn join_prefetch(entries: HashMap<u64, Arc<InFlight>>) -> Result<()> {
let mut joins: FuturesUnordered<_> = entries
.into_values()
.map(|entry| async move { entry.join().await.map(|_bytes| ()) })
.collect();
while let Some(r) = joins.next().await {
r.map_err(|e| reconstruct_shared_error(&e))?;
}
Ok(())
}