use std::collections::{BTreeSet, HashMap};
use std::ops::Range;
use std::sync::Arc;
use anyhow::Result;
use async_stream::try_stream;
use bytes::{BufMut, Bytes, BytesMut};
use futures::stream::{self, Stream, StreamExt};
use micromegas_tracing::prelude::*;
use object_store::{ObjectStore, ObjectStoreExt, path::Path};
use super::backend::{BackendDiskStats, FillHint, RangeCacheBackend};
use super::blocks::{assemble_range, block_byte_range, blocks_for_range};
use super::metric_tags::{self, PrefixTags};
mod error;
mod fetch;
mod scheduler;
pub use error::{RangeError, StreamRangesCaller};
use scheduler::{
FetchScheduler, FulfillGuard, Ownership, Priority, decode_size, reconstruct_shared_error,
};
pub const DEFAULT_BLOCK_SIZE: u64 = 1024 * 1024;
pub const DEMAND_WINDOW_BLOCKS: u64 = 8;
pub const DEFAULT_TOTAL_FETCH_PERMITS: usize = 32;
pub const DEFAULT_DEMAND_RESERVED_FETCH_PERMITS: usize = 8;
pub const DEFAULT_MAX_COALESCED_GET_BYTES: u64 = 8 * 1024 * 1024;
pub const DEFAULT_PROMOTE_WHOLE_BATCH: bool = false;
#[derive(Clone)]
pub struct RangeCache {
origin: Arc<dyn ObjectStore>,
backend: Arc<dyn RangeCacheBackend>,
block_size: u64,
ns: String,
scheduler: Arc<FetchScheduler>,
max_coalesced_get_bytes: u64,
prefix_labels: Arc<[&'static str]>,
prefix_tags: Arc<[PrefixTags]>,
other_tags: PrefixTags,
}
impl RangeCache {
#[allow(clippy::too_many_arguments)]
pub fn new(
origin: Arc<dyn ObjectStore>,
backend: Arc<dyn RangeCacheBackend>,
block_size: u64,
ns: String,
total_fetch_permits: usize,
demand_reserved_fetch_permits: usize,
max_coalesced_get_bytes: u64,
promote_whole_batch: bool,
) -> Self {
Self {
origin,
backend,
block_size,
ns,
scheduler: Arc::new(FetchScheduler::new(
total_fetch_permits,
demand_reserved_fetch_permits,
promote_whole_batch,
)),
max_coalesced_get_bytes,
prefix_labels: Arc::from(Vec::new()),
prefix_tags: Arc::from(Vec::new()),
other_tags: PrefixTags::new(metric_tags::PREFIX_OTHER),
}
}
pub fn with_prefix_labels(mut self, labels: Arc<[&'static str]>) -> Self {
let tags: Vec<PrefixTags> = labels.iter().map(|&label| PrefixTags::new(label)).collect();
self.prefix_tags = Arc::from(tags);
self.prefix_labels = labels;
self
}
fn classify_tags(&self, key: &str) -> &PrefixTags {
match metric_tags::longest_prefix_match(&self.prefix_labels, key) {
Some(i) => &self.prefix_tags[i],
None => &self.other_tags,
}
}
pub fn classify(&self, key: &str) -> &'static str {
self.classify_tags(key).label
}
pub fn fetch_budget_stats(&self) -> (usize, usize, usize, usize) {
self.scheduler.fetch_budget_stats()
}
pub fn inflight_len(&self) -> usize {
self.scheduler.inflight_len()
}
pub fn backend_disk_stats(&self) -> Option<BackendDiskStats> {
self.backend.disk_stats()
}
pub fn block_size(&self) -> u64 {
self.block_size
}
#[span_fn]
pub async fn size(&self, key: &str) -> Result<u64> {
let meta_key = format!("meta:{}:{key}", self.ns);
let prefix_tag = self.classify_tags(key).prefix;
if let Some(data) = self.backend.get(&meta_key).await
&& data.len() == 8
{
imetric!("range_cache_size_backend_hit", "count", prefix_tag, 1_u64);
return decode_size(&data);
}
match self
.scheduler
.own_or_join(meta_key.clone(), Priority::Demand, None)
{
Ownership::Owner(entry) => {
let origin = self.origin.clone();
let backend = self.backend.clone();
let scheduler = self.scheduler.clone();
let key_owned = key.to_string();
let meta_key_owned = meta_key.clone();
let task_entry = entry.clone();
tokio::spawn(async move {
let guard = FulfillGuard::new(
scheduler.clone(),
vec![(meta_key_owned.clone(), task_entry.clone())],
);
imetric!("range_cache_origin_head", "count", prefix_tag, 1_u64);
let head_path = Path::from(key_owned.as_str());
let head_result = instrument_named!(
origin.head(&head_path),
"range_cache_origin_head_latency"
)
.await;
match head_result {
Ok(object_meta) => {
let size = object_meta.size;
debug!("range_cache origin head key={key_owned} size={size}");
let size_bytes = Bytes::from(size.to_le_bytes().to_vec());
backend
.put(meta_key_owned.clone(), size_bytes.clone(), FillHint::Demand)
.await;
task_entry.fulfill(Ok(size_bytes));
}
Err(e) => {
task_entry.fulfill(Err(Arc::new(anyhow::Error::from(e))));
}
}
scheduler.remove_entry(&meta_key_owned);
guard.disarm();
});
let data = entry
.join()
.await
.map_err(|e| reconstruct_shared_error(&e))?;
decode_size(&data)
}
Ownership::Joiner(entry) => {
let data = entry
.join()
.await
.map_err(|e| reconstruct_shared_error(&e))?;
decode_size(&data)
}
}
}
#[span_fn]
pub async fn stream_ranges(
&self,
key: &str,
ranges: Vec<Range<u64>>,
caller: StreamRangesCaller,
) -> Result<impl Stream<Item = Result<Bytes>> + Send + 'static> {
let file_size = match self.size(key).await {
Ok(s) => s,
Err(e) => {
caller.emit_error_metric();
return Err(e);
}
};
self.stream_ranges_inner(key, ranges, file_size, caller)
.await
}
#[span_fn]
pub async fn stream_ranges_with_size(
&self,
key: &str,
ranges: Vec<Range<u64>>,
file_size: u64,
caller: StreamRangesCaller,
) -> Result<impl Stream<Item = Result<Bytes>> + Send + 'static> {
self.stream_ranges_inner(key, ranges, file_size, caller)
.await
}
async fn stream_ranges_inner(
&self,
key: &str,
ranges: Vec<Range<u64>>,
file_size: u64,
caller: StreamRangesCaller,
) -> Result<impl Stream<Item = Result<Bytes>> + Send + 'static> {
for r in &ranges {
if r.end > file_size {
caller.emit_error_metric();
return Err(RangeError::OutOfBounds {
requested_end: r.end,
file_size,
}
.into());
}
}
let cache = self.clone();
let key = key.to_string();
Ok(try_stream! {
for r in ranges {
if r.start >= r.end {
continue;
}
let blk_range = blocks_for_range(r.start, r.end, cache.block_size);
let mut windows = cache.stream_demand_windows(&key, file_size, blk_range);
while let Some((w, result)) = windows.next().await {
let block_map = result.inspect_err(|_| caller.emit_error_metric())?;
yield cache.assemble_window(&block_map, &w, r.start, r.end, file_size);
}
}
})
}
fn stream_demand_windows(
&self,
key: &str,
file_size: u64,
blk_range: Range<u64>,
) -> impl Stream<Item = (Vec<u64>, Result<HashMap<u64, Bytes>>)> + Send + 'static {
let window_indices: Vec<Vec<u64>> = (blk_range.start..blk_range.end)
.collect::<Vec<u64>>()
.chunks(DEMAND_WINDOW_BLOCKS as usize)
.map(|w| w.to_vec())
.collect();
let cache = self.clone();
let key = key.to_string();
stream::iter(window_indices)
.map(move |w| {
let cache = cache.clone();
let key = key.clone();
async move {
let result = cache
.fetch_blocks(&key, file_size, &w, Priority::Demand)
.await;
(w, result)
}
})
.buffered(2)
}
fn assemble_window(
&self,
block_map: &HashMap<u64, Bytes>,
window: &[u64],
req_start: u64,
req_end: u64,
file_size: u64,
) -> Bytes {
let blocks: Vec<(u64, Bytes)> = window
.iter()
.map(|idx| {
let data = block_map
.get(idx)
.cloned()
.expect("fetch_blocks returns every requested index");
(*idx, data)
})
.collect();
let win_start = window[0] * self.block_size;
let win_end = block_byte_range(
*window.last().expect("window is non-empty"),
self.block_size,
file_size,
)
.end;
let local_start = req_start.max(win_start);
let local_end = req_end.min(win_end);
assemble_range(&blocks, self.block_size, local_start, local_end)
}
#[span_fn]
pub async fn get_range(&self, key: &str, range: Range<u64>) -> Result<Bytes> {
let mut stream = Box::pin(
self.stream_ranges(key, vec![range], StreamRangesCaller::Range)
.await?,
);
let mut buf = BytesMut::new();
while let Some(chunk) = stream.next().await {
buf.put_slice(&chunk?);
}
Ok(buf.freeze())
}
#[span_fn]
pub async fn get_range_with_size(
&self,
key: &str,
file_size: u64,
range: Range<u64>,
) -> Result<Bytes> {
let mut stream = Box::pin(
self.stream_ranges_with_size(key, vec![range], file_size, StreamRangesCaller::Range)
.await?,
);
let mut buf = BytesMut::new();
while let Some(chunk) = stream.next().await {
buf.put_slice(&chunk?);
}
Ok(buf.freeze())
}
#[span_fn]
pub async fn get_ranges(&self, key: &str, ranges: &[Range<u64>]) -> Result<Vec<Bytes>> {
if ranges.is_empty() {
return Ok(vec![]);
}
let owned_ranges: Vec<Range<u64>> = ranges.to_vec();
let stream = Box::pin(
self.stream_ranges(key, owned_ranges, StreamRangesCaller::Ranges)
.await?,
);
collect_ranges_from_stream(ranges, stream).await
}
#[span_fn]
pub async fn get_ranges_with_size(
&self,
key: &str,
file_size: u64,
ranges: &[Range<u64>],
) -> Result<Vec<Bytes>> {
if ranges.is_empty() {
return Ok(vec![]);
}
let owned_ranges: Vec<Range<u64>> = ranges.to_vec();
let stream = Box::pin(
self.stream_ranges_with_size(key, owned_ranges, file_size, StreamRangesCaller::Ranges)
.await?,
);
collect_ranges_from_stream(ranges, stream).await
}
pub async fn prefetch_ranges(&self, key: &str, ranges: &[Range<u64>]) -> Result<()> {
if ranges.is_empty() {
return Ok(());
}
let file_size = self.size(key).await?;
let mut all_block_indices = BTreeSet::new();
for r in ranges {
let start = r.start;
let end = r.end;
if end > file_size {
return Err(RangeError::OutOfBounds {
requested_end: end,
file_size,
}
.into());
}
if start < end {
let blk = blocks_for_range(start, end, self.block_size);
all_block_indices.extend(blk.start..blk.end);
}
}
self.prefetch_blocks(
key,
file_size,
&all_block_indices.into_iter().collect::<Vec<_>>(),
)
.await
}
pub async fn prefetch_blocks(&self, key: &str, file_size: u64, indices: &[u64]) -> Result<()> {
self.fetch_blocks(key, file_size, indices, Priority::Prefetch)
.await?;
Ok(())
}
}
async fn collect_ranges_from_stream(
ranges: &[Range<u64>],
mut stream: impl Stream<Item = Result<Bytes>> + Unpin,
) -> Result<Vec<Bytes>> {
let mut result = Vec::with_capacity(ranges.len());
let mut pending: Option<Bytes> = None;
for r in ranges {
let start = r.start;
let end = r.end;
if start >= end {
result.push(Bytes::new());
continue;
}
let need = (end - start) as usize;
let mut collected = BytesMut::with_capacity(need);
while collected.len() < need {
let chunk = match pending.take() {
Some(c) => c,
None => stream
.next()
.await
.expect("stream_ranges under-yielded for a non-degenerate range")?,
};
let remaining = need - collected.len();
if chunk.len() > remaining {
collected.put_slice(&chunk[..remaining]);
pending = Some(chunk.slice(remaining..));
} else {
collected.put_slice(&chunk);
}
}
result.push(collected.freeze());
}
Ok(result)
}