Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-sim/src/actions.rs
Line
Count
Source
1
// Copyright 2026 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
//! Simulated execution. An action is a duration per phase and an outcome
16
//! per attempt; the worker around it is the production `LocalWorker`.
17
18
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
19
use core::time::Duration;
20
use std::collections::HashMap;
21
use std::sync::Arc;
22
23
use nativelink_error::{Code, Error, make_err};
24
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::{
25
    ActionResourceUsage, StartExecute,
26
};
27
use nativelink_util::action_messages::{ActionResult, OperationId};
28
use nativelink_util::common::DigestInfo;
29
use nativelink_util::digest_hasher::DigestHasherFunc;
30
use nativelink_worker::running_actions_manager::{Metrics, RunningAction, RunningActionsManager};
31
use parking_lot::Mutex;
32
use tokio::sync::Notify;
33
34
use crate::trace::Event;
35
36
/// What one attempt at an action does.
37
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38
#[allow(variant_size_differences, reason = "a few bytes")]
39
pub enum Run {
40
    /// Exits 0.
41
    Succeed,
42
    /// Exits with this code.
43
    Exit(i32),
44
    /// The worker fails it with this code, as a lost input or a crash
45
    /// in the runner would.
46
    Error(Code),
47
    /// Never finishes until killed.
48
    Hang,
49
    /// The execution task panics.
50
    Panic,
51
}
52
53
/// One action of a scenario. Its digest comes from `seed`, so two clients
54
/// that name the same seed ask for the same action.
55
#[derive(Clone, Debug)]
56
pub struct Action {
57
    pub(crate) seed: u32,
58
    pub(crate) priority: i32,
59
    pub(crate) cacheable: bool,
60
    pub(crate) prepare: Duration,
61
    pub(crate) run: Duration,
62
    pub(crate) upload: Duration,
63
    /// One per attempt; the last repeats.
64
    pub(crate) attempts: Vec<Run>,
65
    pub(crate) properties: Vec<(String, String)>,
66
    pub(crate) timeout: Option<Duration>,
67
}
68
69
impl Action {
70
    /// A cacheable action that runs for one second and succeeds.
71
11.3k
    pub fn new(seed: u32) -> Self {
72
11.3k
        Self {
73
11.3k
            seed,
74
11.3k
            priority: 0,
75
11.3k
            cacheable: true,
76
11.3k
            prepare: Duration::ZERO,
77
11.3k
            run: Duration::from_secs(1),
78
11.3k
            upload: Duration::ZERO,
79
11.3k
            attempts: vec![Run::Succeed],
80
11.3k
            properties: Vec::new(),
81
11.3k
            timeout: None,
82
11.3k
        }
83
11.3k
    }
84
85
    #[must_use]
86
1.74k
    pub const fn priority(mut self, priority: i32) -> Self {
87
1.74k
        self.priority = priority;
88
1.74k
        self
89
1.74k
    }
90
91
    /// Sent with `skip_cache_lookup`, so it never coalesces with another.
92
    #[must_use]
93
338
    pub const fn uncacheable(mut self) -> Self {
94
338
        self.cacheable = false;
95
338
        self
96
338
    }
97
98
    #[must_use]
99
9.30k
    pub const fn run_ms(mut self, ms: u64) -> Self {
100
9.30k
        self.run = Duration::from_millis(ms);
101
9.30k
        self
102
9.30k
    }
103
104
    #[must_use]
105
0
    pub const fn prepare_ms(mut self, ms: u64) -> Self {
106
0
        self.prepare = Duration::from_millis(ms);
107
0
        self
108
0
    }
109
110
    #[must_use]
111
0
    pub const fn upload_ms(mut self, ms: u64) -> Self {
112
0
        self.upload = Duration::from_millis(ms);
113
0
        self
114
0
    }
115
116
    /// What each attempt does, in order; the last repeats.
117
    #[must_use]
118
262
    pub fn attempts(mut self, attempts: &[Run]) -> Self {
119
262
        assert!(!attempts.is_empty(), "an action needs at least one attempt");
120
262
        self.attempts = attempts.to_vec();
121
262
        self
122
262
    }
123
124
    #[must_use]
125
1
    pub fn property(mut self, name: &str, value: &str) -> Self {
126
1
        self.properties.push((name.to_string(), value.to_string()));
127
1
        self
128
1
    }
129
130
    /// `Action.timeout`.
131
    #[must_use]
132
0
    pub const fn timeout_s(mut self, seconds: u64) -> Self {
133
0
        self.timeout = Some(Duration::from_secs(seconds));
134
0
        self
135
0
    }
136
137
53.3k
    pub fn label(&self) -> String {
138
53.3k
        format!("a{}", self.seed)
139
53.3k
    }
140
141
0
    pub const fn seed(&self) -> u32 {
142
0
        self.seed
143
0
    }
144
}
145
146
#[derive(Debug)]
147
pub(crate) struct Entry {
148
    pub(crate) action: Action,
149
    pub(crate) attempts: AtomicU32,
150
}
151
152
/// Every action a scenario submitted, by its digest, so the worker side can
153
/// find what a `StartExecute` should do.
154
#[derive(Debug, Default)]
155
pub struct Registry {
156
    entries: Mutex<HashMap<DigestInfo, Arc<Entry>>>,
157
}
158
159
impl Registry {
160
15.9k
    pub(crate) fn insert(&self, digest: DigestInfo, action: Action) {
161
15.9k
        self.entries.lock().entry(digest).or_insert_with(|| 
{15.9k
162
15.9k
            Arc::new(Entry {
163
15.9k
                action,
164
15.9k
                attempts: AtomicU32::new(0),
165
15.9k
            })
166
15.9k
        });
167
15.9k
    }
168
169
10.6k
    pub(crate) fn label(&self, digest: &DigestInfo) -> Option<String> {
170
10.6k
        self.entries.lock().get(digest).map(|e| e.action.label())
171
10.6k
    }
172
173
10.6k
    pub(crate) fn lookup(&self, digest: &DigestInfo) -> Option<Arc<Entry>> {
174
10.6k
        self.entries.lock().get(digest).cloned()
175
10.6k
    }
176
}
177
178
21.3k
pub(crate) fn start_execute_digest(start_execute: &StartExecute) -> Option<DigestInfo> {
179
21.3k
    start_execute
180
21.3k
        .execute_request
181
21.3k
        .as_ref()
?0
182
        .action_digest
183
21.3k
        .as_ref()
184
21.3k
        .and_then(|digest| DigestInfo::try_from(digest).ok())
185
21.3k
}
186
187
/// What one attempt at an action does on the worker that got it.
188
#[derive(Clone, Debug)]
189
pub struct Plan {
190
    /// The action's name in the trace.
191
    pub action: String,
192
    /// A stable name for the scheduler's operation.
193
    pub key: String,
194
    pub prepare: Duration,
195
    pub run_for: Duration,
196
    pub upload: Duration,
197
    pub run: Run,
198
}
199
200
/// Where a simulated worker learns what an action does, and where it
201
/// reports what it did: the in-process scenario (`Net`), or a seeded
202
/// distribution in a worker of its own process (the fleet simulator).
203
pub trait ActionSource: Send + Sync + core::fmt::Debug + 'static {
204
    fn plan(&self, start_execute: &StartExecute) -> Result<Plan, Error>;
205
206
    fn record(&self, event: Event);
207
208
    /// The scheduler asked for a kill. The in-process network already
209
    /// records it.
210
2
    fn killed(&self, _operation_id: &OperationId) {}
211
}
212
213
/// The `RunningActionsManager` of one simulated worker.
214
#[derive(Debug)]
215
pub struct SimActionsManager {
216
    worker: String,
217
    source: Arc<dyn ActionSource>,
218
    running: Arc<Mutex<HashMap<OperationId, Arc<KillSwitch>>>>,
219
    metrics: Arc<Metrics>,
220
}
221
222
impl SimActionsManager {
223
1.19k
    pub fn new(worker: &str, source: Arc<dyn ActionSource>) -> Self {
224
1.19k
        Self {
225
1.19k
            worker: worker.to_string(),
226
1.19k
            source,
227
1.19k
            running: Arc::new(Mutex::new(HashMap::new())),
228
1.19k
            metrics: Arc::new(Metrics::default()),
229
1.19k
        }
230
1.19k
    }
231
232
    /// Executions this worker has not finished.
233
20.2k
    pub fn running(&self) -> usize {
234
20.2k
        self.running.lock().len()
235
20.2k
    }
236
}
237
238
#[derive(Debug, Default)]
239
struct KillSwitch {
240
    killed: AtomicBool,
241
    notify: Notify,
242
}
243
244
impl KillSwitch {
245
277
    fn kill(&self) {
246
277
        self.killed.store(true, Ordering::Release);
247
277
        self.notify.notify_waiters();
248
277
    }
249
250
10.7k
    async fn wait(&self) {
251
        loop {
252
10.7k
            let notified = self.notify.notified();
253
10.7k
            if self.killed.load(Ordering::Acquire) {
254
0
                return;
255
10.7k
            }
256
10.7k
            notified.await;
257
        }
258
0
    }
259
}
260
261
impl RunningActionsManager for SimActionsManager {
262
    type RunningAction = SimRunningAction;
263
264
10.6k
    async fn create_and_add_action(
265
10.6k
        self: &Arc<Self>,
266
10.6k
        _worker_id: String,
267
10.6k
        start_execute: StartExecute,
268
10.6k
    ) -> Result<Arc<SimRunningAction>, Error> {
269
10.6k
        let plan = self.source.plan(&start_execute)
?0
;
270
10.6k
        let operation_id = OperationId::from(start_execute.operation_id);
271
10.6k
        let kill = Arc::new(KillSwitch::default());
272
10.6k
        self.running
273
10.6k
            .lock()
274
10.6k
            .insert(operation_id.clone(), kill.clone());
275
10.6k
        Ok(Arc::new(SimRunningAction {
276
10.6k
            worker: self.worker.clone(),
277
10.6k
            action: plan.action,
278
10.6k
            key: plan.key,
279
10.6k
            operation_id,
280
10.6k
            prepare: plan.prepare,
281
10.6k
            run_for: plan.run_for,
282
10.6k
            upload: plan.upload,
283
10.6k
            run: plan.run,
284
10.6k
            kill,
285
10.6k
            running: self.running.clone(),
286
10.6k
            source: self.source.clone(),
287
10.6k
            work_directory: String::new(),
288
10.6k
            started: AtomicBool::new(false),
289
10.6k
            ended: AtomicBool::new(false),
290
10.6k
        }))
291
10.6k
    }
292
293
10.0k
    async fn cache_action_result(
294
10.0k
        &self,
295
10.0k
        _action_digest: DigestInfo,
296
10.0k
        _action_result: &mut ActionResult,
297
10.0k
        _hasher: DigestHasherFunc,
298
10.0k
    ) -> Result<(), Error> {
299
10.0k
        Ok(())
300
10.0k
    }
301
302
646
    async fn kill_all(&self) {
303
646
        self.source.record(Event::KillAll {
304
646
            worker: self.worker.clone(),
305
646
        });
306
646
        let switches: Vec<_> = self.running.lock().values().cloned().collect();
307
646
        for 
switch277
in switches {
308
277
            switch.kill();
309
277
        }
310
646
    }
311
312
2
    async fn kill_operation(&self, operation_id: &OperationId) -> Result<(), Error> {
313
2
        self.source.killed(operation_id);
314
2
        let switch = self.running.lock().get(operation_id).cloned();
315
2
        if let Some(
switch0
) = switch {
316
0
            switch.kill();
317
2
        }
318
2
        Ok(())
319
2
    }
320
321
1.19k
    fn metrics(&self) -> &Arc<Metrics> {
322
1.19k
        &self.metrics
323
1.19k
    }
324
}
325
326
#[derive(Debug)]
327
pub struct SimRunningAction {
328
    worker: String,
329
    action: String,
330
    key: String,
331
    operation_id: OperationId,
332
    prepare: Duration,
333
    run_for: Duration,
334
    upload: Duration,
335
    run: Run,
336
    kill: Arc<KillSwitch>,
337
    running: Arc<Mutex<HashMap<OperationId, Arc<KillSwitch>>>>,
338
    source: Arc<dyn ActionSource>,
339
    work_directory: String,
340
    started: AtomicBool,
341
    ended: AtomicBool,
342
}
343
344
/// An execution whose task was aborted (the worker dropped it on a
345
/// disconnect) still ends in the trace and leaves the running set.
346
impl Drop for SimRunningAction {
347
10.6k
    fn drop(&mut self) {
348
10.6k
        if self.started.load(Ordering::Acquire) && 
!10.6k
self.ended10.6k
.load(Ordering::Acquire) {
349
348
            self.end(true);
350
10.3k
        }
351
10.6k
        self.running.lock().remove(&self.operation_id);
352
10.6k
    }
353
}
354
355
impl SimRunningAction {
356
    /// Sleeps `duration` unless killed first; `Err` when killed.
357
31.4k
    async fn phase(&self, duration: Duration) -> Result<(), Error> {
358
31.4k
        if duration.is_zero() {
359
20.6k
            return Ok(());
360
10.7k
        }
361
10.7k
        tokio::select! {
362
10.7k
            () = tokio::time::sleep(duration) => 
Ok(())10.4k
,
363
10.7k
            () = self.kill.wait() => 
Err(make_err!(Code::Aborted, "Killed"))0
,
364
        }
365
31.0k
    }
366
367
10.6k
    fn end(&self, killed: bool) {
368
10.6k
        if self.ended.swap(true, Ordering::AcqRel) {
369
0
            return;
370
10.6k
        }
371
10.6k
        self.source.record(Event::ExecEnd {
372
10.6k
            worker: self.worker.clone(),
373
10.6k
            action: self.action.clone(),
374
10.6k
            key: self.key.clone(),
375
10.6k
            killed,
376
10.6k
        });
377
10.6k
    }
378
}
379
380
impl RunningAction for SimRunningAction {
381
236
    fn get_operation_id(&self) -> &OperationId {
382
236
        &self.operation_id
383
236
    }
384
385
10.6k
    async fn prepare_action(self: Arc<Self>) -> Result<Arc<Self>, Error> {
386
10.6k
        self.phase(self.prepare).await
?0
;
387
10.6k
        Ok(self)
388
10.6k
    }
389
390
10.6k
    async fn execute(self: Arc<Self>) -> Result<Arc<Self>, Error> {
391
10.6k
        self.started.store(true, Ordering::Release);
392
10.6k
        self.source.record(Event::ExecStart {
393
10.6k
            worker: self.worker.clone(),
394
10.6k
            action: self.action.clone(),
395
10.6k
            key: self.key.clone(),
396
10.6k
        });
397
10.6k
        let 
ran10.3k
= match self.run {
398
            Run::Hang => {
399
0
                self.kill.wait().await;
400
0
                Err(make_err!(Code::Aborted, "Killed"))
401
            }
402
10.6k
            _ => self.phase(self.run_for).await,
403
        };
404
10.3k
        if let Err(
err0
) = ran {
405
0
            self.end(true);
406
0
            return Err(err);
407
10.3k
        }
408
10.3k
        self.end(false);
409
10.3k
        match self.run {
410
0
            Run::Panic => panic!("simulated action {} panicked", self.action),
411
258
            Run::Error(code) => Err(make_err!(code, "Simulated failure of {}", self.action)),
412
10.0k
            Run::Succeed | Run::Exit(_) | Run::Hang => Ok(self),
413
        }
414
10.3k
    }
415
416
10.0k
    async fn upload_results(self: Arc<Self>) -> Result<Arc<Self>, Error> {
417
10.0k
        self.phase(self.upload).await
?0
;
418
10.0k
        Ok(self)
419
10.0k
    }
420
421
10.3k
    async fn cleanup(self: Arc<Self>) -> Result<Arc<Self>, Error> {
422
10.3k
        self.running.lock().remove(&self.operation_id);
423
10.3k
        Ok(self)
424
10.3k
    }
425
426
10.0k
    async fn get_finished_result(self: Arc<Self>) -> Result<ActionResult, Error> {
427
10.0k
        let exit_code = match self.run {
428
0
            Run::Exit(code) => code,
429
10.0k
            _ => 0,
430
        };
431
10.0k
        Ok(ActionResult {
432
10.0k
            exit_code,
433
10.0k
            ..ActionResult::default()
434
10.0k
        })
435
10.0k
    }
436
437
10.0k
    fn resource_usage(&self) -> Option<ActionResourceUsage> {
438
10.0k
        None
439
10.0k
    }
440
441
0
    fn get_work_directory(&self) -> &String {
442
0
        &self.work_directory
443
0
    }
444
}