hitbox_tower/layer.rs
1//! Tower layer and builder for HTTP caching.
2//!
3//! This module provides [`Cache`], a Tower [`Layer`] that wraps services with
4//! caching behavior, and [`CacheBuilder`] for fluent configuration.
5//!
6//! # Examples
7//!
8//! ```
9//! use std::time::Duration;
10//! use hitbox::Config;
11//! use hitbox::policy::PolicyConfig;
12//! use hitbox_tower::Cache;
13//! use hitbox_moka::MokaBackend;
14//! use hitbox_http::extractors::Method;
15//! use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
16//!
17//! # use http_body_util::Full;
18//! # type Body = Full<bytes::Bytes>;
19//! let config = Config::builder()
20//! .request_predicate(NeutralRequestPredicate::new())
21//! .response_predicate(NeutralResponsePredicate::new())
22//! .extractor(Method::new())
23//! .policy(PolicyConfig::builder().ttl(Duration::from_secs(60)).build())
24//! .build();
25//! # let _: Config<
26//! # hitbox_http::predicates::NeutralRequestPredicate<Body>,
27//! # hitbox_http::predicates::NeutralResponsePredicate<Body>,
28//! # Method<hitbox_http::extractors::NeutralExtractor<Body>>,
29//! # > = config;
30//!
31//! let cache_layer = Cache::builder()
32//! .backend(MokaBackend::builder().max_entries(1000).build())
33//! .config(config)
34//! .build();
35//! ```
36//!
37//! [`Layer`]: tower::Layer
38
39use std::sync::Arc;
40
41use hitbox::backend::CacheBackend;
42use hitbox::concurrency::NoopConcurrencyManager;
43use hitbox_core::DisabledOffload;
44use hitbox_http::DEFAULT_CACHE_STATUS_HEADER;
45use http::header::HeaderName;
46use tower::Layer;
47
48use crate::service::CacheService;
49
50/// Marker type for unset builder fields.
51pub struct NotSet;
52
53/// Tower [`Layer`] that adds HTTP caching to a service.
54///
55/// `Cache` wraps any Tower service with caching behavior. When a request arrives,
56/// the layer evaluates predicates to determine cacheability, generates a cache key
57/// using extractors, and either returns a cached response or forwards to the
58/// upstream service.
59///
60/// # Type Parameters
61///
62/// * `B` - Cache backend (e.g., [`MokaBackend`], `RedisBackend`). Must implement
63/// [`CacheBackend`].
64/// * `C` - Configuration providing predicates, extractors, and policy. Use
65/// [`hitbox::Config`] to build custom configuration.
66/// * `CM` - Concurrency manager for dogpile prevention. Use [`NoopConcurrencyManager`]
67/// to disable or [`BroadcastConcurrencyManager`] to enable.
68/// * `O` - Offload strategy for background revalidation. Use [`DisabledOffload`]
69/// for synchronous behavior.
70///
71/// # Examples
72///
73/// Create with the builder pattern:
74///
75/// ```
76/// use hitbox_tower::Cache;
77/// use hitbox_moka::MokaBackend;
78/// use hitbox::Config;
79/// use hitbox_http::extractors::Method;
80/// use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
81/// # use http_body_util::Full;
82/// # type Body = Full<bytes::Bytes>;
83///
84/// let config = Config::builder()
85/// .request_predicate(NeutralRequestPredicate::new())
86/// .response_predicate(NeutralResponsePredicate::new())
87/// .extractor(Method::new())
88/// .build();
89/// # let _: Config<
90/// # NeutralRequestPredicate<Body>,
91/// # NeutralResponsePredicate<Body>,
92/// # Method<hitbox_http::extractors::NeutralExtractor<Body>>,
93/// # > = config;
94///
95/// let cache_layer = Cache::builder()
96/// .backend(MokaBackend::builder().max_entries(1000).build())
97/// .config(config)
98/// .build();
99/// ```
100///
101/// [`Layer`]: tower::Layer
102/// [`MokaBackend`]: hitbox_moka::MokaBackend
103/// [`CacheBackend`]: hitbox::backend::CacheBackend
104/// [`NoopConcurrencyManager`]: hitbox::concurrency::NoopConcurrencyManager
105/// [`BroadcastConcurrencyManager`]: hitbox::concurrency::BroadcastConcurrencyManager
106/// [`DisabledOffload`]: hitbox_core::DisabledOffload
107#[derive(Clone)]
108pub struct Cache<B, C, CM, O = DisabledOffload> {
109 /// The cache backend for storing and retrieving responses.
110 pub backend: Arc<B>,
111 /// Configuration with predicates, extractors, and cache policy.
112 pub configuration: C,
113 /// Offload strategy for background tasks.
114 pub offload: O,
115 /// Concurrency manager for dogpile prevention.
116 pub concurrency_manager: CM,
117 /// Header name for cache status (HIT/MISS/STALE).
118 pub cache_status_header: HeaderName,
119}
120
121impl<S, B, C, CM, O> Layer<S> for Cache<B, C, CM, O>
122where
123 C: Clone,
124 CM: Clone,
125 O: Clone,
126{
127 type Service = CacheService<S, B, C, CM, O>;
128
129 fn layer(&self, upstream: S) -> Self::Service {
130 CacheService::new(
131 upstream,
132 Arc::clone(&self.backend),
133 self.configuration.clone(),
134 self.offload.clone(),
135 self.concurrency_manager.clone(),
136 self.cache_status_header.clone(),
137 )
138 }
139}
140
141impl Cache<NotSet, NotSet, NoopConcurrencyManager, DisabledOffload> {
142 /// Creates a new [`CacheBuilder`].
143 ///
144 /// Both [`backend()`](CacheBuilder::backend) and [`config()`](CacheBuilder::config)
145 /// must be called before [`build()`](CacheBuilder::build).
146 ///
147 /// # Examples
148 ///
149 /// ```
150 /// use hitbox_tower::Cache;
151 /// use hitbox_moka::MokaBackend;
152 /// use hitbox::Config;
153 /// use hitbox_http::extractors::Method;
154 /// use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
155 /// # use http_body_util::Full;
156 /// # type Body = Full<bytes::Bytes>;
157 ///
158 /// let config = Config::builder()
159 /// .request_predicate(NeutralRequestPredicate::new())
160 /// .response_predicate(NeutralResponsePredicate::new())
161 /// .extractor(Method::new())
162 /// .build();
163 /// # let _: Config<
164 /// # NeutralRequestPredicate<Body>,
165 /// # NeutralResponsePredicate<Body>,
166 /// # Method<hitbox_http::extractors::NeutralExtractor<Body>>,
167 /// # > = config;
168 ///
169 /// let cache_layer = Cache::builder()
170 /// .backend(MokaBackend::builder().max_entries(1000).build())
171 /// .config(config)
172 /// .build();
173 /// ```
174 pub fn builder() -> CacheBuilder<NotSet, NotSet, NoopConcurrencyManager, DisabledOffload> {
175 CacheBuilder::new()
176 }
177}
178
179/// Fluent builder for constructing a [`Cache`] layer.
180///
181/// Use [`Cache::builder()`] to create a new builder. Both [`backend()`](Self::backend)
182/// and [`config()`](Self::config) must be called before [`build()`](Self::build).
183///
184/// # Type Parameters
185///
186/// The type parameters change as you call builder methods:
187///
188/// * `B` - Backend type, set by [`backend()`](Self::backend)
189/// * `C` - Configuration type, set by [`config()`](Self::config)
190/// * `CM` - Concurrency manager type, set by [`concurrency_manager()`](Self::concurrency_manager)
191/// * `O` - Offload type, set by [`offload()`](Self::offload)
192///
193/// # Examples
194///
195/// ```
196/// use std::time::Duration;
197/// use hitbox_tower::Cache;
198/// use hitbox_moka::MokaBackend;
199/// use hitbox::Config;
200/// use hitbox::policy::PolicyConfig;
201/// use hitbox_http::extractors::Method;
202/// use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
203/// use http::header::HeaderName;
204/// # use http_body_util::Full;
205/// # type Body = Full<bytes::Bytes>;
206///
207/// let config = Config::builder()
208/// .request_predicate(NeutralRequestPredicate::new())
209/// .response_predicate(NeutralResponsePredicate::new())
210/// .extractor(Method::new())
211/// .policy(PolicyConfig::builder().ttl(Duration::from_secs(300)).build())
212/// .build();
213/// # let _: Config<
214/// # NeutralRequestPredicate<Body>,
215/// # NeutralResponsePredicate<Body>,
216/// # Method<hitbox_http::extractors::NeutralExtractor<Body>>,
217/// # > = config;
218///
219/// let layer = Cache::builder()
220/// .backend(MokaBackend::builder().max_entries(10_000).build())
221/// .config(config)
222/// .cache_status_header(HeaderName::from_static("x-custom-cache"))
223/// .build();
224/// ```
225pub struct CacheBuilder<B, C, CM, O = DisabledOffload> {
226 backend: B,
227 configuration: C,
228 offload: O,
229 concurrency_manager: CM,
230 cache_status_header: Option<HeaderName>,
231}
232
233impl CacheBuilder<NotSet, NotSet, NoopConcurrencyManager, DisabledOffload> {
234 /// Creates a new builder.
235 ///
236 /// Prefer using [`Cache::builder()`] instead of calling this directly.
237 pub fn new() -> Self {
238 Self {
239 backend: NotSet,
240 configuration: NotSet,
241 offload: DisabledOffload,
242 concurrency_manager: NoopConcurrencyManager,
243 cache_status_header: None,
244 }
245 }
246}
247
248impl Default for CacheBuilder<NotSet, NotSet, NoopConcurrencyManager, DisabledOffload> {
249 fn default() -> Self {
250 Self::new()
251 }
252}
253
254impl<B, C, CM, O> CacheBuilder<B, C, CM, O> {
255 /// Sets the cache backend for storing responses.
256 ///
257 /// Common backends:
258 ///
259 /// - `MokaBackend` — In-memory cache (from `hitbox-moka`)
260 /// - `RedisBackend` — Distributed cache via Redis (from `hitbox-redis`)
261 ///
262 /// # Examples
263 ///
264 /// ```
265 /// use hitbox_tower::Cache;
266 /// use hitbox_moka::MokaBackend;
267 ///
268 /// let builder = Cache::builder()
269 /// .backend(MokaBackend::builder().max_entries(1000).build());
270 /// ```
271 pub fn backend<NB: CacheBackend>(self, backend: NB) -> CacheBuilder<NB, C, CM, O> {
272 CacheBuilder {
273 backend,
274 configuration: self.configuration,
275 offload: self.offload,
276 concurrency_manager: self.concurrency_manager,
277 cache_status_header: self.cache_status_header,
278 }
279 }
280
281 /// Sets the cache configuration with predicates, extractors, and policy.
282 ///
283 /// Use [`Config::builder()`](hitbox::Config::builder) to create a configuration with:
284 /// - Request predicates (which requests to cache)
285 /// - Response predicates (which responses to cache)
286 /// - Extractors (how to generate cache keys)
287 /// - Policy (TTL, stale handling)
288 ///
289 /// # Examples
290 ///
291 /// ```
292 /// use std::time::Duration;
293 /// use hitbox_tower::Cache;
294 /// use hitbox_moka::MokaBackend;
295 /// use hitbox::Config;
296 /// use hitbox::policy::PolicyConfig;
297 /// use hitbox_http::extractors::Method;
298 /// use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
299 /// # use http_body_util::Full;
300 /// # type Body = Full<bytes::Bytes>;
301 ///
302 /// let config = Config::builder()
303 /// .request_predicate(NeutralRequestPredicate::new())
304 /// .response_predicate(NeutralResponsePredicate::new())
305 /// .extractor(Method::new())
306 /// .policy(PolicyConfig::builder().ttl(Duration::from_secs(60)).build())
307 /// .build();
308 /// # let _: Config<
309 /// # NeutralRequestPredicate<Body>,
310 /// # NeutralResponsePredicate<Body>,
311 /// # Method<hitbox_http::extractors::NeutralExtractor<Body>>,
312 /// # > = config;
313 ///
314 /// let layer = Cache::builder()
315 /// .backend(MokaBackend::builder().max_entries(1000).build())
316 /// .config(config)
317 /// .build();
318 /// ```
319 pub fn config<NC>(self, configuration: NC) -> CacheBuilder<B, NC, CM, O> {
320 CacheBuilder {
321 backend: self.backend,
322 configuration,
323 offload: self.offload,
324 concurrency_manager: self.concurrency_manager,
325 cache_status_header: self.cache_status_header,
326 }
327 }
328
329 /// Sets the concurrency manager for dogpile prevention.
330 ///
331 /// The dogpile effect occurs when a cache entry expires and multiple
332 /// concurrent requests all try to refresh it simultaneously. A concurrency
333 /// manager prevents this by coordinating requests.
334 ///
335 /// Options:
336 /// - [`NoopConcurrencyManager`] — No coordination (default)
337 /// - [`BroadcastConcurrencyManager`] — One request fetches, others wait
338 ///
339 /// [`NoopConcurrencyManager`]: hitbox::concurrency::NoopConcurrencyManager
340 /// [`BroadcastConcurrencyManager`]: hitbox::concurrency::BroadcastConcurrencyManager
341 pub fn concurrency_manager<NCM>(self, concurrency_manager: NCM) -> CacheBuilder<B, C, NCM, O> {
342 CacheBuilder {
343 backend: self.backend,
344 configuration: self.configuration,
345 offload: self.offload,
346 concurrency_manager,
347 cache_status_header: self.cache_status_header,
348 }
349 }
350
351 /// Sets the offload strategy for background revalidation.
352 ///
353 /// When serving stale content, the offload strategy determines how
354 /// background refresh is performed.
355 ///
356 /// Defaults to [`DisabledOffload`] (synchronous revalidation).
357 ///
358 /// [`DisabledOffload`]: hitbox_core::DisabledOffload
359 pub fn offload<NO>(self, offload: NO) -> CacheBuilder<B, C, CM, NO> {
360 CacheBuilder {
361 backend: self.backend,
362 configuration: self.configuration,
363 offload,
364 concurrency_manager: self.concurrency_manager,
365 cache_status_header: self.cache_status_header,
366 }
367 }
368
369 /// Sets the header name for cache status.
370 ///
371 /// The cache status header indicates whether a response was served from cache.
372 /// Possible values are `HIT`, `MISS`, or `STALE`.
373 ///
374 /// Defaults to [`DEFAULT_CACHE_STATUS_HEADER`] (`x-cache-status`).
375 ///
376 /// # Examples
377 ///
378 /// ```
379 /// use hitbox_tower::Cache;
380 /// use hitbox_moka::MokaBackend;
381 /// use http::header::HeaderName;
382 ///
383 /// let builder = Cache::builder()
384 /// .backend(MokaBackend::builder().max_entries(1000).build())
385 /// .cache_status_header(HeaderName::from_static("x-custom-cache"));
386 /// ```
387 pub fn cache_status_header(self, header_name: HeaderName) -> Self {
388 CacheBuilder {
389 cache_status_header: Some(header_name),
390 ..self
391 }
392 }
393}
394
395impl<B, C, CM, O> CacheBuilder<B, C, CM, O>
396where
397 B: CacheBackend,
398{
399 /// Builds the [`Cache`] layer.
400 ///
401 /// Both [`backend()`](Self::backend) and [`config()`](Self::config) must
402 /// be called before this method.
403 pub fn build(self) -> Cache<B, C, CM, O> {
404 Cache {
405 backend: Arc::new(self.backend),
406 configuration: self.configuration,
407 offload: self.offload,
408 concurrency_manager: self.concurrency_manager,
409 cache_status_header: self
410 .cache_status_header
411 .unwrap_or(DEFAULT_CACHE_STATUS_HEADER),
412 }
413 }
414}