Skip to content

Commit 6b8e235

Browse files
authored
Merge pull request #21 from msalinas92/feature/bypass-rule
Cache ByPass by header
2 parents 4a09532 + 5c47e17 commit 6b8e235

File tree

4 files changed

+74
-21
lines changed

4 files changed

+74
-21
lines changed

src/config.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ pub struct CacheSettings {
3232
/// Memory usage threshold as a percentage (e.g., 80 = 80%).
3333
pub memory_threshold: usize,
3434

35-
/// Percentage of fallback requests that should attempt revalidation.
35+
/// Percentage of fallback requests that should attempt revalidation.
3636
#[serde(default)]
37-
pub refresh_percentage: u8,
37+
pub refresh_percentage: u8,
3838

39-
/// Time-to-live (TTL) for cached responses in seconds.
39+
/// Time-to-live (TTL) for cached responses in seconds.
4040
#[serde(default)]
4141
pub ttl_seconds: u64,
4242
}
@@ -154,11 +154,19 @@ impl Config {
154154

155155
/// Returns the list of headers to ignore (lowercased).
156156
pub fn ignored_headers_set(&self) -> HashSet<String> {
157-
self.ignored_headers
157+
let mut ignored = self
158+
.ignored_headers
158159
.clone()
159160
.unwrap_or_default()
160161
.into_iter()
161162
.map(|h| h.to_ascii_lowercase())
162-
.collect()
163+
.collect::<HashSet<_>>();
164+
165+
// Add bypass and refresh headers as default ignored
166+
ignored.insert("x-bypass-cache".to_string());
167+
ignored.insert("x-refresh-cache".to_string());
168+
ignored.insert("cache-control".to_string());
169+
170+
ignored
163171
}
164-
}
172+
}

src/proxy.rs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ use once_cell::sync::Lazy;
1919
use sha2::{Digest, Sha256};
2020
use std::sync::Arc;
2121
use tokio::sync::{Semaphore, mpsc};
22-
use tokio::time::{Instant};
22+
use tokio::time::Instant;
2323

2424
use crate::config::{CONFIG, StorageBackend};
2525
use crate::memory::memory;
26+
use crate::rules::bypass::should_bypass_cache;
2627
use crate::rules::latency::{get_max_latency_for_path, mark_latency_fail, should_failover};
2728
use crate::rules::refresh::should_refresh;
2829
use crate::storage::{azure, gcs, local, s3};
@@ -121,7 +122,9 @@ pub async fn proxy_handler(req: Request<Body>) -> impl IntoResponse {
121122
let key = hash_uri(&key_source);
122123
tracing::debug!("🔑 Cache key generated: {}", key);
123124

124-
let force_refresh = should_refresh(&key);
125+
//Refresh force by percetange hit rule
126+
let bypass_cache = should_bypass_cache(req.headers());
127+
let force_refresh = should_refresh(&key) || bypass_cache;
125128

126129
// If the URI is in failover mode, serve from cache
127130
if should_failover(&uri) && !force_refresh {
@@ -189,20 +192,25 @@ pub async fn proxy_handler(req: Request<Body>) -> impl IntoResponse {
189192
let exceeded_latency = elapsed_ms > threshold_ms;
190193
let fallback_active = should_failover(&uri);
191194

192-
if is_success && (exceeded_latency || !fallback_active) {
193-
memory::load_into_memory(vec![(key.clone(), cached_response)]).await;
194-
let _ = CACHE_WRITER
195-
.send((key.clone(), body_bytes.clone(), headers_vec))
196-
.await;
197-
counter!("cachebolt_memory_store_total", "uri" => uri.clone()).increment(1);
195+
if !bypass_cache {
196+
if is_success && (exceeded_latency || !fallback_active) {
197+
memory::load_into_memory(vec![(key.clone(), cached_response)]).await;
198+
let _ = CACHE_WRITER
199+
.send((key.clone(), body_bytes.clone(), headers_vec))
200+
.await;
201+
counter!("cachebolt_memory_store_total", "uri" => uri.clone())
202+
.increment(1);
203+
} else {
204+
tracing::info!(
205+
"⚠️ Skipping cache store for '{}' (status: {}, exceeded_latency: {}, fallback_active: {})",
206+
uri,
207+
status,
208+
exceeded_latency,
209+
fallback_active
210+
);
211+
}
198212
} else {
199-
tracing::info!(
200-
"⚠️ Skipping cache store for '{}' (status: {}, exceeded_latency: {}, fallback_active: {})",
201-
uri,
202-
status,
203-
exceeded_latency,
204-
fallback_active
205-
);
213+
tracing::info!("⏩ Cache bypass activated for '{}' due to client header", uri);
206214
}
207215

208216
Response::from_parts(parts, Body::from(body_bytes))

src/rules/bypass.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (C) 2025 Matías Salinas ([email protected])
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use hyper::HeaderMap;
16+
17+
/// Returns true if the client explicitly requests to bypass *all* cache layers.
18+
///
19+
/// When true:
20+
/// - The cache will be skipped for read and write.
21+
/// - The backend will be hit directly.
22+
pub fn should_bypass_cache(headers: &HeaderMap) -> bool {
23+
if let Some(value) = headers.get("cache-control") {
24+
if value.to_str().unwrap_or("").to_ascii_lowercase().contains("no-cache") {
25+
return true;
26+
}
27+
}
28+
29+
if let Some(value) = headers.get("x-bypass-cache") {
30+
if value.to_str().unwrap_or("").to_ascii_lowercase() == "true" {
31+
return true;
32+
}
33+
}
34+
35+
false
36+
}

src/rules/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414

1515
pub mod latency;
1616
pub mod refresh;
17+
pub mod bypass;

0 commit comments

Comments
 (0)