pub struct WorkSpec {Show 13 fields
pub work_type: WorkType,
pub sched_policy: SchedPolicy,
pub num_workers: Option<usize>,
pub affinity: AffinityIntent,
pub mem_policy: MemPolicy,
pub mpol_flags: MpolFlags,
pub nice: Option<i32>,
pub comm: Option<Cow<'static, str>>,
pub pcomm: Option<Cow<'static, str>>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub numa_node: Option<u32>,
pub workers_pct: Option<f64>,
}Fields§
§work_type: WorkTypeWhat each worker does.
sched_policy: SchedPolicyLinux scheduling policy.
num_workers: Option<usize>Number of workers. None means use Ctx::workers_per_cgroup.
Composition-sensitive: different work groups within the same
cgroup commonly want different worker counts (e.g. an
antagonist with 4 spinners alongside a victim with 1
SCHED_FIFO worker). For that reason CgroupDef does NOT
expose a cgroup-level default for num_workers — multi-group
cgroups set the count per-WorkSpec here.
Type asymmetry with crate::workload::WorkloadConfig::num_workers
(usize, no Option) is deliberate. WorkSpec is the
declarative spec layer where None is a meaningful
“inherit the cgroup-level default” sentinel;
resolve_num_workers coalesces it to a
concrete usize against the Ctx before
WorkloadConfig::for_scenario_engine
constructs the spawn-time config. The coalesce happens at
the resolution boundary, not silently inside any builder.
affinity: AffinityIntentPer-worker affinity intent. Resolved to ResolvedAffinity at
runtime via resolve_affinity_for_cgroup().
mem_policy: MemPolicyNUMA memory placement policy. Applied via set_mempolicy(2)
after fork, before the work loop.
Validated against the resolved cpuset per-WorkSpec at
apply-setup time. Because validation is per-group, a
cgroup-level default would mask per-group failures with
confusing diagnostics — CgroupDef deliberately does not
expose a cgroup-level default for mem_policy; multi-group
cgroups set it per-WorkSpec here.
mpol_flags: MpolFlagsOptional mode flags for set_mempolicy(2).
nice: Option<i32>Per-worker nice value applied via setpriority(2) after
fork, before the work loop. See crate::workload::WorkloadConfig::nice
for range, None-vs-Some(n) semantics, and CAP_SYS_NICE
rules.
To inherit a cgroup-level default stored at
CgroupDef::default_nice,
leave this None. Some(0) opts out of the cgroup-level
merge — see crate::workload::WorkloadConfig::nice for the underlying
setpriority(PRIO_PROCESS, 0, 0) semantics.
comm: Option<Cow<'static, str>>Per-worker comm set via prctl(PR_SET_NAME) at thread
creation time. The setter rejects > 15 bytes
(TASK_COMM_LEN-1) at construction so the operator sees the
cap at the call site instead of debugging a kernel-truncated
comm — see validate_task_comm_string. None inherits the
binary name. Useful for scheduler matchers that filter on
task->comm (e.g. layered’s CommPrefix). The comm is
applied once per worker; it is NOT live-propagated after
the worker enters its work loop.
pcomm: Option<Cow<'static, str>>The thread-group leader’s comm — what schedulers read as
task->group_leader->comm. When set, apply_setup coalesces
every WorkSpec sharing this pcomm value (within one
CgroupDef) into ONE forked thread-group leader. The leader’s
task->comm is set via prctl(PR_SET_NAME); the setter
rejects > 15 bytes (TASK_COMM_LEN-1) at construction so the
task->group_leader->comm == pcomm invariant every worker
thread observes for the leader’s lifetime matches the
requested string exactly (no silent kernel truncation).
WorkSpecs with pcomm = None (or empty pcomm string,
treated as None) spawn via the conventional fork path —
one process per worker.
Dispatch is apply_setup-only. The WorkSpec::pcomm
setter itself always accepts a valid value (subject to the
existing 15-byte / NUL / empty-string checks). The bail
fires later at WorkloadConfig dispatch-construction
time — direct calls to
crate::workload::WorkloadHandle::spawn (composed entries)
and the scenario-engine spawn-dispatch sites
(crate::scenario::ops::Op::Spawn / apply_setup non-pcomm
path) all reject a pcomm-bearing
WorkSpec when they synthesize the per-spawn WorkloadConfig.
Those paths always fork one process per worker (fork mode),
so task->group_leader->comm would be left at the parent’s
task->comm at fork time (the scenario runner’s binary name)
and scheduler matchers filtering on the leader’s comm would
see zero matches. The bail surfaces the misuse at the call
site instead of producing a workload that silently fails to
match its fixture. To drive the pcomm container path without
going through CgroupDef, callers may invoke
crate::workload::WorkloadHandle::spawn_pcomm_cgroup
directly with a &[WorkSpec] slice.
This is the AUTHORITATIVE source for the pcomm dispatch:
apply_setup reads it directly from each WorkSpec.
crate::scenario::ops::types::CgroupDef::pcomm is a
convenience method that writes the same value into every
WorkSpec at builder time; there is no separate cgroup-level
pcomm field.
Per-thread comm goes through Self::comm and the worker’s
own prctl(PR_SET_NAME) at thread creation time. Models
real workloads like chrome (pcomm) hosting
ThreadPoolForeg and GPU Process worker threads
(per-thread comm), or java (pcomm) hosting GC Thread
and C2 CompilerThre worker threads.
Declarative-only field — absent from
crate::workload::WorkloadConfig by design (same shape as
Self::num_workers / Self::workers_pct). pcomm is the
operator-facing intent that drives the apply_setup pcomm-
aware coalesce path; by the time a WorkloadConfig is
constructed for spawn, the per-WorkSpec pcomm has either
routed through spawn_pcomm_cgroup (then no longer needed
at the WorkloadConfig layer) or hit the dispatch-construction
bail at
WorkloadConfig::for_scenario_engine.
uid: Option<u32>Effective UID set via setresuid(uid, uid, uid) after fork.
None inherits the parent’s euid. Useful for scheduler
matchers that filter on task->real_cred->euid (e.g.
layered’s UIDEquals).
gid: Option<u32>Effective GID set via setresgid(gid, gid, gid) after fork.
None inherits the parent’s egid.
numa_node: Option<u32>Restrict worker affinity to the CPUs of this NUMA node.
Applied via sched_setaffinity after fork. Useful for
scheduler matchers that check bpf_cpumask_subset(cpus_ptr, node_cpumask) (e.g. layered’s NumaNode).
workers_pct: Option<f64>Optional fraction-of-cpuset worker count. When Some(p), the
dispatch site computes ceil(cpuset_cpus * p) and writes the
result into num_workers. The denominator is the cgroup’s
currently-recorded cpuset at dispatch time:
apply_setupdispatch: the cgroup was just created and its cpuset just resolved viaCpusetSpec::resolve(ctx)(or inherited fromctx.topo.usable_cpuset()when theCgroupDefhas no.cpuset(...)), so the denominator matches the declaredCpusetSpec.Op::Spawn(SpawnPlacement::Cgroup)dispatch: the denominator is whatever cpuset is currently recorded for the cgroup. A priorOp::SetCpusetthat narrowed the cgroup will narrow the denominator too. Workers already spawned by a priorapply_setupare not re-counted.
Cannot coexist with num_workers = Some(_) — validation
rejects that combination because it’s ambiguous which source
wins. Values > 1.0 are accepted as deliberate oversubscription
(e.g. workers_pct(2.0) on a 10-CPU cpuset produces 20
workers). NaN/Inf/negative are rejected at construction time.
Declarative-only field — absent from
crate::workload::WorkloadConfig by design. The same
asymmetry as Self::num_workers: WorkSpec is the
operator-facing declarative spec where workers_pct(p) is
a meaningful “scale with the cpuset” intent;
Self::resolve_workers_pct (called at apply_setup and at
each dispatch site) computes the concrete worker count
against the dispatch-time cpuset and writes the result into
the WorkSpec’s num_workers field, which then flows through
the standard resolve_num_workers →
crate::workload::WorkloadConfig::num_workers migration
boundary. There is no workers_pct field on WorkloadConfig
because by spawn time the scale-with-cpuset intent has
already collapsed to a concrete count.
Implementations§
Source§impl WorkSpec
impl WorkSpec
Sourcepub fn workers_pct(self, pct: f64) -> Self
pub fn workers_pct(self, pct: f64) -> Self
Set the worker count as a fraction of the resolved cpuset
CPU count. Apply-setup computes ceil(cpuset_cpus * pct) and
writes the result into num_workers. Use this when the worker
count should scale with the cpuset rather than hardcoding a
per-topology constant.
Setting BOTH workers(n) and workers_pct(p) on the same
WorkSpec is rejected at apply-setup time because the two sources
would silently fight; pick one. Values > 1.0 are accepted as
deliberate oversubscription; NaN, infinite, and non-positive
values are rejected here at construction time via an assertion.
§Panics
Panics when pct is NaN, infinite, or <= 0.0. The builder
returns Self, so the construction-time gate uses assert!
rather than a fallible Result. Negative or zero fractions
would resolve to zero workers — caught at apply-setup time by
resolve_num_workers’s zero-workers rejection anyway, but the
construction-time message is more actionable.
Extreme finite values (e.g. 1e100) pass the construction gate
and saturate to usize::MAX via the as cast in
resolve_workers_pct (RFC 2484 / Rust 1.45+). Attempting to
spawn that many workers would OOM the host. The framework
imposes no upper cap; as a rule of thumb keep pct near the
intended oversubscription factor (e.g. 1.0, 2.0, 4.0).
Sourcepub fn sched_policy(self, p: SchedPolicy) -> Self
pub fn sched_policy(self, p: SchedPolicy) -> Self
Set the Linux scheduling policy.
Sourcepub fn affinity(self, a: AffinityIntent) -> Self
pub fn affinity(self, a: AffinityIntent) -> Self
Set the per-worker affinity intent.
Sourcepub fn mem_policy(self, p: MemPolicy) -> Self
pub fn mem_policy(self, p: MemPolicy) -> Self
Set the NUMA memory placement policy.
Sourcepub fn mpol_flags(self, f: MpolFlags) -> Self
pub fn mpol_flags(self, f: MpolFlags) -> Self
Set the NUMA memory policy mode flags.
Sourcepub fn nice(self, n: i32) -> Self
pub fn nice(self, n: i32) -> Self
Set the per-worker nice value applied via setpriority(2).
Stores Some(n) on the spec; the spawn pipeline calls
setpriority(PRIO_PROCESS, 0, n) unconditionally (including
n == 0). The “skip the syscall, inherit the parent’s nice”
state is the type-level default None — leave the builder
unchained for inherit semantics. Values below the calling
task’s current nice require CAP_SYS_NICE; see
crate::workload::WorkloadConfig::nice for the full can_nice rule.
Sourcepub fn comm(self, name: impl Into<Cow<'static, str>>) -> Self
pub fn comm(self, name: impl Into<Cow<'static, str>>) -> Self
Set the worker process name via prctl(PR_SET_NAME).
§Panics
Panics on programmer-error inputs — same three cases as
Self::pcomm:
- Empty string (silent kernel-comm clobber).
- Interior NUL byte (prctl C-string truncation).
- More than 15 bytes (
TASK_COMM_LEN - 1—__set_task_commtruncates at 15 so the framework rejects at construction to keep the kernel-observed comm equal to the requested value).
See validate_task_comm_string for the centralized
rationale; name.len() is the BYTE length (UTF-8 multi-byte
chars count as their byte width, not their codepoint count).
Sourcepub fn pcomm(self, name: impl Into<Cow<'static, str>>) -> Self
pub fn pcomm(self, name: impl Into<Cow<'static, str>>) -> Self
Set the thread-group leader’s comm. Triggers fork-then-thread
spawn through apply_setup (or via
crate::workload::WorkloadHandle::spawn_pcomm_cgroup for
the direct entry point): one forked leader process whose
task->comm is name, threads spawned inside it. Each
thread additionally sets its own task->comm via
Self::comm at thread creation time.
§Panics
Panics on programmer-error inputs:
- Empty string — the empty pcomm has no observable effect
(kernel sets task->comm to “”), so it’s a no-op disguised
as configuration.
apply_setuptreats empty asNoneto keep the dispatch contract unambiguous, but accepting the builder call would silently drop user intent. Reject up front. - Interior NUL byte —
prctl(PR_SET_NAME)takes a C string; any embedded NUL truncates the kernel-side comm at the first NUL silently, producing a comm value the caller didn’t ask for. Reject so the operator sees the error immediately instead of debugging a truncated comm. - More than 15 bytes —
__set_task_commwritesmin(strlen(buf), sizeof(tsk->comm) - 1)bytes andTASK_COMM_LEN = 16, so the 16th byte (and beyond) is silently dropped. Rejecting at construction time means thetask->group_leader->comm == pcomminvariant the rest of the framework relies on holds exactly, and the operator sees the cap at the call site instead of debugging a truncated comm.
Trait Implementations§
Source§impl Default for WorkSpec
impl Default for WorkSpec
Source§fn default() -> Self
fn default() -> Self
Single SpinWait worker under the kernel’s default scheduling
class — the framework’s no-customization baseline. Every
other field is None / inherit so a test that needs a
specific knob (affinity, mem_policy, nice, etc.) sets
only that one via the corresponding WorkSpec::with_*
builder. num_workers = None defers count selection to
CgroupDef’s merged-works contract (the cgroup-level
default applies; see CgroupDef::workers /
CgroupDef::merged_works). The workers_pct mutex with
num_workers only fires when BOTH are Some(_) — at
default neither is set, so the
WorkSpec::resolve_workers_pct arm that emits the
WorkSpec sets BOTH workers(...) and workers_pct(...) bail
does not trigger.
Source§impl<'de> Deserialize<'de> for WorkSpec
impl<'de> Deserialize<'de> for WorkSpec
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl StructuralPartialEq for WorkSpec
Auto Trait Implementations§
impl Freeze for WorkSpec
impl RefUnwindSafe for WorkSpec
impl Send for WorkSpec
impl Sync for WorkSpec
impl Unpin for WorkSpec
impl UnwindSafe for WorkSpec
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more