This commit is contained in:
Mateusz Gruszczyński committed 2026-11-07 10:23:28 +01:00
1 parent ff42a13dbf
commit e7b4991316
14 files changed
+176 -50

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.35.4",
"version": "0.35.5",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
},
"servers": [
+3 -3
View File
@@ -435,8 +435,6 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
$("process-summary").hidden = d.process_threads == null;
if (d.process_threads != null) $("process-summary").textContent = `${fmt(d.process_threads)} ${t("threads")}`;
setOptionalMetric("system-cpu", d.cpu_percent != null, `${fmt(d.cpu_percent, 1)}%`);
const sourceSupported = !!d.source && d.source !== "unsupported";
$("system-source").hidden = !sourceSupported; if (sourceSupported) $("system-source").textContent = t("realSample");
const systemMemory = knownPair(d.memory_used_bytes, d.memory_total_bytes, value => fmtBytes(value));
setOptionalMetric("system-memory", systemMemory != null, systemMemory);
const ramMeter = $("memory-meter"), ramFill = $("memory-meter-fill");
@@ -457,7 +455,9 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
setOptionalMetric("system-swap", systemSwap != null, systemSwap);
const loadSupported = Array.isArray(d.load_average) && d.load_average.length > 0;
setOptionalMetric("system-load", loadSupported, loadSupported ? d.load_average.map(n => fmt(n, 2)).join(" / ") : "");
setOptionalMetric("system-uptime", d.system_uptime_seconds != null, d.system_uptime_seconds == null ? "" : uptime(d.system_uptime_seconds));
const systemUptime = $("system-uptime"), systemUptimeMeta = systemUptime?.closest(".system-uptime-meta");
if (systemUptimeMeta) systemUptimeMeta.hidden = d.system_uptime_seconds == null;
if (systemUptime && d.system_uptime_seconds != null) systemUptime.textContent = uptime(d.system_uptime_seconds);
}
$("system-chart").hidden = live.cpu_percent == null; if (appendSample && live.cpu_percent != null) { systemSamples.push(Number(live.cpu_percent)); systemSamples = systemSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
$("system-sparkline").setAttribute("points", systemSamples.map((n, i) => `${i * 300 / Math.max(1, systemSamples.length - 1)},${95 - Math.max(0, Math.min(100, n)) * .9}`).join(" "));
+59 -14
View File
@@ -1004,6 +1004,10 @@ html[data-dashboard-density=compact] #panel-overview .stat>span {
line-height: 1.35;
}
html[data-dashboard-density=compact] #panel-overview .stat-cache-outcomes .cache-mix-header>span {
font-size: 11px;
}
html[data-dashboard-density=compact] #panel-overview .stat strong {
font-size: 22px;
margin: 3px 0 2px;
@@ -1028,6 +1032,10 @@ html[data-dashboard-density=ultra] #panel-overview .stat>span {
line-height: 1.08;
}
html[data-dashboard-density=ultra] #panel-overview .stat-cache-outcomes .cache-mix-header>span {
line-height: 1.08;
}
html[data-dashboard-density=ultra] #panel-overview .stat strong {
font-size: 18px;
margin: 2px 0 1px;
@@ -2097,7 +2105,12 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
flex: 1 1 auto;
min-width: 0;
color: var(--muted);
font-size: 11px;
font-family: var(--sans);
font-size: 12px;
font-weight: 600;
line-height: 1.35;
letter-spacing: normal;
text-transform: none;
}
#panel-overview .stat-cache-outcomes .cache-mix-caption {
@@ -2132,16 +2145,18 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
}
#panel-overview .cache-mix-legend {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 5px 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 7px;
line-height: 1.25;
white-space: nowrap;
}
#panel-overview .cache-mix-legend>span {
display: flex;
display: inline-flex;
align-items: center;
gap: 4px;
gap: 3px;
min-width: 0;
}
@@ -2164,19 +2179,49 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
}
#panel-overview .cache-mix-legend b {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: auto;
display: inline-flex;
align-items: baseline;
gap: 2px;
margin-left: 1px;
color: var(--text);
font: 500 10px var(--mono);
overflow-wrap: anywhere;
text-align: right;
font: 500 9px var(--mono);
}
#panel-overview .cache-mix-legend .cache-mix-percent {
color: var(--muted);
font: 400 10px var(--mono);
font: 400 8px var(--mono);
}
#panel-overview .cache-mix-legend .cache-mix-percent::before {
content: "· ";
}
#panel-overview .system-card .system-uptime-meta,
#panel-overview .connection-card .connection-backends-meta {
white-space: nowrap;
}
#panel-overview .system-card .system-uptime-meta b,
#panel-overview .connection-card .connection-backends-meta b {
color: var(--text);
font-weight: 650;
}
#panel-overview .connection-card .connection-heading-main {
width: 100%;
min-width: 0;
}
#panel-overview .connection-card .connection-current-main {
display: inline-flex;
align-items: baseline;
gap: 6px;
min-width: 0;
}
#panel-overview .connection-card .connection-backends-meta {
margin-left: auto;
line-height: inherit;
}
/* Live values stay on one line so refreshes do not change card heights. */
+7 -7
View File
@@ -334,7 +334,8 @@
<div>
<h3 class="overview-chart-heading" data-i18n="systemLoad">System load</h3>
<h2><span id="system-cpu">-</span> <small>CPU</small></h2>
</div><span class="meta" id="system-source">-</span>
</div><span class="meta system-uptime-meta"><span data-i18n="systemUptime">System uptime</span>: <b
id="system-uptime">-</b></span>
</div><svg preserveAspectRatio="none" aria-label="System CPU history" data-i18n-aria="systemChart" id="system-chart"
role="img" viewbox="0 0 300 100">
<path class="chart-grid" d="M0 25H300 M0 50H300 M0 75H300"></path>
@@ -348,14 +349,15 @@
<div class="memory-line"><span data-i18n="systemSwap">System swap</span><b id="system-swap">-</b></div>
<div class="chart-footer"><span data-i18n="loadAverage">Load 1 / 5 / 15 min</span><span id="system-load">-</span>
</div>
<div class="chart-footer"><span data-i18n="systemUptime">System uptime</span><span id="system-uptime">-</span></div>
</article>
<article class="card connection-card" data-dashboard-widget="connection_chart">
<div class="card-heading">
<div>
<div class="connection-heading-main">
<h3 class="overview-chart-heading" data-i18n="connectionActivity">Connection activity</h3>
<h2><span id="connection-current">0</span> <small data-i18n="connections">Open
connections</small></h2>
<h2><span class="connection-current-main"><span id="connection-current">0</span> <small
data-i18n="connections">Open connections</small></span><small
class="meta connection-backends-meta"><span data-i18n="backends">Backends</span>: <b
id="connection-backends">0</b></small></h2>
</div>
</div>
<svg preserveAspectRatio="none" aria-label="Connection activity history" data-i18n-aria="connectionActivity"
@@ -368,6 +370,4 @@
connections</span> <b id="connection-open-value">0</b></span><span
class="connection-exchange-key"><i></i><span data-i18n="activeExchanges">Active
exchanges</span> <b id="connection-exchange-value">0</b></span></div>
<div class="chart-footer"><span><span data-i18n="backends">Backends</span>: <b id="connection-backends">0</b></span>
</div>
</article>
@@ -517,5 +517,4 @@
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="www_php_requests-line" points="0,55 300,55"></polyline>
</svg>
<small data-i18n="webMetricPerSecond">Requests per second</small>
</article>
+39 -2
View File
@@ -6,7 +6,6 @@ mod tests {
h.insert("cache-control", value.parse().unwrap());
h
}
// Legacy policy; varnish-compatible behavior is tested separately.
fn settings() -> Settings {
Settings {
enabled: true,
@@ -1559,7 +1558,10 @@ mod tests {
let second = cache
.ticket(1, "http", "example.test", "app", "/second", &headers)
.unwrap();
fill(&cache, first.clone(), "first", &s).await;
// Stored bodies are trimmed to their real size, so the first object must
// itself be large for the next fill reservation to need its space.
let large: &'static str = Box::leak("f".repeat(60 * 1024).into_boxed_str());
fill(&cache, first.clone(), large, &s).await;
assert!(cache.get(&first).is_some());
let before = cache.view();
assert!(before.hot_allocated_bytes <= s.max_bytes);
@@ -1821,4 +1823,39 @@ mod tests {
assert_eq!(cache.view().entries, 0);
assert_eq!(cache.view().fills_active, 0);
}
#[tokio::test]
async fn stored_entries_do_not_pin_origin_buffers_or_spare_capacity() {
let cache = Cache::new(1);
let s = settings();
let t = ticket(&cache, &HeaderMap::new());
// Header values parsed by hyper are slices of a larger read buffer.
let read_buffer = Bytes::from(vec![b'x'; 64 * 1024]);
let mut h = headers("max-age=60");
h.insert(
"x-origin",
HeaderValue::from_maybe_shared(read_buffer.slice(0..16)).unwrap(),
);
h.insert(
::http::header::ETAG,
HeaderValue::from_maybe_shared(read_buffer.slice(16..24)).unwrap(),
);
let p = plan(StatusCode::OK, &h, &s, Duration::ZERO).unwrap();
let mut response = Response::new(Body::from("small body"));
*response.headers_mut() = h;
cache
.wrap(response, t.clone(), p, &s)
.into_body()
.collect()
.await
.unwrap();
assert!(cache.get(&t).is_some());
assert!(
read_buffer.try_into_mut().is_ok(),
"cached headers must not keep the origin read buffer alive"
);
let store = cache.store.lock().unwrap();
let entry = store.entries.values().next().expect("stored object");
assert_eq!(entry.body.len(), 10);
assert!(entry.body_allocated <= 10 + 10 / 8 + 1);
}
}
+40 -2
View File
@@ -1,3 +1,16 @@
fn detached_header_value(value: &HeaderValue) -> HeaderValue {
let mut detached = HeaderValue::from_bytes(value.as_bytes())
.expect("existing HTTP header value must remain valid when copied");
detached.set_sensitive(value.is_sensitive());
detached
}
fn detach_header_map(headers: &mut HeaderMap) {
for value in headers.values_mut() {
*value = detached_header_value(value);
}
}
struct ByteGaugeGuard<'a> {
counter: &'a AtomicU64,
bytes: u64,
@@ -102,6 +115,9 @@ impl Candidate {
let (body, body_allocation) = if let Some(body) = self.shared.take() {
(body, self.shared_allocated)
} else {
if self.data.capacity() > self.data.len().saturating_add(self.data.len() / 8) {
self.data.shrink_to_fit();
}
let allocated = self.data.capacity().max(self.data.len());
(Bytes::from(std::mem::take(&mut self.data)), allocated)
};
@@ -111,6 +127,12 @@ impl Candidate {
let domain = self.ticket.domain.clone();
let path = self.ticket.path.clone();
let target = self.ticket.target.clone();
detach_header_map(&mut self.headers);
self.origin_etag = self.origin_etag.as_ref().map(detached_header_value);
self.origin_last_modified = self
.origin_last_modified
.as_ref()
.map(detached_header_value);
let tags = cache_tags(&self.headers);
let cost = hot_entry_cost(
body_allocation,
@@ -226,7 +248,21 @@ impl HttpBody for FillBody {
let needed = candidate.data.len().saturating_add(data.len());
if needed <= candidate.settings.max_object_bytes {
let before = candidate.data.capacity();
let required_growth = needed.saturating_sub(before);
let target_capacity = if needed <= before {
before
} else if let Some(length) =
candidate.plan.expected_length.filter(|length| {
*length >= needed
&& *length <= candidate.settings.max_object_bytes
})
{
length
} else {
needed
.max(before.saturating_mul(2))
.min(candidate.settings.max_object_bytes)
};
let required_growth = target_capacity.saturating_sub(before);
if required_growth > 0
&& !candidate
.fill_gauge
@@ -246,7 +282,9 @@ impl HttpBody for FillBody {
} else if required_growth > 0
&& candidate
.data
.try_reserve_exact(needed.saturating_sub(candidate.data.len()))
.try_reserve_exact(
target_capacity.saturating_sub(candidate.data.len()),
)
.is_err()
{
candidate.cache.record_reason(