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/scenario.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
//! Scenarios as Rust builders, and the runner. Each backend runs on a
16
//! thread of its own with a fresh current-thread runtime in paused time, so
17
//! a run's clock and tasks end with it.
18
19
use core::fmt::Write as _;
20
use core::time::Duration;
21
use std::collections::{BTreeMap, BTreeSet};
22
use std::sync::Arc;
23
24
use nativelink_config::schedulers::SimpleSpec;
25
use nativelink_error::{Code, Error, make_err};
26
use nativelink_redis_tester::Fault;
27
28
use crate::actions::Action;
29
use crate::checker::{Facts, Invariant, Violation, check, verdict};
30
use crate::clock::SimClock;
31
use crate::net::Link;
32
use crate::trace::{Event, Outcome, Record, Trace, render};
33
use crate::world::{Backend, WorkerSpec, World};
34
35
/// One thing that happens at a point of the timeline.
36
#[derive(Clone, Debug)]
37
pub enum Step {
38
    /// Client `op` calls `Execute` for `action`.
39
    Execute {
40
        op: String,
41
        action: Action,
42
    },
43
    /// Client `op` calls `Execute` for the action client `of` asked for.
44
    ExecuteSame {
45
        op: String,
46
        of: String,
47
    },
48
    /// Client `op` drops its stream and does not come back.
49
    ClientGone {
50
        op: String,
51
    },
52
    AddWorkers {
53
        count: usize,
54
        spec: WorkerSpec,
55
    },
56
    /// The worker process dies.
57
    KillWorker {
58
        worker: usize,
59
    },
60
    /// The worker's connection closes; the worker reconnects.
61
    CutWorker {
62
        worker: usize,
63
    },
64
    /// The worker's link carries nothing (`Silent`) or works again (`Up`).
65
    SetLink {
66
        worker: usize,
67
        link: Link,
68
    },
69
    /// The scheduler process dies and a new one starts at once.
70
    SchedulerRestart,
71
    SchedulerDown,
72
    SchedulerUp,
73
    /// Every Redis connection closes, then writes answer `READONLY`.
74
    RedisFailover {
75
        readonly_writes: u64,
76
    },
77
    /// Redis connections close and are refused (`true`) or accepted again.
78
    RedisPartition {
79
        partitioned: bool,
80
    },
81
    /// `command` (`*` for any) answers `fault` `times` times.
82
    RedisFail {
83
        command: String,
84
        fault: Fault,
85
        times: u64,
86
    },
87
    /// Every Redis key starting with `prefix` is deleted.
88
    RedisEvict {
89
        prefix: String,
90
    },
91
    /// Every Redis reply waits this long (`None` clears it).
92
    RedisLatency {
93
        ms: Option<u64>,
94
    },
95
    /// What the clock reads jumps forward; timers do not move.
96
    ClockJump {
97
        ms: u64,
98
    },
99
}
100
101
impl Step {
102
11.3k
    pub fn execute(op: &str, action: Action) -> Self {
103
11.3k
        Self::Execute {
104
11.3k
            op: op.to_string(),
105
11.3k
            action,
106
11.3k
        }
107
11.3k
    }
108
109
1
    pub fn execute_same(op: &str, of: &str) -> Self {
110
1
        Self::ExecuteSame {
111
1
            op: op.to_string(),
112
1
            of: of.to_string(),
113
1
        }
114
1
    }
115
116
2.05k
    pub fn client_gone(op: &str) -> Self {
117
2.05k
        Self::ClientGone { op: op.to_string() }
118
2.05k
    }
119
120
174
    pub const fn add_workers(count: usize, spec: WorkerSpec) -> Self {
121
174
        Self::AddWorkers { count, spec }
122
174
    }
123
124
3
    pub fn redis_fail(command: &str, fault: Fault, times: u64) -> Self {
125
3
        Self::RedisFail {
126
3
            command: command.to_string(),
127
3
            fault,
128
3
            times,
129
3
        }
130
3
    }
131
132
1
    pub fn redis_evict(prefix: &str) -> Self {
133
1
        Self::RedisEvict {
134
1
            prefix: prefix.to_string(),
135
1
        }
136
1
    }
137
138
29.8k
    const fn needs_redis(&self) -> bool {
139
29.8k
        matches!(
140
29.8k
            self,
141
            Self::RedisFailover { .. }
142
                | Self::RedisPartition { .. }
143
                | Self::RedisFail { .. }
144
                | Self::RedisEvict { .. }
145
                | Self::RedisLatency { .. }
146
        )
147
29.8k
    }
148
149
    /// The step as the builder call that makes it, for printing a shrunk
150
    /// property case.
151
0
    pub fn to_code(&self) -> String {
152
0
        match self {
153
0
            Self::Execute { op, action } => {
154
0
                let mut code = format!("Step::execute({op:?}, Action::new({})", action.seed);
155
0
                if action.priority != 0 {
156
0
                    write!(code, ".priority({})", action.priority).unwrap();
157
0
                }
158
0
                if !action.cacheable {
159
0
                    code.push_str(".uncacheable()");
160
0
                }
161
0
                write!(code, ".run_ms({})", action.run.as_millis()).unwrap();
162
0
                if action.attempts.len() > 1 || action.attempts[0] != crate::actions::Run::Succeed {
163
0
                    write!(code, ".attempts(&{:?})", action.attempts).unwrap();
164
0
                }
165
0
                code.push(')');
166
0
                code
167
            }
168
0
            Self::ExecuteSame { op, of } => format!("Step::execute_same({op:?}, {of:?})"),
169
0
            Self::ClientGone { op } => format!("Step::client_gone({op:?})"),
170
0
            Self::AddWorkers { count, spec } => format!(
171
                "Step::add_workers({count}, WorkerSpec::default().max_inflight({}))",
172
                spec.max_inflight
173
            ),
174
0
            Self::KillWorker { worker } => format!("Step::KillWorker {{ worker: {worker} }}"),
175
0
            Self::CutWorker { worker } => format!("Step::CutWorker {{ worker: {worker} }}"),
176
0
            Self::SetLink { worker, link } => {
177
0
                format!("Step::SetLink {{ worker: {worker}, link: Link::{link:?} }}")
178
            }
179
0
            Self::SchedulerRestart => "Step::SchedulerRestart".to_string(),
180
0
            Self::SchedulerDown => "Step::SchedulerDown".to_string(),
181
0
            Self::SchedulerUp => "Step::SchedulerUp".to_string(),
182
0
            Self::RedisFailover { readonly_writes } => {
183
0
                format!("Step::RedisFailover {{ readonly_writes: {readonly_writes} }}")
184
            }
185
0
            Self::RedisPartition { partitioned } => {
186
0
                format!("Step::RedisPartition {{ partitioned: {partitioned} }}")
187
            }
188
            Self::RedisFail {
189
0
                command,
190
0
                fault,
191
0
                times,
192
0
            } => format!("Step::redis_fail({command:?}, Fault::{fault:?}, {times})"),
193
0
            Self::RedisEvict { prefix } => format!("Step::redis_evict({prefix:?})"),
194
0
            Self::RedisLatency { ms } => format!("Step::RedisLatency {{ ms: {ms:?} }}"),
195
0
            Self::ClockJump { ms } => format!("Step::ClockJump {{ ms: {ms} }}"),
196
        }
197
0
    }
198
}
199
200
/// What a scenario should do on one backend.
201
#[derive(Clone, Debug, PartialEq, Eq)]
202
pub enum Expect {
203
    Pass,
204
    /// Fails exactly these invariants until `closed_by` lands.
205
    KnownFailure {
206
        invariants: BTreeSet<Invariant>,
207
        closed_by: &'static str,
208
    },
209
    /// Not run on this backend.
210
    NotApplicable,
211
}
212
213
impl Expect {
214
8
    pub fn known_failure(invariants: &[Invariant], closed_by: &'static str) -> Self {
215
8
        Self::KnownFailure {
216
8
            invariants: invariants.iter().copied().collect(),
217
8
            closed_by,
218
8
        }
219
8
    }
220
221
80
    fn invariants(&self) -> BTreeSet<Invariant> {
222
80
        match self {
223
8
            Self::KnownFailure { invariants, .. } => invariants.clone(),
224
72
            Self::Pass | Self::NotApplicable => BTreeSet::new(),
225
        }
226
80
    }
227
}
228
229
type SpecEdit = Arc<dyn Fn(&mut SimpleSpec) + Send + Sync>;
230
231
#[derive(Clone)]
232
pub struct Scenario {
233
    name: String,
234
    seed: u64,
235
    backends: Vec<Backend>,
236
    /// Applied in order to the default `SimpleSpec`; the spec itself is not
237
    /// `Clone`.
238
    spec_edits: Vec<SpecEdit>,
239
    steps: Vec<(u64, Step)>,
240
    settle: Duration,
241
    expect: BTreeMap<Backend, Expect>,
242
    ordered: bool,
243
    command_bound: Option<u64>,
244
    /// The backends are known to end operations differently until this
245
    /// item closes.
246
    disagree: Option<&'static str>,
247
}
248
249
impl core::fmt::Debug for Scenario {
250
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
251
0
        f.debug_struct("Scenario")
252
0
            .field("name", &self.name)
253
0
            .field("seed", &self.seed)
254
0
            .field("steps", &self.steps)
255
0
            .finish_non_exhaustive()
256
0
    }
257
}
258
259
impl Scenario {
260
130
    pub fn new(name: &str, seed: u64) -> Self {
261
130
        Self {
262
130
            name: name.to_string(),
263
130
            seed,
264
130
            backends: vec![Backend::Memory, Backend::FakeRedis],
265
130
            spec_edits: Vec::new(),
266
130
            steps: Vec::new(),
267
130
            settle: Duration::from_mins(1),
268
130
            expect: BTreeMap::new(),
269
130
            ordered: false,
270
130
            command_bound: None,
271
130
            disagree: None,
272
130
        }
273
130
    }
274
275
    /// The backends are known to end operations differently until
276
    /// `closed_by` lands; the run fails if they agree.
277
    #[must_use]
278
0
    pub const fn backends_disagree(mut self, closed_by: &'static str) -> Self {
279
0
        self.disagree = Some(closed_by);
280
0
        self
281
0
    }
282
283
0
    pub fn name(&self) -> &str {
284
0
        &self.name
285
0
    }
286
287
    #[must_use]
288
85
    pub fn backends(mut self, backends: &[Backend]) -> Self {
289
85
        self.backends = backends.to_vec();
290
85
        self
291
85
    }
292
293
    /// Changes the scheduler's config.
294
    #[must_use]
295
3
    pub fn spec(mut self, edit: impl Fn(&mut SimpleSpec) + Send + Sync + 'static) -> Self {
296
3
        self.spec_edits.push(Arc::new(edit));
297
3
        self
298
3
    }
299
300
182
    fn make_spec(&self) -> SimpleSpec {
301
182
        let mut spec = SimpleSpec::default();
302
182
        for 
edit6
in &self.spec_edits {
303
6
            edit(&mut spec);
304
6
        }
305
182
        spec
306
182
    }
307
308
    /// `count` workers at time zero.
309
    #[must_use]
310
127
    pub fn workers(self, count: usize, spec: WorkerSpec) -> Self {
311
127
        self.at(0, Step::add_workers(count, spec))
312
127
    }
313
314
    #[must_use]
315
13.7k
    pub fn at(mut self, ms: u64, step: Step) -> Self {
316
13.7k
        self.steps.push((ms, step));
317
13.7k
        self
318
13.7k
    }
319
320
    /// The worker's link is silent from `ms` for `for_ms`.
321
    #[must_use]
322
44
    pub fn silence(self, worker: usize, ms: u64, for_ms: u64) -> Self {
323
44
        self.at(
324
44
            ms,
325
44
            Step::SetLink {
326
44
                worker,
327
44
                link: Link::Silent,
328
44
            },
329
        )
330
44
        .at(
331
44
            ms + for_ms,
332
44
            Step::SetLink {
333
44
                worker,
334
44
                link: Link::Up,
335
44
            },
336
        )
337
44
    }
338
339
    /// Redis is unreachable from `ms` for `for_ms`.
340
    #[must_use]
341
0
    pub fn redis_partition(self, ms: u64, for_ms: u64) -> Self {
342
0
        self.at(ms, Step::RedisPartition { partitioned: true })
343
0
            .at(ms + for_ms, Step::RedisPartition { partitioned: false })
344
0
    }
345
346
    /// How long the final settle may take.
347
    #[must_use]
348
119
    pub const fn settle(mut self, budget: Duration) -> Self {
349
119
        self.settle = budget;
350
119
        self
351
119
    }
352
353
    /// Check dispatch order (I5).
354
    #[must_use]
355
0
    pub const fn ordered(mut self) -> Self {
356
0
        self.ordered = true;
357
0
        self
358
0
    }
359
360
    /// The I11 bound on store commands between two matching passes,
361
    /// measured from one second after the last `Execute` step, so it reads
362
    /// the steady cost rather than the submissions.
363
    #[must_use]
364
2
    pub const fn command_bound(mut self, bound: u64) -> Self {
365
2
        self.command_bound = Some(bound);
366
2
        self
367
2
    }
368
369
    #[must_use]
370
9
    pub fn expect(mut self, backend: Backend, expect: Expect) -> Self {
371
9
        self.expect.insert(backend, expect);
372
9
        self
373
9
    }
374
375
0
    pub fn steps(&self) -> &[(u64, Step)] {
376
0
        &self.steps
377
0
    }
378
379
    #[must_use]
380
0
    pub fn with_steps(mut self, steps: Vec<(u64, Step)>) -> Self {
381
0
        self.steps = steps;
382
0
        self
383
0
    }
384
385
235
    fn expectation(&self, backend: Backend) -> Expect {
386
29.8k
        if 
backend == Backend::Memory235
&&
self.steps.iter()117
.
any117
(|(_, s)| s.needs_redis()) {
387
6
            return Expect::NotApplicable;
388
229
        }
389
229
        self.expect.get(&backend).cloned().unwrap_or(Expect::Pass)
390
235
    }
391
392
    /// Runs on every backend and checks each verdict against its
393
    /// expectation, and the backends against each other.
394
46
    pub fn run(&self) -> Result<Vec<Report>, Error> {
395
46
        let mut reports = Vec::new();
396
87
        for &backend in 
&self.backends46
{
397
87
            if self.expectation(backend) == Expect::NotApplicable {
398
7
                continue;
399
80
            }
400
80
            let report = self.run_on(backend)
?0
;
401
            // Full traces for a reader, when asked for.
402
80
            if let Some(
dir0
) = std::env::var_os("SIM_DUMP_DIR") {
403
0
                let path =
404
0
                    std::path::Path::new(&dir).join(format!("{}-{backend:?}.trace", self.name));
405
0
                drop(std::fs::write(path, report.trace()));
406
80
            }
407
80
            reports.push(report);
408
        }
409
46
        let mut problems = String::new();
410
80
        for report in 
&reports46
{
411
80
            let expected = self.expectation(report.backend).invariants();
412
80
            let actual = verdict(&report.violations);
413
80
            if expected != actual {
414
1
                writeln!(
415
1
                    problems,
416
                    "{} on {:?}: expected {} but got {}",
417
                    self.name,
418
                    report.backend,
419
1
                    names(&expected),
420
1
                    names(&actual)
421
                )
422
1
                .unwrap();
423
1
                for 
violation0
in &report.violations {
424
0
                    writeln!(problems, "  {violation}").unwrap();
425
0
                }
426
1
                writeln!(problems, "  trace:\n{}", report.tail(40)).unwrap();
427
79
            }
428
        }
429
46
        if problems.is_empty() {
430
45
            self.check_agreement(&reports, &mut problems);
431
45
        
}1
432
46
        if problems.is_empty() {
433
45
            self.record_known_failures(&reports);
434
45
            Ok(reports)
435
        } else {
436
1
            Err(make_err!(Code::Internal, "{problems}"))
437
        }
438
46
    }
439
440
    /// Appends each known failure this run confirmed, with the item that
441
    /// closes it, to the file `SIM_KNOWN_FAILURES` names. The nightly prints
442
    /// that file as the SIM column of STATUS.md.
443
45
    fn record_known_failures(&self, reports: &[Report]) {
444
45
        let Some(
path0
) = std::env::var_os("SIM_KNOWN_FAILURES") else {
445
45
            return;
446
        };
447
0
        let mut lines = String::new();
448
0
        for report in reports {
449
            if let Expect::KnownFailure {
450
0
                invariants,
451
0
                closed_by,
452
0
            } = self.expectation(report.backend)
453
0
            {
454
0
                writeln!(
455
0
                    lines,
456
0
                    "{}\t{:?}\t{}\t{closed_by}",
457
0
                    self.name,
458
0
                    report.backend,
459
0
                    names(&invariants)
460
0
                )
461
0
                .unwrap();
462
0
            }
463
        }
464
0
        if let Some(closed_by) = self.disagree {
465
0
            writeln!(lines, "{}\tbackends disagree\t\t{closed_by}", self.name).unwrap();
466
0
        }
467
0
        if lines.is_empty() {
468
0
            return;
469
0
        }
470
0
        let file = std::fs::OpenOptions::new()
471
0
            .create(true)
472
0
            .append(true)
473
0
            .open(path);
474
0
        if let Ok(mut file) = file {
475
0
            drop(std::io::Write::write_all(&mut file, lines.as_bytes()));
476
0
        }
477
45
    }
478
479
    /// Where the backends must agree: when their expectations are the same,
480
    /// every operation ends the same way, and an ordered scenario dispatches
481
    /// in the same order.
482
45
    fn check_agreement(&self, reports: &[Report], problems: &mut String) {
483
45
        let Some((first, rest)) = reports.split_first() else {
484
0
            return;
485
        };
486
45
        for 
other34
in rest {
487
34
            if self.expectation(first.backend) != self.expectation(other.backend) {
488
4
                continue;
489
30
            }
490
30
            if let Some(
closed_by1
) = self.disagree {
491
1
                if first.outcomes() == other.outcomes() {
492
0
                    writeln!(
493
0
                        problems,
494
0
                        "{}: {:?} and {:?} were expected to disagree until {closed_by}, but agree",
495
0
                        self.name, first.backend, other.backend
496
0
                    )
497
0
                    .unwrap();
498
1
                }
499
29
            } else if first.outcomes() != other.outcomes() {
500
0
                writeln!(
501
0
                    problems,
502
0
                    "{}: {:?} and {:?} disagree on outcomes:\n  {:?}\n  {:?}",
503
0
                    self.name,
504
0
                    first.backend,
505
0
                    other.backend,
506
0
                    first.outcomes(),
507
0
                    other.outcomes()
508
0
                )
509
0
                .unwrap();
510
29
            }
511
30
            if self.ordered && 
first.dispatch_order()1
!= other.dispatch_order() {
512
0
                writeln!(
513
0
                    problems,
514
0
                    "{}: {:?} and {:?} dispatch in different orders:\n  {:?}\n  {:?}",
515
0
                    self.name,
516
0
                    first.backend,
517
0
                    other.backend,
518
0
                    first.dispatch_order(),
519
0
                    other.dispatch_order()
520
0
                )
521
0
                .unwrap();
522
30
            }
523
        }
524
45
    }
525
526
    /// Runs on one backend without checking expectations.
527
182
    pub fn run_on(&self, backend: Backend) -> Result<Report, Error> {
528
182
        let scenario = self.clone();
529
        #[expect(
530
            clippy::disallowed_methods,
531
            reason = "each run needs its own thread for its thread-local clock and its own runtime"
532
        )]
533
182
        let handle = std::thread::Builder::new()
534
182
            .name(format!("sim-{}-{backend:?}", self.name))
535
182
            .stack_size(16 * 1024 * 1024)
536
182
            .spawn(move || {
537
                #[expect(
538
                    clippy::disallowed_methods,
539
                    reason = "a paused current-thread runtime per run"
540
                )]
541
182
                let runtime = tokio::runtime::Builder::new_current_thread()
542
182
                    .enable_all()
543
182
                    .start_paused(true)
544
182
                    // Reads the fake Redis's socket after every task, so its
545
182
                    // replies land in the tick they were sent in.
546
182
                    .event_interval(1)
547
182
                    .build()
548
182
                    .map_err(|e| 
make_err!0
(
Code::Internal0
, "Could not build a runtime: {e}"))
?0
;
549
                #[expect(
550
                    clippy::disallowed_methods,
551
                    reason = "the run owns this runtime and nothing else blocks on it"
552
                )]
553
182
                runtime.block_on(scenario.run_async(backend))
554
182
            })
555
182
            .map_err(|e| 
make_err!0
(
Code::Internal0
, "Could not start the run's thread: {e}"))
?0
;
556
182
        handle
557
182
            .join()
558
182
            .map_err(|_| 
make_err!0
(
Code::Internal0
, "{} on {backend:?} panicked", self.name))
?0
559
182
    }
560
561
182
    async fn run_async(self, backend: Backend) -> Result<Report, Error> {
562
182
        let clock = SimClock::start();
563
        // See `clock`: on the fake Redis the runtime must never idle, and
564
        // time steps by hand.
565
182
        let _awake = (backend == Backend::FakeRedis).then(|| 
{68
566
68
            nativelink_util::spawn!("sim_keep_awake", async {
567
                loop {
568
9.67M
                    tokio::task::yield_now().await;
569
                }
570
            })
571
68
        });
572
182
        let trace = Trace::default();
573
182
        let mut world = World::new(backend, self.make_spec(), self.seed, trace.clone()).await
?0
;
574
182
        let clock = match world.redis() {
575
68
            Some(redis) => {
576
68
                let redis = redis.clone();
577
2.00M
                
clock68
.
stepped68
(
Arc::new68
(move || redis.commands_total()))
578
            }
579
114
            None => clock,
580
        };
581
182
        let mut steps = self.steps.clone();
582
182
        steps.sort_by_key(|(at, _)| *at);
583
182
        if let Some(
last_submit180
) = steps
584
182
            .iter()
585
20.5k
            .
filter182
(|(_, s)| matches!(s, Step::Execute { .. } | Step::ExecuteSame { .. }))
586
182
            .map(|(at, _)| *at)
587
182
            .max()
588
180
        {
589
180
            world.measure_from(last_submit + 1_000);
590
180
        
}2
591
182
        let mut silences: Vec<(String, u64, u64)> = Vec::new();
592
20.5k
        for (at, step) in 
steps182
{
593
20.5k
            let now = SimClock::now_ms();
594
20.5k
            if at > now {
595
13.2k
                clock.advance(Duration::from_millis(at - now)).await;
596
7.26k
            }
597
            // Silences are the periods a link is silent: a second silence
598
            // while silent changes nothing, and any other setting ends it.
599
20.5k
            if let Step::SetLink { 
worker112
,
link112
} = &step {
600
112
                let name = crate::world::worker_name(*worker);
601
112
                let open = silences
602
112
                    .iter_mut()
603
112
                    .find(|(w, _, end)| 
*w == name67
&&
*end == u64::MAX59
);
604
112
                match (open, *link == Link::Silent) {
605
51
                    (None, true) => silences.push((name, at, u64::MAX)),
606
51
                    (Some(open), false) => open.2 = at,
607
10
                    _ => {}
608
                }
609
20.4k
            }
610
20.5k
            apply(&mut world, &step).await
?0
;
611
            // What the step woke runs at the step's time, not the next tick's.
612
20.5k
            SimClock::yield_all().await;
613
        }
614
182
        let quiet = world.settle(&clock, self.settle).await;
615
182
        trace.record(Event::Settled { quiet });
616
182
        world.record_ledger().await
?0
;
617
618
182
        let spec = world.spec();
619
182
        let facts = Facts {
620
182
            max_inflight: world
621
182
                .worker_specs()
622
182
                .into_iter()
623
1.19k
                .
map182
(|(name, spec)| (name, spec.max_inflight))
624
182
                .collect(),
625
182
            cacheable: trace
626
182
                .records()
627
182
                .iter()
628
134k
                .
filter_map182
(|r| match &r.event {
629
15.9k
                    Event::Submit { op, action, .. } => {
630
15.9k
                        world.action_of(op).map(|a| (action.clone(), a.cacheable))
631
                    }
632
118k
                    _ => None,
633
134k
                })
634
182
                .collect(),
635
182
            dispatch_budget: dispatch_budget(spec),
636
182
            abandon_bound_ms: abandon_bound_ms(spec),
637
182
            evict_bound_ms: evict_bound_ms(spec),
638
182
            silences,
639
182
            ordered: self.ordered,
640
182
            command_bound: self.command_bound,
641
182
            max_commands_per_pass: world.max_commands_per_pass(),
642
        };
643
182
        let passes = world.passes();
644
182
        world.shutdown();
645
182
        let records = trace.records();
646
182
        let violations = check(&records, &facts);
647
182
        Ok(Report {
648
182
            backend,
649
182
            records,
650
182
            facts,
651
182
            violations,
652
182
            passes,
653
182
        })
654
182
    }
655
}
656
657
2
fn names(invariants: &BTreeSet<Invariant>) -> String {
658
2
    if invariants.is_empty() {
659
1
        return "pass".to_string();
660
1
    }
661
1
    invariants
662
1
        .iter()
663
1
        .map(ToString::to_string)
664
1
        .collect::<Vec<_>>()
665
1
        .join(", ")
666
2
}
667
668
/// Executions an action may get: the first, the job retries and the
669
/// worker-loss retries, at the scheduler's defaults when unset.
670
182
const fn dispatch_budget(spec: &SimpleSpec) -> u64 {
671
182
    let job = if spec.max_job_retries == 0 {
672
182
        3
673
    } else {
674
0
        spec.max_job_retries
675
    };
676
182
    let loss = if spec.max_worker_loss_retries == 0 {
677
182
        nativelink_scheduler::simple_scheduler_state_manager::RetryLimits::DEFAULT_WORKER_LOSS_RETRIES
678
    } else {
679
0
        spec.max_worker_loss_retries
680
    };
681
182
    1 + job as u64 + loss as u64
682
182
}
683
684
/// The client keepalive (10 s) after the client action timeout, plus the
685
/// 5 s retirement pass.
686
182
const fn abandon_bound_ms(spec: &SimpleSpec) -> u64 {
687
182
    let timeout_s = if spec.client_action_timeout_s == 0 {
688
182
        60
689
    } else {
690
0
        spec.client_action_timeout_s
691
    };
692
182
    (timeout_s + 10 + 5) * 1000
693
182
}
694
695
/// The worker timeout, plus the 1 s eviction tick and one keepalive.
696
182
const fn evict_bound_ms(spec: &SimpleSpec) -> u64 {
697
182
    let timeout_s = if spec.worker_timeout_s == 0 {
698
182
        10
699
    } else {
700
0
        spec.worker_timeout_s
701
    };
702
182
    (timeout_s + 2) * 1000
703
182
}
704
705
20.5k
async fn apply(world: &mut World, step: &Step) -> Result<(), Error> {
706
20.5k
    let fault = |what: String| Event::Fault { 
what187
}187
;
707
20.5k
    match step {
708
15.9k
        Step::Execute { op, action } => world.execute(op, action).await
?0
,
709
2
        Step::ExecuteSame { op, of } => {
710
2
            let action = world
711
2
                .action_of(of)
712
2
                .ok_or_else(|| 
make_err!0
(
Code::InvalidArgument0
, "No client named {of}"))
?0
;
713
2
            world.execute(op, &action).await
?0
;
714
        }
715
4.05k
        Step::ClientGone { op } => world.client_gone(op),
716
226
        Step::AddWorkers { count, spec } => world.add_workers(*count, spec),
717
71
        Step::KillWorker { worker } => world.kill_worker(*worker),
718
69
        Step::CutWorker { worker } => {
719
69
            world.trace().record(fault(format!("cut w{worker}")));
720
69
            world.cut_worker(*worker);
721
69
        }
722
112
        Step::SetLink { worker, link } => {
723
112
            world
724
112
                .trace()
725
112
                .record(fault(format!("link w{worker} {link:?}")));
726
112
            world.set_link(*worker, *link);
727
112
        }
728
        Step::SchedulerRestart => {
729
4
            world.scheduler_down();
730
4
            world.scheduler_up().await
?0
;
731
        }
732
1
        Step::SchedulerDown => world.scheduler_down(),
733
1
        Step::SchedulerUp => world.scheduler_up().await
?0
,
734
2
        Step::RedisFailover { readonly_writes } => {
735
2
            world
736
2
                .trace()
737
2
                .record(fault(format!("redis failover readonly={readonly_writes}")));
738
2
            redis(world)
?0
.failover(*readonly_writes);
739
        }
740
0
        Step::RedisPartition { partitioned } => {
741
0
            world
742
0
                .trace()
743
0
                .record(fault(format!("redis partition={partitioned}")));
744
0
            redis(world)?.partition(*partitioned);
745
        }
746
        Step::RedisFail {
747
3
            command,
748
3
            fault: kind,
749
3
            times,
750
        } => {
751
3
            world
752
3
                .trace()
753
3
                .record(fault(format!("redis {command} {kind:?} x{times}")));
754
3
            redis(world)
?0
.fail_commands(command, None, kind.clone(), Some(*times));
755
        }
756
1
        Step::RedisEvict { prefix } => {
757
1
            let evicted = redis(world)
?0
.evict_prefix(prefix);
758
1
            world
759
1
                .trace()
760
1
                .record(fault(format!("redis evict {prefix:?} ({evicted} keys)")));
761
        }
762
0
        Step::RedisLatency { ms } => {
763
0
            world
764
0
                .trace()
765
0
                .record(fault(format!("redis latency {ms:?} ms")));
766
0
            redis(world)?.set_latency(ms.map(Duration::from_millis));
767
        }
768
3
        Step::ClockJump { ms } => {
769
3
            SimClock::jump(Duration::from_millis(*ms));
770
3
            world.trace().record(Event::ClockJump { ms: *ms });
771
3
        }
772
    }
773
20.5k
    Ok(())
774
20.5k
}
775
776
6
fn redis(
777
6
    world: &World,
778
6
) -> Result<
779
6
    &nativelink_redis_tester::FakeRedisBackend<
780
6
        nativelink_store::redis_store::RedisSubscriptionManager,
781
6
    >,
782
6
    Error,
783
6
> {
784
6
    world.redis().ok_or_else(|| 
{0
785
0
        make_err!(
786
0
            Code::InvalidArgument,
787
            "A Redis step on {:?}",
788
0
            world.backend()
789
        )
790
0
    })
791
6
}
792
793
/// The result of one scenario on one backend.
794
#[derive(Clone, Debug)]
795
pub struct Report {
796
    pub backend: Backend,
797
    pub records: Vec<Record>,
798
    pub facts: Facts,
799
    pub violations: Vec<Violation>,
800
    /// Matching passes the scheduler ran.
801
    pub passes: u64,
802
}
803
804
impl Report {
805
    /// How each client operation ended, by op name; `None` for one that
806
    /// left or never ended.
807
86
    pub fn outcomes(&self) -> BTreeMap<String, Option<Outcome>> {
808
86
        let mut out = BTreeMap::new();
809
10.4k
        for r in 
&self.records86
{
810
10.4k
            match &r.event {
811
816
                Event::Submit { op, .. } => {
812
816
                    out.entry(op.clone()).or_insert(None);
813
816
                }
814
808
                Event::ClientDone { op, outcome } => {
815
808
                    out.insert(op.clone(), Some(*outcome));
816
808
                }
817
8.85k
                _ => {}
818
            }
819
        }
820
86
        out
821
86
    }
822
823
    /// Actions in the order of their first dispatch.
824
4
    pub fn dispatch_order(&self) -> Vec<String> {
825
4
        let mut seen = BTreeSet::new();
826
4
        self.records
827
4
            .iter()
828
28
            .
filter_map4
(|r| match &r.event {
829
28
                Event::Dispatch { action, .. } if seen.insert(action.clone()) => {
830
28
                    Some(action.clone())
831
                }
832
302
                _ => None,
833
330
            })
834
4
            .collect()
835
4
    }
836
837
0
    pub fn trace(&self) -> String {
838
0
        render(&self.records)
839
0
    }
840
841
    /// The trace with the records of each millisecond sorted, so two runs
842
    /// that interleave tasks differently inside one tick compare equal.
843
10
    pub fn canonical_trace(&self) -> String {
844
10
        let mut records = self.records.clone();
845
1.17k
        
records10
.
sort_by_cached_key10
(|r| (r.at_ms, r.event.to_string()));
846
10
        render(&records)
847
10
    }
848
849
    /// The last `lines` records as text.
850
1
    pub fn tail(&self, lines: usize) -> String {
851
1
        let start = self.records.len().saturating_sub(lines);
852
1
        render(&self.records[start..])
853
1
    }
854
855
    /// Records whose event matches.
856
13
    pub fn count(&self, matches: impl Fn(&Event) -> bool) -> usize {
857
338
        
self.records.iter()13
.
filter13
(|r| matches(&r.event)).
count13
()
858
13
    }
859
}