Coverage Report

Created: 2026-08-05 11:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-worker/src/local_worker.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::hash::BuildHasher;
16
use core::pin::Pin;
17
use core::str;
18
use core::sync::atomic::{AtomicU64, Ordering};
19
use core::time::Duration;
20
use std::borrow::Cow;
21
use std::collections::HashMap;
22
use std::env;
23
use std::process::Stdio;
24
use std::sync::{Arc, Weak};
25
26
use futures::future::BoxFuture;
27
use futures::stream::FuturesUnordered;
28
use futures::{Future, FutureExt, StreamExt, TryFutureExt, select};
29
use nativelink_config::cas_server::{EnvironmentSource, LocalWorkerConfig};
30
use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err};
31
use nativelink_metric::{MetricsComponent, RootMetricsComponent};
32
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::update_for_worker::Update;
33
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::worker_api_client::WorkerApiClient;
34
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::{
35
    ActionResourceUsage, ExecuteComplete, ExecuteResult, GoingAwayRequest, KeepAliveRequest,
36
    UpdateForWorker, execute_result,
37
};
38
use nativelink_store::fast_slow_store::FastSlowStore;
39
use nativelink_util::action_messages::{ActionResult, ActionStage, OperationId};
40
use nativelink_util::common::fs;
41
use nativelink_util::digest_hasher::DigestHasherFunc;
42
use nativelink_util::metrics_utils::{AsyncCounterWrapper, CounterWithTime};
43
use nativelink_util::shutdown_guard::ShutdownGuard;
44
use nativelink_util::store_trait::Store;
45
use nativelink_util::{spawn, tls_utils};
46
use opentelemetry::context::Context;
47
use tokio::sync::{broadcast, mpsc};
48
use tokio::{process, time};
49
use tokio_stream::wrappers::UnboundedReceiverStream;
50
use tonic::Streaming;
51
use tracing::{Level, debug, error, event, info, info_span, instrument, trace, warn};
52
53
use crate::running_actions_manager::{
54
    ExecutionConfiguration, Metrics as RunningActionManagerMetrics, RunningAction,
55
    RunningActionsManager, RunningActionsManagerArgs, RunningActionsManagerImpl,
56
};
57
use crate::worker_api_client_wrapper::{WorkerApiClientTrait, WorkerApiClientWrapper};
58
use crate::worker_utils::make_connect_worker_request;
59
60
/// Amount of time to wait if we have actions in transit before we try to
61
/// consider an error to have occurred.
62
const ACTIONS_IN_TRANSIT_TIMEOUT_S: f32 = 10.;
63
64
/// If we lose connection to the worker api server we will wait this many seconds
65
/// before trying to connect.
66
const CONNECTION_RETRY_DELAY_S: f32 = 0.5;
67
68
/// Default endpoint timeout. If this value gets modified the documentation in
69
/// `cas_server.rs` must also be updated.
70
const DEFAULT_ENDPOINT_TIMEOUT_S: f32 = 5.;
71
72
/// Default maximum amount of time a task is allowed to run for.
73
/// If this value gets modified the documentation in `cas_server.rs` must also be updated.
74
const DEFAULT_MAX_ACTION_TIMEOUT: Duration = Duration::from_mins(20);
75
const DEFAULT_MAX_UPLOAD_TIMEOUT: Duration = Duration::from_mins(10);
76
const DEFAULT_MAX_CLEANUP_WAIT: Duration = Duration::from_secs(30);
77
const DEFAULT_MAX_CLEANUP_BACKOFF: Duration = Duration::from_millis(500);
78
79
struct FinishedActionResult {
80
    action_result: ActionResult,
81
    resource_usage: Option<ActionResourceUsage>,
82
}
83
84
struct LocalWorkerImpl<'a, T: WorkerApiClientTrait + 'static, U: RunningActionsManager> {
85
    config: &'a LocalWorkerConfig,
86
    // According to the tonic documentation it is a cheap operation to clone this.
87
    grpc_client: T,
88
    worker_id: String,
89
    running_actions_manager: Arc<U>,
90
    // Number of actions that have been received in `Update::StartAction`, but
91
    // not yet processed by running_actions_manager's spawn. This number should
92
    // always be zero if there are no actions running and no actions being waited
93
    // on by the scheduler.
94
    actions_in_transit: Arc<AtomicU64>,
95
    metrics: Arc<Metrics>,
96
}
97
98
7
pub async fn preconditions_met<H: BuildHasher + Sync>(
99
7
    precondition_script: Option<String>,
100
7
    extra_envs: &HashMap<String, String, H>,
101
7
) -> Result<(), Error> {
102
7
    let Some(
precondition_script2
) = &precondition_script else {
103
        // No script means we are always ok to proceed.
104
5
        return Ok(());
105
    };
106
    // TODO: Might want to pass some information about the command to the
107
    //       script, but at this point it's not even been downloaded yet,
108
    //       so that's not currently possible.  Perhaps we'll move this in
109
    //       future to pass useful information through?  Or perhaps we'll
110
    //       have a pre-condition and a pre-execute script instead, although
111
    //       arguably entrypoint already gives us that.
112
113
2
    let maybe_split_cmd = shlex::split(precondition_script);
114
2
    let (command, args) = match &maybe_split_cmd {
115
2
        Some(split_cmd) => (&split_cmd[0], &split_cmd[1..]),
116
        None => {
117
0
            return Err(make_input_err!(
118
0
                "Could not parse the value of precondition_script: '{}'",
119
0
                precondition_script,
120
0
            ));
121
        }
122
    };
123
124
2
    let precondition_process = process::Command::new(command)
125
2
        .args(args)
126
2
        .kill_on_drop(true)
127
2
        .stdin(Stdio::null())
128
2
        .stdout(Stdio::piped())
129
2
        .stderr(Stdio::null())
130
2
        .env_clear()
131
2
        .envs(extra_envs)
132
2
        .spawn()
133
2
        .err_tip(|| 
format!0
("Could not execute precondition command {precondition_script:?}"))
?0
;
134
2
    let output = precondition_process.wait_with_output().await
?0
;
135
2
    let stdout = str::from_utf8(&output.stdout).unwrap_or("");
136
2
    trace!(status = %output.status, %stdout, "Preconditions script returned");
137
2
    if output.status.code() == Some(0) {
138
1
        Ok(())
139
    } else {
140
1
        Err(make_err!(
141
1
            Code::ResourceExhausted,
142
1
            "Preconditions script returned status {} - {}",
143
1
            output.status,
144
1
            stdout
145
1
        ))
146
    }
147
7
}
148
149
impl<'a, T: WorkerApiClientTrait + 'static, U: RunningActionsManager> LocalWorkerImpl<'a, T, U> {
150
9
    fn new(
151
9
        config: &'a LocalWorkerConfig,
152
9
        grpc_client: T,
153
9
        worker_id: String,
154
9
        running_actions_manager: Arc<U>,
155
9
        metrics: Arc<Metrics>,
156
9
    ) -> Self {
157
9
        Self {
158
9
            config,
159
9
            grpc_client,
160
9
            worker_id,
161
9
            running_actions_manager,
162
9
            // Number of actions that have been received in `Update::StartAction`, but
163
9
            // not yet processed by running_actions_manager's spawn. This number should
164
9
            // always be zero if there are no actions running and no actions being waited
165
9
            // on by the scheduler.
166
9
            actions_in_transit: Arc::new(AtomicU64::new(0)),
167
9
            metrics,
168
9
        }
169
9
    }
170
171
    /// Starts a background spawn/thread that will send a message to the server every `timeout / 2`.
172
9
    async fn start_keep_alive(&self) -> Result<(), Error> 
{8
173
        // According to tonic's documentation this call should be cheap and is the same stream.
174
8
        let mut grpc_client = self.grpc_client.clone();
175
8
        let timeout = self
176
8
            .config
177
8
            .worker_api_endpoint
178
8
            .timeout
179
8
            .unwrap_or(DEFAULT_ENDPOINT_TIMEOUT_S);
180
181
8
        info!(timeout, "Started KeepAlive");
182
183
        // We always send 2 keep alive requests per timeout. Http2 should manage most of our
184
        // timeout issues, this is a secondary check to ensure we can still send data.
185
8
        let mut interval = time::interval(Duration::from_secs_f32(timeout) / 2);
186
8
        interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
187
188
        // Skip the first interval as it happens immediately and we don't need a keep alive until timeout/2 has passed
189
8
        interval.tick().await;
190
191
        // Explicitly spawn the keep alive loop so it goes onto a different thread from the execute commands
192
1
        drop(
193
7
            spawn!("keep alives", async move {
194
                loop {
195
8
                    interval.tick().await;
196
2
                    if let Err(
e1
) = grpc_client.keep_alive(KeepAliveRequest {}).await {
197
1
                        error!(?e, "Failed to send KeepAlive in LocalWorker");
198
1
                        return;
199
1
                    }
200
1
                    debug!("Sent KeepAlive");
201
                }
202
1
            })
203
7
            .await,
204
        );
205
1
        Ok(())
206
1
    }
207
208
9
    async fn run(
209
9
        &self,
210
9
        update_for_worker_stream: Streaming<UpdateForWorker>,
211
9
        shutdown_rx: &mut broadcast::Receiver<ShutdownGuard>,
212
9
    ) -> Result<(), Error> {
213
        // This big block of logic is designed to help simplify upstream components. Upstream
214
        // components can write standard futures that return a `Result<(), Error>` and this block
215
        // will forward the error up to the client and disconnect from the scheduler.
216
        // It is a common use case that an item sent through update_for_worker_stream will always
217
        // have a response but the response will be triggered through a callback to the scheduler.
218
        // This can be quite tricky to manage, so what we have done here is given access to a
219
        // `futures` variable which because this is in a single thread as well as a channel that you
220
        // send a future into that makes it into the `futures` variable.
221
        // This means that if you want to perform an action based on the result of the future
222
        // you use the `.map()` method and the new action will always come to live in this spawn,
223
        // giving mutable access to stuff in this struct.
224
        // NOTE: If you ever return from this function it will disconnect from the scheduler.
225
9
        let mut futures = FuturesUnordered::new();
226
9
        futures.push(self.start_keep_alive().boxed());
227
228
9
        let (add_future_channel, add_future_rx) = mpsc::unbounded_channel();
229
9
        let mut add_future_rx = UnboundedReceiverStream::new(add_future_rx).fuse();
230
231
9
        let mut update_for_worker_stream = update_for_worker_stream.fuse();
232
        // A notify which is triggered every time actions_in_flight is subtracted.
233
9
        let actions_notify = Arc::new(tokio::sync::Notify::new());
234
        // A counter of actions that are in-flight, this is similar to actions_in_transit but
235
        // includes the AC upload and notification to the scheduler.
236
9
        let actions_in_flight = Arc::new(AtomicU64::new(0));
237
        // Set to true when shutting down, this stops any new StartAction.
238
9
        let mut shutting_down = false;
239
240
        loop {
241
28
            select! {
242
28
                
maybe_update10
= update_for_worker_stream.next() => if
!shutting_down10
||
maybe_update0
.
is_some0
() {
243
10
                    match maybe_update
244
10
                        .err_tip(|| "UpdateForWorker stream closed early")
?2
245
8
                        .err_tip(|| "Got error in UpdateForWorker stream")
?0
246
                        .update
247
8
                        .err_tip(|| "Expected update to exist in UpdateForWorker")
?0
248
                    {
249
                        Update::ConnectionResult(_) => {
250
0
                            return Err(make_input_err!(
251
0
                                "Got ConnectionResult in LocalWorker::run which should never happen"
252
0
                            ));
253
                        }
254
                        // TODO(palfrey) We should possibly do something with this notification.
255
0
                        Update::Disconnect(()) => {
256
0
                            self.metrics.disconnects_received.inc();
257
0
                        }
258
0
                        Update::KeepAlive(()) => {
259
0
                            self.metrics.keep_alives_received.inc();
260
0
                        }
261
1
                        Update::KillOperationRequest(kill_operation_request) => {
262
1
                            let operation_id = OperationId::from(kill_operation_request.operation_id);
263
1
                            if let Err(
err0
) = self.running_actions_manager.kill_operation(&operation_id).await {
264
0
                                error!(
265
                                    %operation_id,
266
                                    ?err,
267
                                    "Failed to send kill request for operation"
268
                                );
269
1
                            }
270
                        }
271
7
                        Update::StartAction(start_execute) => {
272
                            // Don't accept any new requests if we're shutting down.
273
7
                            if shutting_down {
274
0
                                if let Some(instance_name) = start_execute.execute_request.map(|request| request.instance_name) {
275
0
                                    self.grpc_client.clone().execution_response(
276
0
                                        ExecuteResult{
277
0
                                            instance_name,
278
0
                                            operation_id: start_execute.operation_id,
279
0
                                            result: Some(execute_result::Result::InternalError(make_err!(Code::ResourceExhausted, "Worker shutting down").into())),
280
0
                                            resource_usage: None,
281
0
                                        }
282
0
                                    ).await?;
283
0
                                }
284
0
                                continue;
285
7
                            }
286
287
7
                            self.metrics.start_actions_received.inc();
288
289
7
                            let execute_request = start_execute.execute_request.as_ref();
290
7
                            let operation_id = start_execute.operation_id.clone();
291
7
                            let operation_id_to_log = operation_id.clone();
292
7
                            let maybe_instance_name = execute_request.map(|v| v.instance_name.clone());
293
7
                            let action_digest = execute_request.and_then(|v| v.action_digest.clone());
294
7
                            let digest_hasher = execute_request
295
7
                                .ok_or_else(|| 
make_input_err!0
("Expected execute_request to be set"))
296
7
                                .and_then(|v| DigestHasherFunc::try_from(v.digest_function))
297
7
                                .err_tip(|| "In LocalWorkerImpl::new()")
?0
;
298
299
7
                            let start_action_fut = {
300
7
                                let precondition_script_cfg = self.config.experimental_precondition_script.clone();
301
7
                                let mut extra_envs: HashMap<String, String> = HashMap::new();
302
7
                                if let Some(
ref additional_environment0
) = self.config.additional_environment {
303
0
                                    for (name, source) in additional_environment {
304
0
                                        let value = match source {
305
0
                                            EnvironmentSource::Property(property) => start_execute
306
0
                                                .platform.as_ref().and_then(|p|p.properties.iter().find(|pr| &pr.name == property))
307
0
                                                .map_or_else(|| Cow::Borrowed(""), |v| Cow::Borrowed(v.value.as_str())),
308
0
                                            EnvironmentSource::Value(value) => Cow::Borrowed(value.as_str()),
309
0
                                            EnvironmentSource::FromEnvironment => Cow::Owned(env::var(name).unwrap_or_default()),
310
0
                                            other => {
311
0
                                                debug!(?other, "Worker doesn't support this type of additional environment");
312
0
                                                continue;
313
                                            }
314
                                        };
315
0
                                        extra_envs.insert(name.clone(), value.into_owned());
316
                                    }
317
7
                                }
318
7
                                let actions_in_transit = self.actions_in_transit.clone();
319
7
                                let worker_id = self.worker_id.clone();
320
7
                                let running_actions_manager = self.running_actions_manager.clone();
321
7
                                let mut grpc_client = self.grpc_client.clone();
322
7
                                let complete = ExecuteComplete {
323
7
                                    operation_id: operation_id.clone(),
324
7
                                };
325
7
                                self.metrics.clone().wrap(move |metrics| async move 
{6
326
6
                                    metrics.preconditions.wrap(preconditions_met(precondition_script_cfg, &extra_envs))
327
6
                                    .and_then(|()| 
running_actions_manager5
.
create_and_add_action5
(
worker_id5
,
start_execute5
))
328
6
                                    .map(move |r| {
329
                                        // Now that we either failed or registered our action, we can
330
                                        // consider the action to no longer be in transit.
331
6
                                        actions_in_transit.fetch_sub(1, Ordering::Release);
332
6
                                        r
333
6
                                    })
334
6
                                    .and_then(|action| 
{5
335
5
                                        debug!(
336
5
                                            operation_id = %action.get_operation_id(),
337
                                            "Received request to run action"
338
                                        );
339
5
                                        action
340
5
                                            .clone()
341
5
                                            .prepare_action()
342
5
                                            .and_then(RunningAction::execute)
343
5
                                            .and_then(|result| async move 
{2
344
                                                // Notify that execution has completed so it can schedule a new action.
345
2
                                                drop(grpc_client.execution_complete(complete).await);
346
2
                                                Ok(result)
347
4
                                            })
348
5
                                            .and_then(RunningAction::upload_results)
349
5
                                            .and_then(|action| async move 
{2
350
2
                                                let resource_usage = action.resource_usage();
351
2
                                                let action_result = action.get_finished_result().await
?0
;
352
2
                                                Ok(FinishedActionResult {
353
2
                                                    action_result,
354
2
                                                    resource_usage,
355
2
                                                })
356
4
                                            })
357
                                            // Note: We need ensure we run cleanup even if one of the other steps fail.
358
5
                                            .then(|result| async move 
{4
359
4
                                                if let Err(
e0
) = action.cleanup().await {
360
0
                                                    return Result::<FinishedActionResult, Error>::Err(e).merge(result);
361
4
                                                }
362
4
                                                result
363
8
                                            })
364
6
                                    
}5
).await
365
11
                                })
366
                            };
367
368
7
                            let make_publish_future = {
369
7
                                let mut grpc_client = self.grpc_client.clone();
370
371
7
                                let running_actions_manager = self.running_actions_manager.clone();
372
7
                                let worker_id = self.worker_id.clone();
373
5
                                move |res: Result<FinishedActionResult, Error>| async move {
374
5
                                    let instance_name = maybe_instance_name
375
5
                                        .err_tip(|| "`instance_name` could not be resolved; this is likely an internal error in local_worker.")
?0
;
376
5
                                    match res {
377
2
                                        Ok(FinishedActionResult { mut action_result, resource_usage }) => {
378
                                            // Save in the action cache before notifying the scheduler that we've completed.
379
2
                                            if let Some(digest_info) = action_digest.clone().and_then(|action_digest| action_digest.try_into().ok()) &&
380
2
                                                let Err(
err0
) = running_actions_manager.cache_action_result(digest_info, &mut action_result, digest_hasher).await {
381
0
                                                    error!(
382
                                                        ?err,
383
                                                        ?action_digest,
384
                                                        "Error saving action in store",
385
                                                    );
386
2
                                                }
387
2
                                            let action_stage = ActionStage::Completed(action_result);
388
2
                                            let resource_usage = resource_usage.map(|mut resource_usage| 
{0
389
0
                                                resource_usage.operation_id.clone_from(&operation_id);
390
0
                                                resource_usage.worker_id.clone_from(&worker_id);
391
0
                                                resource_usage
392
0
                                            });
393
2
                                            grpc_client.execution_response(
394
2
                                                ExecuteResult{
395
2
                                                    instance_name,
396
2
                                                    operation_id,
397
2
                                                    result: Some(execute_result::Result::ExecuteResponse(action_stage.into())),
398
2
                                                    resource_usage,
399
2
                                                }
400
2
                                            )
401
2
                                            .await
402
0
                                            .err_tip(|| "Error while calling execution_response")?;
403
                                        },
404
3
                                        Err(e) => {
405
3
                                            let is_cas_blob_missing = e.code == Code::NotFound
406
2
                                                && e.message_string().contains("not found in either fast or slow store");
407
3
                                            if is_cas_blob_missing {
408
1
                                                warn!(
409
                                                    ?e,
410
                                                    "Missing CAS inputs during prepare_action, returning FAILED_PRECONDITION"
411
                                                );
412
1
                                                let action_result = ActionResult {
413
1
                                                    error: Some(make_err!(
414
1
                                                        Code::FailedPrecondition,
415
1
                                                        "{}",
416
1
                                                        e.message_string()
417
1
                                                    )),
418
1
                                                    ..ActionResult::default()
419
1
                                                };
420
1
                                                let action_stage = ActionStage::Completed(action_result);
421
1
                                                grpc_client.execution_response(ExecuteResult{
422
1
                                                    instance_name,
423
1
                                                    operation_id,
424
1
                                                    result: Some(execute_result::Result::ExecuteResponse(action_stage.into())),
425
1
                                                    resource_usage: None,
426
1
                                                }).await.
err_tip0
(|| "Error calling execution_response with missing inputs")
?0
;
427
                                            } else {
428
2
                                                grpc_client.execution_response(ExecuteResult{
429
2
                                                    instance_name,
430
2
                                                    operation_id,
431
2
                                                    result: Some(execute_result::Result::InternalError(e.into())),
432
2
                                                    resource_usage: None,
433
2
                                                }).await.
err_tip0
(|| "Error calling execution_response with error")
?0
;
434
                                            }
435
                                        },
436
                                    }
437
0
                                    Ok(())
438
5
                                }
439
                            };
440
441
7
                            self.actions_in_transit.fetch_add(1, Ordering::Release);
442
443
7
                            let add_future_channel = add_future_channel.clone();
444
445
7
                            info_span!(
446
                                "worker_start_action_ctx",
447
                                operation_id = operation_id_to_log,
448
7
                                digest_function = %digest_hasher.to_string(),
449
7
                            ).in_scope(|| {
450
7
                                let _guard = Context::current_with_value(digest_hasher)
451
7
                                    .attach();
452
453
7
                                let actions_in_flight = actions_in_flight.clone();
454
7
                                let actions_notify = actions_notify.clone();
455
7
                                let actions_in_flight_fail = actions_in_flight.clone();
456
7
                                let actions_notify_fail = actions_notify.clone();
457
7
                                actions_in_flight.fetch_add(1, Ordering::Release);
458
459
7
                                futures.push(
460
7
                                    spawn!("worker_start_action", start_action_fut).map(move |res| 
{5
461
5
                                        let res = res.err_tip(|| "Failed to launch spawn")
?0
;
462
5
                                        if let Err(
err3
) = &res {
463
3
                                            error!(?err, "Error executing action");
464
2
                                        }
465
5
                                        add_future_channel
466
5
                                            .send(make_publish_future(res).then(move |res| 
{0
467
0
                                                actions_in_flight.fetch_sub(1, Ordering::Release);
468
0
                                                actions_notify.notify_one();
469
0
                                                core::future::ready(res)
470
5
                                            
}0
).boxed())
471
5
                                            .map_err(|err|
472
0
                                                Error::from_std_err(Code::Internal, &err).append("LocalWorker could not send future")
473
0
                                                )?;
474
5
                                        Ok(())
475
5
                                    })
476
7
                                    .or_else(move |err| 
{0
477
                                        // If the make_publish_future is not run we still need to notify.
478
0
                                        actions_in_flight_fail.fetch_sub(1, Ordering::Release);
479
0
                                        actions_notify_fail.notify_one();
480
0
                                        core::future::ready(Err(err))
481
0
                                    })
482
7
                                    .boxed()
483
                                );
484
7
                            });
485
                        }
486
                    }
487
0
                },
488
28
                
res5
= add_future_rx.next() => {
489
5
                    let fut = res.err_tip(|| "New future stream receives should never be closed")
?0
;
490
5
                    futures.push(fut);
491
                },
492
28
                
res7
= futures.next() =>
res7
.
err_tip7
(|| "Keep-alive should always pending. Likely unable to send data to scheduler")
?1
?0
,
493
28
                
complete_msg0
= shutdown_rx.recv().fuse() => {
494
0
                    warn!("Worker loop received shutdown signal. Shutting down worker...",);
495
0
                    let mut grpc_client = self.grpc_client.clone();
496
0
                    let shutdown_guard = complete_msg.map_err(|e|
497
0
                        Error::from_std_err(Code::Internal, &e).append("Failed to receive shutdown message"))?;
498
0
                    let actions_in_flight = actions_in_flight.clone();
499
0
                    let actions_notify = actions_notify.clone();
500
0
                    let shutdown_future = async move {
501
                        // Wait for in-flight operations to be fully completed.
502
0
                        while actions_in_flight.load(Ordering::Acquire) > 0 {
503
0
                            actions_notify.notified().await;
504
                        }
505
                        // Sending this message immediately evicts all jobs from
506
                        // this worker, of which there should be none.
507
0
                        if let Err(e) = grpc_client.going_away(GoingAwayRequest {}).await {
508
0
                            error!("Failed to send GoingAwayRequest: {e}",);
509
0
                            return Err(e);
510
0
                        }
511
                        // Allow shutdown to occur now.
512
0
                        drop(shutdown_guard);
513
0
                        Ok::<(), Error>(())
514
0
                    };
515
0
                    futures.push(shutdown_future.boxed());
516
0
                    shutting_down = true;
517
                },
518
            };
519
        }
520
        // Unreachable.
521
3
    }
522
}
523
524
type ConnectionFactory<T> = Box<dyn Fn() -> BoxFuture<'static, Result<T, Error>> + Send + Sync>;
525
526
pub struct LocalWorker<T: WorkerApiClientTrait + 'static, U: RunningActionsManager> {
527
    config: Arc<LocalWorkerConfig>,
528
    running_actions_manager: Arc<U>,
529
    connection_factory: ConnectionFactory<T>,
530
    sleep_fn: Option<Box<dyn Fn(Duration) -> BoxFuture<'static, ()> + Send + Sync>>,
531
    metrics: Arc<Metrics>,
532
}
533
534
impl<
535
    T: WorkerApiClientTrait + core::fmt::Debug + 'static,
536
    U: RunningActionsManager + core::fmt::Debug,
537
> core::fmt::Debug for LocalWorker<T, U>
538
{
539
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
540
0
        f.debug_struct("LocalWorker")
541
0
            .field("config", &self.config)
542
0
            .field("running_actions_manager", &self.running_actions_manager)
543
0
            .field("metrics", &self.metrics)
544
0
            .finish_non_exhaustive()
545
0
    }
546
}
547
548
/// Creates a new `LocalWorker`. The `cas_store` must be an instance of
549
/// `FastSlowStore` and will be checked at runtime.
550
2
pub async fn new_local_worker(
551
2
    config: Arc<LocalWorkerConfig>,
552
2
    cas_store: Store,
553
2
    ac_store: Option<Store>,
554
2
    historical_store: Store,
555
2
) -> Result<LocalWorker<WorkerApiClientWrapper, RunningActionsManagerImpl>, Error> {
556
2
    let fast_slow_store = cas_store
557
2
        .downcast_ref::<FastSlowStore>(None)
558
2
        .err_tip(|| "Expected store for LocalWorker's store to be a FastSlowStore")
?0
559
2
        .get_arc()
560
2
        .err_tip(|| "FastSlowStore's Arc doesn't exist")
?0
;
561
562
    // Log warning about CAS configuration for multi-worker setups
563
2
    event!(
564
2
        Level::INFO,
565
2
        worker_name = %config.name,
566
        "Starting worker '{}'. IMPORTANT: If running multiple workers, all workers \
567
        must share the same CAS storage path to avoid 'Object not found' errors.",
568
2
        config.name
569
    );
570
571
2
    if let Ok(
path1
) = fs::canonicalize(&config.work_directory).await {
572
1
        fs::remove_dir_all(&path).await.err_tip(|| 
{0
573
0
            format!(
574
                "Could not remove work_directory '{}' in LocalWorker",
575
0
                &path.as_path().to_str().unwrap_or("bad path")
576
            )
577
0
        })?;
578
1
    }
579
580
2
    fs::create_dir_all(&config.work_directory)
581
2
        .await
582
2
        .err_tip(|| 
format!0
("Could not make work_directory : {}",
config.work_directory0
))
?0
;
583
2
    let entrypoint = if config.entrypoint.is_empty() {
584
2
        None
585
    } else {
586
0
        Some(config.entrypoint.clone())
587
    };
588
2
    let max_action_timeout = if config.max_action_timeout_s == 0 {
589
2
        DEFAULT_MAX_ACTION_TIMEOUT
590
    } else {
591
0
        Duration::from_secs(config.max_action_timeout_s as u64)
592
    };
593
2
    let max_upload_timeout = if config.max_upload_timeout_s == 0 {
594
2
        DEFAULT_MAX_UPLOAD_TIMEOUT
595
    } else {
596
0
        Duration::from_secs(config.max_upload_timeout_s as u64)
597
    };
598
2
    let max_cleanup_wait = if config.max_cleanup_wait_s == 0 {
599
2
        DEFAULT_MAX_CLEANUP_WAIT
600
    } else {
601
0
        Duration::from_secs(config.max_cleanup_wait_s as u64)
602
    };
603
2
    let max_cleanup_backoff = if config.max_cleanup_backoff_ms == 0 {
604
2
        DEFAULT_MAX_CLEANUP_BACKOFF
605
    } else {
606
0
        Duration::from_millis(config.max_cleanup_backoff_ms as u64)
607
    };
608
609
    // Initialize directory cache if configured
610
2
    let directory_cache = if let Some(
cache_config0
) = &config.directory_cache {
611
        use std::path::PathBuf;
612
613
        use crate::directory_cache::{
614
            DirectoryCache, DirectoryCacheConfig as WorkerDirCacheConfig,
615
        };
616
617
0
        let cache_root = if cache_config.cache_root.is_empty() {
618
0
            PathBuf::from(&config.work_directory).parent().map_or_else(
619
0
                || PathBuf::from("/tmp/nativelink_directory_cache"),
620
0
                |p| p.join("directory_cache"),
621
            )
622
        } else {
623
0
            PathBuf::from(&cache_config.cache_root)
624
        };
625
626
0
        let worker_cache_config = WorkerDirCacheConfig {
627
0
            max_entries: cache_config.max_entries,
628
0
            max_size_bytes: cache_config.max_size_bytes,
629
0
            cache_root,
630
0
            experimental_subtree_caching: cache_config.experimental_subtree_caching,
631
0
            max_concurrent_fetches: cache_config.max_concurrent_fetches,
632
0
            experimental_get_tree_prefetch: cache_config.experimental_get_tree_prefetch,
633
0
        };
634
635
0
        match DirectoryCache::new(worker_cache_config, fast_slow_store.clone()).await {
636
0
            Ok(cache) => {
637
0
                tracing::info!("Directory cache initialized successfully");
638
0
                Some(Arc::new(cache))
639
            }
640
0
            Err(e) => {
641
0
                tracing::warn!("Failed to initialize directory cache: {:?}", e);
642
0
                None
643
            }
644
        }
645
    } else {
646
2
        None
647
    };
648
649
    #[cfg(target_os = "linux")]
650
2
    let use_namespaces = if let Some(
use_namespaces0
) = &config.use_namespaces {
651
0
        if *use_namespaces
652
0
            && !crate::namespace_utils::namespaces_supported(
653
0
                config.use_mount_namespace.unwrap_or_default(),
654
0
            )
655
        {
656
0
            return Err(make_err!(Code::Unavailable, "Namespaces not supported"));
657
0
        }
658
0
        if !*use_namespaces {
659
0
            crate::running_actions_manager::UseNamespaces::No
660
0
        } else if config.use_mount_namespace.unwrap_or_default() {
661
0
            crate::running_actions_manager::UseNamespaces::YesAndMount
662
        } else {
663
0
            crate::running_actions_manager::UseNamespaces::Yes
664
        }
665
2
    } else if config
666
2
        .use_mount_namespace
667
2
        .is_some_and(core::convert::identity)
668
    {
669
0
        return Err(make_err!(
670
0
            Code::Unavailable,
671
0
            "Mount namespaces not supported"
672
0
        ));
673
    } else {
674
2
        crate::running_actions_manager::UseNamespaces::No
675
    };
676
677
    #[cfg(not(target_os = "linux"))]
678
    if config.use_namespaces.is_some_and(core::convert::identity) {
679
        return Err(make_err!(
680
            Code::Unavailable,
681
            "Namespaces not supported on non-Linux OSes"
682
        ));
683
    }
684
    #[cfg(not(target_os = "linux"))]
685
    if config
686
        .use_mount_namespace
687
        .is_some_and(core::convert::identity)
688
    {
689
        return Err(make_err!(
690
            Code::Unavailable,
691
            "Mount namespaces not supported on non-Linux OSes"
692
        ));
693
    }
694
695
2
    let running_actions_manager =
696
2
        Arc::new(RunningActionsManagerImpl::new(RunningActionsManagerArgs {
697
2
            root_action_directory: config.work_directory.clone(),
698
2
            execution_configuration: ExecutionConfiguration {
699
2
                entrypoint,
700
2
                additional_environment: config.additional_environment.clone(),
701
2
            },
702
2
            cas_store: fast_slow_store,
703
2
            ac_store,
704
2
            historical_store,
705
2
            upload_action_result_config: &config.upload_action_result,
706
2
            max_action_timeout,
707
2
            max_upload_timeout,
708
2
            max_cleanup_wait,
709
2
            max_cleanup_backoff,
710
2
            timeout_handled_externally: config.timeout_handled_externally,
711
2
            directory_cache,
712
2
            #[cfg(target_os = "linux")]
713
2
            use_namespaces,
714
2
        })
?0
);
715
2
    let local_worker = LocalWorker::new_with_connection_factory_and_actions_manager(
716
2
        config.clone(),
717
2
        running_actions_manager,
718
2
        Box::new(move || 
{0
719
0
            let config = config.clone();
720
0
            Box::pin(async move {
721
0
                let timeout = config
722
0
                    .worker_api_endpoint
723
0
                    .timeout
724
0
                    .unwrap_or(DEFAULT_ENDPOINT_TIMEOUT_S);
725
0
                let timeout_duration = Duration::from_secs_f32(timeout);
726
0
                let tls_config =
727
0
                    tls_utils::load_client_config(&config.worker_api_endpoint.tls_config)
728
0
                        .err_tip(|| "Parsing local worker TLS configuration")?;
729
0
                let endpoint =
730
0
                    tls_utils::endpoint_from(&config.worker_api_endpoint.uri, tls_config)
731
0
                        .map_err(|e| {
732
0
                            Error::from_std_err(Code::InvalidArgument, &e)
733
0
                                .append("Invalid URI for worker endpoint")
734
0
                        })?
735
0
                        .connect_timeout(timeout_duration)
736
0
                        .timeout(timeout_duration);
737
738
0
                let transport = endpoint.connect().await.map_err(|e| {
739
0
                    Error::from_std_err(Code::Internal, &e).append(format!(
740
                        "Could not connect to endpoint {}",
741
0
                        config.worker_api_endpoint.uri
742
                    ))
743
0
                })?;
744
0
                Ok(WorkerApiClient::new(transport).into())
745
0
            })
746
0
        }),
747
2
        Box::new(move |d| 
Box::pin0
(
time::sleep0
(
d0
))),
748
    );
749
2
    Ok(local_worker)
750
2
}
751
752
impl<T: WorkerApiClientTrait + 'static, U: RunningActionsManager> LocalWorker<T, U> {
753
12
    pub fn new_with_connection_factory_and_actions_manager(
754
12
        config: Arc<LocalWorkerConfig>,
755
12
        running_actions_manager: Arc<U>,
756
12
        connection_factory: ConnectionFactory<T>,
757
12
        sleep_fn: Box<dyn Fn(Duration) -> BoxFuture<'static, ()> + Send + Sync>,
758
12
    ) -> Self {
759
12
        let metrics = Arc::new(Metrics::new(Arc::downgrade(
760
12
            running_actions_manager.metrics(),
761
        )));
762
12
        Self {
763
12
            config,
764
12
            running_actions_manager,
765
12
            connection_factory,
766
12
            sleep_fn: Some(sleep_fn),
767
12
            metrics,
768
12
        }
769
12
    }
770
771
    #[allow(
772
        clippy::missing_const_for_fn,
773
        reason = "False positive on stable, but not on nightly"
774
    )]
775
0
    pub fn name(&self) -> &String {
776
0
        &self.config.name
777
0
    }
778
779
14
    async fn register_worker(
780
14
        &self,
781
14
        client: &mut T,
782
14
    ) -> Result<(String, Streaming<UpdateForWorker>), Error> {
783
14
        let mut extra_envs: HashMap<String, String> = HashMap::new();
784
14
        if let Some(
ref additional_environment0
) = self.config.additional_environment {
785
0
            for (name, source) in additional_environment {
786
0
                let value = match source {
787
0
                    EnvironmentSource::Value(value) => Cow::Borrowed(value.as_str()),
788
                    EnvironmentSource::FromEnvironment => {
789
0
                        Cow::Owned(env::var(name).unwrap_or_default())
790
                    }
791
0
                    other => {
792
0
                        debug!(
793
                            ?other,
794
                            "Worker registration doesn't support this type of additional environment"
795
                        );
796
0
                        continue;
797
                    }
798
                };
799
0
                extra_envs.insert(name.clone(), value.into_owned());
800
            }
801
14
        }
802
803
14
        let connect_worker_request = make_connect_worker_request(
804
14
            self.config.name.clone(),
805
14
            &self.config.platform_properties,
806
14
            &extra_envs,
807
14
            self.config.max_inflight_tasks,
808
14
        )
809
14
        .await
?0
;
810
14
        let 
mut update_for_worker_stream10
= client
811
14
            .connect_worker(connect_worker_request)
812
14
            .await
813
10
            .err_tip(|| "Could not call connect_worker() in worker")
?0
814
10
            .into_inner();
815
816
10
        let 
first_msg_update9
= update_for_worker_stream
817
10
            .next()
818
10
            .await
819
10
            .err_tip(|| "Got EOF expected UpdateForWorker")
?1
820
9
            .err_tip(|| "Got error when receiving UpdateForWorker")
?0
821
            .update;
822
823
9
        let worker_id = match first_msg_update {
824
9
            Some(Update::ConnectionResult(connection_result)) => connection_result.worker_id,
825
0
            other => {
826
0
                return Err(make_input_err!(
827
0
                    "Expected first response from scheduler to be a ConnectionResult got : {:?}",
828
0
                    other
829
0
                ));
830
            }
831
        };
832
9
        Ok((worker_id, update_for_worker_stream))
833
10
    }
834
835
    #[instrument(skip(self), level = Level::INFO)]
836
10
    pub async fn run(
837
10
        mut self,
838
10
        mut shutdown_rx: broadcast::Receiver<ShutdownGuard>,
839
10
    ) -> Result<(), Error> {
840
        // Belt-and-suspenders QoS bump: the main binary already calls
841
        // this before runtime creation so the tokio worker threads
842
        // inherit P-core preference via pthread QoS inheritance, but
843
        // any thread that reaches this point should also be tagged in
844
        // case it was spawned by a path that bypassed `on_thread_start`.
845
        // No-op on non-macOS.
846
        let _ = crate::qos::set_user_initiated();
847
848
        let sleep_fn = self
849
            .sleep_fn
850
            .take()
851
            .err_tip(|| "Could not unwrap sleep_fn in LocalWorker::run")?;
852
        let sleep_fn_pin = Pin::new(&sleep_fn);
853
4
        let error_handler = Box::pin(move |err| async move {
854
4
            error!(?err, "Error");
855
4
            (sleep_fn_pin)(Duration::from_secs_f32(CONNECTION_RETRY_DELAY_S)).await;
856
8
        });
857
858
        loop {
859
            // First connect to our endpoint.
860
            let mut client = match (self.connection_factory)().await {
861
                Ok(client) => client,
862
                Err(e) => {
863
                    (error_handler)(e).await;
864
                    continue; // Try to connect again.
865
                }
866
            };
867
868
            debug!("Connected to endpoint");
869
870
            // Next register our worker with the scheduler.
871
            let (inner, update_for_worker_stream) = match self.register_worker(&mut client).await {
872
                Err(e) => {
873
                    (error_handler)(e).await;
874
                    continue; // Try to connect again.
875
                }
876
                Ok((worker_id, update_for_worker_stream)) => (
877
                    LocalWorkerImpl::new(
878
                        &self.config,
879
                        client,
880
                        worker_id,
881
                        self.running_actions_manager.clone(),
882
                        self.metrics.clone(),
883
                    ),
884
                    update_for_worker_stream,
885
                ),
886
            };
887
            info!(
888
                worker_id = %inner.worker_id,
889
                "Worker registered with scheduler"
890
            );
891
892
            // Now listen for connections and run all other services.
893
            if let Err(err) = inner.run(update_for_worker_stream, &mut shutdown_rx).await {
894
                // Give in-transit actions a chance to settle before we kill
895
                // them, so their results still reach the scheduler.
896
                const ITERATIONS: usize = 1_000;
897
898
                let sleep_duration = ACTIONS_IN_TRANSIT_TIMEOUT_S / ITERATIONS as f32;
899
                let mut drained = false;
900
                for _ in 0..ITERATIONS {
901
                    if inner.actions_in_transit.load(Ordering::Acquire) == 0 {
902
                        drained = true;
903
                        break;
904
                    }
905
                    (sleep_fn_pin)(Duration::from_secs_f32(sleep_duration)).await;
906
                }
907
                if !drained {
908
                    // Deliberately not fatal. Returning here propagates out of
909
                    // the worker's main loop and aborts the process, so a
910
                    // scheduler blip that happened to catch an action in
911
                    // transit took the whole worker down — and every action it
912
                    // held then had to run again elsewhere. At fleet scale that
913
                    // is a restart storm. kill_all() below discards these
914
                    // actions anyway, so the wait is a courtesy and overrunning
915
                    // it costs nothing beyond the actions we were already
916
                    // giving up on.
917
                    error!(
918
                        actions_in_transit = inner.actions_in_transit.load(Ordering::Acquire),
919
                        "Actions in transit did not reach zero before we disconnected from the scheduler"
920
                    );
921
                }
922
923
                error!(?err, "Worker disconnected from scheduler, reconnecting");
924
                // Kill off any existing actions because if we re-connect, we'll
925
                // get some more and it might resource lock us.
926
                self.running_actions_manager.kill_all().await;
927
928
                (error_handler)(err).await; // Try to connect again.
929
            }
930
        }
931
        // Unreachable.
932
0
    }
933
}
934
935
#[derive(Debug, MetricsComponent)]
936
pub struct Metrics {
937
    #[metric(
938
        help = "Total number of actions sent to this worker to process. This does not mean it started them, it just means it received a request to execute it."
939
    )]
940
    start_actions_received: CounterWithTime,
941
    #[metric(help = "Total number of disconnects received from the scheduler.")]
942
    disconnects_received: CounterWithTime,
943
    #[metric(help = "Total number of keep-alives received from the scheduler.")]
944
    keep_alives_received: CounterWithTime,
945
    #[metric(
946
        help = "Stats about the calls to check if an action satisfies the config supplied script."
947
    )]
948
    preconditions: AsyncCounterWrapper,
949
    #[metric]
950
    #[allow(
951
        clippy::struct_field_names,
952
        reason = "TODO Fix this. Triggers on nightly"
953
    )]
954
    running_actions_manager_metrics: Weak<RunningActionManagerMetrics>,
955
}
956
957
impl RootMetricsComponent for Metrics {}
958
959
impl Metrics {
960
12
    fn new(running_actions_manager_metrics: Weak<RunningActionManagerMetrics>) -> Self {
961
12
        Self {
962
12
            start_actions_received: CounterWithTime::default(),
963
12
            disconnects_received: CounterWithTime::default(),
964
12
            keep_alives_received: CounterWithTime::default(),
965
12
            preconditions: AsyncCounterWrapper::default(),
966
12
            running_actions_manager_metrics,
967
12
        }
968
12
    }
969
}
970
971
impl Metrics {
972
7
    async fn wrap<U, T: Future<Output = U>, F: FnOnce(Arc<Self>) -> T>(
973
7
        self: Arc<Self>,
974
7
        fut: F,
975
7
    ) -> U 
{6
976
6
        fut(self).await
977
5
    }
978
}