use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasher, Hash, Hasher, RandomState};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
use std::time::Instant;
use crate::policy::{Digest, Ecosystem};
use crate::store::rows::{Generation, ProjectRow, ReferenceId, ReferenceRow};
const SHARDS: usize = 16;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ProjectKey {
pub ecosystem: Ecosystem,
pub name: String,
}
impl ProjectKey {
pub fn new(ecosystem: Ecosystem, name: impl Into<String>) -> ProjectKey {
ProjectKey {
ecosystem,
name: name.into(),
}
}
}
impl Hash for ProjectKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.ecosystem.as_tag().hash(state);
self.name.hash(state);
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum Representation {
NpmFull,
NpmAbbreviated,
NpmVersion(String),
PypiHtml,
PypiJson,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct RenderKey {
pub project: ProjectKey,
pub representation: Representation,
}
#[derive(Clone, Debug)]
pub struct RenderedResponse {
pub body: Arc<[u8]>,
pub content_type: &'static str,
pub project_generation: Generation,
pub blocklist_revision: u64,
pub digest_generation: u64,
pub deadline_utc_micros: i64,
pub deadline_monotonic: Instant,
}
impl RenderedResponse {
pub fn is_reusable(
&self,
project_generation: Generation,
blocklist_revision: u64,
digest_generation: u64,
now_utc_micros: i64,
now_monotonic: Instant,
) -> bool {
self.project_generation == project_generation
&& self.blocklist_revision == blocklist_revision
&& self.digest_generation == digest_generation
&& now_utc_micros < self.deadline_utc_micros
&& now_monotonic < self.deadline_monotonic
}
}
#[derive(Clone, Debug)]
pub struct CachedProject {
pub row: ProjectRow,
pub first_seen: HashMap<ReferenceId, i64>,
pub pins: HashMap<ReferenceId, Vec<Digest>>,
pub advertised: OnceLock<Arc<HashSet<ReferenceId>>>,
}
impl CachedProject {
pub fn approximate_bytes(&self) -> u64 {
self.row.payload.len() as u64
+ self.first_seen.len() as u64 * 48
+ self.pins.len() as u64 * 144
+ 256
}
pub fn pins_of(&self, id: &ReferenceId) -> &[Digest] {
self.pins.get(id).map_or(&[], Vec::as_slice)
}
}
#[derive(Clone, Copy, Debug)]
pub struct AbsentMark {
pub observed_monotonic: Instant,
}
pub struct MemoryCaches {
pub projects: ShardedCache<ProjectKey, CachedProject>,
pub rendered: ShardedCache<RenderKey, RenderedResponse>,
pub references: ShardedCache<ReferenceId, ReferenceRow>,
pub absent: ShardedCache<ProjectKey, AbsentMark>,
stored_parses: AtomicU64,
}
impl MemoryCaches {
pub fn new(budget_bytes: u64) -> MemoryCaches {
let absent = (budget_bytes / 32).max(64 * 1024);
let references = (budget_bytes / 16).max(64 * 1024);
let rest = budget_bytes
.saturating_sub(absent)
.saturating_sub(references)
.max(2);
MemoryCaches {
projects: ShardedCache::new(rest / 2),
rendered: ShardedCache::new(rest - rest / 2),
references: ShardedCache::new(references),
absent: ShardedCache::new(absent),
stored_parses: AtomicU64::new(0),
}
}
pub fn note_stored_parse(&self) {
self.stored_parses.fetch_add(1, Ordering::Relaxed);
}
pub fn stored_parses(&self) -> u64 {
self.stored_parses.load(Ordering::Relaxed)
}
}
pub struct ShardedCache<K, V> {
shards: Box<[RwLock<Shard<K, V>>]>,
shard_budget_bytes: u64,
hasher: RandomState,
}
struct Shard<K, V> {
entries: HashMap<K, Entry<V>>,
ring: Vec<K>,
hand: usize,
used_bytes: u64,
invalidations: u64,
}
struct Entry<V> {
value: Arc<V>,
bytes: u64,
referenced: AtomicBool,
}
impl<K: Hash + Eq + Clone, V> ShardedCache<K, V> {
pub fn new(budget_bytes: u64) -> ShardedCache<K, V> {
let shards = (0..SHARDS)
.map(|_| {
RwLock::new(Shard {
entries: HashMap::new(),
ring: Vec::new(),
hand: 0,
used_bytes: 0,
invalidations: 0,
})
})
.collect::<Vec<_>>();
ShardedCache {
shards: shards.into_boxed_slice(),
shard_budget_bytes: (budget_bytes / SHARDS as u64).max(1),
hasher: RandomState::new(),
}
}
pub fn get(&self, key: &K) -> Option<Arc<V>> {
let shard = self.shard(key).read().ok()?;
let entry = shard.entries.get(key)?;
entry.referenced.store(true, Ordering::Relaxed);
Some(Arc::clone(&entry.value))
}
pub fn insert(&self, key: K, value: Arc<V>, bytes: u64) {
let Ok(mut shard) = self.shard(&key).write() else {
return;
};
shard.install(key, value, bytes, self.shard_budget_bytes);
}
pub fn get_with_seen(&self, key: &K) -> (u64, Option<Arc<V>>) {
let Ok(shard) = self.shard(key).read() else {
return (u64::MAX, None);
};
let value = shard.entries.get(key).map(|entry| {
entry.referenced.store(true, Ordering::Relaxed);
Arc::clone(&entry.value)
});
(shard.invalidations, value)
}
pub fn insert_if_current(&self, key: K, value: Arc<V>, bytes: u64, seen: u64) {
let Ok(mut shard) = self.shard(&key).write() else {
return;
};
if shard.invalidations != seen {
return;
}
shard.install(key, value, bytes, self.shard_budget_bytes);
}
pub fn remove(&self, key: &K) {
if let Ok(mut shard) = self.shard(key).write() {
shard.invalidations = shard.invalidations.wrapping_add(1);
shard.remove(key);
}
}
pub fn len(&self) -> usize {
self.shards
.iter()
.filter_map(|shard| shard.read().ok())
.map(|shard| shard.entries.len())
.sum()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn shard(&self, key: &K) -> &RwLock<Shard<K, V>> {
let hash = self.hasher.hash_one(key);
&self.shards[(hash % SHARDS as u64) as usize]
}
}
impl<K: Hash + Eq + Clone, V> Shard<K, V> {
fn install(&mut self, key: K, value: Arc<V>, bytes: u64, budget_bytes: u64) {
self.remove(&key);
if bytes > budget_bytes {
return;
}
while self.used_bytes + bytes > budget_bytes {
if !self.reclaim_one() {
return;
}
}
self.used_bytes += bytes;
self.ring.push(key.clone());
self.entries.insert(
key,
Entry {
value,
bytes,
referenced: AtomicBool::new(true),
},
);
}
fn remove(&mut self, key: &K) {
if let Some(entry) = self.entries.remove(key) {
self.used_bytes -= entry.bytes;
if let Some(position) = self.ring.iter().position(|held| held == key) {
self.ring.remove(position);
if self.hand > position {
self.hand -= 1;
}
}
}
}
fn reclaim_one(&mut self) -> bool {
if self.ring.is_empty() {
return false;
}
for _ in 0..self.ring.len() * 2 {
if self.hand >= self.ring.len() {
self.hand = 0;
}
let key = self.ring[self.hand].clone();
let Some(entry) = self.entries.get(&key) else {
self.ring.remove(self.hand);
continue;
};
if entry.referenced.swap(false, Ordering::Relaxed) {
self.hand += 1;
continue;
}
self.remove(&key);
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cache() -> ShardedCache<ProjectKey, u32> {
ShardedCache::new(256 * SHARDS as u64)
}
fn shard() -> Shard<&'static str, u32> {
Shard {
entries: HashMap::new(),
ring: Vec::new(),
hand: 0,
used_bytes: 0,
invalidations: 0,
}
}
fn put(shard: &mut Shard<&'static str, u32>, key: &'static str) {
shard.used_bytes += 1;
shard.ring.push(key);
shard.entries.insert(
key,
Entry {
value: Arc::new(0),
bytes: 1,
referenced: AtomicBool::new(true),
},
);
}
#[test]
fn a_read_entry_survives_the_sweep_that_reclaims_an_unread_one() {
let mut shard = shard();
put(&mut shard, "first");
put(&mut shard, "second");
assert!(shard.reclaim_one());
assert!(!shard.entries.contains_key("first"));
assert!(shard.entries.contains_key("second"));
put(&mut shard, "third");
shard.entries["second"]
.referenced
.store(true, Ordering::Relaxed);
shard.entries["third"]
.referenced
.store(false, Ordering::Relaxed);
assert!(shard.reclaim_one());
assert!(
shard.entries.contains_key("second"),
"an entry read since the last pass survives it"
);
assert!(!shard.entries.contains_key("third"));
assert_eq!(shard.used_bytes, 1, "byte accounting follows the reclaim");
}
#[test]
fn a_shard_never_exceeds_its_budget() {
let cache: ShardedCache<u64, u64> = ShardedCache::new(128 * SHARDS as u64);
for index in 0..1_000u64 {
cache.insert(index, Arc::new(index), 64);
}
assert!(
cache.len() <= 2 * SHARDS,
"every shard holds at most two entries, so the whole cache holds at most {}",
2 * SHARDS
);
assert!(!cache.is_empty(), "and it does not evict itself empty");
}
#[test]
fn an_oversized_entry_does_not_empty_a_shard() {
let cache = cache();
let small = ProjectKey::new(Ecosystem::Npm, "small");
cache.insert(small.clone(), Arc::new(1), 8);
cache.insert(
ProjectKey::new(Ecosystem::Npm, "huge"),
Arc::new(2),
1 << 40,
);
assert_eq!(cache.get(&small).as_deref(), Some(&1));
assert!(
cache
.get(&ProjectKey::new(Ecosystem::Npm, "huge"))
.is_none()
);
}
#[test]
fn reuse_requires_every_condition_of_spec_10() {
let now = 1_000_000i64;
let monotonic = Instant::now();
let rendered = RenderedResponse {
body: Arc::from(b"{}".as_slice()),
content_type: "application/json",
project_generation: Generation(4),
blocklist_revision: 7,
digest_generation: 2,
deadline_utc_micros: now + 1,
deadline_monotonic: monotonic + std::time::Duration::from_secs(60),
};
assert!(rendered.is_reusable(Generation(4), 7, 2, now, monotonic));
assert!(!rendered.is_reusable(Generation(5), 7, 2, now, monotonic));
assert!(!rendered.is_reusable(Generation(4), 8, 2, now, monotonic));
assert!(!rendered.is_reusable(Generation(4), 7, 3, now, monotonic));
assert!(
!rendered.is_reusable(Generation(4), 7, 2, now + 1, monotonic),
"the deadline is exclusive: at it, the entry is spent"
);
assert!(
!rendered.is_reusable(
Generation(4),
7,
2,
now,
monotonic + std::time::Duration::from_secs(61)
),
"a backward wall-clock jump cannot extend an entry past its monotonic deadline"
);
}
}