1use crate::{CacheKey, CachePolicy, CacheStorage, PutHandle, StoredEntry, tee::TeeingReader};
4use futures_lite::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
5use std::{
6 fmt::{self, Debug, Formatter},
7 io,
8 pin::Pin,
9 task::{Context, Poll},
10};
11use trillium_http::{Body, Headers};
12use trillium_server_common::{Runtime, RuntimeTrait};
13
14pub struct TieredStorage<Hot, Cold> {
67 hot: Hot,
68 cold: Cold,
69 runtime: Runtime,
70}
71
72impl<Hot, Cold> TieredStorage<Hot, Cold> {
73 pub fn new(hot: Hot, cold: Cold, runtime: impl RuntimeTrait) -> Self {
79 Self {
80 hot,
81 cold,
82 runtime: runtime.into(),
83 }
84 }
85
86 pub fn hot(&self) -> &Hot {
88 &self.hot
89 }
90
91 pub fn cold(&self) -> &Cold {
93 &self.cold
94 }
95}
96
97impl<Hot: Clone, Cold: Clone> Clone for TieredStorage<Hot, Cold> {
98 fn clone(&self) -> Self {
99 Self {
100 hot: self.hot.clone(),
101 cold: self.cold.clone(),
102 runtime: self.runtime.clone(),
103 }
104 }
105}
106
107impl<Hot: CacheStorage, Cold: CacheStorage> Debug for TieredStorage<Hot, Cold> {
108 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
109 f.debug_struct("TieredStorage")
110 .field("hot", &self.hot)
111 .field("cold", &self.cold)
112 .finish_non_exhaustive()
113 }
114}
115
116impl<Hot, Cold> CacheStorage for TieredStorage<Hot, Cold>
117where
118 Hot: CacheStorage + Clone,
119 Cold: CacheStorage + Clone,
120{
121 type StoredEntry = TieredEntry<Hot, Cold>;
122 type PutHandle = TieredPutHandle<Hot, Cold>;
123
124 async fn get(&self, key: &CacheKey) -> Vec<Self::StoredEntry> {
125 let hot = self.hot.get(key).await;
126 if !hot.is_empty() {
127 return hot.into_iter().map(TieredEntry::Hot).collect();
128 }
129 self.cold
130 .get(key)
131 .await
132 .into_iter()
133 .map(|entry| TieredEntry::Cold {
134 entry,
135 hot: self.hot.clone(),
136 key: key.clone(),
137 })
138 .collect()
139 }
140
141 async fn put(&self, key: CacheKey, policy: CachePolicy) -> io::Result<Self::PutHandle> {
142 let hot = self.hot.put(key.clone(), policy.clone()).await?;
143 Ok(TieredPutHandle {
144 hot,
145 hot_store: self.hot.clone(),
146 cold: self.cold.clone(),
147 runtime: self.runtime.clone(),
148 key,
149 policy,
150 })
151 }
152
153 async fn invalidate(&self, key: &CacheKey) {
154 self.hot.invalidate(key).await;
155 self.cold.invalidate(key).await;
156 }
157}
158
159pub enum TieredEntry<Hot: CacheStorage, Cold: CacheStorage> {
165 Hot(Hot::StoredEntry),
167 Cold {
169 entry: Cold::StoredEntry,
171 hot: Hot,
173 key: CacheKey,
175 },
176}
177
178impl<Hot, Cold> Clone for TieredEntry<Hot, Cold>
179where
180 Hot: CacheStorage + Clone,
181 Cold: CacheStorage,
182{
183 fn clone(&self) -> Self {
184 match self {
185 Self::Hot(entry) => Self::Hot(entry.clone()),
186 Self::Cold { entry, hot, key } => Self::Cold {
187 entry: entry.clone(),
188 hot: hot.clone(),
189 key: key.clone(),
190 },
191 }
192 }
193}
194
195impl<Hot: CacheStorage, Cold: CacheStorage> Debug for TieredEntry<Hot, Cold> {
196 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
197 match self {
198 Self::Hot(entry) => f.debug_tuple("Hot").field(entry).finish(),
199 Self::Cold { entry, key, .. } => f
200 .debug_struct("Cold")
201 .field("entry", entry)
202 .field("key", key)
203 .finish_non_exhaustive(),
204 }
205 }
206}
207
208impl<Hot, Cold> StoredEntry for TieredEntry<Hot, Cold>
209where
210 Hot: CacheStorage + Clone,
211 Cold: CacheStorage,
212{
213 fn policy(&self) -> &CachePolicy {
214 match self {
215 Self::Hot(entry) => entry.policy(),
216 Self::Cold { entry, .. } => entry.policy(),
217 }
218 }
219
220 async fn refresh_policy(&mut self, new_policy: CachePolicy) -> io::Result<()> {
221 match self {
222 Self::Hot(entry) => entry.refresh_policy(new_policy).await,
223 Self::Cold { entry, .. } => entry.refresh_policy(new_policy).await,
224 }
225 }
226
227 async fn open(self) -> io::Result<Body> {
228 match self {
229 Self::Hot(entry) => entry.open().await,
230 Self::Cold { entry, hot, key } => {
231 let policy = entry.policy().clone();
232 let cold_body = entry.open().await?;
233 let len = cold_body.len();
234 match hot.put(key, policy).await {
235 Ok(put_handle) => {
236 let tee = TeeingReader::new(cold_body, put_handle, u64::MAX);
237 Ok(Body::new_with_trailers(tee, len))
238 }
239 Err(e) => {
240 log::warn!("cache: promotion put failed: {e}, serving cold entry only");
241 Ok(cold_body)
242 }
243 }
244 }
245 }
246 }
247}
248
249pub struct TieredPutHandle<Hot: CacheStorage, Cold: CacheStorage> {
255 hot: Hot::PutHandle,
256 hot_store: Hot,
257 cold: Cold,
258 runtime: Runtime,
259 key: CacheKey,
260 policy: CachePolicy,
261}
262
263impl<Hot: CacheStorage, Cold: CacheStorage> Debug for TieredPutHandle<Hot, Cold> {
264 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
265 f.debug_struct("TieredPutHandle")
266 .field("key", &self.key)
267 .finish_non_exhaustive()
268 }
269}
270
271impl<Hot: CacheStorage, Cold: CacheStorage> Unpin for TieredPutHandle<Hot, Cold> {}
275
276impl<Hot: CacheStorage, Cold: CacheStorage> AsyncWrite for TieredPutHandle<Hot, Cold> {
277 fn poll_write(
278 self: Pin<&mut Self>,
279 cx: &mut Context<'_>,
280 buf: &[u8],
281 ) -> Poll<io::Result<usize>> {
282 Pin::new(&mut self.get_mut().hot).poll_write(cx, buf)
283 }
284
285 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
286 Pin::new(&mut self.get_mut().hot).poll_flush(cx)
287 }
288
289 fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
290 Pin::new(&mut self.get_mut().hot).poll_close(cx)
291 }
292}
293
294impl<Hot, Cold> PutHandle for TieredPutHandle<Hot, Cold>
295where
296 Hot: CacheStorage + Clone,
297 Cold: CacheStorage,
298{
299 async fn finalize(self, trailers: Option<Headers>) -> io::Result<()> {
300 let Self {
301 hot,
302 hot_store,
303 cold,
304 runtime,
305 key,
306 policy,
307 } = self;
308 hot.finalize(trailers).await?;
309
310 let log_key = key.clone();
311 let _detached = runtime.spawn(async move {
312 if let Err(e) = flush_to_cold(hot_store, cold, key, policy).await {
313 log::warn!("cache: tiered background flush to cold failed for {log_key}: {e}");
314 }
315 });
316 Ok(())
317 }
318}
319
320async fn flush_to_cold<Hot, Cold>(
325 hot_store: Hot,
326 cold: Cold,
327 key: CacheKey,
328 policy: CachePolicy,
329) -> io::Result<()>
330where
331 Hot: CacheStorage,
332 Cold: CacheStorage,
333{
334 let Some(entry) = hot_store
335 .get(&key)
336 .await
337 .into_iter()
338 .find(|entry| entry.policy().same_variant_as(&policy))
339 else {
340 return Ok(());
341 };
342
343 let mut body = entry.open().await?;
344 let mut put = cold.put(key, policy).await?;
345 let mut buf = [0u8; 8192];
346 loop {
347 let n = body.read(&mut buf).await?;
348 if n == 0 {
349 break;
350 }
351 put.write_all(&buf[..n]).await?;
352 }
353 let trailers = body.trailers();
354 put.finalize(trailers).await
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::{InMemoryStorage, test_helpers::*};
361 use std::time::{Duration, SystemTime};
362 use trillium_http::{KnownHeaderName::*, Method, Status};
363 use trillium_testing::{TestResult, harness, runtime, test};
364
365 fn key() -> CacheKey {
366 CacheKey::new(Method::Get, "http://example.com/".parse().unwrap())
367 }
368
369 fn tiered() -> TieredStorage<InMemoryStorage, InMemoryStorage> {
370 TieredStorage::new(InMemoryStorage::new(), InMemoryStorage::new(), runtime())
371 }
372
373 async fn store_into(storage: &impl CacheStorage, key: CacheKey, body: &[u8]) {
374 let conn = exchange(
375 Method::Get,
376 &[],
377 Status::Ok,
378 &[(CacheControl, "max-age=600")],
379 );
380 let policy = policy_from(&conn, SystemTime::now(), private_cache());
381 let mut handle = storage.put(key, policy).await.unwrap();
382 handle.write_all(body).await.unwrap();
383 handle.finalize(None).await.unwrap();
384 }
385
386 async fn read_body(entry: impl StoredEntry) -> Vec<u8> {
387 let mut body = entry.open().await.unwrap();
388 let mut buf = Vec::new();
389 body.read_to_end(&mut buf).await.unwrap();
390 buf
391 }
392
393 async fn cold_settles<Hot, Cold>(
395 storage: &TieredStorage<Hot, Cold>,
396 key: &CacheKey,
397 ) -> Vec<Cold::StoredEntry>
398 where
399 Hot: CacheStorage + Clone,
400 Cold: CacheStorage + Clone,
401 {
402 for _ in 0..200 {
403 let entries = storage.cold().get(key).await;
404 if !entries.is_empty() {
405 return entries;
406 }
407 storage.runtime.delay(Duration::from_millis(5)).await;
408 }
409 panic!("cold tier never populated");
410 }
411
412 #[test(harness)]
413 async fn hot_populated_synchronously_cold_written_back() -> TestResult {
414 let storage = tiered();
415 store_into(&storage, key(), b"hello").await;
416
417 let entries = storage.get(&key()).await;
419 assert_eq!(entries.len(), 1);
420 assert!(matches!(entries[0], TieredEntry::Hot(_)));
421 assert_eq!(read_body(entries[0].clone()).await, b"hello");
422
423 let cold = cold_settles(&storage, &key()).await;
425 assert_eq!(cold.len(), 1);
426 assert_eq!(read_body(cold[0].clone()).await, b"hello");
427 Ok(())
428 }
429
430 #[test(harness)]
431 async fn cold_hit_promotes_into_hot() -> TestResult {
432 let storage = tiered();
433 store_into(storage.cold(), key(), b"promoted").await;
435 assert!(storage.hot().get(&key()).await.is_empty());
436
437 let entries = storage.get(&key()).await;
438 assert_eq!(entries.len(), 1);
439 assert!(matches!(entries[0], TieredEntry::Cold { .. }));
440 assert_eq!(read_body(entries[0].clone()).await, b"promoted");
442
443 let hot = storage.hot().get(&key()).await;
444 assert_eq!(hot.len(), 1);
445 assert_eq!(read_body(hot[0].clone()).await, b"promoted");
446 Ok(())
447 }
448
449 #[test(harness)]
450 async fn invalidate_clears_both_tiers() -> TestResult {
451 let storage = tiered();
452 store_into(&storage, key(), b"x").await;
453 cold_settles(&storage, &key()).await;
454 storage.invalidate(&key()).await;
455 assert!(storage.get(&key()).await.is_empty());
456 assert!(storage.hot().get(&key()).await.is_empty());
457 assert!(storage.cold().get(&key()).await.is_empty());
458 Ok(())
459 }
460
461 #[test(harness)]
462 async fn drop_put_handle_without_finalize_stores_nothing() -> TestResult {
463 let storage = tiered();
464 let conn = exchange(
465 Method::Get,
466 &[],
467 Status::Ok,
468 &[(CacheControl, "max-age=600")],
469 );
470 let policy = policy_from(&conn, SystemTime::now(), private_cache());
471 let mut handle = storage.put(key(), policy).await.unwrap();
472 handle.write_all(b"partial").await.unwrap();
473 drop(handle);
474 assert!(storage.hot().get(&key()).await.is_empty());
475 assert!(storage.cold().get(&key()).await.is_empty());
476 Ok(())
477 }
478
479 #[cfg(feature = "fs")]
482 #[test(harness)]
483 async fn memory_over_filesystem_promotes_from_disk() -> TestResult {
484 use crate::FileSystemStorage;
485
486 let dir = tempfile::tempdir().unwrap();
487 {
488 let storage = TieredStorage::new(
489 InMemoryStorage::new(),
490 FileSystemStorage::new(dir.path()),
491 runtime(),
492 );
493 store_into(&storage, key(), b"on-disk").await;
494 cold_settles(&storage, &key()).await;
495 }
496
497 let reopened = TieredStorage::new(
499 InMemoryStorage::new(),
500 FileSystemStorage::new(dir.path()),
501 runtime(),
502 );
503 assert!(reopened.hot().get(&key()).await.is_empty());
504
505 let entries = reopened.get(&key()).await;
506 assert_eq!(entries.len(), 1);
507 assert!(matches!(entries[0], TieredEntry::Cold { .. }));
508 assert_eq!(read_body(entries[0].clone()).await, b"on-disk");
509
510 let hot = reopened.hot().get(&key()).await;
512 assert_eq!(hot.len(), 1);
513 assert_eq!(read_body(hot[0].clone()).await, b"on-disk");
514 Ok(())
515 }
516}