ktstr/scenario/
dynamic.rs

1//! Dynamic cgroup add/remove scenario implementations.
2
3use super::backdrop::Backdrop;
4use super::ops::{CgroupDef, CpusetSpec, HoldSpec, Step, execute_scenario};
5use super::{Ctx, collect_all, dfl_wl, setup_cgroups};
6use crate::assert::AssertResult;
7use crate::workload::*;
8use anyhow::Result;
9use std::thread;
10use std::time::{Duration, Instant};
11
12/// Add up to two cgroups mid-run alongside two steady cgroups.
13///
14/// `cg_0` and `cg_1` run for the whole scenario; `cg_2` / `cg_3`
15/// appear mid-run as a step-local CgroupDef set and tear down at
16/// the step boundary. Steady cgroups go on the Backdrop;
17/// mid-run additions stay step-local.
18pub fn custom_cgroup_add_midrun(ctx: &Ctx) -> Result<AssertResult> {
19    let max_new = ctx.topo.total_cpus().saturating_sub(3).min(2);
20    if max_new == 0 {
21        return Ok(AssertResult::skip("need >=4 CPUs"));
22    }
23
24    let extra_names: &[&str] = &["cg_2", "cg_3"];
25    let phase2_setup: Vec<CgroupDef> = extra_names[..max_new]
26        .iter()
27        .map(|&name| CgroupDef::named(name))
28        .collect();
29
30    let backdrop = Backdrop::new()
31        .push_cgroup(CgroupDef::named("cg_0"))
32        .push_cgroup(CgroupDef::named("cg_1"));
33    let steps = vec![
34        // Phase 1: settle with just the two steady cgroups.
35        Step::new(vec![], ctx.settled_hold(0.5)),
36        // Phase 2: add the step-local extras.
37        Step::with_defs(phase2_setup, HoldSpec::frac(0.5)),
38    ];
39
40    execute_scenario(ctx, backdrop, steps)
41}
42
43/// Remove half of up to four cgroups mid-run.
44///
45/// The kept half (`cg_0`, `cg_1`) lives on the Backdrop and
46/// persists across both Steps. The ephemeral half (`cg_2` / `cg_3`,
47/// up to the topology limit) is a step-local Step-0 CgroupDef set
48/// whose automatic per-Step teardown IS the "remove mid-run"
49/// event — no explicit `Op::stop_cgroup` / `Op::remove_cgroup`
50/// ops required. Step 1 is then a pure hold with only the
51/// Backdrop cgroups still present.
52pub fn custom_cgroup_remove_midrun(ctx: &Ctx) -> Result<AssertResult> {
53    let n = 4.min(ctx.topo.total_cpus().saturating_sub(1));
54    if n < 2 {
55        return Ok(AssertResult::skip("need >=3 CPUs"));
56    }
57    let half = n / 2;
58
59    let cgroup_names: &[&str] = &["cg_0", "cg_1", "cg_2", "cg_3"];
60
61    let mut backdrop = Backdrop::new();
62    for &name in &cgroup_names[..half] {
63        backdrop = backdrop.push_cgroup(CgroupDef::named(name));
64    }
65
66    let step0_defs: Vec<CgroupDef> = cgroup_names[half..n]
67        .iter()
68        .map(|&name| CgroupDef::named(name))
69        .collect();
70
71    let steps = vec![
72        Step::with_defs(step0_defs, ctx.settled_hold(0.5)),
73        Step::new(vec![], HoldSpec::frac(0.5)),
74    ];
75
76    execute_scenario(ctx, backdrop, steps)
77}
78
79/// Rapid create/destroy cycling. Custom logic for dynamic naming.
80pub fn custom_cgroup_rapid_churn(ctx: &Ctx) -> Result<AssertResult> {
81    let (handles, mut guard) = setup_cgroups(ctx, 2, &dfl_wl(ctx))?;
82    let deadline = Instant::now() + ctx.duration;
83    let mut i = 0usize;
84    // Cap on the number of distinct ephemeral cgroup names. The
85    // remove path is best-effort (see comment below); without a cap
86    // a long scenario with persistent EBUSY/ENOENT churn would
87    // accumulate one cgroup per iteration in the cgroupfs tree until
88    // the guard's Drop reaps them at scenario teardown. Reusing the
89    // same 100 names via `i % 100` bounds peak resident cgroup count
90    // to at most 100 leaked entries, while still exercising the
91    // rapid create→remove churn the test is designed to drive.
92    // `create_cgroup` is idempotent on a name whose dir already
93    // exists (`if !p.exists()` in `CgroupManager::create_cgroup`), so
94    // a cycle that lapped a still-resident sibling is a no-op
95    // re-create rather than an error.
96    //
97    // Each ephemeral name is registered in the `setup_cgroups` guard
98    // via `add_cgroup_no_cpuset` (NOT `ctx.cgroups.create_cgroup`)
99    // so the guard's Drop reaps any cgroup whose best-effort
100    // remove_cgroup below failed. Without registration, a
101    // best-effort failure would silently leak the cgroup until the
102    // next iteration with the same modulo-100 name happened to win
103    // its create→remove race; if no such iteration arrived before
104    // the loop exited, the cgroup persisted past scenario teardown
105    // (the comment claiming the guard reaped it was wrong — the
106    // guard only reaps names it has been told about). Duplicate
107    // pushes of the same name across cycles are harmless: Drop
108    // iterates `names` and calls remove_cgroup for each, and once
109    // the directory is already gone remove_cgroup short-circuits
110    // to Ok(()) via its `!p.exists()` early return, so the second
111    // (and later) removals produce no error and fire no warn.
112    const MAX_EPHEMERAL_NAMES: usize = 100;
113    while Instant::now() < deadline {
114        let n = format!("ephemeral_{}", i % MAX_EPHEMERAL_NAMES);
115        guard.add_cgroup_no_cpuset(&n)?;
116        thread::sleep(Duration::from_millis(100));
117        // Best-effort teardown: rapid-churn drives cgroup
118        // create/destroy at 10 Hz, racing the freeze/drain path.
119        // EBUSY (kernel still draining from a sibling step) or
120        // ENOENT (already removed by the guard's Drop on early
121        // exit) here leaves the cgroup tree slightly larger than
122        // expected for one iteration; the guard's Drop reaps any
123        // leaked cgroups at scenario teardown. Bailing would
124        // truncate the churn workload and mask the race.
125        if let Err(e) = ctx.cgroups.remove_cgroup(&n) {
126            tracing::warn!(cgroup = %n, err = %format!("{e:#}"), "rapid churn: remove_cgroup failed; guard Drop will reap on scenario teardown");
127        }
128        i = i.wrapping_add(1);
129    }
130    Ok(collect_all(handles, &ctx.assert))
131}
132
133/// Add a third cpuset-partitioned cgroup mid-run; tear it down
134/// via automatic step boundary.
135///
136/// `cg_0` / `cg_1` hold their cpusets for the full scenario on the
137/// Backdrop. `cg_2` lives only in the middle Step — the automatic
138/// step-boundary teardown removes it before the final hold runs,
139/// replacing the pre-refactor explicit stop + remove ops.
140pub fn custom_cgroup_cpuset_add_remove(ctx: &Ctx) -> Result<AssertResult> {
141    if ctx.topo.all_cpus().len() < 4 {
142        return Ok(AssertResult::skip("need >=4 CPUs"));
143    }
144
145    let backdrop = Backdrop::new().extend_cgroups([
146        CgroupDef::named("cg_0").cpuset(CpusetSpec::disjoint(0, 3)),
147        CgroupDef::named("cg_1").cpuset(CpusetSpec::disjoint(1, 3)),
148    ]);
149    let steps = vec![
150        // Phase 1: settle the two steady cgroups.
151        Step::new(vec![], ctx.settled_hold(1.0 / 3.0)),
152        // Phase 2: add cg_2; auto teardown at step end removes it.
153        Step::with_defs(
154            vec![CgroupDef::named("cg_2").cpuset(CpusetSpec::disjoint(2, 3))],
155            HoldSpec::frac(1.0 / 3.0),
156        ),
157        // Phase 3: only cg_0 / cg_1 continue — cg_2 is gone.
158        Step::new(vec![], HoldSpec::frac(1.0 / 3.0)),
159    ];
160
161    execute_scenario(ctx, backdrop, steps)
162}
163
164/// Add a third cgroup under load alongside heavy and bursty cgroups.
165///
166/// The heavy `cg_0` and bursty `cg_1` run for the full scenario on
167/// the Backdrop. The mid-run `cg_2` appears in the second Step and
168/// tears down at the step boundary.
169pub fn custom_cgroup_add_during_imbalance(ctx: &Ctx) -> Result<AssertResult> {
170    let backdrop = Backdrop::new().extend_cgroups([
171        CgroupDef::named("cg_0").workers(8),
172        CgroupDef::named("cg_1")
173            .workers(2)
174            .work_type(WorkType::bursty(
175                Duration::from_millis(50),
176                Duration::from_millis(100),
177            )),
178    ]);
179    let steps = vec![
180        // Phase 1: settle with cg_0 and cg_1 alone.
181        Step::new(vec![], ctx.settled_hold(0.5)),
182        // Phase 2: add cg_2 as step-local.
183        Step::with_defs(
184            vec![CgroupDef::named("cg_2").workers(4)],
185            HoldSpec::frac(0.5),
186        ),
187    ];
188
189    execute_scenario(ctx, backdrop, steps)
190}