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}