Files
Varaverk/Plugin/unraid/Tools/api_cache_writer.php
T
Gmer4Lfe 1f4b751fbb Cache the watchdog payload, and stop reading trailing comments as conf values
The tab collected over SSH on every 30s poll — 8.3s a load with HOST2 down.
vv_parse_conf_scalar() captured to end of line, so a commented toggle parsed as
"true   # HOST2 back online": every threshold read right because (int) stops at
the first non-digit, and 38 booleans read wrong. The Fallback tab has been
showing failover disabled while it was on.
2026-08-14 10:45:17 -04:00

159 lines
8.3 KiB
PHP

<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Background cache writer. Builds the full monitor payload and the arrs payload once a
// minute and writes them to the cache, so page loads serve from a file instead of paying
// for collection.
//
// OPERATIONAL MODEL
// This is what makes the monitor and arrs tabs fast. api/monitor.php and api/arrs.php read
// the files this process writes and only fall back to collecting for themselves on a miss.
// The expensive work — GraphQL, docker stats, SSH to partners, HTTP to every arr instance —
// happens here, on a schedule, off the request path.
//
// Runs from cron every minute via api_cache_writer.sh, a bash wrapper the scheduler
// requires. Not reachable over HTTP, and refuses to run if it ever is.
//
// The payload assembled here is deliberately identical to api/monitor.php's. The two are
// maintained together: a field added there and not here is a field that is only ever served
// on a cache miss.
//
// DESIGN PRINCIPLES
// One API round trip for the whole payload.
// vv_api_data() is called once up front and static-cached for the life of the process,
// so the API-first collectors below share a single GraphQL query rather than issuing
// one each.
//
// Writes two caches, not one.
// monitor and arrs have different consumers and different costs, so they are written
// under separate keys and either can be served while the other is stale.
//
// Every payload carries its own timestamp, so consumers can render age rather than
// presenting minute-old numbers as current.
//
// Reports its own duration on stdout.
// The elapsed time goes to the job log, which is the only place a slow collection cycle
// becomes visible — this process has no other output and no failure anyone would see.
//
// OPERATIONAL SAFEGUARDS
// Refuses to run under a web server.
// PHP_SAPI is checked first and a non-CLI invocation is answered with a 404 and no
// output. Otherwise a browser hitting this path would trigger the full expensive
// collection — including SSH to every partner — outside any cache or rate limit, once
// per request.
//
// Read-only with respect to the system. Every collector observes; none start, stop, or
// change anything. The only writes are the two cache files.
//
// Cache writes are atomic.
// vv_cache_write() writes a .tmp and renames, so a page load landing mid-write reads
// the previous complete payload rather than a truncated one.
//
// Every collector degrades to empty rather than fatal.
// The library suppresses its filesystem reads and redirects stderr on every shell call,
// so absent hardware yields an empty section. On a payload this wide that property is
// what keeps one missing subsystem from failing the whole cycle and leaving both caches
// to expire.
//
// A failed cycle is survivable by design.
// Nothing here clears the previous cache before building the new one. A run that dies
// partway leaves the last good payload in place, and the readers' own age windows —
// 300s for monitor, 300s for arrs — are several cycles wide, so a single missed minute
// is invisible.
//
// OUTPUT
// VV_CACHE_DIR/monitor.json consumed by api/monitor.php
// VV_CACHE_DIR/arrs.json consumed by api/arrs.php
// stdout one timing line, captured into the job log
//
// DEPENDS ON
// include/monitor.php, include/common.php, include/unraid_api.php,
// include/vms.php, include/docker_folders.php, include/arrs.php
// Tools/api_cache_writer.sh the bash wrapper cron actually invokes
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// Collecting this payload means GraphQL, docker stats and SSH to every partner. It must never
// be triggerable by an HTTP request.
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit(1);
}
$_base = dirname(__DIR__);
require_once $_base . '/include/monitor.php';
require_once $_base . '/include/vms.php';
require_once $_base . '/include/docker_folders.php';
require_once $_base . '/include/arrs.php';
$t = microtime(true);
// ── Monitor payload ───────────────────────────────────────────────────────────
// Call vv_api_data() once — result is static-cached for the rest of this process.
vv_api_data();
// ── AI ────────────────────────────────────────────────────────────────────────
// One collection, two consumers. vv_ai_stats() is the expensive part of the AI subsystem —
// roughly a second, most of it waiting on Ollama and nvidia-smi — and it is written to its own
// cache here so the AI tab's banner, the Scheduler dock and the Monitor row all read the same
// numbers from the same moment instead of each paying for their own.
//
// The monitor block is derived from that same array rather than collected again. Must stay in
// step with api/monitor.php's own block: this file is what the Monitor tab normally reads, since
// the endpoint only assembles a payload on a cache miss, so a key added there and not here
// leaves the card that consumes it loading forever on every ordinary page load and working only
// on the one request that happens to miss.
$_vv_ai = null;
if (vv_ai_ui_on()) {
require_once $_base . '/include/ai.php';
$_vv_ai_stats = vv_ai_stats();
vv_cache_write('ai', $_vv_ai_stats);
$_vv_ai = vv_ai_monitor_block($_vv_ai_stats);
}
$monitor = [
'system' => vv_system_info(),
'fallback' => vv_fallback_state(),
'fallback_active' => vv_fallback_active(),
'partner' => vv_partner_state(),
'resources' => vv_system_resources(),
'cpu' => vv_cpu_per_core(),
'mem' => vv_memory_breakdown(),
'net' => vv_network_stats(),
'gpu' => vv_gpu_stats(),
'gpus' => vv_gpu_stats_all(),
'gpu_procs' => vv_gpu_processes(),
'containers' => vv_docker_containers(),
'stopped' => vv_docker_stopped(),
'transcode' => vv_transcode_sessions(),
'ups' => vv_ups_stats(),
'parity' => vv_parity_status(),
'storage' => vv_storage_pools(),
'array_disks' => vv_array_disks(),
'disk_io' => vv_disk_io_rates(),
'watchdog' => vv_watchdog_summary(),
'scripts' => vv_scripts_status(),
'rsync' => vv_rsync_status(),
'thresholds' => vv_disk_thresholds(),
'vms' => vv_get_vms(),
'docker_folders' => vv_get_docker_folders(),
'remote_hosts' => vv_remote_hosts_stats(),
'ai' => $_vv_ai,
'_api_status' => vv_api_get_status(),
'ts' => time(),
];
vv_cache_write('monitor', $monitor);
// ── Arrs payload ──────────────────────────────────────────────────────────────
$arrs = vv_arrs_all();
vv_cache_write('arrs', $arrs);
// ── Watchdog payload ──────────────────────────────────────────────────────────
// Here for the same reason as the other two: vv_wd_all() SSHes to every partner, which cost 8.3s
// on this host with HOST2 down, and the Watchdog tab polls every 30 seconds. Collected off the
// request path it is paid once a minute by cron instead of by whoever has the tab open.
require_once $_base . '/include/watchdog.php';
vv_cache_write('watchdog', vv_wd_all());
$elapsed = round((microtime(true) - $t) * 1000);
echo "Cache written in {$elapsed}ms — monitor + arrs + watchdog\n";