Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions examples/examples/memoization_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

use std::time::Duration;

use hitbox::CacheStatus;
use hitbox::{CacheStatus, ForwardReason};
use hitbox_fn::Cache;
use hitbox_fn::prelude::*;
use hitbox_moka::MokaBackend;
Expand Down Expand Up @@ -124,7 +124,7 @@ async fn main() {
let (r1, c1) = get_user(UserId(1)).cache(&cache).with_context().await;
let (r2, c2) = get_user(UserId(1)).cache(&cache).with_context().await;
assert_eq!(r1, r2);
assert_eq!(c1.status, CacheStatus::Miss);
assert_eq!(c1.status, CacheStatus::Forward(ForwardReason::Miss));
assert_eq!(c2.status, CacheStatus::Hit);

// 2. Multiple args
Expand All @@ -140,14 +140,14 @@ async fn main() {
.cache(&cache)
.with_context()
.await;
assert_eq!(c1.status, CacheStatus::Miss);
assert_eq!(c1.status, CacheStatus::Forward(ForwardReason::Miss));
assert_eq!(c2.status, CacheStatus::Hit);
assert_eq!(c3.status, CacheStatus::Miss); // Different org = different key
assert_eq!(c3.status, CacheStatus::Forward(ForwardReason::Miss)); // Different org = different key

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

// 5. Zero-argument function
let (_, c1) = get_config().cache(&cache).with_context().await;
let (_, c2) = get_config().cache(&cache).with_context().await;
assert_eq!(c1.status, CacheStatus::Miss);
assert_eq!(c1.status, CacheStatus::Forward(ForwardReason::Miss));
assert_eq!(c2.status, CacheStatus::Hit);
}
2 changes: 2 additions & 0 deletions hitbox-backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Changed
- Propagate `created_at` through `CacheBackend::set()`/`get()` ([#269](https://github.com/hit-box/hitbox/pull/269))

## [0.2.1] - 2026-02-05

Expand Down
10 changes: 8 additions & 2 deletions hitbox-backend/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,8 @@ pub trait CacheBackend: Backend {
)))
})?;

let cached_value = CacheValue::new(deserialized, meta.expire, meta.stale);
let cached_value =
CacheValue::new(deserialized, meta.expire, meta.stale, meta.created_at);

// Refill L1 if read mode is Refill (data came from L2).
// CompositionFormat will create L1-only envelope, so only L1 gets populated.
Expand Down Expand Up @@ -357,7 +358,12 @@ pub trait CacheBackend: Backend {
let result = self
.write(
key,
CacheValue::new(Bytes::from(compressed_value), value.expire(), value.stale()),
CacheValue::new(
Bytes::from(compressed_value),
value.expire(),
value.stale(),
value.created_at(),
),
)
.await;
crate::metrics::record_write(backend_label.as_str(), write_timer.elapsed());
Expand Down
4 changes: 4 additions & 0 deletions hitbox-backend/src/composition/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ mod tests {
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
None,
);

// Write and read
Expand Down Expand Up @@ -311,6 +312,7 @@ mod tests {
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
None,
);

// Populate only L2
Expand Down Expand Up @@ -355,6 +357,7 @@ mod tests {
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
None,
);

// Write through nested composition
Expand Down Expand Up @@ -409,6 +412,7 @@ mod tests {
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
None,
);

let mut ctx: BoxContext = CacheContext::default().boxed();
Expand Down
20 changes: 19 additions & 1 deletion hitbox-backend/src/composition/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

use std::any::Any;

use hitbox_core::{BoxContext, CacheContext, CacheStatus, Context, ReadMode, ResponseSource};
use hitbox_core::{
BoxContext, CacheContext, CacheStatus, CacheTiming, Context, ReadMode, ResponseSource,
};
use smallbox::smallbox;

use super::CompositionFormat;
Expand Down Expand Up @@ -90,6 +92,22 @@ impl Context for CompositionContext {
self.inner.set_read_mode(mode);
}

fn timing(&self) -> Option<&CacheTiming> {
self.inner.timing()
}

fn set_timing(&mut self, timing: Option<CacheTiming>) {
self.inner.set_timing(timing);
}

fn stored(&self) -> bool {
self.inner.stored()
}

fn set_stored(&mut self, stored: bool) {
self.inner.set_stored(stored);
}

fn as_any(&self) -> &dyn Any {
self
}
Expand Down
26 changes: 19 additions & 7 deletions hitbox-backend/src/composition/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ impl CompositionEnvelope {
l1_data,
header.decode_expire(),
header.decode_stale(),
None,
)))
}
1 => {
Expand All @@ -289,6 +290,7 @@ impl CompositionEnvelope {
l2_data,
header.decode_expire(),
header.decode_stale(),
None,
)))
}
2 => {
Expand All @@ -313,8 +315,18 @@ impl CompositionEnvelope {
let l2_data = Bytes::copy_from_slice(&data[l1_end..l2_end]);

Ok(CompositionEnvelope::Both {
l1: CacheValue::new(l1_data, header.decode_expire(), header.decode_stale()),
l2: CacheValue::new(l2_data, header.decode_expire(), header.decode_stale()),
l1: CacheValue::new(
l1_data,
header.decode_expire(),
header.decode_stale(),
None,
),
l2: CacheValue::new(
l2_data,
header.decode_expire(),
header.decode_stale(),
None,
),
})
}
_ => Err(BackendError::InternalError(Box::new(io::Error::new(
Expand Down Expand Up @@ -343,7 +355,7 @@ mod tests {
let expire = Some(Utc::now() + Duration::hours(1));
let stale = None;

let envelope = CompositionEnvelope::L1(CacheValue::new(data.clone(), expire, stale));
let envelope = CompositionEnvelope::L1(CacheValue::new(data.clone(), expire, stale, None));

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

let envelope = CompositionEnvelope::Both {
l1: CacheValue::new(l1_data.clone(), expire, stale),
l2: CacheValue::new(l2_data.clone(), expire, stale),
l1: CacheValue::new(l1_data.clone(), expire, stale, None),
l2: CacheValue::new(l2_data.clone(), expire, stale, None),
};

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

let envelope = CompositionEnvelope::Both {
l1: CacheValue::new(l1_data.clone(), None, None),
l2: CacheValue::new(l2_data.clone(), None, None),
l1: CacheValue::new(l1_data.clone(), None, None, None),
l2: CacheValue::new(l2_data.clone(), None, None, None),
};

let serialized = envelope.serialize().unwrap();
Expand Down
12 changes: 8 additions & 4 deletions hitbox-backend/src/composition/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,12 @@ impl Format for CompositionFormat {
.map_err(|e| FormatError::Serialize(Box::new(e)))?;
crate::metrics::record_compress(&self.l1_label, compress_timer.elapsed());

let composition =
CompositionEnvelope::L1(CacheValue::new(Bytes::from(l1_compressed), None, None));
let composition = CompositionEnvelope::L1(CacheValue::new(
Bytes::from(l1_compressed),
None,
None,
None,
));

return composition
.serialize()
Expand Down Expand Up @@ -259,8 +263,8 @@ impl Format for CompositionFormat {

// Pack both compressed values into CompositionEnvelope
let composition = CompositionEnvelope::Both {
l1: CacheValue::new(Bytes::from(l1_compressed), None, None),
l2: CacheValue::new(Bytes::from(l2_compressed), None, None),
l1: CacheValue::new(Bytes::from(l1_compressed), None, None, None),
l2: CacheValue::new(Bytes::from(l2_compressed), None, None, None),
};

// Serialize the CompositionEnvelope using zero-copy repr(C) format
Expand Down
Loading
Loading