Coverage Report

Created: 2026-09-18 20:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-service/src/worker_api_server.rs
Line
Count
Source
1
// Copyright 2024 The NativeLink Authors. 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
use core::convert::Into;
16
use core::pin::Pin;
17
use core::time::Duration;
18
use std::collections::HashMap;
19
use std::sync::Arc;
20
use std::time::{SystemTime, UNIX_EPOCH};
21
22
use futures::stream::unfold;
23
use futures::{Stream, StreamExt};
24
use nativelink_config::cas_server::WorkerApiConfig;
25
use nativelink_error::{make_err, Code, Error, ResultExt};
26
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::update_for_scheduler::Update;
27
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::worker_api_server::{
28
    WorkerApi, WorkerApiServer as Server,
29
};
30
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::{
31
    execute_result, ExecuteComplete, ExecuteResult, GoingAwayRequest, KeepAliveRequest, UpdateForScheduler, UpdateForWorker
32
};
33
use nativelink_scheduler::worker::Worker;
34
use nativelink_scheduler::worker_scheduler::WorkerScheduler;
35
use nativelink_util::background_spawn;
36
use nativelink_util::action_messages::{OperationId, WorkerId};
37
use nativelink_util::operation_state_manager::UpdateOperationType;
38
use nativelink_util::platform_properties::PlatformProperties;
39
use rand::RngCore;
40
use tokio::sync::mpsc;
41
use tokio::time::interval;
42
use tonic::{Response, Status};
43
use tracing::{debug, error, warn, instrument, Level};
44
use uuid::Uuid;
45
46
pub type ConnectWorkerStream =
47
    Pin<Box<dyn Stream<Item = Result<UpdateForWorker, Status>> + Send + Sync + 'static>>;
48
49
pub type NowFn = Box<dyn Fn() -> Result<Duration, Error> + Send + Sync>;
50
51
/// How often workers are told to kill operations the scheduler no longer
52
/// has executing on them, unless overridden by
53
/// `kill_revoked_operations_interval_s` in the worker API config.
54
const DEFAULT_KILL_REVOKED_OPERATIONS_INTERVAL_S: u64 = 5;
55
56
pub struct WorkerApiServer {
57
    scheduler: Arc<dyn WorkerScheduler>,
58
    now_fn: Arc<NowFn>,
59
    node_id: [u8; 6],
60
}
61
62
impl core::fmt::Debug for WorkerApiServer {
63
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64
0
        f.debug_struct("WorkerApiServer")
65
0
            .field("node_id", &self.node_id)
66
0
            .finish_non_exhaustive()
67
0
    }
68
}
69
70
impl WorkerApiServer {
71
0
    pub fn new(
72
0
        config: &WorkerApiConfig,
73
0
        schedulers: &HashMap<String, Arc<dyn WorkerScheduler>>,
74
0
    ) -> Result<Self, Error> {
75
0
        let node_id = {
76
0
            let mut out = [0; 6];
77
0
            rand::rng().fill_bytes(&mut out);
78
0
            out
79
        };
80
0
        let kill_revoked_enabled = !config.disable_kill_revoked_operations;
81
0
        let kill_revoked_interval_s = if config.kill_revoked_operations_interval_s == 0 {
82
0
            DEFAULT_KILL_REVOKED_OPERATIONS_INTERVAL_S
83
        } else {
84
0
            config.kill_revoked_operations_interval_s
85
        };
86
0
        for scheduler in schedulers.values() {
87
            // This will protect us from holding a reference to the scheduler forever in the
88
            // event our ExecutionServer dies. Our scheduler is a weak ref, so the spawn will
89
            // eventually see the Arc went away and return.
90
0
            let weak_scheduler = Arc::downgrade(scheduler);
91
0
            background_spawn!("worker_api_server", async move {
92
0
                let mut timeout_ticker = interval(Duration::from_secs(1));
93
0
                let mut kill_ticker = interval(Duration::from_secs(kill_revoked_interval_s));
94
                loop {
95
0
                    tokio::select! {
96
0
                        _ = timeout_ticker.tick() => {
97
0
                            let timestamp = SystemTime::now()
98
0
                                .duration_since(UNIX_EPOCH)
99
0
                                .expect("Error: system time is now behind unix epoch");
100
0
                            match weak_scheduler.upgrade() {
101
0
                                Some(scheduler) => {
102
0
                                    if let Err(err) =
103
0
                                        scheduler.remove_timedout_workers(timestamp.as_secs()).await
104
                                    {
105
0
                                        error!(?err, "Failed to remove_timedout_workers",);
106
0
                                    }
107
                                }
108
                                // If we fail to upgrade, our service is probably destroyed, so return.
109
0
                                None => return,
110
                            }
111
                        }
112
0
                        _ = kill_ticker.tick(), if kill_revoked_enabled => {
113
0
                            match weak_scheduler.upgrade() {
114
0
                                Some(scheduler) => {
115
0
                                    if let Err(err) = scheduler.kill_revoked_operations().await {
116
0
                                        error!(?err, "Failed to kill_revoked_operations");
117
0
                                    }
118
                                }
119
0
                                None => return,
120
                            }
121
                        }
122
                    }
123
                }
124
0
            });
125
        }
126
127
0
        Self::new_with_now_fn(
128
0
            config,
129
0
            schedulers,
130
0
            Box::new(move || {
131
0
                SystemTime::now().duration_since(UNIX_EPOCH).map_err(|err| {
132
0
                    Error::from_std_err(Code::Internal, &err)
133
0
                        .append("System time is now behind unix epoch")
134
0
                })
135
0
            }),
136
0
            node_id,
137
        )
138
0
    }
139
140
    /// Same as `new()`, but you can pass a custom `now_fn`, that returns a Duration since `UNIX_EPOCH`
141
    /// representing the current time. Used mostly in  unit tests.
142
7
    pub fn new_with_now_fn(
143
7
        config: &WorkerApiConfig,
144
7
        schedulers: &HashMap<String, Arc<dyn WorkerScheduler>>,
145
7
        now_fn: NowFn,
146
7
        node_id: [u8; 6],
147
7
    ) -> Result<Self, Error> {
148
7
        let scheduler = schedulers
149
7
            .get(&config.scheduler)
150
7
            .err_tip(|| 
{0
151
0
                format!(
152
                    "Scheduler needs config for '{}' because it exists in worker_api",
153
                    config.scheduler
154
                )
155
0
            })?
156
7
            .clone();
157
7
        Ok(Self {
158
7
            scheduler,
159
7
            now_fn: Arc::new(now_fn),
160
7
            node_id,
161
7
        })
162
7
    }
163
164
0
    pub fn into_service(self) -> Server<Self> {
165
0
        Server::new(self)
166
0
    }
167
168
7
    async fn inner_connect_worker(
169
7
        &self,
170
7
        mut update_stream: impl Stream<Item = Result<UpdateForScheduler, Status>>
171
7
        + Unpin
172
7
        + Send
173
7
        + 'static,
174
7
    ) -> Result<Response<ConnectWorkerStream>, Error> {
175
7
        let first_message = update_stream
176
7
            .next()
177
7
            .await
178
7
            .err_tip(|| "Missing first message for connect_worker")
?0
179
7
            .err_tip(|| "Error reading first message for connect_worker")
?0
;
180
7
        let Some(Update::ConnectWorkerRequest(connect_worker_request)) = first_message.update
181
        else {
182
0
            return Err(make_err!(
183
0
                Code::Internal,
184
0
                "First message was not a ConnectWorkerRequest"
185
0
            ));
186
        };
187
188
7
        let (tx, rx) = mpsc::unbounded_channel();
189
190
        // First convert our proto platform properties into one our scheduler understands.
191
7
        let platform_properties = {
192
7
            let mut platform_properties = PlatformProperties::default();
193
7
            for 
property0
in connect_worker_request.properties {
194
0
                let platform_property_value = self
195
0
                    .scheduler
196
0
                    .get_platform_property_manager()
197
0
                    .make_prop_value(&property.name, &property.value)
198
0
                    .err_tip(|| "Bad Property during connect_worker()")?;
199
0
                platform_properties
200
0
                    .properties
201
0
                    .insert(property.name.clone(), platform_property_value);
202
            }
203
7
            platform_properties
204
        };
205
206
        // Now register the worker with the scheduler.
207
7
        let worker_id = {
208
7
            let worker_id = WorkerId(format!(
209
7
                "{}{}",
210
7
                connect_worker_request.worker_id_prefix,
211
7
                Uuid::now_v6(&self.node_id).hyphenated()
212
7
            ));
213
7
            let worker = Worker::new(
214
7
                worker_id.clone(),
215
7
                platform_properties,
216
7
                tx,
217
7
                (self.now_fn)()
?0
.as_secs(),
218
7
                connect_worker_request.max_inflight_tasks,
219
            );
220
7
            self.scheduler
221
7
                .add_worker(worker)
222
7
                .await
223
7
                .err_tip(|| "Failed to add worker in inner_connect_worker()")
?0
;
224
7
            worker_id
225
        };
226
227
7
        WorkerConnection::start(
228
7
            self.scheduler.clone(),
229
7
            self.now_fn.clone(),
230
7
            worker_id.clone(),
231
7
            update_stream,
232
        );
233
234
7
        Ok(Response::new(Box::pin(unfold(
235
7
            (rx, worker_id),
236
9
            move |state| async move {
237
9
                let (mut rx, worker_id) = state;
238
9
                if let Some(update_for_worker) = rx.recv().await {
239
9
                    return Some((Ok(update_for_worker), (rx, worker_id)));
240
0
                }
241
0
                warn!(
242
                    ?worker_id,
243
                    "UpdateForWorker channel was closed, thus closing connection to worker node",
244
                );
245
246
0
                None
247
18
            },
248
        ))))
249
7
    }
250
251
7
    pub async fn inner_connect_worker_for_testing(
252
7
        &self,
253
7
        update_stream: impl Stream<Item = Result<UpdateForScheduler, Status>> + Unpin + Send + 'static,
254
7
    ) -> Result<Response<ConnectWorkerStream>, Error> {
255
7
        self.inner_connect_worker(update_stream).await
256
7
    }
257
}
258
259
#[tonic::async_trait]
260
impl WorkerApi for WorkerApiServer {
261
    type ConnectWorkerStream = ConnectWorkerStream;
262
263
    #[instrument(
264
        err,
265
        level = Level::ERROR,
266
        skip_all,
267
        fields(request = ?grpc_request.get_ref())
268
    )]
269
    async fn connect_worker(
270
        &self,
271
        grpc_request: tonic::Request<tonic::Streaming<UpdateForScheduler>>,
272
    ) -> Result<Response<Self::ConnectWorkerStream>, Status> {
273
        let resp = self
274
            .inner_connect_worker(grpc_request.into_inner())
275
            .await
276
            .map_err(Into::into);
277
        if resp.is_ok() {
278
            debug!(return = "Ok(<stream>)");
279
        }
280
        resp
281
    }
282
}
283
284
struct WorkerConnection {
285
    scheduler: Arc<dyn WorkerScheduler>,
286
    now_fn: Arc<NowFn>,
287
    worker_id: WorkerId,
288
}
289
290
impl WorkerConnection {
291
7
    fn start(
292
7
        scheduler: Arc<dyn WorkerScheduler>,
293
7
        now_fn: Arc<NowFn>,
294
7
        worker_id: WorkerId,
295
7
        mut connection: impl Stream<Item = Result<UpdateForScheduler, Status>> + Unpin + Send + 'static,
296
7
    ) {
297
7
        let instance = Self {
298
7
            scheduler,
299
7
            now_fn,
300
7
            worker_id,
301
7
        };
302
303
7
        background_spawn!("worker_api", async move 
{2
304
2
            let mut had_going_away = false;
305
3
            while let Some(
maybe_update2
) = connection.next().await {
306
2
                let update = match maybe_update.map(|u| u.update) {
307
2
                    Ok(Some(update)) => update,
308
                    Ok(None) => {
309
0
                        tracing::warn!(worker_id=?instance.worker_id, "Empty update");
310
0
                        continue;
311
                    }
312
0
                    Err(err) => {
313
0
                        tracing::warn!(worker_id=?instance.worker_id, ?err, "Error from worker");
314
0
                        break;
315
                    }
316
                };
317
2
                let 
result1
= match update {
318
0
                    Update::ConnectWorkerRequest(_connect_worker_request) => Err(make_err!(
319
0
                        Code::Internal,
320
0
                        "Got ConnectWorkerRequest after initial message for {}",
321
0
                        instance.worker_id
322
0
                    )),
323
1
                    Update::KeepAliveRequest(keep_alive_request) => {
324
1
                        instance.inner_keep_alive(keep_alive_request).await
325
                    }
326
0
                    Update::GoingAwayRequest(going_away_request) => {
327
0
                        had_going_away = true;
328
0
                        instance.inner_going_away(going_away_request).await
329
                    }
330
1
                    Update::ExecuteResult(execute_result) => {
331
1
                        instance.inner_execution_response(execute_result).await
332
                    }
333
0
                    Update::ExecuteComplete(execute_complete) => {
334
0
                        instance.execution_complete(execute_complete).await
335
                    }
336
                };
337
1
                if let Err(
err0
) = result {
338
0
                    tracing::warn!(worker_id=?instance.worker_id, ?err, "Error processing worker message");
339
1
                }
340
            }
341
0
            tracing::debug!(worker_id=?instance.worker_id, "Update for scheduler dropped");
342
0
            if !had_going_away {
343
0
                drop(instance.scheduler.remove_worker(&instance.worker_id).await);
344
0
            }
345
0
        });
346
7
    }
347
348
1
    async fn inner_keep_alive(&self, _keep_alive_request: KeepAliveRequest) -> Result<(), Error> {
349
1
        self.scheduler
350
1
            .worker_keep_alive_received(&self.worker_id, (self.now_fn)()
?0
.as_secs())
351
1
            .await
352
1
            .err_tip(|| "Could not process keep_alive from worker in inner_keep_alive()")
?0
;
353
1
        Ok(())
354
1
    }
355
356
0
    async fn inner_going_away(&self, _going_away_request: GoingAwayRequest) -> Result<(), Error> {
357
0
        self.scheduler
358
0
            .remove_worker(&self.worker_id)
359
0
            .await
360
0
            .err_tip(|| "While calling WorkerApiServer::inner_going_away")?;
361
0
        Ok(())
362
0
    }
363
364
1
    async fn inner_execution_response(&self, execute_result: ExecuteResult) -> Result<(), Error> {
365
1
        let operation_id = OperationId::from(execute_result.operation_id.clone());
366
367
1
        if let Some(
resource_usage0
) = execute_result.resource_usage {
368
0
            self.scheduler
369
0
                .record_action_resource_usage(&self.worker_id, &operation_id, resource_usage)
370
0
                .await
371
0
                .err_tip(|| {
372
0
                    format!("Failed to record resource usage for operation {operation_id}")
373
0
                })?;
374
1
        }
375
376
1
        match execute_result
377
1
            .result
378
1
            .err_tip(|| "Expected result to exist in ExecuteResult")
?0
379
        {
380
1
            execute_result::Result::ExecuteResponse(finished_result) => {
381
1
                let action_stage = finished_result
382
1
                    .try_into()
383
1
                    .err_tip(|| "Failed to convert ExecuteResponse into an ActionStage")
?0
;
384
1
                self.scheduler
385
1
                    .update_action(
386
1
                        &self.worker_id,
387
1
                        &operation_id,
388
1
                        UpdateOperationType::UpdateWithActionStage(action_stage),
389
1
                    )
390
1
                    .await
391
0
                    .err_tip(|| format!("Failed to operation {operation_id}"))?;
392
            }
393
0
            execute_result::Result::InternalError(e) => {
394
0
                self.scheduler
395
0
                    .update_action(
396
0
                        &self.worker_id,
397
0
                        &operation_id,
398
0
                        UpdateOperationType::UpdateWithError(e.into()),
399
0
                    )
400
0
                    .await
401
0
                    .err_tip(|| format!("Failed to operation {operation_id}"))?;
402
            }
403
        }
404
0
        Ok(())
405
0
    }
406
407
0
    async fn execution_complete(&self, execute_complete: ExecuteComplete) -> Result<(), Error> {
408
0
        let operation_id = OperationId::from(execute_complete.operation_id);
409
0
        self.scheduler
410
0
            .update_action(
411
0
                &self.worker_id,
412
0
                &operation_id,
413
0
                UpdateOperationType::ExecutionComplete,
414
0
            )
415
0
            .await
416
0
            .err_tip(|| format!("Failed to operation {operation_id}"))?;
417
0
        Ok(())
418
0
    }
419
}