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/net.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 network between the simulated clients and workers and the real
16
//! servers. A worker connects through `InProcessWorkerClient`, which hands
17
//! its messages to `WorkerApiServer::inner_connect_worker_for_testing` and
18
//! turns the server's stream back into the `tonic::Streaming` the worker
19
//! reads, through a `ChannelBody`. Every message crosses the trace on the
20
//! way, when it is delivered. A link can be cut (the connection closes) or
21
//! silenced (it stays open and holds what is sent until it is back, as TCP
22
//! does across a blip), and a scheduler restart cuts them all.
23
24
use core::sync::atomic::Ordering;
25
use std::collections::HashMap;
26
use std::sync::Arc;
27
28
use futures::StreamExt;
29
use hyper::body::Frame;
30
use nativelink_error::{Error, ResultExt, make_err};
31
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::update_for_scheduler::Update as SchedulerUpdate;
32
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::update_for_worker::Update as WorkerUpdate;
33
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::{
34
    ConnectWorkerRequest, ExecuteAccepted, ExecuteComplete, ExecuteDeclined, ExecuteResult,
35
    GoingAwayRequest, KeepAliveRequest, StartExecute, UpdateForScheduler, UpdateForWorker,
36
    execute_result,
37
};
38
use nativelink_service::execution_server::ExecutionServer;
39
use nativelink_service::worker_api_server::WorkerApiServer;
40
use nativelink_util::channel_body_for_tests::ChannelBody;
41
use nativelink_util::common::encode_stream_proto;
42
use nativelink_util::background_spawn;
43
use nativelink_worker::worker_api_client_wrapper::WorkerApiClientTrait;
44
use parking_lot::Mutex;
45
use tokio::sync::{mpsc, watch};
46
use tokio_stream::wrappers::UnboundedReceiverStream;
47
use tonic::codec::Codec;
48
use tonic::{Response, Status, Streaming};
49
use tonic_prost::ProstCodec;
50
51
use crate::actions::{ActionSource, Plan, Registry, start_execute_digest};
52
use crate::trace::{Event, Trace};
53
54
/// What a worker's link carries.
55
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
56
pub enum Link {
57
    #[default]
58
    Up,
59
    /// Open, but nothing gets through in either direction until the link
60
    /// is up again; then everything held is delivered in order.
61
    Silent,
62
    /// The worker's `ExecuteAccepted` messages are lost.
63
    DropAccepts,
64
    /// The worker's results reach the scheduler twice.
65
    DuplicateResults,
66
}
67
68
#[derive(Debug)]
69
struct LinkState {
70
    link: watch::Sender<Link>,
71
    /// Bumped to close the worker's current connection.
72
    cuts: watch::Sender<u64>,
73
}
74
75
impl Default for LinkState {
76
1.19k
    fn default() -> Self {
77
1.19k
        Self {
78
1.19k
            link: watch::channel(Link::Up).0,
79
1.19k
            cuts: watch::channel(0).0,
80
1.19k
        }
81
1.19k
    }
82
}
83
84
/// The servers a client or worker reaches, and the state every connection
85
/// shares.
86
pub struct Net {
87
    worker_api: Mutex<Option<Arc<WorkerApiServer>>>,
88
    execution: Mutex<Option<ExecutionServer>>,
89
    /// Bumped when the scheduler goes down; every connection closes.
90
    epoch: watch::Sender<u64>,
91
    links: Mutex<HashMap<String, LinkState>>,
92
    /// Scheduler operation id to the scenario's action label.
93
    actions: Mutex<HashMap<String, String>>,
94
    /// Scheduler operation id to a stable key, `<action>#<n>` for the
95
    /// action's `n`th operation, since the ids themselves can be random.
96
    keys: Mutex<HashMap<String, String>>,
97
    /// Operations seen per action.
98
    per_action: Mutex<HashMap<String, u64>>,
99
    registry: Arc<Registry>,
100
    trace: Trace,
101
}
102
103
impl core::fmt::Debug for Net {
104
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
105
0
        f.debug_struct("Net").finish_non_exhaustive()
106
0
    }
107
}
108
109
impl Net {
110
182
    pub(crate) fn new(registry: Arc<Registry>, trace: Trace) -> Arc<Self> {
111
182
        Arc::new(Self {
112
182
            worker_api: Mutex::new(None),
113
182
            execution: Mutex::new(None),
114
182
            epoch: watch::channel(0).0,
115
182
            links: Mutex::new(HashMap::new()),
116
182
            actions: Mutex::new(HashMap::new()),
117
182
            keys: Mutex::new(HashMap::new()),
118
182
            per_action: Mutex::new(HashMap::new()),
119
182
            registry,
120
182
            trace,
121
182
        })
122
182
    }
123
124
16.8k
    pub const fn trace(&self) -> &Trace {
125
16.8k
        &self.trace
126
16.8k
    }
127
128
187
    pub(crate) fn serve(&self, worker_api: Arc<WorkerApiServer>, execution: ExecutionServer) {
129
187
        *self.worker_api.lock() = Some(worker_api);
130
187
        *self.execution.lock() = Some(execution);
131
187
    }
132
133
    /// Takes the servers away and closes every connection to them.
134
187
    pub(crate) fn unserve(&self) {
135
187
        self.worker_api.lock().take();
136
187
        self.execution.lock().take();
137
187
        self.epoch.send_modify(|epoch| *epoch += 1);
138
187
    }
139
140
16.6k
    pub(crate) fn execution(&self) -> Option<ExecutionServer> {
141
16.6k
        self.execution.lock().clone()
142
16.6k
    }
143
144
18.5k
    pub(crate) fn epoch(&self) -> watch::Receiver<u64> {
145
18.5k
        self.epoch.subscribe()
146
18.5k
    }
147
148
112
    pub(crate) fn set_link(&self, worker: &str, link: Link) {
149
112
        self.links
150
112
            .lock()
151
112
            .entry(worker.to_string())
152
112
            .or_default()
153
112
            .link
154
112
            .send_replace(link);
155
112
    }
156
157
212k
    fn link(&self, worker: &str) -> Link {
158
212k
        self.links
159
212k
            .lock()
160
212k
            .get(worker)
161
212k
            .map(|l| *l.link.borrow())
162
212k
            .unwrap_or_default()
163
212k
    }
164
165
1.98k
    fn links(&self, worker: &str) -> watch::Receiver<Link> {
166
1.98k
        self.links
167
1.98k
            .lock()
168
1.98k
            .entry(worker.to_string())
169
1.98k
            .or_default()
170
1.98k
            .link
171
1.98k
            .subscribe()
172
1.98k
    }
173
174
1.32k
    pub(crate) fn cut(&self, worker: &str) {
175
1.32k
        self.links
176
1.32k
            .lock()
177
1.32k
            .entry(worker.to_string())
178
1.32k
            .or_default()
179
1.32k
            .cuts
180
1.32k
            .send_modify(|cuts| *cuts += 1);
181
1.32k
    }
182
183
1.98k
    fn cuts(&self, worker: &str) -> watch::Receiver<u64> {
184
1.98k
        self.links
185
1.98k
            .lock()
186
1.98k
            .entry(worker.to_string())
187
1.98k
            .or_default()
188
1.98k
            .cuts
189
1.98k
            .subscribe()
190
1.98k
    }
191
192
    /// The stable key of a scheduler operation id for `action`, minted on
193
    /// first sight.
194
21.3k
    pub(crate) fn key_of(&self, operation_id: &str, action: &str) -> String {
195
21.3k
        self.keys
196
21.3k
            .lock()
197
21.3k
            .entry(operation_id.to_string())
198
21.3k
            .or_insert_with(|| 
{10.2k
199
10.2k
                let mut per_action = self.per_action.lock();
200
10.2k
                let n = per_action.entry(action.to_string()).or_default();
201
10.2k
                *n += 1;
202
10.2k
                format!("{action}#{n}")
203
10.2k
            })
204
21.3k
            .clone()
205
21.3k
    }
206
207
31.0k
    fn action_of(&self, operation_id: &str) -> String {
208
31.0k
        self.actions
209
31.0k
            .lock()
210
31.0k
            .get(operation_id)
211
31.0k
            .cloned()
212
31.0k
            .unwrap_or_else(|| 
format!0
("?{operation_id}"))
213
31.0k
    }
214
215
    /// Records a message from the scheduler to `worker`.
216
12.6k
    fn on_update_for_worker(&self, worker: &str, update: &UpdateForWorker) {
217
12.6k
        match &update.update {
218
1.83k
            Some(WorkerUpdate::ConnectionResult(result)) => {
219
1.83k
                self.trace.record(Event::WorkerConnected {
220
1.83k
                    worker: worker.to_string(),
221
1.83k
                    dispatch_ack: result.dispatch_ack,
222
1.83k
                });
223
1.83k
            }
224
10.6k
            Some(WorkerUpdate::StartAction(start_execute)) => {
225
10.6k
                let action = start_execute_digest(start_execute)
226
10.6k
                    .and_then(|digest| self.registry.label(&digest))
227
10.6k
                    .unwrap_or_else(|| 
"?"0
.
to_string0
());
228
10.6k
                self.actions
229
10.6k
                    .lock()
230
10.6k
                    .insert(start_execute.operation_id.clone(), action.clone());
231
10.6k
                let key = self.key_of(&start_execute.operation_id, &action);
232
10.6k
                self.trace.record(Event::Dispatch {
233
10.6k
                    worker: worker.to_string(),
234
10.6k
                    action,
235
10.6k
                    key,
236
10.6k
                });
237
            }
238
2
            Some(WorkerUpdate::KillOperationRequest(kill)) => {
239
2
                self.trace.record(Event::Kill {
240
2
                    worker: worker.to_string(),
241
2
                    action: self.action_of(&kill.operation_id),
242
2
                });
243
2
            }
244
142
            Some(WorkerUpdate::KeepAlive(()) | WorkerUpdate::Disconnect(())) | None => {}
245
        }
246
12.6k
    }
247
248
99.8k
    fn on_update_for_scheduler(&self, worker: &str, update: &SchedulerUpdate) {
249
99.8k
        let worker = worker.to_string();
250
99.8k
        let 
event31.0k
= match update {
251
10.6k
            SchedulerUpdate::ExecuteAccepted(accepted) => Event::Accept {
252
10.6k
                worker,
253
10.6k
                action: self.action_of(&accepted.operation_id),
254
10.6k
            },
255
0
            SchedulerUpdate::ExecuteDeclined(declined) => Event::Decline {
256
0
                worker,
257
0
                action: self.action_of(&declined.operation_id),
258
0
            },
259
10.0k
            SchedulerUpdate::ExecuteComplete(complete) => Event::Complete {
260
10.0k
                worker,
261
10.0k
                action: self.action_of(&complete.operation_id),
262
10.0k
            },
263
10.3k
            SchedulerUpdate::ExecuteResult(result) => Event::Result {
264
10.3k
                worker,
265
10.3k
                action: self.action_of(&result.operation_id),
266
257
                ok: matches!(
267
10.3k
                    result.result,
268
                    Some(execute_result::Result::ExecuteResponse(_))
269
                ),
270
            },
271
            SchedulerUpdate::ConnectWorkerRequest(_)
272
            | SchedulerUpdate::KeepAliveRequest(_)
273
68.8k
            | SchedulerUpdate::GoingAwayRequest(_) => return,
274
        };
275
31.0k
        self.trace.record(event);
276
99.8k
    }
277
}
278
279
impl ActionSource for Net {
280
10.6k
    fn plan(&self, start_execute: &StartExecute) -> Result<Plan, Error> {
281
10.6k
        let digest = start_execute_digest(start_execute)
282
10.6k
            .err_tip(|| "StartExecute without an action digest")
?0
;
283
10.6k
        let entry = self
284
10.6k
            .registry
285
10.6k
            .lookup(&digest)
286
10.6k
            .err_tip(|| 
format!0
("No simulated action for {digest}"))
?0
;
287
10.6k
        let attempt = entry.attempts.fetch_add(1, Ordering::AcqRel) as usize;
288
10.6k
        let label = entry.action.label();
289
10.6k
        Ok(Plan {
290
10.6k
            key: self.key_of(&start_execute.operation_id, &label),
291
10.6k
            action: label,
292
10.6k
            prepare: entry.action.prepare,
293
10.6k
            run_for: entry.action.run,
294
10.6k
            upload: entry.action.upload,
295
10.6k
            run: entry.action.attempts[attempt.min(entry.action.attempts.len() - 1)],
296
10.6k
        })
297
10.6k
    }
298
299
21.9k
    fn record(&self, event: Event) {
300
21.9k
        self.trace.record(event);
301
21.9k
    }
302
}
303
304
/// The worker's end of one connection: where its messages go, and what a
305
/// silent link holds back.
306
#[derive(Debug, Default)]
307
struct OutboxState {
308
    tx: Option<mpsc::UnboundedSender<Result<UpdateForScheduler, Status>>>,
309
    held: Vec<SchedulerUpdate>,
310
}
311
312
type Outbox = Arc<Mutex<OutboxState>>;
313
314
/// Delivers `update` from `worker` to the scheduler, as many times as the
315
/// link says.
316
99.8k
fn deliver_up(
317
99.8k
    net: &Net,
318
99.8k
    worker: &str,
319
99.8k
    tx: &mpsc::UnboundedSender<Result<UpdateForScheduler, Status>>,
320
99.8k
    update: SchedulerUpdate,
321
99.8k
) -> Result<(), ()> {
322
99.8k
    let copies = match (net.link(worker), &update) {
323
2
        (Link::DropAccepts, SchedulerUpdate::ExecuteAccepted(_)) => 0,
324
4
        (Link::DuplicateResults, SchedulerUpdate::ExecuteResult(_)) => 2,
325
99.8k
        _ => 1,
326
    };
327
    // A copy the network made is not the worker's doing: the trace
328
    // records what the worker sent.
329
99.8k
    if copies > 0 {
330
99.8k
        net.on_update_for_scheduler(worker, &update);
331
99.8k
    
}2
332
99.8k
    let mut update = Some(update);
333
99.8k
    for copy in 
1..=copies99.8k
{
334
99.8k
        let message = if copy == copies {
335
99.8k
            update.take()
336
        } else {
337
4
            update.clone()
338
        };
339
99.8k
        tx.send(Ok(UpdateForScheduler { update: message }))
340
99.8k
            .map_err(drop)
?0
;
341
    }
342
99.8k
    Ok(())
343
99.8k
}
344
345
/// `WorkerApiClientTrait` over the in-process `Net`.
346
#[derive(Clone, Debug)]
347
pub struct InProcessWorkerClient {
348
    worker: String,
349
    net: Arc<Net>,
350
    outbox: Outbox,
351
}
352
353
impl InProcessWorkerClient {
354
1.99k
    pub(crate) fn connect(worker: &str, net: &Arc<Net>) -> Result<Self, Error> {
355
1.99k
        if net.worker_api.lock().is_none() {
356
4
            return Err(make_err!(
357
4
                nativelink_error::Code::Unavailable,
358
4
                "No scheduler is serving"
359
4
            ));
360
1.98k
        }
361
1.98k
        Ok(Self {
362
1.98k
            worker: worker.to_string(),
363
1.98k
            net: net.clone(),
364
1.98k
            outbox: Arc::new(Mutex::new(OutboxState::default())),
365
1.98k
        })
366
1.99k
    }
367
368
99.9k
    async fn send(&self, update: SchedulerUpdate) -> Result<(), Error> {
369
99.9k
        let disconnected = || 
{5
370
5
            make_err!(
371
5
                nativelink_error::Code::Unavailable,
372
                "worker update with disconnected channel"
373
            )
374
5
        };
375
99.8k
        let tx = {
376
99.9k
            let mut outbox = self.outbox.lock();
377
99.9k
            let 
tx99.9k
= outbox.tx.clone().ok_or_else(disconnected)
?5
;
378
99.9k
            if self.net.link(&self.worker) == Link::Silent {
379
152
                outbox.held.push(update);
380
152
                return Ok(());
381
99.8k
            }
382
99.8k
            tx
383
        };
384
99.8k
        deliver_up(&self.net, &self.worker, &tx, update).map_err(|()| 
{0
385
0
            self.outbox.lock().tx.take();
386
0
            disconnected()
387
0
        })
388
99.9k
    }
389
}
390
391
impl WorkerApiClientTrait for InProcessWorkerClient {
392
1.98k
    async fn connect_worker(
393
1.98k
        &mut self,
394
1.98k
        request: ConnectWorkerRequest,
395
1.98k
    ) -> Result<Response<Streaming<UpdateForWorker>>, Status> {
396
1.98k
        let server = self
397
1.98k
            .net
398
1.98k
            .worker_api
399
1.98k
            .lock()
400
1.98k
            .clone()
401
1.98k
            .ok_or_else(|| 
Status::unavailable0
("No scheduler is serving"))
?0
;
402
1.98k
        let mut epoch = self.net.epoch();
403
1.98k
        let start_epoch = *epoch.borrow_and_update();
404
1.98k
        let mut cuts = self.net.cuts(&self.worker);
405
1.98k
        let start_cuts = *cuts.borrow_and_update();
406
407
1.98k
        let (tx, rx) = mpsc::unbounded_channel();
408
1.98k
        tx.send(Ok(UpdateForScheduler {
409
1.98k
            update: Some(SchedulerUpdate::ConnectWorkerRequest(request)),
410
1.98k
        }))
411
1.98k
        .map_err(|_| 
Status::internal0
("Could not queue ConnectWorkerRequest"))
?0
;
412
1.98k
        *self.outbox.lock() = OutboxState {
413
1.98k
            tx: Some(tx.clone()),
414
1.98k
            held: Vec::new(),
415
1.98k
        };
416
417
1.98k
        let mut from_server = server
418
1.98k
            .inner_connect_worker_for_testing(UnboundedReceiverStream::new(rx))
419
1.98k
            .await
420
1.98k
            .map_err(Status::from)
?0
421
1.98k
            .into_inner();
422
423
1.98k
        let (frames, body) = ChannelBody::new();
424
1.98k
        let net = self.net.clone();
425
1.98k
        let worker = self.worker.clone();
426
1.98k
        let outbox = self.outbox.clone();
427
1.98k
        let mut links = self.net.links(&self.worker);
428
        // The connection lives in this task: it ends when the server's stream
429
        // ends, the link is cut or the scheduler goes down, and closing it
430
        // closes both directions.
431
1.98k
        drop(background_spawn!("sim_worker_link", async move {
432
25.3k
            let 
deliver_down1.98k
= async |update: UpdateForWorker| -> bool {
433
12.6k
                net.on_update_for_worker(&worker, &update);
434
12.6k
                let Ok(bytes) = encode_stream_proto(&update) else {
435
0
                    return false;
436
                };
437
12.6k
                frames.send(Frame::data(bytes)).await.is_ok()
438
25.3k
            };
439
1.98k
            let mut held_down: Vec<UpdateForWorker> = Vec::new();
440
            loop {
441
15.0k
                tokio::select! {
442
15.0k
                    
update13.3k
= from_server.next() => {
443
13.0k
                        let Some(Ok(update)) = update else { 
break327
};
444
13.0k
                        if net.link(&worker) == Link::Silent {
445
383
                            held_down.push(update);
446
383
                            continue;
447
12.6k
                        }
448
12.6k
                        if !deliver_down(update).await {
449
0
                            break;
450
12.6k
                        }
451
                    }
452
15.0k
                    
changed86
= links.changed() => {
453
86
                        if changed.is_err() {
454
0
                            break;
455
86
                        }
456
86
                        if *links.borrow_and_update() == Link::Silent {
457
47
                            continue;
458
39
                        }
459
                        // Back up: what was held goes through, in order.
460
39
                        let mut delivered = true;
461
39
                        for 
update27
in held_down.drain(..) {
462
27
                            if !deliver_down(update).await {
463
0
                                delivered = false;
464
0
                                break;
465
27
                            }
466
                        }
467
39
                        let held_up = core::mem::take(&mut outbox.lock().held);
468
39
                        for 
update12
in held_up {
469
12
                            if deliver_up(&net, &worker, &tx, update).is_err() {
470
0
                                delivered = false;
471
0
                                break;
472
12
                            }
473
                        }
474
39
                        if !delivered {
475
0
                            break;
476
39
                        }
477
                    }
478
15.0k
                    
changed410
= epoch.changed() => {
479
410
                        if changed.is_err() || *epoch.borrow() != start_epoch {
480
410
                            break;
481
0
                        }
482
                    }
483
15.0k
                    
changed133
= cuts.changed() => {
484
133
                        if changed.is_err() || *cuts.borrow() != start_cuts {
485
133
                            break;
486
0
                        }
487
                    }
488
                }
489
            }
490
870
            *outbox.lock() = OutboxState::default();
491
870
            drop(tx);
492
870
            net.trace().record(Event::WorkerDisconnected { worker });
493
870
        }));
494
495
1.98k
        let mut codec = ProstCodec::<UpdateForWorker, UpdateForWorker>::default();
496
1.98k
        Ok(Response::new(Streaming::new_request(
497
1.98k
            codec.decoder(),
498
1.98k
            body,
499
1.98k
            None,
500
1.98k
            None,
501
1.98k
        )))
502
1.98k
    }
503
504
68.9k
    async fn keep_alive(&mut self, request: KeepAliveRequest) -> Result<(), Error> {
505
68.9k
        self.send(SchedulerUpdate::KeepAliveRequest(request)).await
506
68.9k
    }
507
508
0
    async fn going_away(&mut self, request: GoingAwayRequest) -> Result<(), Error> {
509
0
        self.send(SchedulerUpdate::GoingAwayRequest(request)).await
510
0
    }
511
512
10.3k
    async fn execution_response(&mut self, request: ExecuteResult) -> Result<(), Error> {
513
10.3k
        self.send(SchedulerUpdate::ExecuteResult(request)).await
514
10.3k
    }
515
516
10.6k
    async fn execute_accepted(&mut self, request: ExecuteAccepted) -> Result<(), Error> {
517
10.6k
        self.send(SchedulerUpdate::ExecuteAccepted(request)).await
518
10.6k
    }
519
520
0
    async fn execute_declined(&mut self, request: ExecuteDeclined) -> Result<(), Error> {
521
0
        self.send(SchedulerUpdate::ExecuteDeclined(request)).await
522
0
    }
523
524
10.0k
    async fn execution_complete(&mut self, request: ExecuteComplete) -> Result<(), Error> {
525
10.0k
        self.send(SchedulerUpdate::ExecuteComplete(request)).await
526
10.0k
    }
527
}