Skip to content

Commit 72f5169

Browse files
author
Paul C
committed
fix: 400-600% CPU — move all monitor.lock() calls to spawn_blocking
The new get_top_processes endpoint called refresh_processes() (reads /proc for every process) directly on the tokio async runtime while holding the monitor Mutex. Combined with the 2-second background monitoring loop also locking the same mutex on the async runtime, this starved tokio worker threads and pinned multiple CPU cores. Wrapped ALL monitor.lock().collect() and top_processes() calls in tokio::task::spawn_blocking across api/mod.rs and main.rs so they run on the blocking thread pool instead of starving the async executor. Also fixes: - Plugin backends not starting on reboot (start_all_backends was never called — wired into startup and reload) - Mobile menu broken after update (CSS used static cache buster while JS used Date.now(), stale CSS lacked mobile sidebar fixes) v16.11.3
1 parent 52fc882 commit 72f5169

5 files changed

Lines changed: 85 additions & 48 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "wolfstack"
3-
version = "16.11.2"
3+
version = "16.11.3"
44
edition = "2024"
55
authors = ["Wolf Software Systems Ltd"]
66
description = "Server management platform for the Wolf software suite"

src/api/mod.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,10 @@ pub async fn disable_2fa(req: HttpRequest, state: web::Data<AppState>, path: web
744744
/// GET /api/metrics — current system metrics
745745
pub async fn get_metrics(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
746746
if let Err(resp) = require_auth(&req, &state) { return resp; }
747-
let metrics = state.monitor.lock().unwrap().collect();
747+
let st = state.into_inner();
748+
let metrics = tokio::task::spawn_blocking(move || {
749+
st.monitor.lock().unwrap().collect()
750+
}).await.unwrap();
748751
HttpResponse::Ok().json(metrics)
749752
}
750753

@@ -758,7 +761,10 @@ pub async fn get_metrics_history(req: HttpRequest, state: web::Data<AppState>) -
758761
/// GET /api/metrics/processes — top CPU and memory consuming processes
759762
pub async fn get_top_processes(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
760763
if let Err(resp) = require_auth(&req, &state) { return resp; }
761-
let (top_cpu, top_mem) = state.monitor.lock().unwrap().top_processes(10);
764+
let st = state.into_inner();
765+
let (top_cpu, top_mem) = tokio::task::spawn_blocking(move || {
766+
st.monitor.lock().unwrap().top_processes(10)
767+
}).await.unwrap_or_default();
762768
HttpResponse::Ok().json(serde_json::json!({
763769
"top_cpu": top_cpu,
764770
"top_mem": top_mem,
@@ -2765,15 +2771,20 @@ pub async fn agent_status(req: HttpRequest, state: web::Data<AppState>) -> HttpR
27652771
}
27662772

27672773
// Fallback: first request before cache is populated (only happens once at startup)
2768-
let metrics = state.monitor.lock().unwrap().collect();
2769-
let components = installer::get_all_status();
2774+
let st = state.clone().into_inner();
2775+
let (metrics, components, docker_count, lxc_count, vm_count, has_docker, has_lxc, has_kvm) =
2776+
tokio::task::spawn_blocking(move || {
2777+
let m = st.monitor.lock().unwrap().collect();
2778+
let c = installer::get_all_status();
2779+
let dc = containers::docker_list_all().len() as u32;
2780+
let lc = containers::lxc_list_all().len() as u32;
2781+
let vc = st.vms.lock().unwrap().list_vms().len() as u32;
2782+
let hd = containers::docker_status().installed;
2783+
let hl = containers::lxc_status().installed;
2784+
let hk = containers::kvm_installed();
2785+
(m, c, dc, lc, vc, hd, hl, hk)
2786+
}).await.unwrap();
27702787
let hostname = metrics.hostname.clone();
2771-
let docker_count = containers::docker_list_all().len() as u32;
2772-
let lxc_count = containers::lxc_list_all().len() as u32;
2773-
let vm_count = state.vms.lock().unwrap().list_vms().len() as u32;
2774-
let has_docker = containers::docker_status().installed;
2775-
let has_lxc = containers::lxc_status().installed;
2776-
let has_kvm = containers::kvm_installed();
27772788
let public_ip = state.cluster.get_node(&state.cluster.self_id).and_then(|n| n.public_ip);
27782789
let msg = AgentMessage::StatusReport {
27792790
node_id: state.cluster.self_id.clone(),
@@ -12279,7 +12290,10 @@ pub async fn scan_issues(
1227912290
) -> HttpResponse {
1228012291
if let Err(resp) = require_auth(&req, &state) { return resp; }
1228112292

12282-
let metrics = state.monitor.lock().unwrap().collect();
12293+
let st = state.clone().into_inner();
12294+
let metrics = tokio::task::spawn_blocking(move || {
12295+
st.monitor.lock().unwrap().collect()
12296+
}).await.unwrap();
1228312297
let issues = collect_issues(&metrics);
1228412298

1228512299
HttpResponse::Ok().json(serde_json::json!({

src/main.rs

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ async fn main() -> std::io::Result<()> {
253253
containers::lxc_autostart_all();
254254
networking::apply_all_wireguard_bridges();
255255
kubernetes::apply_all_wolfnet_routes();
256+
plugins::start_all_backends();
256257
});
257258

258259
// Check if TLS will be available (so the frontend knows the correct protocol for URLs)
@@ -331,27 +332,27 @@ async fn main() -> std::io::Result<()> {
331332
tokio::spawn(async move {
332333
loop {
333334
tokio::time::sleep(Duration::from_secs(2)).await;
334-
let (metrics, components) = {
335-
let mut monitor = state_clone.monitor.lock().unwrap();
336-
let m = monitor.collect();
337-
let c = installer::get_all_status_cached();
338-
(m, c)
339-
};
335+
// Run all blocking sysinfo/subprocess work off the async runtime
336+
let sc = state_clone.clone();
337+
let (metrics, components, docker_count, lxc_count, vm_count, has_docker, has_lxc, has_kvm) =
338+
tokio::task::spawn_blocking(move || {
339+
let mut monitor = sc.monitor.lock().unwrap();
340+
let m = monitor.collect();
341+
drop(monitor); // release mutex before spawning subprocesses
342+
let c = installer::get_all_status_cached();
343+
let dc = containers::docker_count();
344+
let lc = containers::lxc_count();
345+
let vc = sc.vms.lock().unwrap().list_vms().len() as u32;
346+
let hd = containers::has_docker_cached();
347+
let hl = containers::has_lxc_cached();
348+
let hk = containers::has_kvm_cached();
349+
(m, c, dc, lc, vc, hd, hl, hk)
350+
}).await.unwrap();
340351
// Record historical snapshot
341352
{
342353
let mut history = state_clone.metrics_history.lock().unwrap();
343354
history.push(&metrics);
344355
}
345-
// Use lightweight counts (1 subprocess each) instead of full listing
346-
// (which spawns 3+ subprocesses per container for docker inspect)
347-
let docker_count = containers::docker_count();
348-
let lxc_count = containers::lxc_count();
349-
let vm_count = state_clone.vms.lock().unwrap().list_vms().len() as u32;
350-
// Use cached runtime detection (TTL 120s) instead of spawning
351-
// 'which', 'docker info', etc. on every 2-second cycle
352-
let has_docker = containers::has_docker_cached();
353-
let has_lxc = containers::has_lxc_cached();
354-
let has_kvm = containers::has_kvm_cached();
355356

356357
// Cache the agent status report for instant polling responses
357358
let self_id = cluster_clone.self_id.clone();
@@ -508,7 +509,10 @@ async fn main() -> std::io::Result<()> {
508509
let mut all_issues: Vec<(String, String, api::Issue)> = Vec::new();
509510

510511
// Local node
511-
let metrics = scan_state.monitor.lock().unwrap().collect();
512+
let ss = scan_state.clone();
513+
let metrics = tokio::task::spawn_blocking(move || {
514+
ss.monitor.lock().unwrap().collect()
515+
}).await.unwrap();
512516
let local_hostname = metrics.hostname.clone();
513517
let local_cluster = {
514518
let nodes = scan_cluster.get_all_nodes();
@@ -1103,24 +1107,26 @@ a{color:#dc2626;text-decoration:none;}a:hover{text-decoration:underline;}
11031107

11041108
if is_configured {
11051109
// Collect local metrics (sync — release mutex before any .await)
1110+
let ai_sc = ai_state.clone();
11061111
let (hostname, cpu_pct, mem_used_gb, mem_total_gb, disk_used_gb, disk_total_gb,
1107-
docker_count, lxc_count, vm_count, uptime_secs) = {
1108-
let mut monitor = ai_state.monitor.lock().unwrap();
1109-
let m = monitor.collect();
1110-
let docker_count = containers::docker_count();
1111-
let lxc_count = containers::lxc_count();
1112-
let vm_count = ai_state.vms.lock().unwrap().list_vms().len() as u32;
1113-
1114-
let mem_used = m.memory_used_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
1115-
let mem_total = m.memory_total_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
1116-
let root_disk = m.disks.iter().find(|d| d.mount_point == "/").or_else(|| m.disks.first());
1117-
let disk_used = root_disk.map(|d| d.used_bytes as f64 / (1024.0 * 1024.0 * 1024.0)).unwrap_or(0.0);
1118-
let disk_total = root_disk.map(|d| d.total_bytes as f64 / (1024.0 * 1024.0 * 1024.0)).unwrap_or(0.0);
1119-
1120-
(m.hostname.clone(), m.cpu_usage_percent, mem_used, mem_total,
1121-
disk_used, disk_total, docker_count, lxc_count, vm_count, m.uptime_secs)
1122-
};
1123-
// MutexGuard is now dropped — safe to .await below
1112+
docker_count, lxc_count, vm_count, uptime_secs) =
1113+
tokio::task::spawn_blocking(move || {
1114+
let mut monitor = ai_sc.monitor.lock().unwrap();
1115+
let m = monitor.collect();
1116+
drop(monitor);
1117+
let docker_count = containers::docker_count();
1118+
let lxc_count = containers::lxc_count();
1119+
let vm_count = ai_sc.vms.lock().unwrap().list_vms().len() as u32;
1120+
1121+
let mem_used = m.memory_used_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
1122+
let mem_total = m.memory_total_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
1123+
let root_disk = m.disks.iter().find(|d| d.mount_point == "/").or_else(|| m.disks.first());
1124+
let disk_used = root_disk.map(|d| d.used_bytes as f64 / (1024.0 * 1024.0 * 1024.0)).unwrap_or(0.0);
1125+
let disk_total = root_disk.map(|d| d.total_bytes as f64 / (1024.0 * 1024.0 * 1024.0)).unwrap_or(0.0);
1126+
1127+
(m.hostname.clone(), m.cpu_usage_percent, mem_used, mem_total,
1128+
disk_used, disk_total, docker_count, lxc_count, vm_count, m.uptime_secs)
1129+
}).await.unwrap();
11241130

11251131
// Collect per-guest CPU stats from Proxmox nodes in the cluster
11261132
let pve_nodes: Vec<_> = ai_state.cluster.get_all_nodes().into_iter()

src/plugins/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ fn load_manifest(path: &std::path::Path) -> Result<PluginManifest, String> {
179179
pub fn reload() {
180180
let mut plugins = PLUGINS.write().unwrap();
181181
*plugins = scan_plugins();
182+
drop(plugins);
183+
start_all_backends();
182184
}
183185

184186
/// Get all loaded plugins
@@ -264,8 +266,22 @@ pub fn set_enabled(id: &str, enabled: bool) -> Result<String, String> {
264266
Ok(format!("Plugin '{}' {}", id, if enabled { "enabled" } else { "disabled" }))
265267
}
266268

269+
/// Start all enabled plugin backends (called on startup and after reload)
270+
pub fn start_all_backends() {
271+
let plugins = PLUGINS.read().unwrap().clone();
272+
for plugin in &plugins {
273+
if plugin.status == "active" && plugin.manifest.has_backend {
274+
if let Err(e) = start_backend(&plugin.manifest.id) {
275+
tracing::warn!("Failed to start plugin '{}' backend: {}", plugin.manifest.id, e);
276+
} else if plugin.manifest.api_port.is_some() {
277+
tracing::info!("Started plugin '{}' backend on port {}",
278+
plugin.manifest.id, plugin.manifest.api_port.unwrap());
279+
}
280+
}
281+
}
282+
}
283+
267284
/// Start a plugin's backend binary (if it has one)
268-
#[allow(dead_code)]
269285
pub fn start_backend(id: &str) -> Result<(), String> {
270286
let plugin = get(id).ok_or_else(|| format!("Plugin '{}' not found", id))?;
271287
if !plugin.manifest.has_backend { return Ok(()); }

web/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
2121
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
2222
<script src="/js/vendor/three.min.js?v=15.10.7a"></script>
23-
<link rel="stylesheet" href="/css/style.css?v=16.11.0">
23+
<link rel="stylesheet" id="main-css" href="/css/style.css?v=16.11.0">
2424
<link rel="stylesheet" href="/css/xterm.min.css?v=16.11.0">
25+
<script>document.getElementById('main-css').href='/css/style.css?v='+Date.now();</script>
2526
<script src="/js/vendor/xterm.min.js?v=15.10.7a"></script>
2627
<script src="/js/vendor/xterm-addon-fit.min.js?v=15.10.7a"></script>
2728
<style>

0 commit comments

Comments
 (0)