feat(core): 10s TTL cache on probe_power_state
Cache sits between override resolution and platform_probe; override paths (test + env-var) bypass it entirely so they always take effect immediately. OnceLock<Mutex<Option<CachedState>>> initialised lazily on first probe. Two test-only helpers (force_clear_cache, force_set_cache) expose the cache slot for unit tests. Mutex import ungated — now used in production cache code as well as test override. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ pub enum PowerState {
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Parse a `/sys/class/power_supply/`-style directory and decide
|
||||
/// whether the machine is on AC, on battery, or in an unknown state.
|
||||
@@ -64,8 +66,30 @@ fn read_trimmed(path: &Path) -> Option<String> {
|
||||
fs::read_to_string(path).ok().map(|s| s.trim().to_owned())
|
||||
}
|
||||
|
||||
const POWER_STATE_TTL: Duration = Duration::from_secs(10);
|
||||
|
||||
struct CachedState {
|
||||
state: PowerState,
|
||||
fetched_at: Instant,
|
||||
}
|
||||
|
||||
fn cache_slot() -> &'static Mutex<Option<CachedState>> {
|
||||
static SLOT: OnceLock<Mutex<Option<CachedState>>> = OnceLock::new();
|
||||
SLOT.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use std::sync::Mutex;
|
||||
pub(crate) fn force_clear_cache() {
|
||||
*cache_slot().lock().expect("power cache mutex poisoned") = None;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn force_set_cache(state: PowerState) {
|
||||
*cache_slot().lock().expect("power cache mutex poisoned") = Some(CachedState {
|
||||
state,
|
||||
fetched_at: Instant::now(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Top-level power-state probe.
|
||||
///
|
||||
@@ -79,7 +103,9 @@ use std::sync::Mutex;
|
||||
/// `Unknown` is treated as `OnAc` by callers, which preserves today's
|
||||
/// pre-clamp behaviour on platforms where the probe doesn't fire.
|
||||
///
|
||||
/// (TTL cache is added in Task 1.4.)
|
||||
/// Results are cached for `POWER_STATE_TTL` (10 seconds). Override
|
||||
/// paths (test and env-var) bypass the cache entirely so they always
|
||||
/// take effect immediately.
|
||||
pub fn probe_power_state() -> PowerState {
|
||||
#[cfg(test)]
|
||||
if let Some(state) = test_get_override() {
|
||||
@@ -88,7 +114,19 @@ pub fn probe_power_state() -> PowerState {
|
||||
if let Some(state) = env_override() {
|
||||
return state;
|
||||
}
|
||||
platform_probe()
|
||||
|
||||
let mut slot = cache_slot().lock().expect("power cache mutex poisoned");
|
||||
if let Some(cached) = &*slot {
|
||||
if cached.fetched_at.elapsed() < POWER_STATE_TTL {
|
||||
return cached.state;
|
||||
}
|
||||
}
|
||||
let fresh = platform_probe();
|
||||
*slot = Some(CachedState {
|
||||
state: fresh,
|
||||
fetched_at: Instant::now(),
|
||||
});
|
||||
fresh
|
||||
}
|
||||
|
||||
fn env_override() -> Option<PowerState> {
|
||||
@@ -257,4 +295,28 @@ mod tests {
|
||||
std::env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ttl_cache_returns_cached_value_within_window() {
|
||||
with_override(None, || {
|
||||
force_clear_cache();
|
||||
force_set_cache(PowerState::OnBattery);
|
||||
// No override active, no env var; probe should hit cache.
|
||||
assert_eq!(probe_power_state(), PowerState::OnBattery);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ttl_cache_clears_via_force_clear() {
|
||||
with_override(None, || {
|
||||
force_set_cache(PowerState::OnBattery);
|
||||
force_clear_cache();
|
||||
// Override flips to OnAc; with cleared cache, override
|
||||
// (which has higher priority) drives the result.
|
||||
});
|
||||
with_override(Some(PowerState::OnAc), || {
|
||||
force_clear_cache();
|
||||
assert_eq!(probe_power_state(), PowerState::OnAc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user