Skip to content

Commit c5b9f5c

Browse files
authored
feat: introduce TTL support with ExpiringCache decorator (#132)
* feat: introduce TTL support with ExpiringCache decorator - Added time-based expiration functionality through the `Expiring<C>` decorator, allowing caches to manage per-entry TTLs. - Implemented `Clock` trait and `MockClock` for time management, enabling deterministic testing. - Introduced `ExpirationIndex` for efficient deadline tracking and management of expired entries. - Updated `CacheBuilder` to support `DynExpiringCache` with default TTL configuration. - Added benchmarks to evaluate the overhead of the TTL decorator against a standard LRU cache. This implementation enhances the cache library's capabilities by allowing for more flexible cache management strategies, particularly in scenarios requiring time-based expiration. * docs: correct module references and improve documentation clarity - Updated doc comments in `builder.rs` to accurately reference the `CacheBuilder` module. - Refined documentation in `expiration_index.rs` to enhance clarity regarding the `LazyMinHeap` wrapper. - Adjusted comments in `expiring.rs` to simplify the description of ordering enforcement in the `purge_one` method. These changes improve the accuracy and readability of the documentation, ensuring users have a clearer understanding of the cache library's components and their interactions.
1 parent d54410a commit c5b9f5c

20 files changed

Lines changed: 3061 additions & 17 deletions

File tree

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ default = ["policy-s3-fifo", "policy-lru", "policy-fast-lru", "policy-lru-k", "p
3636
metrics = []
3737
serde = ["dep:serde"]
3838
concurrency = ["parking_lot"]
39+
# Time-based expiration (`Expiring<C>` decorator, `Clock` trait, `DynExpiringCache`).
40+
# See `docs/design/ttl.md`.
41+
ttl = []
3942

4043
# Eviction policy feature flags. Enable only the policies you need for smaller builds.
4144
# Use `default-features = false` and select specific policies, or use `policy-all` for every policy.
@@ -150,6 +153,13 @@ name = "policy_s3_fifo"
150153
path = "benches/policy/s3_fifo.rs"
151154
harness = false
152155

156+
# TTL decorator overhead (requires the `ttl` feature)
157+
[[bench]]
158+
name = "ttl_overhead"
159+
path = "benches/ttl_overhead.rs"
160+
harness = false
161+
required-features = ["ttl"]
162+
153163
# Development profile - optimized for fast compilation and good debugging
154164
[profile.dev]
155165
opt-level = 0

benches/ttl_overhead.rs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//! Overhead of the TTL decorator versus a bare LRU cache.
2+
//!
3+
//! Three workload groups (zipfian-read, scan + point lookup, mixed
4+
//! read/write) compare:
5+
//!
6+
//! - `LruCore<u64, u64>` — the policy as it exists today.
7+
//! - `Expiring<LruCore<u64, u64>, u64, Arc<u64>, MockClock>` with a
8+
//! 60-second default TTL — long enough that the bench never fires an
9+
//! expiry but the decorator still does its bookkeeping on every op.
10+
//!
11+
//! Run with: `cargo bench --bench ttl_overhead --features ttl`.
12+
13+
#![cfg(feature = "ttl")]
14+
15+
use std::sync::Arc;
16+
use std::time::Duration;
17+
18+
use bench_support::workload::{Workload, WorkloadSpec};
19+
use cachekit::policy::expiring::Expiring;
20+
use cachekit::policy::lru::LruCore;
21+
use cachekit::time::MockClock;
22+
use cachekit::traits::Cache;
23+
use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main};
24+
25+
const CAPACITY: usize = 8_192;
26+
const OPS: u64 = 4_096;
27+
const UNIVERSE: u64 = 16_384;
28+
const SEED: u64 = 0xc0fee;
29+
const DEFAULT_TTL: Duration = Duration::from_secs(60);
30+
31+
fn fresh_lru() -> LruCore<u64, u64> {
32+
let mut cache = LruCore::new(CAPACITY);
33+
for i in 0..CAPACITY as u64 {
34+
cache.insert(i, Arc::new(i));
35+
}
36+
cache
37+
}
38+
39+
fn fresh_expiring() -> Expiring<LruCore<u64, u64>, u64, Arc<u64>, MockClock> {
40+
let inner = fresh_lru();
41+
Expiring::with_default_ttl(inner, MockClock::new(), Some(DEFAULT_TTL))
42+
}
43+
44+
fn make_generator(workload: Workload) -> bench_support::workload::WorkloadGenerator {
45+
WorkloadSpec {
46+
universe: UNIVERSE,
47+
workload,
48+
seed: SEED,
49+
}
50+
.generator()
51+
}
52+
53+
// ---------------------------------------------------------------------------
54+
// Group: zipfian-read (skewed get-only workload)
55+
// ---------------------------------------------------------------------------
56+
57+
fn bench_zipfian_read(c: &mut Criterion) {
58+
let mut group = c.benchmark_group("ttl_overhead/zipfian_read");
59+
group.throughput(Throughput::Elements(OPS));
60+
61+
group.bench_function("plain_lru", |b| {
62+
b.iter_batched(
63+
fresh_lru,
64+
|mut cache| {
65+
let mut wl = make_generator(Workload::Zipfian { exponent: 1.1 });
66+
for _ in 0..OPS {
67+
let _ = std::hint::black_box(cache.get(&wl.next_key()));
68+
}
69+
},
70+
BatchSize::SmallInput,
71+
)
72+
});
73+
74+
group.bench_function("expiring_lru", |b| {
75+
b.iter_batched(
76+
fresh_expiring,
77+
|mut cache| {
78+
let mut wl = make_generator(Workload::Zipfian { exponent: 1.1 });
79+
for _ in 0..OPS {
80+
let _ = std::hint::black_box(cache.get(&wl.next_key()));
81+
}
82+
},
83+
BatchSize::SmallInput,
84+
)
85+
});
86+
87+
group.finish();
88+
}
89+
90+
// ---------------------------------------------------------------------------
91+
// Group: scan + point lookup (mixed sequential scan and skewed lookups)
92+
// ---------------------------------------------------------------------------
93+
94+
fn bench_scan_plus_point(c: &mut Criterion) {
95+
let workload = Workload::ScanResistance {
96+
scan_start_prob: 0.05,
97+
scan_length: 32,
98+
point_exponent: 1.0,
99+
};
100+
let mut group = c.benchmark_group("ttl_overhead/scan_plus_point");
101+
group.throughput(Throughput::Elements(OPS));
102+
103+
group.bench_function("plain_lru", |b| {
104+
b.iter_batched(
105+
fresh_lru,
106+
|mut cache| {
107+
let mut wl = make_generator(workload);
108+
for _ in 0..OPS {
109+
let _ = std::hint::black_box(cache.get(&wl.next_key()));
110+
}
111+
},
112+
BatchSize::SmallInput,
113+
)
114+
});
115+
116+
group.bench_function("expiring_lru", |b| {
117+
b.iter_batched(
118+
fresh_expiring,
119+
|mut cache| {
120+
let mut wl = make_generator(workload);
121+
for _ in 0..OPS {
122+
let _ = std::hint::black_box(cache.get(&wl.next_key()));
123+
}
124+
},
125+
BatchSize::SmallInput,
126+
)
127+
});
128+
129+
group.finish();
130+
}
131+
132+
// ---------------------------------------------------------------------------
133+
// Group: mixed read/write (90% read, 10% insert, both zipfian)
134+
// ---------------------------------------------------------------------------
135+
136+
fn bench_mixed_rw(c: &mut Criterion) {
137+
let mut group = c.benchmark_group("ttl_overhead/mixed_rw");
138+
group.throughput(Throughput::Elements(OPS));
139+
140+
group.bench_function("plain_lru", |b| {
141+
b.iter_batched(
142+
fresh_lru,
143+
|mut cache| {
144+
let mut wl = make_generator(Workload::Zipfian { exponent: 1.0 });
145+
for i in 0..OPS {
146+
let key = wl.next_key();
147+
if i % 10 == 0 {
148+
cache.insert(key, Arc::new(key));
149+
} else {
150+
let _ = std::hint::black_box(cache.get(&key));
151+
}
152+
}
153+
},
154+
BatchSize::SmallInput,
155+
)
156+
});
157+
158+
group.bench_function("expiring_lru", |b| {
159+
b.iter_batched(
160+
fresh_expiring,
161+
|mut cache| {
162+
let mut wl = make_generator(Workload::Zipfian { exponent: 1.0 });
163+
for i in 0..OPS {
164+
let key = wl.next_key();
165+
if i % 10 == 0 {
166+
cache.insert(key, Arc::new(key));
167+
} else {
168+
let _ = std::hint::black_box(cache.get(&key));
169+
}
170+
}
171+
},
172+
BatchSize::SmallInput,
173+
)
174+
});
175+
176+
group.finish();
177+
}
178+
179+
criterion_group!(
180+
benches,
181+
bench_zipfian_read,
182+
bench_scan_plus_point,
183+
bench_mixed_rw
184+
);
185+
criterion_main!(benches);

0 commit comments

Comments
 (0)