use std::collections::{BTreeMap, BTreeSet};
use std::hash::{Hash, Hasher};
use std::mem::size_of;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use super::super::extended_ttl::{EffectiveExpiry, ExpiryDecision, ExtendedTtlPolicy};
use super::cache::BlobCacheKey;
use super::{BlobCacheHit, BlobCachePolicy};
#[derive(Debug)]
pub(super) struct Entry {
pub(super) bytes: Arc<[u8]>,
pub(super) content_metadata: Option<Arc<BTreeMap<String, String>>>,
pub(super) tags: BTreeSet<String>,
pub(super) dependencies: BTreeSet<String>,
pub(super) l1_charge_bytes: usize,
pub(super) size: usize,
pub(super) visited: AtomicBool,
pub(super) expires_at_unix_ms: Option<u64>,
pub(super) priority: u8,
pub(super) version: Option<u64>,
pub(super) namespace_generation: u64,
pub(super) slot_index: usize,
pub(super) last_access_unix_ms: u64,
pub(super) extended: ExtendedTtlPolicy,
}
impl Entry {
pub(super) fn new(
bytes: Vec<u8>,
content_metadata: BTreeMap<String, String>,
tags: BTreeSet<String>,
dependencies: BTreeSet<String>,
policy: BlobCachePolicy,
namespace_generation: u64,
now_ms: u64,
namespace: &str,
key: &str,
) -> Self {
let size = bytes.len();
let l1_charge_bytes = Self::estimate_l1_charge_bytes(
size,
namespace,
key,
&content_metadata,
&tags,
&dependencies,
);
Self {
bytes: Arc::<[u8]>::from(bytes),
content_metadata: if content_metadata.is_empty() {
None
} else {
Some(Arc::new(content_metadata))
},
tags,
dependencies,
l1_charge_bytes,
size,
visited: AtomicBool::new(true),
expires_at_unix_ms: effective_expires_at_unix_ms(policy, now_ms, namespace, key),
priority: policy.priority_value(),
version: policy.version_value(),
namespace_generation,
slot_index: 0,
last_access_unix_ms: now_ms,
extended: policy.extended_value(),
}
}
pub(super) fn hit(&self) -> BlobCacheHit {
BlobCacheHit::new(
Arc::clone(&self.bytes),
self.content_metadata.clone(),
self.version,
self.expires_at_unix_ms,
)
}
pub(super) fn hit_stale(&self, window_remaining_ms: u64) -> BlobCacheHit {
BlobCacheHit::new_stale(
Arc::clone(&self.bytes),
self.content_metadata.clone(),
self.version,
self.expires_at_unix_ms,
window_remaining_ms,
)
}
pub(super) fn is_expired_at(&self, now_ms: u64) -> bool {
self.expires_at_unix_ms
.is_some_and(|expires_at| now_ms >= expires_at)
}
pub(super) fn estimate_l1_charge_bytes(
payload_bytes: usize,
namespace: &str,
key: &str,
content_metadata: &BTreeMap<String, String>,
tags: &BTreeSet<String>,
dependencies: &BTreeSet<String>,
) -> usize {
const MAP_ENTRY_OVERHEAD_BYTES: usize = 64;
const TREE_NODE_OVERHEAD_BYTES: usize = 64;
const INDEX_ENTRY_OVERHEAD_BYTES: usize = 64;
const EXPIRY_INDEX_OVERHEAD_BYTES: usize = 64;
let key_heap_bytes = namespace.len().saturating_add(key.len());
let mut charge = payload_bytes
.saturating_add(size_of::<Self>())
.saturating_add(size_of::<BlobCacheKey>())
.saturating_add(MAP_ENTRY_OVERHEAD_BYTES)
.saturating_add(size_of::<BlobCacheKey>().saturating_mul(2))
.saturating_add(EXPIRY_INDEX_OVERHEAD_BYTES.saturating_mul(2))
.saturating_add(key_heap_bytes)
.saturating_add(key_heap_bytes.saturating_mul(2));
for (metadata_key, metadata_value) in content_metadata {
charge = charge
.saturating_add(metadata_key.len())
.saturating_add(metadata_value.len())
.saturating_add(TREE_NODE_OVERHEAD_BYTES);
}
for label in tags.iter().chain(dependencies.iter()) {
charge = charge
.saturating_add(label.len())
.saturating_add(TREE_NODE_OVERHEAD_BYTES)
.saturating_add(size_of::<BlobCacheKey>())
.saturating_add(key_heap_bytes)
.saturating_add(INDEX_ENTRY_OVERHEAD_BYTES);
}
charge
}
}
pub(super) fn jitter_seed(namespace: &str, key: &str, now_ms: u64) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
namespace.hash(&mut hasher);
key.hash(&mut hasher);
now_ms.hash(&mut hasher);
hasher.finish()
}
pub(super) fn effective_expires_at_unix_ms(
policy: BlobCachePolicy,
now_ms: u64,
namespace: &str,
key: &str,
) -> Option<u64> {
let extended = policy.extended_value();
let jittered_ttl = policy.ttl_ms_value().map(|base| {
if extended.jitter_pct > 0 {
EffectiveExpiry::jittered_ttl_ms(
base,
extended.jitter_pct,
jitter_seed(namespace, key, now_ms),
)
} else {
base
}
});
match (jittered_ttl, policy.expires_at_unix_ms_value()) {
(Some(ttl), Some(abs)) => Some(now_ms.saturating_add(ttl).min(abs)),
(Some(ttl), None) => Some(now_ms.saturating_add(ttl)),
(None, Some(abs)) => Some(abs),
(None, None) => None,
}
}