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/fleet.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
//! The fleet simulator's workers (RFC-0061): production `LocalWorker`s over
16
//! the production gRPC client, against a real scheduler, with execution
17
//! drawn from a seeded distribution instead of a scenario.
18
19
use core::fmt;
20
use core::sync::atomic::{AtomicU64, Ordering};
21
use core::time::Duration;
22
use std::collections::HashMap;
23
use std::sync::Arc;
24
25
use nativelink_config::cas_server::{EndpointConfig, LocalWorkerConfig, WorkerProperty};
26
use nativelink_error::Error;
27
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::StartExecute;
28
use nativelink_util::action_messages::OperationId;
29
use nativelink_util::background_spawn;
30
use nativelink_util::shutdown_guard::ShutdownGuard;
31
use nativelink_worker::local_worker::{LocalWorker, connect_to_scheduler};
32
use tokio::sync::broadcast;
33
use tokio::task::JoinHandle;
34
35
use crate::actions::{ActionSource, Plan, Run, SimActionsManager};
36
use crate::property::Rng;
37
use crate::trace::Event;
38
39
/// What the fleet did, counted as it happens.
40
#[derive(Debug, Default)]
41
pub struct Counters {
42
    /// Connection attempts to the scheduler.
43
    pub connects: AtomicU64,
44
    /// Actions a worker accepted and started.
45
    pub started: AtomicU64,
46
    /// Executions that ran to the end.
47
    pub finished: AtomicU64,
48
    /// Executions drawn to exit non-zero.
49
    pub failed: AtomicU64,
50
    /// Executions cut short by a kill or a disconnect.
51
    pub killed: AtomicU64,
52
    /// Kill requests from the scheduler.
53
    pub kill_requests: AtomicU64,
54
    /// `kill_all`, after a worker lost its connection.
55
    pub kill_all_calls: AtomicU64,
56
}
57
58
impl fmt::Display for Counters {
59
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60
0
        let get = |c: &AtomicU64| c.load(Ordering::Relaxed);
61
0
        write!(
62
0
            f,
63
            "connects={} started={} finished={} failed={} killed={} kill_requests={} kill_all_calls={}",
64
0
            get(&self.connects),
65
0
            get(&self.started),
66
0
            get(&self.finished),
67
0
            get(&self.failed),
68
0
            get(&self.killed),
69
0
            get(&self.kill_requests),
70
0
            get(&self.kill_all_calls),
71
        )
72
0
    }
73
}
74
75
/// Execution drawn per operation: a run time uniform in `run_ms` and a
76
/// non-zero exit for `fail_percent` of operations. The draw is a function
77
/// of the seed and the operation id, so a requeued operation behaves the
78
/// same on whichever worker gets it.
79
#[derive(Debug)]
80
pub struct Distribution {
81
    pub seed: u64,
82
    pub run_ms: (u64, u64),
83
    pub fail_percent: u64,
84
    pub counters: Arc<Counters>,
85
}
86
87
/// FNV-1a, for a stable draw per operation.
88
12.2k
fn fnv1a(seed: u64, text: &str) -> u64 {
89
12.2k
    let mut hash = 0xcbf2_9ce4_8422_2325 ^ seed;
90
81.6k
    for byte in 
text12.2k
.
bytes12.2k
() {
91
81.6k
        hash ^= u64::from(byte);
92
81.6k
        hash = hash.wrapping_mul(0x0100_0000_01b3);
93
81.6k
    }
94
12.2k
    hash
95
12.2k
}
96
97
impl Distribution {
98
    /// The duration and outcome of `operation_id`.
99
12.2k
    pub fn draw(&self, operation_id: &str) -> (Duration, Run) {
100
12.2k
        let mut rng = Rng::new(fnv1a(self.seed, operation_id));
101
12.2k
        let (low, high) = self.run_ms;
102
12.2k
        let run_for = Duration::from_millis(rng.range(low, high.max(low)));
103
12.2k
        let run = if self.fail_percent > 0 && rng.chance(self.fail_percent) {
104
2.46k
            Run::Exit(1)
105
        } else {
106
9.73k
            Run::Succeed
107
        };
108
12.2k
        (run_for, run)
109
12.2k
    }
110
}
111
112
impl ActionSource for Distribution {
113
0
    fn plan(&self, start_execute: &StartExecute) -> Result<Plan, Error> {
114
0
        let (run_for, run) = self.draw(&start_execute.operation_id);
115
0
        self.counters.started.fetch_add(1, Ordering::Relaxed);
116
0
        if run != Run::Succeed {
117
0
            self.counters.failed.fetch_add(1, Ordering::Relaxed);
118
0
        }
119
0
        Ok(Plan {
120
0
            action: start_execute.operation_id.clone(),
121
0
            key: start_execute.operation_id.clone(),
122
0
            prepare: Duration::ZERO,
123
0
            run_for,
124
0
            upload: Duration::ZERO,
125
0
            run,
126
0
        })
127
0
    }
128
129
0
    fn record(&self, event: Event) {
130
0
        let counter = match event {
131
0
            Event::ExecEnd { killed: true, .. } => &self.counters.killed,
132
0
            Event::ExecEnd { .. } => &self.counters.finished,
133
0
            Event::KillAll { .. } => &self.counters.kill_all_calls,
134
0
            _ => return,
135
        };
136
0
        counter.fetch_add(1, Ordering::Relaxed);
137
0
    }
138
139
0
    fn killed(&self, _operation_id: &OperationId) {
140
0
        self.counters.kill_requests.fetch_add(1, Ordering::Relaxed);
141
0
    }
142
}
143
144
/// One simulated worker's settings.
145
#[derive(Debug, Clone)]
146
pub struct FleetWorker {
147
    pub name: String,
148
    pub scheduler: String,
149
    pub max_inflight: u64,
150
    pub properties: HashMap<String, String>,
151
}
152
153
impl FleetWorker {
154
0
    fn config(&self) -> LocalWorkerConfig {
155
        LocalWorkerConfig {
156
0
            name: self.name.clone(),
157
0
            max_inflight_tasks: self.max_inflight,
158
0
            platform_properties: self
159
0
                .properties
160
0
                .iter()
161
0
                .map(|(k, v)| (k.clone(), WorkerProperty::Values(vec![v.clone()])))
162
0
                .collect(),
163
0
            worker_api_endpoint: EndpointConfig {
164
0
                uri: self.scheduler.clone(),
165
0
                ..Default::default()
166
0
            },
167
0
            ..Default::default()
168
        }
169
0
    }
170
}
171
172
/// A running simulated worker; dropping it does not stop it, `stop` does.
173
#[derive(Debug)]
174
pub struct RunningWorker {
175
    task: JoinHandle<Result<(), Error>>,
176
    // Held so the worker's shutdown channel stays open.
177
    _shutdown: broadcast::Sender<ShutdownGuard>,
178
}
179
180
impl RunningWorker {
181
    /// The process dies: the task is aborted and its connection closes.
182
0
    pub fn stop(&self) {
183
0
        self.task.abort();
184
0
    }
185
186
0
    pub fn is_finished(&self) -> bool {
187
0
        self.task.is_finished()
188
0
    }
189
}
190
191
/// Starts `worker` with execution from `source`.
192
0
pub fn spawn_worker(
193
0
    worker: &FleetWorker,
194
0
    source: Arc<dyn ActionSource>,
195
0
    counters: Arc<Counters>,
196
0
) -> RunningWorker {
197
0
    let config = Arc::new(worker.config());
198
0
    let factory_config = config.clone();
199
0
    let local_worker = LocalWorker::new_with_connection_factory_and_actions_manager(
200
0
        config,
201
0
        Arc::new(SimActionsManager::new(&worker.name, source)),
202
0
        Box::new(move || {
203
0
            let config = factory_config.clone();
204
0
            let counters = counters.clone();
205
0
            Box::pin(async move {
206
0
                counters.connects.fetch_add(1, Ordering::Relaxed);
207
0
                connect_to_scheduler(&config).await
208
0
            })
209
0
        }),
210
0
        Box::new(|duration| Box::pin(tokio::time::sleep(duration))),
211
    );
212
0
    let (shutdown, shutdown_rx) = broadcast::channel(1);
213
0
    let task = background_spawn!("fleet_worker", local_worker.run(shutdown_rx));
214
0
    RunningWorker {
215
0
        task,
216
0
        _shutdown: shutdown,
217
0
    }
218
0
}