Coverage Report

Created: 2026-07-13 10:51

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-scheduler/src/api_worker_scheduler.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::ops::{Deref, DerefMut};
16
use core::sync::atomic::{AtomicU64, Ordering};
17
use core::time::Duration;
18
use std::sync::Arc;
19
use std::time::{Instant, UNIX_EPOCH};
20
21
use async_lock::Mutex;
22
use lru::LruCache;
23
use nativelink_config::schedulers::WorkerAllocationStrategy;
24
use nativelink_error::{Code, Error, ResultExt, error_if, make_err, make_input_err};
25
use nativelink_metric::{
26
    MetricFieldData, MetricKind, MetricPublishKnownKindData, MetricsComponent,
27
    RootMetricsComponent, group,
28
};
29
use nativelink_proto::com::github::trace_machina::nativelink::events::{
30
    Event, OriginEvent, ResponseEvent, event, response_event,
31
};
32
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::ActionResourceUsage;
33
use nativelink_util::action_messages::{OperationId, WorkerId};
34
use nativelink_util::operation_state_manager::{UpdateOperationType, WorkerStateManager};
35
use nativelink_util::origin_event::get_node_id;
36
use nativelink_util::platform_properties::PlatformProperties;
37
use nativelink_util::shutdown_guard::ShutdownGuard;
38
use tokio::sync::{Notify, mpsc};
39
use tonic::async_trait;
40
use tracing::{error, info, trace, warn};
41
use uuid::Uuid;
42
43
/// Metrics for tracking scheduler performance.
44
#[derive(Debug, Default)]
45
pub struct SchedulerMetrics {
46
    /// Total number of worker additions.
47
    pub workers_added: AtomicU64,
48
    /// Total number of worker removals.
49
    pub workers_removed: AtomicU64,
50
    /// Total number of `find_worker_for_action` calls.
51
    pub find_worker_calls: AtomicU64,
52
    /// Total number of successful worker matches.
53
    pub find_worker_hits: AtomicU64,
54
    /// Total number of failed worker matches (no worker found).
55
    pub find_worker_misses: AtomicU64,
56
    /// Total time spent in `find_worker_for_action` (nanoseconds).
57
    pub find_worker_time_ns: AtomicU64,
58
    /// Total number of workers iterated during find operations.
59
    pub workers_iterated: AtomicU64,
60
    /// Total number of action dispatches.
61
    pub actions_dispatched: AtomicU64,
62
    /// Total number of keep-alive updates.
63
    pub keep_alive_updates: AtomicU64,
64
    /// Total number of worker timeouts.
65
    pub worker_timeouts: AtomicU64,
66
}
67
68
use crate::platform_property_manager::PlatformPropertyManager;
69
use crate::worker::{ActionInfoWithProps, Worker, WorkerTimestamp, WorkerUpdate};
70
use crate::worker_capability_index::WorkerCapabilityIndex;
71
use crate::worker_registry::SharedWorkerRegistry;
72
use crate::worker_scheduler::WorkerScheduler;
73
74
#[derive(Debug)]
75
struct Workers(LruCache<WorkerId, Worker>);
76
77
impl Deref for Workers {
78
    type Target = LruCache<WorkerId, Worker>;
79
80
178
    fn deref(&self) -> &Self::Target {
81
178
        &self.0
82
178
    }
83
}
84
85
impl DerefMut for Workers {
86
141
    fn deref_mut(&mut self) -> &mut Self::Target {
87
141
        &mut self.0
88
141
    }
89
}
90
91
// Note: This could not be a derive macro because this derive-macro
92
// does not support LruCache and nameless field structs.
93
impl MetricsComponent for Workers {
94
0
    fn publish(
95
0
        &self,
96
0
        _kind: MetricKind,
97
0
        _field_metadata: MetricFieldData,
98
0
    ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> {
99
0
        let _enter = group!("workers").entered();
100
0
        for (worker_id, worker) in self.iter() {
101
0
            let _enter = group!(worker_id).entered();
102
0
            worker.publish(MetricKind::Component, MetricFieldData::default())?;
103
        }
104
0
        Ok(MetricPublishKnownKindData::Component)
105
0
    }
106
}
107
108
/// A collection of workers that are available to run tasks.
109
#[derive(MetricsComponent)]
110
struct ApiWorkerSchedulerImpl {
111
    /// A `LruCache` of workers available based on `allocation_strategy`.
112
    #[metric(group = "workers")]
113
    workers: Workers,
114
115
    /// The worker state manager.
116
    #[metric(group = "worker_state_manager")]
117
    worker_state_manager: Arc<dyn WorkerStateManager>,
118
    /// The allocation strategy for workers.
119
    allocation_strategy: WorkerAllocationStrategy,
120
    /// A channel to notify the matching engine that the worker pool has changed.
121
    worker_change_notify: Arc<Notify>,
122
    /// Worker registry for tracking worker liveness.
123
    worker_registry: SharedWorkerRegistry,
124
125
    /// Whether the worker scheduler is shutting down.
126
    shutting_down: bool,
127
128
    /// Index for fast worker capability lookup.
129
    /// Used to accelerate `find_worker_for_action` by filtering candidates
130
    /// based on properties before doing linear scan.
131
    capability_index: WorkerCapabilityIndex,
132
}
133
134
impl core::fmt::Debug for ApiWorkerSchedulerImpl {
135
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136
0
        f.debug_struct("ApiWorkerSchedulerImpl")
137
0
            .field("workers", &self.workers)
138
0
            .field("allocation_strategy", &self.allocation_strategy)
139
0
            .field("worker_change_notify", &self.worker_change_notify)
140
0
            .field(
141
0
                "capability_index_size",
142
0
                &self.capability_index.worker_count(),
143
0
            )
144
0
            .field("worker_registry", &self.worker_registry)
145
0
            .finish_non_exhaustive()
146
0
    }
147
}
148
149
impl ApiWorkerSchedulerImpl {
150
    /// Refreshes the lifetime of the worker with the given timestamp.
151
    ///
152
    /// Instead of sending N keepalive messages (one per operation),
153
    /// we now send a single worker heartbeat. The worker registry tracks worker liveness,
154
    /// and timeout detection checks the worker's `last_seen` instead of per-operation timestamps.
155
    ///
156
    /// Note: This only updates the local worker state. The worker registry is updated
157
    /// separately after releasing the inner lock to reduce contention.
158
2
    fn refresh_lifetime(
159
2
        &mut self,
160
2
        worker_id: &WorkerId,
161
2
        timestamp: WorkerTimestamp,
162
2
    ) -> Result<(), Error> {
163
2
        let worker = self.workers.0.peek_mut(worker_id).ok_or_else(|| 
{0
164
0
            make_input_err!(
165
                "Worker not found in worker map in refresh_lifetime() {}",
166
                worker_id
167
            )
168
0
        })?;
169
0
        error_if!(
170
2
            worker.last_update_timestamp > timestamp,
171
            "Worker already had a timestamp of {}, but tried to update it with {}",
172
            worker.last_update_timestamp,
173
            timestamp
174
        );
175
2
        worker.last_update_timestamp = timestamp;
176
177
2
        trace!(
178
            ?worker_id,
179
2
            running_operations = worker.running_action_infos.len(),
180
            "Worker keepalive received"
181
        );
182
183
2
        Ok(())
184
2
    }
185
186
    /// Adds a worker to the pool.
187
    /// Note: This function will not do any task matching.
188
40
    fn add_worker(&mut self, worker: Worker) -> Result<(), Error> {
189
40
        let worker_id = worker.id.clone();
190
40
        let platform_properties = worker.platform_properties.clone();
191
40
        self.workers.put(worker_id.clone(), worker);
192
193
        // Add to capability index for fast matching
194
40
        self.capability_index
195
40
            .add_worker(&worker_id, &platform_properties);
196
197
        // Worker is not cloneable, and we do not want to send the initial connection results until
198
        // we have added it to the map, or we might get some strange race conditions due to the way
199
        // the multi-threaded runtime works.
200
40
        let worker = self.workers.peek_mut(&worker_id).unwrap();
201
40
        let res = worker
202
40
            .send_initial_connection_result()
203
40
            .err_tip(|| "Failed to send initial connection result to worker");
204
40
        if let Err(
err0
) = &res {
205
0
            error!(
206
                ?worker_id,
207
                ?err,
208
                "Worker connection appears to have been closed while adding to pool"
209
            );
210
40
        }
211
40
        self.worker_change_notify.notify_one();
212
40
        res
213
40
    }
214
215
    /// Removes worker from pool.
216
    /// Note: The caller is responsible for any rescheduling of any tasks that might be
217
    /// running.
218
10
    fn remove_worker(&mut self, worker_id: &WorkerId) -> Option<Worker> {
219
        // Remove from capability index
220
10
        self.capability_index.remove_worker(worker_id);
221
222
10
        let result = self.workers.pop(worker_id);
223
10
        self.worker_change_notify.notify_one();
224
10
        result
225
10
    }
226
227
    /// Sets if the worker is draining or not.
228
2
    async fn set_drain_worker(
229
2
        &mut self,
230
2
        worker_id: &WorkerId,
231
2
        is_draining: bool,
232
2
    ) -> Result<(), Error> {
233
2
        let worker = self
234
2
            .workers
235
2
            .get_mut(worker_id)
236
2
            .err_tip(|| 
format!0
("Worker {worker_id} doesn't exist in the pool"))
?0
;
237
2
        worker.is_draining = is_draining;
238
2
        self.worker_change_notify.notify_one();
239
2
        Ok(())
240
2
    }
241
242
60
    fn inner_find_worker_for_action(
243
60
        &self,
244
60
        platform_properties: &PlatformProperties,
245
60
        full_worker_logging: bool,
246
60
    ) -> Option<WorkerId> {
247
        // Do a fast check to see if any workers are available at all for work allocation
248
60
        if !self.workers.iter().any(|(_, w)| 
w48
.
can_accept_work48
()) {
249
14
            if full_worker_logging {
250
2
                info!("All workers are fully allocated");
251
12
            }
252
14
            return None;
253
46
        }
254
255
        // Use capability index to get candidate workers that match STATIC properties
256
        // (Exact, Unknown) and have the required property keys (Priority, Minimum).
257
        // This reduces complexity from O(W × P) to O(P × log(W)) for exact properties.
258
46
        let candidates = self
259
46
            .capability_index
260
46
            .find_matching_workers(platform_properties, full_worker_logging);
261
262
46
        if candidates.is_empty() {
263
1
            if full_worker_logging {
264
0
                info!("No workers in capability index match required properties");
265
1
            }
266
1
            return None;
267
45
        }
268
269
        // Check function for availability AND dynamic Minimum property verification.
270
        // The index only does presence checks for Minimum properties since their
271
        // values change dynamically as jobs are assigned to workers.
272
45
        let worker_matches = |(worker_id, w): &(&WorkerId, &Worker)| -> bool {
273
45
            if !w.can_accept_work() {
274
0
                if full_worker_logging {
275
0
                    info!(
276
                        "Worker {worker_id} cannot accept work: is_paused={}, is_draining={}, inflight={}/{}",
277
                        w.is_paused,
278
                        w.is_draining,
279
0
                        w.running_action_infos.len(),
280
                        w.max_inflight_tasks
281
                    );
282
0
                }
283
0
                return false;
284
45
            }
285
286
            // Verify Minimum properties at runtime (their values are dynamic)
287
45
            if !platform_properties.is_satisfied_by(&w.platform_properties, full_worker_logging) {
288
5
                return false;
289
40
            }
290
291
40
            true
292
45
        };
293
294
        // Now check constraints on filtered candidates.
295
        // Iterate in LRU order based on allocation strategy.
296
45
        let workers_iter = self.workers.iter();
297
298
45
        let worker_id = match self.allocation_strategy {
299
            // Use rfind to get the least recently used that satisfies the properties.
300
45
            WorkerAllocationStrategy::LeastRecentlyUsed => workers_iter
301
45
                .rev()
302
46
                .
filter45
(|(worker_id, _)| candidates.contains(worker_id))
303
45
                .find(&worker_matches)
304
45
                .map(|(_, w)| 
w.id40
.
clone40
()),
305
306
            // Use find to get the most recently used that satisfies the properties.
307
0
            WorkerAllocationStrategy::MostRecentlyUsed => workers_iter
308
0
                .filter(|(worker_id, _)| candidates.contains(worker_id))
309
0
                .find(&worker_matches)
310
0
                .map(|(_, w)| w.id.clone()),
311
        };
312
45
        if full_worker_logging && 
worker_id8
.
is_none8
() {
313
1
            warn!("No workers matched!");
314
44
        }
315
45
        worker_id
316
60
    }
317
318
12
    async fn update_action(
319
12
        &mut self,
320
12
        worker_id: &WorkerId,
321
12
        operation_id: &OperationId,
322
12
        update: UpdateOperationType,
323
12
    ) -> Result<(), Error> {
324
12
        let worker = self.workers.get_mut(worker_id).err_tip(|| 
{0
325
0
            format!("Worker {worker_id} does not exist in SimpleScheduler::update_action")
326
0
        })?;
327
328
        // Ensure the worker is supposed to be running the operation.
329
12
        if !worker.running_action_infos.contains_key(operation_id) {
330
1
            let err = make_err!(
331
1
                Code::Internal,
332
                "Operation {operation_id} should not be running on worker {worker_id} in SimpleScheduler::update_action"
333
            );
334
1
            return Result::<(), _>::Err(err.clone())
335
1
                .merge(self.immediate_evict_worker(worker_id, err, false).await);
336
11
        }
337
338
11
        let (is_finished, due_to_backpressure) = match &update {
339
6
            UpdateOperationType::UpdateWithActionStage(action_stage) => {
340
6
                (action_stage.is_finished(), false)
341
            }
342
0
            UpdateOperationType::KeepAlive => (false, false),
343
3
            UpdateOperationType::UpdateWithError(err) => {
344
3
                (true, err.code == Code::ResourceExhausted)
345
            }
346
2
            UpdateOperationType::UpdateWithDisconnect => (true, false),
347
            UpdateOperationType::ExecutionComplete => {
348
                // No update here, just restoring platform properties.
349
0
                worker.execution_complete(operation_id);
350
0
                self.worker_change_notify.notify_one();
351
0
                return Ok(());
352
            }
353
        };
354
355
        // Update the operation in the worker state manager.
356
        {
357
11
            let 
update_operation_res10
= self
358
11
                .worker_state_manager
359
11
                .update_operation(operation_id, worker_id, update)
360
11
                .await
361
10
                .err_tip(|| "in update_operation on SimpleScheduler::update_action");
362
10
            if let Err(
err0
) = update_operation_res {
363
0
                error!(
364
                    %operation_id,
365
                    ?worker_id,
366
                    ?err,
367
                    "Failed to update_operation on update_action"
368
                );
369
0
                return Err(err);
370
10
            }
371
        }
372
373
10
        if !is_finished {
374
0
            return Ok(());
375
10
        }
376
377
        // Clear this action from the current worker if finished.
378
10
        let complete_action_res = {
379
            // Note: We need to run this before dealing with backpressure logic.
380
10
            let complete_action_res = worker.complete_action(operation_id).await;
381
382
10
            if (due_to_backpressure || !worker.can_accept_work()) && 
worker0
.
has_actions0
() {
383
0
                worker.is_paused = true;
384
10
            }
385
10
            complete_action_res
386
        };
387
388
10
        self.worker_change_notify.notify_one();
389
390
10
        complete_action_res
391
11
    }
392
393
    /// Notifies the specified worker to run the given action and handles errors by evicting
394
    /// the worker if the notification fails.
395
36
    async fn worker_notify_run_action(
396
36
        &mut self,
397
36
        worker_id: WorkerId,
398
36
        operation_id: OperationId,
399
36
        action_info: ActionInfoWithProps,
400
36
    ) -> Result<(), Error> {
401
36
        if let Some(worker) = self.workers.get_mut(&worker_id) {
402
36
            let notify_worker_result = worker
403
36
                .notify_update(WorkerUpdate::RunAction(Box::new((
404
36
                    operation_id,
405
36
                    action_info.clone(),
406
36
                ))))
407
36
                .await;
408
409
36
            if let Err(
notify_worker_result1
) = notify_worker_result {
410
1
                warn!(
411
                    ?worker_id,
412
                    ?action_info,
413
                    ?notify_worker_result,
414
                    "Worker command failed, removing worker",
415
                );
416
417
                // A slightly nasty way of figuring out that the worker disconnected
418
                // from send_msg_to_worker without introducing complexity to the
419
                // code path from here to there.
420
1
                let is_disconnect = notify_worker_result.code == Code::Internal
421
1
                    && notify_worker_result.messages.len() == 1
422
0
                    && notify_worker_result.messages[0] == "Worker Disconnected";
423
424
1
                let err = make_err!(
425
1
                    Code::Internal,
426
                    "Worker command failed, removing worker {worker_id} -- {notify_worker_result:?}",
427
                );
428
429
1
                return Result::<(), _>::Err(err.clone()).merge(
430
1
                    self.immediate_evict_worker(&worker_id, err, is_disconnect)
431
1
                        .await,
432
                );
433
35
            }
434
35
            Ok(())
435
        } else {
436
0
            warn!(
437
                ?worker_id,
438
                %operation_id,
439
                ?action_info,
440
                "Worker not found in worker map in worker_notify_run_action"
441
            );
442
            // Ensure the operation is put back to queued state.
443
0
            self.worker_state_manager
444
0
                .update_operation(
445
0
                    &operation_id,
446
0
                    &worker_id,
447
0
                    UpdateOperationType::UpdateWithDisconnect,
448
0
                )
449
0
                .await
450
        }
451
36
    }
452
453
    /// Evicts the worker from the pool and puts items back into the queue if anything was being executed on it.
454
10
    async fn immediate_evict_worker(
455
10
        &mut self,
456
10
        worker_id: &WorkerId,
457
10
        err: Error,
458
10
        is_disconnect: bool,
459
10
    ) -> Result<(), Error> {
460
10
        let mut result = Ok(());
461
10
        if let Some(mut worker) = self.remove_worker(worker_id) {
462
            // We don't care if we fail to send message to worker, this is only a best attempt.
463
10
            drop(worker.notify_update(WorkerUpdate::Disconnect).await);
464
10
            let update = if is_disconnect {
465
0
                UpdateOperationType::UpdateWithDisconnect
466
            } else {
467
10
                UpdateOperationType::UpdateWithError(err)
468
            };
469
10
            for (
operation_id8
, _) in worker.running_action_infos.drain() {
470
8
                result = result.merge(
471
8
                    self.worker_state_manager
472
8
                        .update_operation(&operation_id, worker_id, update.clone())
473
8
                        .await,
474
                );
475
            }
476
0
        }
477
        // Note: Calling this many time is very cheap, it'll only trigger `do_try_match` once.
478
        // TODO(palfrey) This should be moved to inside the Workers struct.
479
10
        self.worker_change_notify.notify_one();
480
10
        result
481
10
    }
482
}
483
484
#[derive(Debug, MetricsComponent)]
485
pub struct ApiWorkerScheduler {
486
    #[metric]
487
    inner: Mutex<ApiWorkerSchedulerImpl>,
488
    #[metric(group = "platform_property_manager")]
489
    platform_property_manager: Arc<PlatformPropertyManager>,
490
491
    #[metric(
492
        help = "Timeout of how long to evict workers if no response in this given amount of time in seconds."
493
    )]
494
    worker_timeout_s: u64,
495
    /// Shared worker registry for checking worker liveness.
496
    worker_registry: SharedWorkerRegistry,
497
498
    /// Performance metrics for observability.
499
    metrics: Arc<SchedulerMetrics>,
500
501
    /// Channel for publishing origin events such as worker-observed action
502
    /// resource usage. `None` when origin events are disabled.
503
    maybe_origin_event_tx: Option<mpsc::Sender<OriginEvent>>,
504
}
505
506
impl ApiWorkerScheduler {
507
34
    pub fn new(
508
34
        worker_state_manager: Arc<dyn WorkerStateManager>,
509
34
        platform_property_manager: Arc<PlatformPropertyManager>,
510
34
        allocation_strategy: WorkerAllocationStrategy,
511
34
        worker_change_notify: Arc<Notify>,
512
34
        worker_timeout_s: u64,
513
34
        worker_registry: SharedWorkerRegistry,
514
34
        maybe_origin_event_tx: Option<mpsc::Sender<OriginEvent>>,
515
34
    ) -> Arc<Self> {
516
34
        Arc::new(Self {
517
34
            inner: Mutex::new(ApiWorkerSchedulerImpl {
518
34
                workers: Workers(LruCache::unbounded()),
519
34
                worker_state_manager,
520
34
                allocation_strategy,
521
34
                worker_change_notify,
522
34
                worker_registry: worker_registry.clone(),
523
34
                shutting_down: false,
524
34
                capability_index: WorkerCapabilityIndex::new(),
525
34
            }),
526
34
            platform_property_manager,
527
34
            worker_timeout_s,
528
34
            worker_registry,
529
34
            metrics: Arc::new(SchedulerMetrics::default()),
530
34
            maybe_origin_event_tx,
531
34
        })
532
34
    }
533
534
    /// Returns a reference to the worker registry.
535
0
    pub const fn worker_registry(&self) -> &SharedWorkerRegistry {
536
0
        &self.worker_registry
537
0
    }
538
539
36
    pub async fn worker_notify_run_action(
540
36
        &self,
541
36
        worker_id: WorkerId,
542
36
        operation_id: OperationId,
543
36
        action_info: ActionInfoWithProps,
544
36
    ) -> Result<(), Error> {
545
36
        self.metrics
546
36
            .actions_dispatched
547
36
            .fetch_add(1, Ordering::Relaxed);
548
36
        let mut inner = self.inner.lock().await;
549
36
        inner
550
36
            .worker_notify_run_action(worker_id, operation_id, action_info)
551
36
            .await
552
36
    }
553
554
1
    pub async fn running_action_info(
555
1
        &self,
556
1
        worker_id: &WorkerId,
557
1
        operation_id: &OperationId,
558
1
    ) -> Option<ActionInfoWithProps> {
559
1
        let inner = self.inner.lock().await;
560
1
        inner
561
1
            .workers
562
1
            .peek(worker_id)
563
1
            .and_then(|worker| worker.running_action_infos.get(operation_id))
564
1
            .map(|pending_action_info| pending_action_info.action_info.clone())
565
1
    }
566
567
    /// Returns the scheduler metrics for observability.
568
    #[must_use]
569
0
    pub const fn get_metrics(&self) -> &Arc<SchedulerMetrics> {
570
0
        &self.metrics
571
0
    }
572
573
    /// Attempts to find a worker that is capable of running this action.
574
    // TODO(palfrey) This algorithm is not very efficient. Simple testing using a tree-like
575
    // structure showed worse performance on a 10_000 worker * 7 properties * 1000 queued tasks
576
    // simulation of worst cases in a single threaded environment.
577
60
    pub async fn find_worker_for_action(
578
60
        &self,
579
60
        platform_properties: &PlatformProperties,
580
60
        full_worker_logging: bool,
581
60
    ) -> Option<WorkerId> {
582
60
        let start = Instant::now();
583
60
        self.metrics
584
60
            .find_worker_calls
585
60
            .fetch_add(1, Ordering::Relaxed);
586
587
60
        let inner = self.inner.lock().await;
588
60
        let worker_count = inner.workers.len() as u64;
589
60
        let result = inner.inner_find_worker_for_action(platform_properties, full_worker_logging);
590
591
        // Track workers iterated (worst case is all workers)
592
60
        self.metrics
593
60
            .workers_iterated
594
60
            .fetch_add(worker_count, Ordering::Relaxed);
595
596
60
        if result.is_some() {
597
40
            self.metrics
598
40
                .find_worker_hits
599
40
                .fetch_add(1, Ordering::Relaxed);
600
40
        } else {
601
20
            self.metrics
602
20
                .find_worker_misses
603
20
                .fetch_add(1, Ordering::Relaxed);
604
20
        }
605
606
        #[allow(clippy::cast_possible_truncation)]
607
60
        self.metrics
608
60
            .find_worker_time_ns
609
60
            .fetch_add(start.elapsed().as_nanos() as u64, Ordering::Relaxed);
610
60
        result
611
60
    }
612
613
    /// Checks to see if the worker exists in the worker pool. Should only be used in unit tests.
614
    #[must_use]
615
7
    
pub async fn contains_worker_for_test(&self, worker_id: &WorkerId) -> bool0
{
616
7
        let inner = self.inner.lock().await;
617
7
        inner.workers.contains(worker_id)
618
7
    }
619
620
    /// A unit test function used to send the keep alive message to the worker from the server.
621
1
    pub async fn send_keep_alive_to_worker_for_test(
622
1
        &self,
623
1
        worker_id: &WorkerId,
624
1
    ) -> Result<(), Error> {
625
1
        let mut inner = self.inner.lock().await;
626
1
        let worker = inner.workers.get_mut(worker_id).ok_or_else(|| 
{0
627
0
            make_input_err!("WorkerId '{}' does not exist in workers map", worker_id)
628
0
        })?;
629
1
        worker.keep_alive()
630
1
    }
631
}
632
633
#[async_trait]
634
impl WorkerScheduler for ApiWorkerScheduler {
635
2
    fn get_platform_property_manager(&self) -> &PlatformPropertyManager {
636
2
        self.platform_property_manager.as_ref()
637
2
    }
638
639
    async fn record_action_resource_usage(
640
        &self,
641
        worker_id: &WorkerId,
642
        operation_id: &OperationId,
643
        mut resource_usage: ActionResourceUsage,
644
1
    ) -> Result<(), Error> {
645
        // The worker API talks to this `ApiWorkerScheduler` (it is the
646
        // `WorkerScheduler` returned by `SimpleScheduler::new`), so the
647
        // resource-usage origin event must be published here. Previously the
648
        // only override lived on `SimpleScheduler`, which this path never
649
        // reaches, so the event was silently dropped by the trait's no-op
650
        // default and `observed_worker_peak_memory_mib` was never recorded.
651
        let Some(origin_event_tx) = self.maybe_origin_event_tx.as_ref() else {
652
            return Ok(());
653
        };
654
        let Some(action_info) = self.running_action_info(worker_id, operation_id).await else {
655
            return Ok(());
656
        };
657
658
        if resource_usage.operation_id.is_empty() {
659
            resource_usage.operation_id = operation_id.to_string();
660
        }
661
        if resource_usage.worker_id.is_empty() {
662
            resource_usage.worker_id = worker_id.to_string();
663
        }
664
665
        let event = Event {
666
            event: Some(event::Event::Response(ResponseEvent {
667
                event: Some(response_event::Event::ActionResourceUsage(resource_usage)),
668
            })),
669
        };
670
        let origin_event = OriginEvent {
671
            version: 0,
672
            event_id: Uuid::now_v6(&get_node_id(Some(&event)))
673
                .hyphenated()
674
                .to_string(),
675
            parent_event_id: action_info
676
                .scheduler_start_execute_event_id
677
                .clone()
678
                .unwrap_or_default(),
679
            bazel_request_metadata: action_info.origin_metadata.bazel_metadata.clone(),
680
            identity: action_info.origin_metadata.identity,
681
            event: Some(event),
682
        };
683
        // Awaited send (not try_send): apply backpressure when the publisher
684
        // queue is full instead of silently dropping the resource-usage event,
685
        // which is what drives action-level resource sizing in the UI.
686
        if let Err(err) = origin_event_tx.send(origin_event).await {
687
            warn!(?err, "Failed to publish action resource usage origin event");
688
        }
689
        Ok(())
690
1
    }
691
692
40
    async fn add_worker(&self, worker: Worker) -> Result<(), Error> {
693
        let worker_id = worker.id.clone();
694
        let worker_timestamp = worker.last_update_timestamp;
695
        let mut inner = self.inner.lock().await;
696
        if inner.shutting_down {
697
            warn!("Rejected worker add during shutdown: {}", worker_id);
698
            return Err(make_err!(
699
                Code::Unavailable,
700
                "Received request to add worker while shutting down"
701
            ));
702
        }
703
        let result = inner
704
            .add_worker(worker)
705
            .err_tip(|| "Error while adding worker, removing from pool");
706
        if let Err(err) = result {
707
            return Result::<(), _>::Err(err.clone())
708
                .merge(inner.immediate_evict_worker(&worker_id, err, false).await);
709
        }
710
711
        let now = UNIX_EPOCH + Duration::from_secs(worker_timestamp);
712
        self.worker_registry.register_worker(&worker_id, now).await;
713
714
        self.metrics.workers_added.fetch_add(1, Ordering::Relaxed);
715
        Ok(())
716
40
    }
717
718
    async fn update_action(
719
        &self,
720
        worker_id: &WorkerId,
721
        operation_id: &OperationId,
722
        update: UpdateOperationType,
723
12
    ) -> Result<(), Error> {
724
        let mut inner = self.inner.lock().await;
725
        inner.update_action(worker_id, operation_id, update).await
726
12
    }
727
728
    async fn worker_keep_alive_received(
729
        &self,
730
        worker_id: &WorkerId,
731
        timestamp: WorkerTimestamp,
732
2
    ) -> Result<(), Error> {
733
        {
734
            let mut inner = self.inner.lock().await;
735
            inner
736
                .refresh_lifetime(worker_id, timestamp)
737
                .err_tip(|| "Error refreshing lifetime in worker_keep_alive_received()")?;
738
        }
739
        let now = UNIX_EPOCH + Duration::from_secs(timestamp);
740
        self.worker_registry
741
            .update_worker_heartbeat(worker_id, now)
742
            .await;
743
        Ok(())
744
2
    }
745
746
6
    async fn remove_worker(&self, worker_id: &WorkerId) -> Result<(), Error> {
747
        self.worker_registry.remove_worker(worker_id).await;
748
749
        let mut inner = self.inner.lock().await;
750
        inner
751
            .immediate_evict_worker(
752
                worker_id,
753
                make_err!(Code::Internal, "Received request to remove worker"),
754
                false,
755
            )
756
            .await
757
6
    }
758
759
0
    async fn shutdown(&self, shutdown_guard: ShutdownGuard) {
760
        let mut inner = self.inner.lock().await;
761
        inner.shutting_down = true; // should reject further worker registration
762
        while let Some(worker_id) = inner
763
            .workers
764
            .peek_lru()
765
0
            .map(|(worker_id, _worker)| worker_id.clone())
766
        {
767
            if let Err(err) = inner
768
                .immediate_evict_worker(
769
                    &worker_id,
770
                    make_err!(Code::Internal, "Scheduler shutdown"),
771
                    true,
772
                )
773
                .await
774
            {
775
                error!(?err, "Error evicting worker on shutdown.");
776
            }
777
        }
778
        drop(shutdown_guard);
779
0
    }
780
781
5
    async fn remove_timedout_workers(&self, now_timestamp: WorkerTimestamp) -> Result<(), Error> {
782
        // Check worker liveness using both the local timestamp (from LRU)
783
        // and the worker registry. A worker is alive if either source says it's alive.
784
        let timeout = Duration::from_secs(self.worker_timeout_s);
785
        let now = UNIX_EPOCH + Duration::from_secs(now_timestamp);
786
        let timeout_threshold = now_timestamp.saturating_sub(self.worker_timeout_s);
787
788
        let workers_to_check: Vec<(WorkerId, bool)> = {
789
            let inner = self.inner.lock().await;
790
            inner
791
                .workers
792
                .iter()
793
6
                .map(|(worker_id, worker)| {
794
6
                    let local_alive = worker.last_update_timestamp > timeout_threshold;
795
6
                    (worker_id.clone(), local_alive)
796
6
                })
797
                .collect()
798
        };
799
800
        let mut worker_ids_to_remove = Vec::new();
801
        for (worker_id, local_alive) in workers_to_check {
802
            if local_alive {
803
                continue;
804
            }
805
806
            let registry_alive = self
807
                .worker_registry
808
                .is_worker_alive(&worker_id, timeout, now)
809
                .await;
810
811
            if !registry_alive {
812
                trace!(
813
                    ?worker_id,
814
                    local_alive,
815
                    registry_alive,
816
                    timeout_threshold,
817
                    "Worker timed out - neither local nor registry shows alive"
818
                );
819
                worker_ids_to_remove.push(worker_id);
820
            }
821
        }
822
823
        if worker_ids_to_remove.is_empty() {
824
            return Ok(());
825
        }
826
827
        let mut inner = self.inner.lock().await;
828
        let mut result = Ok(());
829
830
        for worker_id in &worker_ids_to_remove {
831
            warn!(?worker_id, "Worker timed out, removing from pool");
832
            result = result.merge(
833
                inner
834
                    .immediate_evict_worker(
835
                        worker_id,
836
                        make_err!(
837
                            Code::Internal,
838
                            "Worker {worker_id} timed out, removing from pool"
839
                        ),
840
                        false,
841
                    )
842
                    .await,
843
            );
844
        }
845
846
        result
847
5
    }
848
849
2
    async fn set_drain_worker(&self, worker_id: &WorkerId, is_draining: bool) -> Result<(), Error> {
850
        let mut inner = self.inner.lock().await;
851
        inner.set_drain_worker(worker_id, is_draining).await
852
2
    }
853
}
854
855
impl RootMetricsComponent for ApiWorkerScheduler {}