/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 futures::{StreamExt, future}; |
23 | | use lru::LruCache; |
24 | | use nativelink_config::schedulers::WorkerAllocationStrategy; |
25 | | use nativelink_error::{Code, Error, ResultExt, error_if, make_err, make_input_err}; |
26 | | use nativelink_metric::{ |
27 | | MetricFieldData, MetricKind, MetricPublishKnownKindData, MetricsComponent, |
28 | | RootMetricsComponent, group, |
29 | | }; |
30 | | use nativelink_proto::com::github::trace_machina::nativelink::events::{ |
31 | | Event, OriginEvent, ResponseEvent, event, response_event, |
32 | | }; |
33 | | use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::ActionResourceUsage; |
34 | | use nativelink_util::action_messages::{OperationId, WorkerId}; |
35 | | use nativelink_util::metrics::{ |
36 | | WorkerDisconnectReason, record_execution_cpu_time, record_execution_peak_memory, |
37 | | record_worker_connected, record_worker_disconnected, record_worker_keepalive, |
38 | | record_worker_state, |
39 | | }; |
40 | | use nativelink_util::operation_state_manager::{UpdateOperationType, WorkerStateManager}; |
41 | | use nativelink_util::origin_event::get_node_id; |
42 | | use nativelink_util::platform_properties::PlatformProperties; |
43 | | use nativelink_util::shutdown_guard::ShutdownGuard; |
44 | | use tokio::sync::{Notify, mpsc}; |
45 | | use tonic::async_trait; |
46 | | use tracing::{debug, error, info, trace, warn}; |
47 | | |
48 | | /// How many state-manager lookups `kill_revoked_operations` has in flight |
49 | | /// at once while checking which running operations were revoked. |
50 | | const MAX_CONCURRENT_REVOKED_CHECKS: usize = 32; |
51 | | use uuid::Uuid; |
52 | | |
53 | | /// Metrics for tracking scheduler performance. |
54 | | #[derive(Debug, Default)] |
55 | | pub struct SchedulerMetrics { |
56 | | /// Total number of worker additions. |
57 | | pub workers_added: AtomicU64, |
58 | | /// Total number of worker removals. |
59 | | pub workers_removed: AtomicU64, |
60 | | /// Total number of `find_worker_for_action` calls. |
61 | | pub find_worker_calls: AtomicU64, |
62 | | /// Total number of successful worker matches. |
63 | | pub find_worker_hits: AtomicU64, |
64 | | /// Total number of failed worker matches (no worker found). |
65 | | pub find_worker_misses: AtomicU64, |
66 | | /// Total time spent in `find_worker_for_action` (nanoseconds). |
67 | | pub find_worker_time_ns: AtomicU64, |
68 | | /// Total number of workers iterated during find operations. |
69 | | pub workers_iterated: AtomicU64, |
70 | | /// Total number of action dispatches. |
71 | | pub actions_dispatched: AtomicU64, |
72 | | /// Total number of keep-alive updates. |
73 | | pub keep_alive_updates: AtomicU64, |
74 | | /// Total number of worker timeouts. |
75 | | pub worker_timeouts: AtomicU64, |
76 | | } |
77 | | |
78 | | use crate::platform_property_manager::PlatformPropertyManager; |
79 | | use crate::worker::{ActionInfoWithProps, Worker, WorkerTimestamp, WorkerUpdate}; |
80 | | use crate::worker_capability_index::WorkerCapabilityIndex; |
81 | | use crate::worker_registry::SharedWorkerRegistry; |
82 | | use crate::worker_scheduler::WorkerScheduler; |
83 | | |
84 | | #[derive(Debug)] |
85 | | struct Workers(LruCache<WorkerId, Worker>); |
86 | | |
87 | | impl Deref for Workers { |
88 | | type Target = LruCache<WorkerId, Worker>; |
89 | | |
90 | 223 | fn deref(&self) -> &Self::Target { |
91 | 223 | &self.0 |
92 | 223 | } |
93 | | } |
94 | | |
95 | | impl DerefMut for Workers { |
96 | 168 | fn deref_mut(&mut self) -> &mut Self::Target { |
97 | 168 | &mut self.0 |
98 | 168 | } |
99 | | } |
100 | | |
101 | | // Note: This could not be a derive macro because this derive-macro |
102 | | // does not support LruCache and nameless field structs. |
103 | | impl MetricsComponent for Workers { |
104 | 0 | fn publish( |
105 | 0 | &self, |
106 | 0 | _kind: MetricKind, |
107 | 0 | _field_metadata: MetricFieldData, |
108 | 0 | ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> { |
109 | 0 | let _enter = group!("workers").entered(); |
110 | 0 | for (worker_id, worker) in self.iter() { |
111 | 0 | let _enter = group!(worker_id).entered(); |
112 | 0 | worker.publish(MetricKind::Component, MetricFieldData::default())?; |
113 | | } |
114 | 0 | Ok(MetricPublishKnownKindData::Component) |
115 | 0 | } |
116 | | } |
117 | | |
118 | | /// A collection of workers that are available to run tasks. |
119 | | #[derive(MetricsComponent)] |
120 | | struct ApiWorkerSchedulerImpl { |
121 | | /// A `LruCache` of workers available based on `allocation_strategy`. |
122 | | #[metric(group = "workers")] |
123 | | workers: Workers, |
124 | | |
125 | | /// The worker state manager. |
126 | | #[metric(group = "worker_state_manager")] |
127 | | worker_state_manager: Arc<dyn WorkerStateManager>, |
128 | | /// The allocation strategy for workers. |
129 | | allocation_strategy: WorkerAllocationStrategy, |
130 | | /// A channel to notify the matching engine that the worker pool has changed. |
131 | | worker_change_notify: Arc<Notify>, |
132 | | /// Worker registry for tracking worker liveness. |
133 | | worker_registry: SharedWorkerRegistry, |
134 | | |
135 | | /// Whether the worker scheduler is shutting down. |
136 | | shutting_down: bool, |
137 | | |
138 | | /// Index for fast worker capability lookup. |
139 | | /// Used to accelerate `find_worker_for_action` by filtering candidates |
140 | | /// based on properties before doing linear scan. |
141 | | capability_index: WorkerCapabilityIndex, |
142 | | } |
143 | | |
144 | | impl core::fmt::Debug for ApiWorkerSchedulerImpl { |
145 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
146 | 0 | f.debug_struct("ApiWorkerSchedulerImpl") |
147 | 0 | .field("workers", &self.workers) |
148 | 0 | .field("allocation_strategy", &self.allocation_strategy) |
149 | 0 | .field("worker_change_notify", &self.worker_change_notify) |
150 | 0 | .field( |
151 | 0 | "capability_index_size", |
152 | 0 | &self.capability_index.worker_count(), |
153 | 0 | ) |
154 | 0 | .field("worker_registry", &self.worker_registry) |
155 | 0 | .finish_non_exhaustive() |
156 | 0 | } |
157 | | } |
158 | | |
159 | | impl ApiWorkerSchedulerImpl { |
160 | | /// Refreshes the lifetime of the worker with the given timestamp. |
161 | | /// |
162 | | /// Instead of sending N keepalive messages (one per operation), |
163 | | /// we now send a single worker heartbeat. The worker registry tracks worker liveness, |
164 | | /// and timeout detection checks the worker's `last_seen` instead of per-operation timestamps. |
165 | | /// |
166 | | /// Note: This only updates the local worker state. The worker registry is updated |
167 | | /// separately after releasing the inner lock to reduce contention. |
168 | 4 | fn refresh_lifetime( |
169 | 4 | &mut self, |
170 | 4 | worker_id: &WorkerId, |
171 | 4 | timestamp: WorkerTimestamp, |
172 | 4 | ) -> Result<(), Error> { |
173 | 4 | let worker = self.workers.0.peek_mut(worker_id).ok_or_else(|| {0 |
174 | 0 | make_input_err!( |
175 | | "Worker not found in worker map in refresh_lifetime() {}", |
176 | | worker_id |
177 | | ) |
178 | 0 | })?; |
179 | 0 | error_if!( |
180 | 4 | worker.last_update_timestamp > timestamp, |
181 | | "Worker already had a timestamp of {}, but tried to update it with {}", |
182 | | worker.last_update_timestamp, |
183 | | timestamp |
184 | | ); |
185 | 4 | worker.last_update_timestamp = timestamp; |
186 | | |
187 | 4 | trace!( |
188 | | ?worker_id, |
189 | 4 | running_operations = worker.running_action_infos.len(), |
190 | | "Worker keepalive received" |
191 | | ); |
192 | 4 | record_worker_keepalive(); |
193 | | |
194 | 4 | Ok(()) |
195 | 4 | } |
196 | | |
197 | | /// Adds a worker to the pool. |
198 | | /// Note: This function will not do any task matching. |
199 | 46 | fn add_worker(&mut self, worker: Worker) -> Result<(), Error> { |
200 | 46 | let worker_id = worker.id.clone(); |
201 | 46 | let platform_properties = worker.platform_properties.clone(); |
202 | | // A replacement is one out and one in, so the gauge only moves for a |
203 | | // genuinely new worker. |
204 | 46 | let replaced = self.workers.put(worker_id.clone(), worker); |
205 | | |
206 | | // Add to capability index for fast matching |
207 | 46 | self.capability_index |
208 | 46 | .add_worker(&worker_id, &platform_properties); |
209 | | |
210 | | // Worker is not cloneable, and we do not want to send the initial connection results until |
211 | | // we have added it to the map, or we might get some strange race conditions due to the way |
212 | | // the multi-threaded runtime works. |
213 | 46 | let worker = self.workers.peek_mut(&worker_id).unwrap(); |
214 | 46 | let res = worker |
215 | 46 | .send_initial_connection_result() |
216 | 46 | .err_tip(|| "Failed to send initial connection result to worker"); |
217 | 46 | if let Err(err0 ) = &res { |
218 | 0 | error!( |
219 | | ?worker_id, |
220 | | ?err, |
221 | | "Worker connection appears to have been closed while adding to pool" |
222 | | ); |
223 | 46 | } |
224 | 46 | if let Some(replaced2 ) = &replaced { |
225 | 2 | if replaced.is_draining { |
226 | 0 | record_worker_state("draining", false); |
227 | 2 | } |
228 | 2 | if replaced.is_paused { |
229 | 0 | record_worker_state("paused", false); |
230 | 2 | } |
231 | 44 | } else { |
232 | 44 | record_worker_connected(); |
233 | 44 | } |
234 | 46 | self.worker_change_notify.notify_one(); |
235 | 46 | res |
236 | 46 | } |
237 | | |
238 | | /// Removes worker from pool. |
239 | | /// Note: The caller is responsible for any rescheduling of any tasks that might be |
240 | | /// running. |
241 | 12 | fn remove_worker(&mut self, worker_id: &WorkerId) -> Option<Worker> { |
242 | | // Remove from capability index |
243 | 12 | self.capability_index.remove_worker(worker_id); |
244 | | |
245 | 12 | let result = self.workers.pop(worker_id); |
246 | 12 | self.worker_change_notify.notify_one(); |
247 | 12 | result |
248 | 12 | } |
249 | | |
250 | | /// Sets if the worker is draining or not. |
251 | 2 | fn set_drain_worker(&mut self, worker_id: &WorkerId, is_draining: bool) -> Result<(), Error> { |
252 | 2 | let worker = self |
253 | 2 | .workers |
254 | 2 | .get_mut(worker_id) |
255 | 2 | .err_tip(|| format!0 ("Worker {worker_id} doesn't exist in the pool"))?0 ; |
256 | 2 | if worker.is_draining != is_draining { |
257 | 2 | record_worker_state("draining", is_draining); |
258 | 2 | }0 |
259 | 2 | worker.is_draining = is_draining; |
260 | 2 | self.worker_change_notify.notify_one(); |
261 | 2 | Ok(()) |
262 | 2 | } |
263 | | |
264 | 72 | fn inner_find_worker_for_action( |
265 | 72 | &self, |
266 | 72 | platform_properties: &PlatformProperties, |
267 | 72 | full_worker_logging: bool, |
268 | 72 | ) -> Option<WorkerId> { |
269 | | // Do a fast check to see if any workers are available at all for work allocation |
270 | 72 | if !self.workers.iter().any(|(_, w)| w60 .can_accept_work60 ()) { |
271 | 14 | if full_worker_logging { |
272 | 2 | info!("All workers are fully allocated"); |
273 | 12 | } |
274 | 14 | return None; |
275 | 58 | } |
276 | | |
277 | | // Use capability index to get candidate workers that match STATIC properties |
278 | | // (Exact, Unknown) and have the required property keys (Priority, Minimum). |
279 | | // This reduces complexity from O(W × P) to O(P × log(W)) for exact properties. |
280 | 58 | let candidates = self |
281 | 58 | .capability_index |
282 | 58 | .find_matching_workers(platform_properties, full_worker_logging); |
283 | | |
284 | 58 | if candidates.is_empty() { |
285 | 1 | if full_worker_logging { |
286 | 0 | info!("No workers in capability index match required properties"); |
287 | 1 | } |
288 | 1 | return None; |
289 | 57 | } |
290 | | |
291 | | // Check function for availability AND dynamic Minimum property verification. |
292 | | // The index only does presence checks for Minimum properties since their |
293 | | // values change dynamically as jobs are assigned to workers. |
294 | 57 | let worker_matches = |(worker_id, w): &(&WorkerId, &Worker)| -> bool { |
295 | 57 | if !w.can_accept_work() { |
296 | 0 | if full_worker_logging { |
297 | 0 | info!( |
298 | | "Worker {worker_id} cannot accept work: is_paused={}, is_draining={}, inflight={}/{}", |
299 | | w.is_paused, |
300 | | w.is_draining, |
301 | 0 | w.running_action_infos.len(), |
302 | | w.max_inflight_tasks |
303 | | ); |
304 | 0 | } |
305 | 0 | return false; |
306 | 57 | } |
307 | | |
308 | | // Verify Minimum properties at runtime (their values are dynamic) |
309 | 57 | platform_properties.is_satisfied_by(&w.platform_properties, full_worker_logging) |
310 | 57 | }; |
311 | | |
312 | | // Now check constraints on filtered candidates. |
313 | | // Iterate in LRU order based on allocation strategy. |
314 | 57 | let workers_iter = self.workers.iter(); |
315 | | |
316 | 57 | let worker_id = match self.allocation_strategy { |
317 | | // Use rfind to get the least recently used that satisfies the properties. |
318 | 57 | WorkerAllocationStrategy::LeastRecentlyUsed => workers_iter |
319 | 57 | .rev() |
320 | 58 | .filter57 (|(worker_id, _)| candidates.contains(worker_id)) |
321 | 57 | .find(&worker_matches) |
322 | 57 | .map(|(_, w)| w.id48 .clone48 ()), |
323 | | |
324 | | // Use find to get the most recently used that satisfies the properties. |
325 | 0 | WorkerAllocationStrategy::MostRecentlyUsed => workers_iter |
326 | 0 | .filter(|(worker_id, _)| candidates.contains(worker_id)) |
327 | 0 | .find(&worker_matches) |
328 | 0 | .map(|(_, w)| w.id.clone()), |
329 | | }; |
330 | 57 | if full_worker_logging && worker_id10 .is_none10 () { |
331 | 1 | warn!("No workers matched!"); |
332 | 56 | } |
333 | 57 | worker_id |
334 | 72 | } |
335 | | |
336 | 14 | async fn update_action( |
337 | 14 | &mut self, |
338 | 14 | worker_id: &WorkerId, |
339 | 14 | operation_id: &OperationId, |
340 | 14 | update: UpdateOperationType, |
341 | 14 | ) -> Result<(), Error> { |
342 | 14 | let worker = self.workers.get_mut(worker_id).err_tip(|| {0 |
343 | 0 | format!("Worker {worker_id} does not exist in SimpleScheduler::update_action") |
344 | 0 | })?; |
345 | | |
346 | | // Ensure the worker is supposed to be running the operation. |
347 | 14 | if !worker.running_action_infos.contains_key(operation_id) { |
348 | 1 | let err = make_err!( |
349 | 1 | Code::Internal, |
350 | | "Operation {operation_id} should not be running on worker {worker_id} in SimpleScheduler::update_action" |
351 | | ); |
352 | 1 | return Result::<(), _>::Err(err.clone()) |
353 | 1 | .merge(self.immediate_evict_worker(worker_id, err, false).await); |
354 | 13 | } |
355 | | |
356 | 13 | let (is_finished, due_to_backpressure) = match &update { |
357 | 7 | UpdateOperationType::UpdateWithActionStage(action_stage) => { |
358 | 7 | (action_stage.is_finished(), false) |
359 | | } |
360 | 0 | UpdateOperationType::KeepAlive => (false, false), |
361 | 4 | UpdateOperationType::UpdateWithError(err) => { |
362 | 4 | (true, err.code == Code::ResourceExhausted) |
363 | | } |
364 | 2 | UpdateOperationType::UpdateWithDisconnect => (true, false), |
365 | | UpdateOperationType::ExecutionComplete => { |
366 | | // No update here, just restoring platform properties. |
367 | 0 | worker.execution_complete(operation_id); |
368 | 0 | self.worker_change_notify.notify_one(); |
369 | 0 | return Ok(()); |
370 | | } |
371 | | }; |
372 | | |
373 | | // Update the operation in the worker state manager. |
374 | 13 | let update_operation_res12 = if worker.is_kill_requested(operation_id) { |
375 | 1 | debug!( |
376 | | %operation_id, |
377 | | ?worker_id, |
378 | | "Ignoring update for operation the worker was told to kill" |
379 | | ); |
380 | 1 | Ok(()) |
381 | | } else { |
382 | 12 | let update_operation_res11 = self |
383 | 12 | .worker_state_manager |
384 | 12 | .update_operation(operation_id, worker_id, update) |
385 | 12 | .await |
386 | 11 | .err_tip(|| "in update_operation on SimpleScheduler::update_action"); |
387 | 11 | if let Err(err1 ) = &update_operation_res { |
388 | 1 | error!( |
389 | | %operation_id, |
390 | | ?worker_id, |
391 | | ?err, |
392 | | "Failed to update_operation on update_action" |
393 | | ); |
394 | 10 | } |
395 | 11 | update_operation_res |
396 | | }; |
397 | | |
398 | 12 | if !is_finished { |
399 | 0 | return update_operation_res; |
400 | 12 | } |
401 | | // The worker is done with this action even if the state-manager update |
402 | | // failed (e.g. the operation was already torn down after its clients |
403 | | // timed out). The worker bookkeeping below must still run, or the |
404 | | // worker's platform properties leak until it can never match again. |
405 | | |
406 | | // Clear this action from the current worker if finished. |
407 | 12 | let complete_action_res = { |
408 | | // Note: We need to run this before dealing with backpressure logic. |
409 | 12 | let was_paused = worker.is_paused; |
410 | 12 | let complete_action_res = worker.complete_action(operation_id); |
411 | | |
412 | 12 | if (due_to_backpressure || !worker.can_accept_work()) && worker0 .has_actions0 () { |
413 | 0 | worker.is_paused = true; |
414 | 12 | } |
415 | | // complete_action clears is_paused on its way through, so compare |
416 | | // the state either side of it rather than testing the flag after. |
417 | | // Testing afterwards counts a re-pause every time and never |
418 | | // unwinds, which leaves the gauge climbing forever. |
419 | 12 | if was_paused != worker.is_paused { |
420 | 0 | record_worker_state("paused", worker.is_paused); |
421 | 12 | } |
422 | 12 | complete_action_res |
423 | | }; |
424 | | |
425 | 12 | self.worker_change_notify.notify_one(); |
426 | | |
427 | 12 | update_operation_res.merge(complete_action_res) |
428 | 13 | } |
429 | | |
430 | | /// Notifies the specified worker to run the given action and handles errors by evicting |
431 | | /// the worker if the notification fails. |
432 | 44 | async fn worker_notify_run_action( |
433 | 44 | &mut self, |
434 | 44 | worker_id: WorkerId, |
435 | 44 | operation_id: OperationId, |
436 | 44 | action_info: ActionInfoWithProps, |
437 | 44 | ) -> Result<(), Error> { |
438 | 44 | if let Some(worker) = self.workers.get_mut(&worker_id) { |
439 | 44 | let notify_worker_result = worker |
440 | 44 | .notify_update(WorkerUpdate::RunAction(Box::new(( |
441 | 44 | operation_id, |
442 | 44 | action_info.clone(), |
443 | 44 | )))) |
444 | 44 | .await; |
445 | | |
446 | 44 | if let Err(notify_worker_result1 ) = notify_worker_result { |
447 | 1 | warn!( |
448 | | ?worker_id, |
449 | | ?action_info, |
450 | | ?notify_worker_result, |
451 | | "Worker command failed, removing worker", |
452 | | ); |
453 | | |
454 | | // A slightly nasty way of figuring out that the worker disconnected |
455 | | // from send_msg_to_worker without introducing complexity to the |
456 | | // code path from here to there. |
457 | 1 | let is_disconnect = notify_worker_result.code == Code::Internal |
458 | 1 | && notify_worker_result.messages.len() == 1 |
459 | 0 | && notify_worker_result.messages[0] == "Worker Disconnected"; |
460 | | |
461 | 1 | let err = make_err!( |
462 | 1 | Code::Internal, |
463 | | "Worker command failed, removing worker {worker_id} -- {notify_worker_result:?}", |
464 | | ); |
465 | | |
466 | 1 | return Result::<(), _>::Err(err.clone()).merge( |
467 | 1 | self.immediate_evict_worker(&worker_id, err, is_disconnect) |
468 | 1 | .await, |
469 | | ); |
470 | 43 | } |
471 | 43 | Ok(()) |
472 | | } else { |
473 | 0 | warn!( |
474 | | ?worker_id, |
475 | | %operation_id, |
476 | | ?action_info, |
477 | | "Worker not found in worker map in worker_notify_run_action" |
478 | | ); |
479 | | // Ensure the operation is put back to queued state. |
480 | 0 | self.worker_state_manager |
481 | 0 | .update_operation( |
482 | 0 | &operation_id, |
483 | 0 | &worker_id, |
484 | 0 | UpdateOperationType::UpdateWithDisconnect, |
485 | 0 | ) |
486 | 0 | .await |
487 | | } |
488 | 44 | } |
489 | | |
490 | | /// Tells the worker to kill an operation it is still running but the |
491 | | /// state manager no longer has executing on it. A worker that cannot be |
492 | | /// reached is evicted, the same as for a failed run request. |
493 | 3 | async fn worker_notify_kill_operation( |
494 | 3 | &mut self, |
495 | 3 | worker_id: &WorkerId, |
496 | 3 | operation_id: OperationId, |
497 | 3 | ) -> Result<(), Error> { |
498 | 3 | let Some(worker) = self.workers.get_mut(worker_id) else { |
499 | | // Gone between the snapshot and now; its actions were requeued. |
500 | 0 | return Ok(()); |
501 | | }; |
502 | | // Already told, or finished in the meantime; nothing more to send. |
503 | 3 | if !worker.running_action_infos.contains_key(&operation_id) |
504 | 3 | || worker.is_kill_requested(&operation_id) |
505 | | { |
506 | 0 | return Ok(()); |
507 | 3 | } |
508 | 3 | info!( |
509 | | ?worker_id, |
510 | | %operation_id, |
511 | | "Killing operation the state manager no longer has executing on this worker" |
512 | | ); |
513 | 3 | if let Err(err1 ) = worker |
514 | 3 | .notify_update(WorkerUpdate::KillOperation(operation_id.clone())) |
515 | 3 | .await |
516 | | { |
517 | 1 | warn!( |
518 | | ?worker_id, |
519 | | %operation_id, |
520 | | ?err, |
521 | | "Worker command failed, removing worker" |
522 | | ); |
523 | 1 | let err = make_err!( |
524 | 1 | Code::Internal, |
525 | | "Worker command failed, removing worker {worker_id} -- {err:?}", |
526 | | ); |
527 | 1 | return Result::<(), _>::Err(err.clone()) |
528 | 1 | .merge(self.immediate_evict_worker(worker_id, err, true).await); |
529 | 2 | } |
530 | 2 | Ok(()) |
531 | 3 | } |
532 | | |
533 | | /// Evicts the worker from the pool and puts items back into the queue if anything was being executed on it. |
534 | 12 | async fn immediate_evict_worker( |
535 | 12 | &mut self, |
536 | 12 | worker_id: &WorkerId, |
537 | 12 | err: Error, |
538 | 12 | is_disconnect: bool, |
539 | 12 | ) -> Result<(), Error> { |
540 | 12 | let mut result = Ok(()); |
541 | 12 | if let Some(mut worker) = self.remove_worker(worker_id) { |
542 | | // Log every eviction here rather than in each caller, so a worker |
543 | | // can never leave the pool unexplained. Without this, a worker that |
544 | | // vanished mid-build left nothing on the scheduler side to |
545 | | // attribute it to, and the only visible symptom was the worker |
546 | | // reconnecting with a fresh id. |
547 | 12 | info!( |
548 | | ?worker_id, |
549 | | is_disconnect, |
550 | 12 | running_actions = worker.running_action_infos.len(), |
551 | 12 | reason = %err.message_string(), |
552 | | "Evicting worker from pool" |
553 | | ); |
554 | 12 | record_worker_disconnected( |
555 | 12 | if is_disconnect { |
556 | 1 | WorkerDisconnectReason::Disconnected |
557 | | } else { |
558 | 11 | WorkerDisconnectReason::Evicted |
559 | | }, |
560 | 12 | worker.is_draining, |
561 | 12 | worker.is_paused, |
562 | | ); |
563 | | // We don't care if we fail to send message to worker, this is only a best attempt. |
564 | 12 | drop(worker.notify_update(WorkerUpdate::Disconnect).await); |
565 | 12 | let update = if is_disconnect { |
566 | 1 | UpdateOperationType::UpdateWithDisconnect |
567 | | } else { |
568 | 11 | UpdateOperationType::UpdateWithError(err) |
569 | | }; |
570 | 12 | for (operation_id10 , _) in worker.running_action_infos.drain() { |
571 | 10 | result = result.merge( |
572 | 10 | self.worker_state_manager |
573 | 10 | .update_operation(&operation_id, worker_id, update.clone()) |
574 | 10 | .await, |
575 | | ); |
576 | | } |
577 | 0 | } |
578 | | // Note: Calling this many time is very cheap, it'll only trigger `do_try_match` once. |
579 | | // TODO(palfrey) This should be moved to inside the Workers struct. |
580 | 12 | self.worker_change_notify.notify_one(); |
581 | 12 | result |
582 | 12 | } |
583 | | } |
584 | | |
585 | | #[derive(Debug, MetricsComponent)] |
586 | | pub struct ApiWorkerScheduler { |
587 | | #[metric] |
588 | | inner: Mutex<ApiWorkerSchedulerImpl>, |
589 | | #[metric(group = "platform_property_manager")] |
590 | | platform_property_manager: Arc<PlatformPropertyManager>, |
591 | | |
592 | | #[metric( |
593 | | help = "Timeout of how long to evict workers if no response in this given amount of time in seconds." |
594 | | )] |
595 | | worker_timeout_s: u64, |
596 | | #[metric( |
597 | | help = "How long a sent kill may go unacknowledged before the worker is evicted, in seconds." |
598 | | )] |
599 | | unacknowledged_kill_timeout_s: u64, |
600 | | /// Shared worker registry for checking worker liveness. |
601 | | worker_registry: SharedWorkerRegistry, |
602 | | |
603 | | /// Performance metrics for observability. |
604 | | metrics: Arc<SchedulerMetrics>, |
605 | | |
606 | | /// Channel for publishing origin events such as worker-observed action |
607 | | /// resource usage. `None` when origin events are disabled. |
608 | | maybe_origin_event_tx: Option<mpsc::Sender<OriginEvent>>, |
609 | | } |
610 | | |
611 | | impl ApiWorkerScheduler { |
612 | | #[expect(clippy::too_many_arguments)] |
613 | 40 | pub fn new( |
614 | 40 | worker_state_manager: Arc<dyn WorkerStateManager>, |
615 | 40 | platform_property_manager: Arc<PlatformPropertyManager>, |
616 | 40 | allocation_strategy: WorkerAllocationStrategy, |
617 | 40 | worker_change_notify: Arc<Notify>, |
618 | 40 | worker_timeout_s: u64, |
619 | 40 | unacknowledged_kill_timeout_s: u64, |
620 | 40 | worker_registry: SharedWorkerRegistry, |
621 | 40 | maybe_origin_event_tx: Option<mpsc::Sender<OriginEvent>>, |
622 | 40 | ) -> Arc<Self> { |
623 | 40 | Arc::new(Self { |
624 | 40 | inner: Mutex::new(ApiWorkerSchedulerImpl { |
625 | 40 | workers: Workers(LruCache::unbounded()), |
626 | 40 | worker_state_manager, |
627 | 40 | allocation_strategy, |
628 | 40 | worker_change_notify, |
629 | 40 | worker_registry: worker_registry.clone(), |
630 | 40 | shutting_down: false, |
631 | 40 | capability_index: WorkerCapabilityIndex::new(), |
632 | 40 | }), |
633 | 40 | platform_property_manager, |
634 | 40 | worker_timeout_s, |
635 | 40 | unacknowledged_kill_timeout_s, |
636 | 40 | worker_registry, |
637 | 40 | metrics: Arc::new(SchedulerMetrics::default()), |
638 | 40 | maybe_origin_event_tx, |
639 | 40 | }) |
640 | 40 | } |
641 | | |
642 | | /// Returns a reference to the worker registry. |
643 | 0 | pub const fn worker_registry(&self) -> &SharedWorkerRegistry { |
644 | 0 | &self.worker_registry |
645 | 0 | } |
646 | | |
647 | 44 | pub async fn worker_notify_run_action( |
648 | 44 | &self, |
649 | 44 | worker_id: WorkerId, |
650 | 44 | operation_id: OperationId, |
651 | 44 | action_info: ActionInfoWithProps, |
652 | 44 | ) -> Result<(), Error> { |
653 | 44 | self.metrics |
654 | 44 | .actions_dispatched |
655 | 44 | .fetch_add(1, Ordering::Relaxed); |
656 | 44 | let mut inner = self.inner.lock().await; |
657 | 44 | inner |
658 | 44 | .worker_notify_run_action(worker_id, operation_id, action_info) |
659 | 44 | .await |
660 | 44 | } |
661 | | |
662 | 1 | pub async fn running_action_info( |
663 | 1 | &self, |
664 | 1 | worker_id: &WorkerId, |
665 | 1 | operation_id: &OperationId, |
666 | 1 | ) -> Option<ActionInfoWithProps> { |
667 | 1 | let inner = self.inner.lock().await; |
668 | 1 | inner |
669 | 1 | .workers |
670 | 1 | .peek(worker_id) |
671 | 1 | .and_then(|worker| worker.running_action_infos.get(operation_id)) |
672 | 1 | .map(|pending_action_info| pending_action_info.action_info.clone()) |
673 | 1 | } |
674 | | |
675 | | /// Returns the scheduler metrics for observability. |
676 | | #[must_use] |
677 | 0 | pub const fn get_metrics(&self) -> &Arc<SchedulerMetrics> { |
678 | 0 | &self.metrics |
679 | 0 | } |
680 | | |
681 | | /// Attempts to find a worker that is capable of running this action. |
682 | | // TODO(palfrey) This algorithm is not very efficient. Simple testing using a tree-like |
683 | | // structure showed worse performance on a 10_000 worker * 7 properties * 1000 queued tasks |
684 | | // simulation of worst cases in a single threaded environment. |
685 | 72 | pub async fn find_worker_for_action( |
686 | 72 | &self, |
687 | 72 | platform_properties: &PlatformProperties, |
688 | 72 | full_worker_logging: bool, |
689 | 72 | ) -> Option<WorkerId> { |
690 | 72 | let start = Instant::now(); |
691 | 72 | self.metrics |
692 | 72 | .find_worker_calls |
693 | 72 | .fetch_add(1, Ordering::Relaxed); |
694 | | |
695 | 72 | let inner = self.inner.lock().await; |
696 | 72 | let worker_count = inner.workers.len() as u64; |
697 | 72 | let result = inner.inner_find_worker_for_action(platform_properties, full_worker_logging); |
698 | | |
699 | | // Track workers iterated (worst case is all workers) |
700 | 72 | self.metrics |
701 | 72 | .workers_iterated |
702 | 72 | .fetch_add(worker_count, Ordering::Relaxed); |
703 | | |
704 | 72 | if result.is_some() { |
705 | 48 | self.metrics |
706 | 48 | .find_worker_hits |
707 | 48 | .fetch_add(1, Ordering::Relaxed); |
708 | 48 | } else { |
709 | 24 | self.metrics |
710 | 24 | .find_worker_misses |
711 | 24 | .fetch_add(1, Ordering::Relaxed); |
712 | 24 | } |
713 | | |
714 | | #[allow(clippy::cast_possible_truncation)] |
715 | 72 | self.metrics |
716 | 72 | .find_worker_time_ns |
717 | 72 | .fetch_add(start.elapsed().as_nanos() as u64, Ordering::Relaxed); |
718 | 72 | result |
719 | 72 | } |
720 | | |
721 | | /// Checks to see if the worker exists in the worker pool. Should only be used in unit tests. |
722 | | #[must_use] |
723 | 7 | pub async fn contains_worker_for_test(&self, worker_id: &WorkerId) -> bool { |
724 | 7 | let inner = self.inner.lock().await; |
725 | 7 | inner.workers.contains(worker_id) |
726 | 7 | } |
727 | | |
728 | | /// A unit test function used to send the keep alive message to the worker from the server. |
729 | 0 | pub async fn send_keep_alive_to_worker_for_test( |
730 | 0 | &self, |
731 | 0 | worker_id: &WorkerId, |
732 | 1 | ) -> Result<(), Error> { |
733 | 1 | let mut inner = self.inner.lock().await; |
734 | 1 | let worker = inner.workers.get_mut(worker_id).ok_or_else(|| {0 |
735 | 0 | make_input_err!("WorkerId '{}' does not exist in workers map", worker_id) |
736 | 0 | })?; |
737 | 1 | worker.keep_alive() |
738 | 1 | } |
739 | | } |
740 | | |
741 | | #[async_trait] |
742 | | impl WorkerScheduler for ApiWorkerScheduler { |
743 | 2 | fn get_platform_property_manager(&self) -> &PlatformPropertyManager { |
744 | 2 | self.platform_property_manager.as_ref() |
745 | 2 | } |
746 | | |
747 | | async fn record_action_resource_usage( |
748 | | &self, |
749 | | worker_id: &WorkerId, |
750 | | operation_id: &OperationId, |
751 | | mut resource_usage: ActionResourceUsage, |
752 | 1 | ) -> Result<(), Error> { |
753 | | // The worker API talks to this `ApiWorkerScheduler` (it is the |
754 | | // `WorkerScheduler` returned by `SimpleScheduler::new`), so the |
755 | | // resource-usage origin event must be published here. Previously the |
756 | | // only override lived on `SimpleScheduler`, which this path never |
757 | | // reaches, so the event was silently dropped by the trait's no-op |
758 | | // default and `observed_worker_peak_memory_mib` was never recorded. |
759 | | // Sampling is optional, so only record when the worker actually took a |
760 | | // reading. A zero here means "not sampled", not "used no memory". |
761 | | let maybe_action_info = self.running_action_info(worker_id, operation_id).await; |
762 | | let action_mnemonic = maybe_action_info |
763 | | .as_ref() |
764 | 1 | .and_then(|action_info| action_info.origin_metadata.bazel_metadata.as_ref()) |
765 | 1 | .map_or_else(String::new, |bazel_metadata| { |
766 | 1 | bazel_metadata.action_mnemonic.clone() |
767 | 1 | }); |
768 | | |
769 | | if resource_usage.sampled { |
770 | | if resource_usage.peak_memory_kb > 0 { |
771 | | record_execution_peak_memory(resource_usage.peak_memory_kb, "", &action_mnemonic); |
772 | | } |
773 | | if resource_usage.cpu_time_ms > 0 { |
774 | | record_execution_cpu_time(resource_usage.cpu_time_ms, "", &action_mnemonic); |
775 | | } |
776 | | } |
777 | | |
778 | | let Some(origin_event_tx) = self.maybe_origin_event_tx.as_ref() else { |
779 | | return Ok(()); |
780 | | }; |
781 | | let Some(action_info) = maybe_action_info else { |
782 | | return Ok(()); |
783 | | }; |
784 | | |
785 | | if resource_usage.operation_id.is_empty() { |
786 | | resource_usage.operation_id = operation_id.to_string(); |
787 | | } |
788 | | if resource_usage.worker_id.is_empty() { |
789 | | resource_usage.worker_id = worker_id.to_string(); |
790 | | } |
791 | | |
792 | | let event = Event { |
793 | | event: Some(event::Event::Response(ResponseEvent { |
794 | | event: Some(response_event::Event::ActionResourceUsage(resource_usage)), |
795 | | })), |
796 | | }; |
797 | | let origin_event = OriginEvent { |
798 | | version: 0, |
799 | | event_id: Uuid::now_v6(&get_node_id(Some(&event))) |
800 | | .hyphenated() |
801 | | .to_string(), |
802 | | parent_event_id: action_info |
803 | | .scheduler_start_execute_event_id |
804 | | .clone() |
805 | | .unwrap_or_default(), |
806 | | bazel_request_metadata: action_info.origin_metadata.bazel_metadata.clone(), |
807 | | identity: action_info.origin_metadata.identity, |
808 | | event: Some(event), |
809 | | }; |
810 | | // Awaited send (not try_send): apply backpressure when the publisher |
811 | | // queue is full instead of silently dropping the resource-usage event, |
812 | | // which is what drives action-level resource sizing in the UI. |
813 | | if let Err(err) = origin_event_tx.send(origin_event).await { |
814 | | warn!(?err, "Failed to publish action resource usage origin event"); |
815 | | } |
816 | | Ok(()) |
817 | 1 | } |
818 | | |
819 | 46 | async fn add_worker(&self, worker: Worker) -> Result<(), Error> { |
820 | | let worker_id = worker.id.clone(); |
821 | | let worker_timestamp = worker.last_update_timestamp; |
822 | | let mut inner = self.inner.lock().await; |
823 | | if inner.shutting_down { |
824 | | warn!("Rejected worker add during shutdown: {}", worker_id); |
825 | | return Err(make_err!( |
826 | | Code::Unavailable, |
827 | | "Received request to add worker while shutting down" |
828 | | )); |
829 | | } |
830 | | let result = inner |
831 | | .add_worker(worker) |
832 | | .err_tip(|| "Error while adding worker, removing from pool"); |
833 | | if let Err(err) = result { |
834 | | return Result::<(), _>::Err(err.clone()) |
835 | | .merge(inner.immediate_evict_worker(&worker_id, err, false).await); |
836 | | } |
837 | | |
838 | | let now = UNIX_EPOCH + Duration::from_secs(worker_timestamp); |
839 | | self.worker_registry.register_worker(&worker_id, now).await; |
840 | | |
841 | | self.metrics.workers_added.fetch_add(1, Ordering::Relaxed); |
842 | | Ok(()) |
843 | 46 | } |
844 | | |
845 | | async fn update_action( |
846 | | &self, |
847 | | worker_id: &WorkerId, |
848 | | operation_id: &OperationId, |
849 | | update: UpdateOperationType, |
850 | 14 | ) -> Result<(), Error> { |
851 | | let mut inner = self.inner.lock().await; |
852 | | inner.update_action(worker_id, operation_id, update).await |
853 | 14 | } |
854 | | |
855 | | async fn worker_keep_alive_received( |
856 | | &self, |
857 | | worker_id: &WorkerId, |
858 | | timestamp: WorkerTimestamp, |
859 | 4 | ) -> Result<(), Error> { |
860 | | { |
861 | | let mut inner = self.inner.lock().await; |
862 | | inner |
863 | | .refresh_lifetime(worker_id, timestamp) |
864 | | .err_tip(|| "Error refreshing lifetime in worker_keep_alive_received()")?; |
865 | | } |
866 | | let now = UNIX_EPOCH + Duration::from_secs(timestamp); |
867 | | self.worker_registry |
868 | | .update_worker_heartbeat(worker_id, now) |
869 | | .await; |
870 | | Ok(()) |
871 | 4 | } |
872 | | |
873 | 6 | async fn remove_worker(&self, worker_id: &WorkerId) -> Result<(), Error> { |
874 | | self.worker_registry.remove_worker(worker_id).await; |
875 | | |
876 | | let mut inner = self.inner.lock().await; |
877 | | inner |
878 | | .immediate_evict_worker( |
879 | | worker_id, |
880 | | make_err!(Code::Internal, "Received request to remove worker"), |
881 | | false, |
882 | | ) |
883 | | .await |
884 | 6 | } |
885 | | |
886 | 0 | async fn shutdown(&self, shutdown_guard: ShutdownGuard) { |
887 | | let mut inner = self.inner.lock().await; |
888 | | inner.shutting_down = true; // should reject further worker registration |
889 | | while let Some(worker_id) = inner |
890 | | .workers |
891 | | .peek_lru() |
892 | 0 | .map(|(worker_id, _worker)| worker_id.clone()) |
893 | | { |
894 | | if let Err(err) = inner |
895 | | .immediate_evict_worker( |
896 | | &worker_id, |
897 | | make_err!(Code::Internal, "Scheduler shutdown"), |
898 | | true, |
899 | | ) |
900 | | .await |
901 | | { |
902 | | error!(?err, "Error evicting worker on shutdown."); |
903 | | } |
904 | | } |
905 | | drop(shutdown_guard); |
906 | 0 | } |
907 | | |
908 | 7 | async fn remove_timedout_workers(&self, now_timestamp: WorkerTimestamp) -> Result<(), Error> { |
909 | | // Check worker liveness using both the local timestamp (from LRU) |
910 | | // and the worker registry. A worker is alive if either source says it's alive. |
911 | | let timeout = Duration::from_secs(self.worker_timeout_s); |
912 | | let now = UNIX_EPOCH + Duration::from_secs(now_timestamp); |
913 | | let timeout_threshold = now_timestamp.saturating_sub(self.worker_timeout_s); |
914 | | |
915 | | let workers_to_check: Vec<(WorkerId, bool, bool)> = { |
916 | | let inner = self.inner.lock().await; |
917 | | inner |
918 | | .workers |
919 | | .iter() |
920 | 8 | .map(|(worker_id, worker)| { |
921 | 8 | let local_alive = worker.last_update_timestamp > timeout_threshold; |
922 | 8 | let kill_overdue = worker.running_action_infos.values().any(|info| {3 |
923 | 3 | info.kill_requested_at.is_some_and(|at| {2 |
924 | 2 | now_timestamp.saturating_sub(at) > self.unacknowledged_kill_timeout_s |
925 | 2 | }) |
926 | 3 | }); |
927 | 8 | (worker_id.clone(), local_alive, kill_overdue) |
928 | 8 | }) |
929 | | .collect() |
930 | | }; |
931 | | |
932 | | let mut worker_ids_to_remove = Vec::new(); |
933 | | for (worker_id, local_alive, kill_overdue) in workers_to_check { |
934 | | // A healthy worker acknowledges a kill in moments; one that |
935 | | // cannot is wedged, and its keepalives keep the liveness checks |
936 | | // below from ever firing while the dead operation holds its |
937 | | // slot (the nativelink#2672 symptom). Evicting requeues its |
938 | | // other operations. |
939 | | if kill_overdue { |
940 | | warn!( |
941 | | ?worker_id, |
942 | | unacknowledged_kill_timeout_s = self.unacknowledged_kill_timeout_s, |
943 | | "Worker did not acknowledge a kill in time, removing from pool" |
944 | | ); |
945 | | worker_ids_to_remove.push((worker_id, true)); |
946 | | continue; |
947 | | } |
948 | | |
949 | | if local_alive { |
950 | | continue; |
951 | | } |
952 | | |
953 | | let registry_alive = self |
954 | | .worker_registry |
955 | | .is_worker_alive(&worker_id, timeout, now) |
956 | | .await; |
957 | | |
958 | | if !registry_alive { |
959 | | trace!( |
960 | | ?worker_id, |
961 | | local_alive, |
962 | | registry_alive, |
963 | | timeout_threshold, |
964 | | "Worker timed out - neither local nor registry shows alive" |
965 | | ); |
966 | | worker_ids_to_remove.push((worker_id, false)); |
967 | | } |
968 | | } |
969 | | |
970 | | if worker_ids_to_remove.is_empty() { |
971 | | return Ok(()); |
972 | | } |
973 | | |
974 | | let mut inner = self.inner.lock().await; |
975 | | let mut result = Ok(()); |
976 | | |
977 | | for (worker_id, kill_overdue) in &worker_ids_to_remove { |
978 | | let err = if *kill_overdue { |
979 | | make_err!( |
980 | | Code::Internal, |
981 | | "Worker {worker_id} did not acknowledge a kill within {}s, removing from pool", |
982 | | self.unacknowledged_kill_timeout_s |
983 | | ) |
984 | | } else { |
985 | | warn!(?worker_id, "Worker timed out, removing from pool"); |
986 | | make_err!( |
987 | | Code::Internal, |
988 | | "Worker {worker_id} timed out, removing from pool" |
989 | | ) |
990 | | }; |
991 | | result = result.merge(inner.immediate_evict_worker(worker_id, err, false).await); |
992 | | } |
993 | | |
994 | | result |
995 | 7 | } |
996 | | |
997 | 2 | async fn set_drain_worker(&self, worker_id: &WorkerId, is_draining: bool) -> Result<(), Error> { |
998 | | let mut inner = self.inner.lock().await; |
999 | | inner.set_drain_worker(worker_id, is_draining) |
1000 | 2 | } |
1001 | | |
1002 | 7 | async fn kill_revoked_operations(&self) -> Result<(), Error> { |
1003 | | let (worker_state_manager, running) = { |
1004 | | let inner = self.inner.lock().await; |
1005 | | let running: Vec<(WorkerId, OperationId)> = inner |
1006 | | .workers |
1007 | | .iter() |
1008 | 7 | .flat_map(|(worker_id, worker)| { |
1009 | 7 | worker |
1010 | 7 | .running_action_infos |
1011 | 7 | .iter() |
1012 | | // An operation already told to die is never swept |
1013 | | // again; the kill itself is not re-sent. A worker |
1014 | | // that received the kill but never reports back is |
1015 | | // evicted by remove_timedout_workers once the kill |
1016 | | // has gone unacknowledged longer than |
1017 | | // `unacknowledged_kill_timeout_s`; a dead worker by |
1018 | | // the ordinary keepalive timeout. |
1019 | 7 | .filter(|(_, pending_action_info)| { |
1020 | 7 | pending_action_info.kill_requested_at.is_none() |
1021 | 7 | }) |
1022 | 7 | .map(|(operation_id, _)| (worker_id6 .clone6 (), operation_id6 .clone6 ())) |
1023 | 7 | }) |
1024 | | .collect(); |
1025 | | (inner.worker_state_manager.clone(), running) |
1026 | | }; |
1027 | | |
1028 | | // On store-backed deployments each check is a network round-trip, |
1029 | | // so run them lock-free with bounded concurrency. |
1030 | | let revoked: Vec<(WorkerId, OperationId)> = futures::stream::iter(running) |
1031 | 6 | .map(|(worker_id, operation_id)| { |
1032 | 6 | let worker_state_manager = worker_state_manager.clone(); |
1033 | 6 | async move { |
1034 | 6 | match worker_state_manager |
1035 | 6 | .is_executing_on_worker(&operation_id, &worker_id) |
1036 | 6 | .await |
1037 | | { |
1038 | 3 | Ok(true) => None, |
1039 | 3 | Ok(false) => Some((worker_id, operation_id)), |
1040 | | // Only kill on positive evidence; try again next pass. |
1041 | 0 | Err(err) => { |
1042 | 0 | warn!( |
1043 | | ?worker_id, |
1044 | | %operation_id, |
1045 | | ?err, |
1046 | | "Could not check whether operation is still executing on worker" |
1047 | | ); |
1048 | 0 | None |
1049 | | } |
1050 | | } |
1051 | 6 | } |
1052 | 6 | }) |
1053 | | .buffer_unordered(MAX_CONCURRENT_REVOKED_CHECKS) |
1054 | | .filter_map(future::ready) |
1055 | | .collect() |
1056 | | .await; |
1057 | | |
1058 | | if revoked.is_empty() { |
1059 | | return Ok(()); |
1060 | | } |
1061 | | |
1062 | | // Re-check lock-free so the scheduler mutex is never held across |
1063 | | // store I/O; the checks above may be stale by the time we get here. |
1064 | | // The remaining TOCTOU window is fine: worker_notify_kill_operation |
1065 | | // re-guards with contains_key + is_kill_requested under the lock. |
1066 | | let mut confirmed = Vec::new(); |
1067 | | for (worker_id, operation_id) in revoked { |
1068 | | // A result of Ok(true) or Err(_) means the operation was |
1069 | | // reassigned to this worker after the first check, or the state |
1070 | | // manager went quiet: nothing to kill on this pass. |
1071 | | let revoked = worker_state_manager |
1072 | | .is_executing_on_worker(&operation_id, &worker_id) |
1073 | | .await |
1074 | 3 | .is_ok_and(|executing| !executing); |
1075 | | if revoked { |
1076 | | confirmed.push((worker_id, operation_id)); |
1077 | | } |
1078 | | } |
1079 | | |
1080 | | if confirmed.is_empty() { |
1081 | | return Ok(()); |
1082 | | } |
1083 | | |
1084 | | let mut inner = self.inner.lock().await; |
1085 | | let mut result = Ok(()); |
1086 | | for (worker_id, operation_id) in confirmed { |
1087 | | result = result.merge( |
1088 | | inner |
1089 | | .worker_notify_kill_operation(&worker_id, operation_id) |
1090 | | .await, |
1091 | | ); |
1092 | | } |
1093 | | result |
1094 | 7 | } |
1095 | | } |
1096 | | |
1097 | | impl RootMetricsComponent for ApiWorkerScheduler {} |