use super::Config;
use crate::{
journal::{
Error,
segmented::variable::{Config as JConfig, Journal},
},
rmap::RMap,
};
use commonware_codec::{CodecShared, EncodeSize, Read, ReadExt, Write, varint::UInt};
use commonware_runtime::{
Buf, BufMut, Metrics, ReadOptions, Storage,
telemetry::metrics::{Counter, Gauge, GaugeExt, MetricsExt as _},
};
use std::collections::{BTreeMap, BTreeSet};
use tracing::debug;
struct Record<V: CodecShared> {
index: u64,
value: V,
}
impl<V: CodecShared> Record<V> {
const fn new(index: u64, value: V) -> Self {
Self { index, value }
}
}
impl<V: CodecShared> Write for Record<V> {
fn write(&self, buf: &mut impl BufMut) {
UInt(self.index).write(buf);
self.value.write(buf);
}
}
impl<V: CodecShared> Read for Record<V> {
type Cfg = V::Cfg;
fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
let index = UInt::read(buf)?.into();
let value = V::read_cfg(buf, cfg)?;
Ok(Self { index, value })
}
}
impl<V: CodecShared> EncodeSize for Record<V> {
fn encode_size(&self) -> usize {
UInt(self.index).encode_size() + self.value.encode_size()
}
}
#[cfg(feature = "arbitrary")]
impl<V: CodecShared> arbitrary::Arbitrary<'_> for Record<V>
where
V: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self::new(u.arbitrary()?, u.arbitrary()?))
}
}
struct Inner<E: Storage + Metrics, V: CodecShared> {
items_per_blob: u64,
journal: Journal<E, Record<V>>,
pending: BTreeSet<u64>,
oldest_allowed: Option<u64>,
indices: BTreeMap<u64, u64>,
intervals: RMap,
items_tracked: Gauge,
gets: Counter,
has: Counter,
syncs: Counter,
}
impl<E: Storage + Metrics, V: CodecShared> Inner<E, V> {
const fn section(&self, index: u64) -> u64 {
(index / self.items_per_blob) * self.items_per_blob
}
async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
let journal = Journal::<E, Record<V>>::init(
context.child("journal"),
JConfig {
partition: cfg.partition,
compression: cfg.compression,
codec_config: cfg.codec_config,
page_cache: cfg.page_cache,
write_buffer: cfg.write_buffer,
},
)
.await?;
let mut indices = BTreeMap::new();
let mut intervals = RMap::new();
let journal = {
debug!("initializing cache");
let mut replay = journal
.replay(0, 0, cfg.replay_buffer, ReadOptions::default())
.await?;
while let Some(result) = replay.next().await {
let (_, offset, _, data) = result?;
indices.insert(data.index, offset);
intervals.insert(data.index);
}
debug!(items = indices.len(), "cache initialized");
replay.finish()?
};
let items_tracked = context.gauge("items_tracked", "Number of items tracked");
let gets = context.counter("gets", "Number of gets performed");
let has = context.counter("has", "Number of has performed");
let syncs = context.counter("syncs", "Number of syncs called");
let _ = items_tracked.try_set(indices.len());
Ok(Self {
items_per_blob: cfg.items_per_blob.get(),
journal,
pending: BTreeSet::new(),
oldest_allowed: None,
indices,
intervals,
items_tracked,
gets,
has,
syncs,
})
}
async fn get(&self, index: u64) -> Result<Option<V>, Error> {
self.gets.inc();
let offset = match self.indices.get(&index) {
Some(offset) => *offset,
None => return Ok(None),
};
let section = self.section(index);
let record = self.journal.get(section, offset).await?;
Ok(Some(record.value))
}
fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
self.intervals.next_gap(index)
}
fn first(&self) -> Option<u64> {
self.intervals.iter().next().map(|(&start, _)| start)
}
fn missing_items(&self, start: u64, max: usize) -> Vec<u64> {
self.intervals.missing_items(start, max)
}
fn has(&self, index: u64) -> bool {
self.has.inc();
self.indices.contains_key(&index)
}
async fn prune(mut self: Box<Self>, min: u64) -> Result<Box<Self>, Error> {
let min = self.section(min);
if let Some(oldest_allowed) = self.oldest_allowed
&& min <= oldest_allowed
{
return Ok(self);
}
debug!(min, "pruning cache");
(self.journal, _) = self.journal.prune(min).await?;
loop {
let next = match self.pending.iter().next() {
Some(section) if *section < min => *section,
_ => break,
};
self.pending.remove(&next);
}
loop {
let next = match self.indices.first_key_value() {
Some((index, _)) if *index < min => *index,
_ => break,
};
self.indices.remove(&next).unwrap();
}
if min > 0 {
self.intervals.remove(0, min - 1);
}
self.oldest_allowed = Some(min);
let _ = self.items_tracked.try_set(self.indices.len());
Ok(self)
}
async fn put(mut self: Box<Self>, index: u64, value: V) -> Result<(Box<Self>, bool), Error> {
let oldest_allowed = self.oldest_allowed.unwrap_or(0);
if index < oldest_allowed {
debug!(index, oldest_allowed, "ignoring put below prune floor");
return Ok((self, false));
}
if self.indices.contains_key(&index) {
return Ok((self, true));
}
let record = Record::new(index, value);
let section = self.section(index);
let offset;
(self.journal, offset, _) = self.journal.append(section, &record).await?;
self.indices.insert(index, offset);
self.intervals.insert(index);
self.pending.insert(section);
let _ = self.items_tracked.try_set(self.indices.len());
Ok((self, true))
}
async fn sync(mut self: Box<Self>) -> Result<Box<Self>, Error> {
self.syncs.inc_by(self.pending.len() as u64);
self.journal = self.journal.sync(&self.pending).await?;
self.pending.clear();
Ok(self)
}
async fn destroy(self) -> Result<(), Error> {
self.journal.destroy().await
}
}
pub struct Cache<E: Storage + Metrics, V: CodecShared>(Box<Inner<E, V>>);
impl<E: Storage + Metrics, V: CodecShared> std::fmt::Debug for Cache<E, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cache")
.field("first_index", &self.0.intervals.first_index())
.field("last_index", &self.0.intervals.last_index())
.finish_non_exhaustive()
}
}
impl<E: Storage + Metrics, V: CodecShared> Cache<E, V> {
pub async fn init(context: E, cfg: Config<V::Cfg>) -> Result<Self, Error> {
Ok(Self(Box::new(Inner::init(context, cfg).await?)))
}
pub async fn get(&self, index: u64) -> Result<Option<V>, Error> {
self.0.get(index).await
}
pub fn next_gap(&self, index: u64) -> (Option<u64>, Option<u64>) {
self.0.next_gap(index)
}
pub fn first(&self) -> Option<u64> {
self.0.first()
}
pub fn missing_items(&self, start: u64, max: usize) -> Vec<u64> {
self.0.missing_items(start, max)
}
pub fn has(&self, index: u64) -> bool {
self.0.has(index)
}
pub async fn prune(mut self, min: u64) -> Result<Self, Error> {
self.0 = self.0.prune(min).await?;
Ok(self)
}
pub async fn put(mut self, index: u64, value: V) -> Result<Self, Error> {
(self.0, _) = self.0.put(index, value).await?;
Ok(self)
}
pub async fn sync(mut self) -> Result<Self, Error> {
self.0 = self.0.sync().await?;
Ok(self)
}
pub async fn put_sync(mut self, index: u64, value: V) -> Result<Self, Error> {
let stored;
(self.0, stored) = self.0.put(index, value).await?;
if !stored {
return Ok(self);
}
self.sync().await
}
pub async fn destroy(self) -> Result<(), Error> {
self.0.destroy().await
}
}
#[cfg(all(test, feature = "arbitrary"))]
mod conformance {
use super::*;
use commonware_codec::conformance::CodecConformance;
commonware_conformance::conformance_tests! {
CodecConformance<Record<u64>>,
}
}
#[cfg(test)]
mod tests {
use super::*;
use commonware_runtime::deterministic::Context;
type TestCache = Cache<Context, u64>;
fn is_send<T: Send>(_: T) {}
#[allow(dead_code)]
fn assert_cache_futures_are_send(cache: &TestCache, key: &u64) {
is_send(cache.get(*key));
}
#[allow(dead_code)]
fn assert_cache_destroy_is_send(cache: TestCache) {
is_send(cache.destroy());
}
}