v0.34.0_1
This commit is contained in:
1 parent
9b3d6731cc
commit
00f29f92c8
68 files changed
+2599
-513
No files matched your search
@@ -98,12 +98,14 @@ impl App {
|
||||
.as_ref()
|
||||
.and_then(|path| path.parent())
|
||||
.map(std::path::Path::to_path_buf);
|
||||
let cache = crate::cache::Cache::start(
|
||||
let cache = crate::cache::Cache::start_with_scope_revisions(
|
||||
cache_revision(&snapshot.envelope.config),
|
||||
cache_data_root,
|
||||
snapshot.envelope.config.cache.clone(),
|
||||
shutdown.clone(),
|
||||
vhost_cache_revisions(&snapshot.envelope.config),
|
||||
);
|
||||
cache.set_control_revision(snapshot.envelope.revision);
|
||||
let active_listeners = snapshot.listeners.clone();
|
||||
let active_streams = snapshot.envelope.config.streams.clone();
|
||||
let stream_metrics = active_streams
|
||||
|
||||
+48
-1
@@ -7,6 +7,17 @@ mod cache_revision_tests {
|
||||
let base = config::demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
let identity = cache_revision(&base);
|
||||
|
||||
let mut file_transport = base.clone();
|
||||
file_transport.web_services.push(crate::web_server::Service {
|
||||
id: "static-test".into(), name: "Static Test".into(),
|
||||
document_root: "/var/www/static-test".into(),
|
||||
..Default::default()
|
||||
});
|
||||
let mut zero_copy = file_transport.clone();
|
||||
zero_copy.web_services[0].sendfile = crate::web_server::SendfileMode::Auto;
|
||||
assert_eq!(cache_revision(&file_transport), cache_revision(&zero_copy));
|
||||
assert!(matches!(cache_invalidation(&file_transport, &zero_copy), CacheInvalidation::None));
|
||||
|
||||
let mut logging = base.clone();
|
||||
logging.logging.file.path = "/tmp/rustproxy-test-access.jsonl".into();
|
||||
assert_eq!(cache_revision(&logging), identity);
|
||||
@@ -107,8 +118,44 @@ mod cache_revision_tests {
|
||||
CacheInvalidation::All(value) if value == cache_identity
|
||||
));
|
||||
|
||||
let mut operational = base.clone();
|
||||
operational.vhosts[0].load_balancing.keepalive.enabled = false;
|
||||
operational.vhosts[0].load_balancing.keepalive.idle_timeout_seconds = 90;
|
||||
operational.vhosts[0].load_balancing.keepalive.max_idle_per_host = 32;
|
||||
operational.vhosts[0].load_balancing.keepalive.tcp_keepalive_seconds = 90;
|
||||
operational.vhosts[0].load_balancing.keepalive.pool_recycle_requests = 1_000;
|
||||
operational.vhosts[0].load_balancing.keepalive.pool_recycle_seconds = 1_800;
|
||||
operational.vhosts[0].load_balancing.keepalive.http1_max_buffer_bytes = 65_536;
|
||||
operational.vhosts[0].backends[0].weight += 1;
|
||||
operational.vhosts[0].backends[0].max_concurrent_requests = 16;
|
||||
operational.vhosts[0].backends[0].queue_timeout_ms = 100;
|
||||
operational.vhosts[0].timing_headers = true;
|
||||
operational.vhosts[0].acl_profile_id = Some("test-only-acl".into());
|
||||
operational.vhosts[0].user_agent_blocking = Default::default();
|
||||
operational.vhosts[0].proxy_timeouts.read_timeout_seconds = Some(180);
|
||||
assert_eq!(cache_revision(&operational), identity);
|
||||
assert_eq!(vhost_cache_revision(&operational.vhosts[0]), vhost_cache_revision(&base.vhosts[0]));
|
||||
assert!(matches!(cache_invalidation(&base, &operational), CacheInvalidation::None));
|
||||
assert!(matches!(cache_invalidation(&operational, &base), CacheInvalidation::None));
|
||||
|
||||
let mut vhost = base.clone();
|
||||
vhost.vhosts[0].backends[0].url = "http://127.0.0.1:9001".into();
|
||||
assert_ne!(cache_revision(&vhost), identity);
|
||||
assert_eq!(cache_revision(&vhost), identity);
|
||||
let affected_id = base.vhosts[0].id.clone();
|
||||
assert!(matches!(cache_invalidation(&base, &vhost), CacheInvalidation::Scopes(ids) if ids == vec![affected_id]));
|
||||
|
||||
// A content change must not invalidate another independent vhost.
|
||||
let mut multiple = base.clone();
|
||||
let mut another = multiple.vhosts[0].clone();
|
||||
another.id = "other-scope".into();
|
||||
another.frontend.hosts = vec!["other.example.test".into()];
|
||||
multiple.vhosts.push(another);
|
||||
let mut changed_one = multiple.clone();
|
||||
changed_one.vhosts[0].backends[0].url = "http://127.0.0.1:9002".into();
|
||||
assert!(matches!(cache_invalidation(&multiple, &changed_one), CacheInvalidation::Scopes(ids)
|
||||
if ids == vec![base.vhosts[0].id.clone()]));
|
||||
let mut changed_global = multiple.clone();
|
||||
changed_global.transform.max_body_bytes += 1;
|
||||
assert!(matches!(cache_invalidation(&multiple, &changed_global), CacheInvalidation::All(_)));
|
||||
}
|
||||
}
|
||||
+86
-30
@@ -78,25 +78,19 @@ impl IntoResponse for ApiError {
|
||||
enum CacheInvalidation {
|
||||
None,
|
||||
All(u64),
|
||||
Scopes(Vec<String>),
|
||||
}
|
||||
|
||||
/// Stable cache compatibility identity. Unlike the configuration revision, this
|
||||
/// changes only when a setting can alter an origin request or cached response.
|
||||
/// Keeping it stable across logging/TLS/admin saves lets the persistent cold
|
||||
/// tier survive a restart after unrelated control-plane edits.
|
||||
/// Global compatibility identity for HOT/COLD cache entries. Vhost-specific
|
||||
/// representation changes use individual scope generations and COLD fingerprints.
|
||||
/// Connection-pool/LB knobs do not enter either identity.
|
||||
fn cache_revision(config: &config::Config) -> u64 {
|
||||
// Preset fields are UI intent metadata. Persist them, but do not rotate the
|
||||
// cache compatibility identity (and purge HOT/COLD) when only the label
|
||||
// changes while the effective cache limits stay identical.
|
||||
let mut cache = config.cache.clone();
|
||||
cache.memory_preset = None;
|
||||
cache.cold_preset = None;
|
||||
// Request bypass switches and response-only diagnostic headers do not alter
|
||||
// already cached representations. Keep cache objects across these policy
|
||||
// edits; the new behavior applies immediately on the next request.
|
||||
// These are request admission or response diagnostics, never stored objects.
|
||||
cache.bypass_query = false;
|
||||
cache.bypass_prefixes.clear();
|
||||
// Remote maintenance permissions cannot change stored object representations.
|
||||
cache.http_invalidation_allowed_cidrs.clear();
|
||||
cache.expose_status = false;
|
||||
cache.expose_hits = false;
|
||||
@@ -104,43 +98,105 @@ fn cache_revision(config: &config::Config) -> u64 {
|
||||
cache.expose_resident_age = false;
|
||||
cache.expose_only_hits = false;
|
||||
cache.via_header = None;
|
||||
// Resource-only transform controls decide how much optimization work may
|
||||
// happen concurrently, but do not change the transformed representation.
|
||||
// Keep those out of the cache identity while including representation and
|
||||
// eligibility controls such as enabled kinds, extensions and max body.
|
||||
let mut transform = config.transform.clone();
|
||||
transform.preset = None;
|
||||
transform.memory_budget_bytes = 0;
|
||||
transform.max_inflight = 0;
|
||||
// A per-vhost diagnostics override is also presentation-only and must not
|
||||
// invalidate cached objects when switched from inherited to HIT-only.
|
||||
let mut vhosts = config.vhosts.clone();
|
||||
for vhost in &mut vhosts {
|
||||
vhost.cache.expose_only_hits = None;
|
||||
// Frontend ALPN/QUIC switches decide how a client reaches a vhost,
|
||||
// not which representation the origin returns. Hot and cold entries
|
||||
// must survive HTTP/2 or HTTP/3 opt-in/out saves.
|
||||
vhost.http2_enabled = true;
|
||||
vhost.http3_enabled = true;
|
||||
// Web services and FPM profiles can change the bytes served by a location.
|
||||
// Until their dependency graph is split into scopes, invalidate globally.
|
||||
let mut web_services = config.web_services.clone();
|
||||
for service in &mut web_services {
|
||||
// Kernel write strategy never changes the HTTP representation.
|
||||
service.sendfile = crate::web_server::SendfileMode::Off;
|
||||
}
|
||||
let encoded = serde_json::to_vec(&(
|
||||
cache_digest(&(
|
||||
&cache,
|
||||
&transform,
|
||||
&config.headers,
|
||||
&config.forwarding,
|
||||
&vhosts,
|
||||
&web_services,
|
||||
&config.php_fpm_profiles,
|
||||
))
|
||||
.expect("serializable cache compatibility view");
|
||||
}
|
||||
|
||||
fn cache_digest<T: serde::Serialize>(value: &T) -> u64 {
|
||||
let encoded = serde_json::to_vec(value).expect("serializable cache compatibility view");
|
||||
let digest = Sha256::digest(encoded);
|
||||
u64::from_be_bytes(digest[..8].try_into().expect("sha256 prefix"))
|
||||
}
|
||||
|
||||
/// Only effective origin/content policies contribute to this scope identity.
|
||||
/// A vhost keepalive or load-balancer control save must retain both cache tiers.
|
||||
fn vhost_cache_revision(vhost: &crate::vhost::Vhost) -> u64 {
|
||||
let mut view = vhost.clone();
|
||||
view.websockets = false;
|
||||
view.websocket_legacy_support = false;
|
||||
view.websocket_rfc8441 = false;
|
||||
view.websocket_rfc9220 = false;
|
||||
view.http2_enabled = true;
|
||||
view.http3_enabled = true;
|
||||
view.enabled = true;
|
||||
view.force_ssl = false;
|
||||
view.www_redirect = false;
|
||||
view.www_redirect_target = None;
|
||||
view.timing_headers = false;
|
||||
view.backend_header = false;
|
||||
view.backend_timing_header = false;
|
||||
view.certificate_id = None;
|
||||
view.tls_policy = None;
|
||||
view.server_header = None;
|
||||
view.diagnostic_headers = None;
|
||||
view.rate_limit = Default::default();
|
||||
view.method_policy = Default::default();
|
||||
view.basic_auth = Default::default();
|
||||
view.tinyauth = Default::default();
|
||||
view.acl = None;
|
||||
view.acl_profile_id = None;
|
||||
view.user_agent_blocking = Default::default();
|
||||
view.proxy_timeouts = Default::default();
|
||||
view.frontend.listener_ids.clear();
|
||||
view.cache.expose_only_hits = None;
|
||||
// The complete LB policy (keepalive, recycling, weights, retries, health,
|
||||
// affinity and queues) governs connection selection, not cache identity.
|
||||
// Sticky authentication checks and bypass still run before the cache lookup.
|
||||
view.load_balancing = Default::default();
|
||||
for member in &mut view.backends {
|
||||
member.weight = 1;
|
||||
member.backup = false;
|
||||
member.drain = false;
|
||||
member.health_check_enabled = true;
|
||||
member.health_check = None;
|
||||
member.max_concurrent_requests = 0;
|
||||
member.queue_timeout_ms = 0;
|
||||
}
|
||||
cache_digest(&view)
|
||||
}
|
||||
|
||||
fn vhost_cache_revisions(config: &config::Config) -> HashMap<String, u64> {
|
||||
config.vhosts.iter().map(|vhost| (vhost.id.clone(), vhost_cache_revision(vhost))).collect()
|
||||
}
|
||||
|
||||
fn cache_invalidation(previous: &config::Config, next: &config::Config) -> CacheInvalidation {
|
||||
let before = cache_revision(previous);
|
||||
let after = cache_revision(next);
|
||||
if before == after {
|
||||
if before != after {
|
||||
return CacheInvalidation::All(after);
|
||||
}
|
||||
let old = vhost_cache_revisions(previous);
|
||||
let new = vhost_cache_revisions(next);
|
||||
let mut changed: Vec<_> = old
|
||||
.iter()
|
||||
.filter(|(id, revision)| new.get(*id) != Some(*revision))
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect();
|
||||
// New scopes do not have previous cache objects, but include the set for
|
||||
// completeness if an ID is ever reused within a single config transaction.
|
||||
changed.extend(new.keys().filter(|id| !old.contains_key(*id)).cloned());
|
||||
changed.sort();
|
||||
changed.dedup();
|
||||
if changed.is_empty() {
|
||||
CacheInvalidation::None
|
||||
} else {
|
||||
CacheInvalidation::All(after)
|
||||
CacheInvalidation::Scopes(changed)
|
||||
}
|
||||
}
|
||||
@@ -147,12 +147,24 @@ impl App {
|
||||
// intentionally decoupled from the configuration revision, so unrelated
|
||||
// control-plane saves do not invalidate hot/cold objects.
|
||||
let envelope = prepared.envelope.clone();
|
||||
// After commit, reject late cache tickets from the old routing
|
||||
// snapshot before any scope purge. Existing unaffected objects stay.
|
||||
this.cache.set_control_revision(envelope.revision);
|
||||
match invalidation {
|
||||
CacheInvalidation::All(cache_revision) => {
|
||||
this.cache.purge(Some(cache_revision));
|
||||
}
|
||||
CacheInvalidation::Scopes(scopes) => {
|
||||
for scope in scopes {
|
||||
this.cache.purge_scope(&scope);
|
||||
}
|
||||
}
|
||||
CacheInvalidation::None => {}
|
||||
}
|
||||
// A COLD entry is usable after restart only if its scope still has
|
||||
// the same origin/content policy. Publish the fingerprints after
|
||||
// the cache generation barriers but before the routing snapshot.
|
||||
this.cache.replace_scope_revisions(vhost_cache_revisions(&envelope.config));
|
||||
this.cache.configure(envelope.config.cache.clone());
|
||||
this.rate_limiter.reset();
|
||||
this.logs.configure(envelope.config.logging.clone());
|
||||
|
||||
Reference in new issue
Block a user