Skip to content

Commit 7c5990b

Browse files
committed
feat(http): implement Cache-Status and Age headers
1 parent 84ddade commit 7c5990b

45 files changed

Lines changed: 1029 additions & 267 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/examples/memoization_derive.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
1111
use std::time::Duration;
1212

13-
use hitbox::CacheStatus;
13+
use hitbox::{CacheStatus, ForwardReason};
1414
use hitbox_fn::Cache;
1515
use hitbox_fn::prelude::*;
1616
use hitbox_moka::MokaBackend;
@@ -124,7 +124,7 @@ async fn main() {
124124
let (r1, c1) = get_user(UserId(1)).cache(&cache).with_context().await;
125125
let (r2, c2) = get_user(UserId(1)).cache(&cache).with_context().await;
126126
assert_eq!(r1, r2);
127-
assert_eq!(c1.status, CacheStatus::Miss);
127+
assert_eq!(c1.status, CacheStatus::Forward(ForwardReason::Miss));
128128
assert_eq!(c2.status, CacheStatus::Hit);
129129

130130
// 2. Multiple args
@@ -140,14 +140,14 @@ async fn main() {
140140
.cache(&cache)
141141
.with_context()
142142
.await;
143-
assert_eq!(c1.status, CacheStatus::Miss);
143+
assert_eq!(c1.status, CacheStatus::Forward(ForwardReason::Miss));
144144
assert_eq!(c2.status, CacheStatus::Hit);
145-
assert_eq!(c3.status, CacheStatus::Miss); // Different org = different key
145+
assert_eq!(c3.status, CacheStatus::Forward(ForwardReason::Miss)); // Different org = different key
146146

147147
// 3. Skip in CacheableResponse (tokens not cached but returned on miss)
148148
let (r1, c1) = authenticate(UserId(1)).cache(&cache).with_context().await;
149149
let (r2, c2) = authenticate(UserId(1)).cache(&cache).with_context().await;
150-
assert_eq!(c1.status, CacheStatus::Miss);
150+
assert_eq!(c1.status, CacheStatus::Forward(ForwardReason::Miss));
151151
assert_eq!(c2.status, CacheStatus::Hit);
152152
assert!(r1.as_ref().unwrap().access_token.is_some()); // Present on miss
153153
assert!(r2.as_ref().unwrap().access_token.is_none()); // Skipped on hit (not in cache)
@@ -165,12 +165,12 @@ async fn main() {
165165
};
166166
let (_, c1) = search(q1).cache(&cache).with_context().await;
167167
let (_, c2) = search(q2).cache(&cache).with_context().await; // Same key despite different request_id
168-
assert_eq!(c1.status, CacheStatus::Miss);
168+
assert_eq!(c1.status, CacheStatus::Forward(ForwardReason::Miss));
169169
assert_eq!(c2.status, CacheStatus::Hit);
170170

171171
// 5. Zero-argument function
172172
let (_, c1) = get_config().cache(&cache).with_context().await;
173173
let (_, c2) = get_config().cache(&cache).with_context().await;
174-
assert_eq!(c1.status, CacheStatus::Miss);
174+
assert_eq!(c1.status, CacheStatus::Forward(ForwardReason::Miss));
175175
assert_eq!(c2.status, CacheStatus::Hit);
176176
}

hitbox-backend/src/backend.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,8 @@ pub trait CacheBackend: Backend {
297297
)))
298298
})?;
299299

300-
let cached_value = CacheValue::new(deserialized, meta.expire, meta.stale);
300+
let cached_value =
301+
CacheValue::new(deserialized, meta.expire, meta.stale, meta.created_at);
301302

302303
// Refill L1 if read mode is Refill (data came from L2).
303304
// CompositionFormat will create L1-only envelope, so only L1 gets populated.
@@ -357,7 +358,12 @@ pub trait CacheBackend: Backend {
357358
let result = self
358359
.write(
359360
key,
360-
CacheValue::new(Bytes::from(compressed_value), value.expire(), value.stale()),
361+
CacheValue::new(
362+
Bytes::from(compressed_value),
363+
value.expire(),
364+
value.stale(),
365+
value.created_at(),
366+
),
361367
)
362368
.await;
363369
crate::metrics::record_write(backend_label.as_str(), write_timer.elapsed());

hitbox-backend/src/composition/compose.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ mod tests {
259259
},
260260
Some(Utc::now() + chrono::Duration::seconds(60)),
261261
None,
262+
None,
262263
);
263264

264265
// Write and read
@@ -311,6 +312,7 @@ mod tests {
311312
},
312313
Some(Utc::now() + chrono::Duration::seconds(60)),
313314
None,
315+
None,
314316
);
315317

316318
// Populate only L2
@@ -355,6 +357,7 @@ mod tests {
355357
},
356358
Some(Utc::now() + chrono::Duration::seconds(60)),
357359
None,
360+
None,
358361
);
359362

360363
// Write through nested composition
@@ -409,6 +412,7 @@ mod tests {
409412
},
410413
Some(Utc::now() + chrono::Duration::seconds(60)),
411414
None,
415+
None,
412416
);
413417

414418
let mut ctx: BoxContext = CacheContext::default().boxed();

hitbox-backend/src/composition/context.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
66
use std::any::Any;
77

8-
use hitbox_core::{BoxContext, CacheContext, CacheStatus, Context, ReadMode, ResponseSource};
8+
use hitbox_core::{
9+
BoxContext, CacheContext, CacheStatus, CacheTiming, Context, ReadMode, ResponseSource,
10+
};
911
use smallbox::smallbox;
1012

1113
use super::CompositionFormat;
@@ -90,6 +92,30 @@ impl Context for CompositionContext {
9092
self.inner.set_read_mode(mode);
9193
}
9294

95+
fn timing(&self) -> Option<&CacheTiming> {
96+
self.inner.timing()
97+
}
98+
99+
fn set_timing(&mut self, timing: Option<CacheTiming>) {
100+
self.inner.set_timing(timing);
101+
}
102+
103+
fn stored(&self) -> bool {
104+
self.inner.stored()
105+
}
106+
107+
fn set_stored(&mut self, stored: bool) {
108+
self.inner.set_stored(stored);
109+
}
110+
111+
fn extensions(&self) -> Option<&(dyn Any + Send + Sync)> {
112+
self.inner.extensions()
113+
}
114+
115+
fn set_extensions(&mut self, ext: Option<Box<dyn Any + Send + Sync>>) {
116+
self.inner.set_extensions(ext);
117+
}
118+
93119
fn as_any(&self) -> &dyn Any {
94120
self
95121
}

hitbox-backend/src/composition/envelope.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ impl CompositionEnvelope {
268268
l1_data,
269269
header.decode_expire(),
270270
header.decode_stale(),
271+
None,
271272
)))
272273
}
273274
1 => {
@@ -289,6 +290,7 @@ impl CompositionEnvelope {
289290
l2_data,
290291
header.decode_expire(),
291292
header.decode_stale(),
293+
None,
292294
)))
293295
}
294296
2 => {
@@ -313,8 +315,18 @@ impl CompositionEnvelope {
313315
let l2_data = Bytes::copy_from_slice(&data[l1_end..l2_end]);
314316

315317
Ok(CompositionEnvelope::Both {
316-
l1: CacheValue::new(l1_data, header.decode_expire(), header.decode_stale()),
317-
l2: CacheValue::new(l2_data, header.decode_expire(), header.decode_stale()),
318+
l1: CacheValue::new(
319+
l1_data,
320+
header.decode_expire(),
321+
header.decode_stale(),
322+
None,
323+
),
324+
l2: CacheValue::new(
325+
l2_data,
326+
header.decode_expire(),
327+
header.decode_stale(),
328+
None,
329+
),
318330
})
319331
}
320332
_ => Err(BackendError::InternalError(Box::new(io::Error::new(
@@ -343,7 +355,7 @@ mod tests {
343355
let expire = Some(Utc::now() + Duration::hours(1));
344356
let stale = None;
345357

346-
let envelope = CompositionEnvelope::L1(CacheValue::new(data.clone(), expire, stale));
358+
let envelope = CompositionEnvelope::L1(CacheValue::new(data.clone(), expire, stale, None));
347359

348360
let serialized = envelope.serialize().unwrap();
349361
let deserialized = CompositionEnvelope::deserialize(&serialized).unwrap();
@@ -366,8 +378,8 @@ mod tests {
366378
let stale = Some(Utc::now() + Duration::minutes(30));
367379

368380
let envelope = CompositionEnvelope::Both {
369-
l1: CacheValue::new(l1_data.clone(), expire, stale),
370-
l2: CacheValue::new(l2_data.clone(), expire, stale),
381+
l1: CacheValue::new(l1_data.clone(), expire, stale, None),
382+
l2: CacheValue::new(l2_data.clone(), expire, stale, None),
371383
};
372384

373385
let serialized = envelope.serialize().unwrap();
@@ -390,8 +402,8 @@ mod tests {
390402
let l2_data = Bytes::from(vec![1u8; 100_000]);
391403

392404
let envelope = CompositionEnvelope::Both {
393-
l1: CacheValue::new(l1_data.clone(), None, None),
394-
l2: CacheValue::new(l2_data.clone(), None, None),
405+
l1: CacheValue::new(l1_data.clone(), None, None, None),
406+
l2: CacheValue::new(l2_data.clone(), None, None, None),
395407
};
396408

397409
let serialized = envelope.serialize().unwrap();

hitbox-backend/src/composition/format.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,12 @@ impl Format for CompositionFormat {
218218
.map_err(|e| FormatError::Serialize(Box::new(e)))?;
219219
crate::metrics::record_compress(&self.l1_label, compress_timer.elapsed());
220220

221-
let composition =
222-
CompositionEnvelope::L1(CacheValue::new(Bytes::from(l1_compressed), None, None));
221+
let composition = CompositionEnvelope::L1(CacheValue::new(
222+
Bytes::from(l1_compressed),
223+
None,
224+
None,
225+
None,
226+
));
223227

224228
return composition
225229
.serialize()
@@ -259,8 +263,8 @@ impl Format for CompositionFormat {
259263

260264
// Pack both compressed values into CompositionEnvelope
261265
let composition = CompositionEnvelope::Both {
262-
l1: CacheValue::new(Bytes::from(l1_compressed), None, None),
263-
l2: CacheValue::new(Bytes::from(l2_compressed), None, None),
266+
l1: CacheValue::new(Bytes::from(l1_compressed), None, None, None),
267+
l2: CacheValue::new(Bytes::from(l2_compressed), None, None, None),
264268
};
265269

266270
// Serialize the CompositionEnvelope using zero-copy repr(C) format

0 commit comments

Comments
 (0)