From 5ca325034f8c3aee79cbb46a3d40e8c09f271174 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Mon, 15 Jun 2026 01:36:23 +0300 Subject: [PATCH] refactor(portal): move snapshot cache helpers into module --- adk-rust/crates/detmir-portal/src/main.rs | 54 ++------------- .../detmir-portal/src/snapshot_cache.rs | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 48 deletions(-) create mode 100644 adk-rust/crates/detmir-portal/src/snapshot_cache.rs diff --git a/adk-rust/crates/detmir-portal/src/main.rs b/adk-rust/crates/detmir-portal/src/main.rs index 376cc18..b33ce2b 100644 --- a/adk-rust/crates/detmir-portal/src/main.rs +++ b/adk-rust/crates/detmir-portal/src/main.rs @@ -3,7 +3,6 @@ use std::fs::{self, File, OpenOptions}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -32,6 +31,7 @@ mod production; mod readiness_api; mod risk_narrative; mod role_access; +mod snapshot_cache; mod workforce_kpi_explain; use api_contracts::api_contract_summary; @@ -54,6 +54,9 @@ use risk_narrative::{ RiskNarrativeInputs, RiskNarrativeQuery, build_risk_narrative, build_risk_narrative_from_report, }; use role_access::{portal_role_from_request, respond_forbidden, role_envelope}; +use snapshot_cache::{ + SnapshotCache, build_fast_health, cached_snapshot, clone_snapshot_cache, new_snapshot_cache, +}; use workforce_kpi_explain::{KpiExplainQuery, build_workforce_kpi_explain}; const INDEX_HTML: &str = include_str!("static/index.html"); @@ -63,7 +66,6 @@ const APP_JS: &str = include_str!("static/app.js"); const API_CONTRACT_OPENAPI: &str = include_str!("contracts/openapi.json"); const API_CONTRACT_TYPESCRIPT: &str = include_str!("contracts/typescript.d.ts"); const UEBA_BASELINE_MIN_SAMPLES: usize = 3; -const SNAPSHOT_CACHE_TTL: Duration = Duration::from_secs(120); const DEFAULT_DEPARTMENT_LABEL: &str = "Не привязано к подразделению"; const LEGACY_UNASSIGNED_DEPARTMENT_LABEL: &str = "Без подразделения"; const PORTAL_SCHEMA_VERSION: &str = "pilot-v1"; @@ -87,14 +89,6 @@ unsafe extern "C" { fn kill(pid: i32, sig: i32) -> i32; } -type SnapshotCache = Arc>>; - -#[derive(Clone, Debug)] -struct CachedSnapshot { - created: Instant, - snapshot: Snapshot, -} - #[derive(Clone, Debug, Parser)] #[command(about = "Read-only AWatch-rus operator/manager/owner web portal")] struct Cli { @@ -1327,11 +1321,11 @@ fn run() -> Result { } let server = Server::http(&args.bind).map_err(|err| anyhow!("bind {}: {err}", args.bind))?; - let snapshot_cache: SnapshotCache = Arc::new(Mutex::new(None)); + let snapshot_cache: SnapshotCache = new_snapshot_cache(); eprintln!("detmir-portal listening on http://{}", args.bind); for request in server.incoming_requests() { let args = args.clone(); - let snapshot_cache = Arc::clone(&snapshot_cache); + let snapshot_cache = clone_snapshot_cache(&snapshot_cache); thread::spawn(move || { let result = if args.evidence_only { handle_evidence_only_request(request, &args) @@ -1665,42 +1659,6 @@ fn handle_evidence_only_request(request: Request, args: &Cli) -> Result<()> { ) } -fn cached_snapshot(args: &Cli, cache: &SnapshotCache) -> Snapshot { - let mut guard = cache.lock().expect("snapshot cache mutex poisoned"); - if let Some(cached) = guard.as_ref() { - if cached.created.elapsed() <= SNAPSHOT_CACHE_TTL { - return cached.snapshot.clone(); - } - } - let snapshot = build_snapshot(args); - *guard = Some(CachedSnapshot { - created: Instant::now(), - snapshot: snapshot.clone(), - }); - snapshot -} - -fn build_fast_health(cache: &SnapshotCache) -> HealthResponse { - match cache.try_lock() { - Ok(guard) => guard - .as_ref() - .map(|cached| build_health(&cached.snapshot)) - .unwrap_or_else(lightweight_health), - Err(_) => lightweight_health(), - } -} - -fn lightweight_health() -> HealthResponse { - let mut sources = BTreeMap::new(); - sources.insert("portal".to_string(), true); - HealthResponse { - ok: true, - generated_at_utc: now(), - version: env!("CARGO_PKG_VERSION").to_string(), - sources, - } -} - fn build_snapshot(args: &Cli) -> Snapshot { let timeout = Duration::from_secs(args.timeout_seconds); let security_events_config = SecurityEventsConfig { diff --git a/adk-rust/crates/detmir-portal/src/snapshot_cache.rs b/adk-rust/crates/detmir-portal/src/snapshot_cache.rs new file mode 100644 index 0000000..107e728 --- /dev/null +++ b/adk-rust/crates/detmir-portal/src/snapshot_cache.rs @@ -0,0 +1,65 @@ +//! Snapshot cache helpers for the portal request path. +//! +//! CONTRACT: this module only owns short-lived in-process cache behavior. +//! It must not change snapshot payloads, source collection, API routes or +//! business calculations. + +use std::collections::BTreeMap; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use crate::{Cli, HealthResponse, Snapshot, build_health, build_snapshot, now}; + +const SNAPSHOT_CACHE_TTL: Duration = Duration::from_secs(120); + +pub(crate) type SnapshotCache = Arc>>; + +#[derive(Clone, Debug)] +pub(crate) struct CachedSnapshot { + created: Instant, + snapshot: Snapshot, +} + +pub(crate) fn new_snapshot_cache() -> SnapshotCache { + Arc::new(Mutex::new(None)) +} + +pub(crate) fn clone_snapshot_cache(cache: &SnapshotCache) -> SnapshotCache { + Arc::clone(cache) +} + +pub(crate) fn cached_snapshot(args: &Cli, cache: &SnapshotCache) -> Snapshot { + let mut guard = cache.lock().expect("snapshot cache mutex poisoned"); + if let Some(cached) = guard.as_ref() { + if cached.created.elapsed() <= SNAPSHOT_CACHE_TTL { + return cached.snapshot.clone(); + } + } + let snapshot = build_snapshot(args); + *guard = Some(CachedSnapshot { + created: Instant::now(), + snapshot: snapshot.clone(), + }); + snapshot +} + +pub(crate) fn build_fast_health(cache: &SnapshotCache) -> HealthResponse { + match cache.try_lock() { + Ok(guard) => guard + .as_ref() + .map(|cached| build_health(&cached.snapshot)) + .unwrap_or_else(lightweight_health), + Err(_) => lightweight_health(), + } +} + +fn lightweight_health() -> HealthResponse { + let mut sources = BTreeMap::new(); + sources.insert("portal".to_string(), true); + HealthResponse { + ok: true, + generated_at_utc: now(), + version: env!("CARGO_PKG_VERSION").to_string(), + sources, + } +}