pub struct PolicyEngine { /* private fields */ }Expand description
Self-healing circuit-breaker policy engine.
On a memory spike it drives the ladder notify -> freeze (short) -> auto-thaw
-> still high? soft-cap -> calm sustained -> lift, never issuing a kill. All
of that lives in tick; the struct just holds the state needed
to make decisions stable across ticks (hysteresis, cooldowns, growth).
The unit of choice is an app: every cgroup of the chosen app is frozen or capped together as one escalation step.
Implementations§
Source§impl PolicyEngine
impl PolicyEngine
pub fn new(cfg: GuardConfig) -> Self
Sourcepub fn tick(
&mut self,
now_ms: u64,
sample: Sample,
targets: &[Target],
live_cgroups: &HashSet<String>,
) -> Vec<Action>
pub fn tick( &mut self, now_ms: u64, sample: Sample, targets: &[Target], live_cgroups: &HashSet<String>, ) -> Vec<Action>
Advance the state machine one tick and return the actions to apply.
targets are the cgroups eligible for action this tick (already
filtered for uid, protect list and min RSS by the Sampler).
live_cgroups is the subset of the engine’s intervened cgroups that
still hold a process (see sampler::live_cgroups), with no min-RSS
or protect filtering applied; it is deliberately independent of
targets. Pruning checks liveness against
this set, not against targets: memory.high also bounds
file-backed pages, so capping
a mapped-file-heavy process can push its rss_kb below the min-RSS
floor on the very next tick, dropping it out of targets even though
the cgroup is very much still alive. Pruning against targets there
would lift the cap while pressure is still Critical and immediately
re-trigger it. Victim selection deliberately keeps using targets.
Sourcepub fn wants_candidates(&self, sample: Sample) -> bool
pub fn wants_candidates(&self, sample: Sample) -> bool
True when the caller should gather targets for the next tick: the level would be above Calm, or memory is already scarce. Scanning while scarce keeps growth rates warm, so a sudden drop below the floor can pick the app that is growing instead of the largest one. A disabled engine never wants them.
Sourcepub fn interventions(&self) -> Vec<(String, Intervention)>
pub fn interventions(&self) -> Vec<(String, Intervention)>
Currently active interventions (cgroup path -> intervention), sorted by cgroup path so callers get a deterministic order.
Sourcepub fn intervened_cgroups(&self) -> Vec<String>
pub fn intervened_cgroups(&self) -> Vec<String>
Cgroup paths the engine currently holds an intervention on — frozen
or capped. Interventions are already keyed by cgroup, so this is
just the key set. For external callers (e.g.
RulesEnforcer::reconcile, D1) that must not fight/revert an active
guard action: rewriting memory.high on a cgroup the engine just
capped would silently no-op the cap and leave PolicyEngine holding
a stale Capped intervention that then blocks victim re-selection
for the rest of the pressure episode.