Coverage Report

Created: 2026-10-08 11:27

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::collections::{BTreeSet, HashMap, HashSet};
19
use std::sync::Arc;
20
use std::time::{Instant, SystemTime, UNIX_EPOCH};
21
22
use async_lock::Mutex;
23
use futures::{StreamExt, future};
24
use lru::LruCache;
25
use nativelink_config::schedulers::{MemoryEscalationSpec, WorkerAllocationStrategy};
26
use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err};
27
use nativelink_metric::{
28
    MetricFieldData, MetricKind, MetricPublishKnownKindData, MetricsComponent,
29
    RootMetricsComponent, group,
30
};
31
use nativelink_proto::com::github::trace_machina::nativelink::events::{
32
    Event, OriginEvent, ResponseEvent, event, response_event,
33
};
34
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::{
35
    ActionResourceUsage, ResourceOutcome, WorkerLoad,
36
};
37
use nativelink_util::action_messages::{ActionStage, OperationId, WorkerId};
38
use nativelink_util::metrics::{
39
    WorkerDisconnectReason, record_dispatch_requeue, record_execution_cpu_time,
40
    record_execution_peak_memory, record_worker_connected, record_worker_disconnected,
41
    record_worker_keepalive, record_worker_keepalive_gap, record_worker_state,
42
};
43
use nativelink_util::operation_state_manager::{
44
    Decline, Escalation, UpdateOperationType, WorkerStateManager,
45
};
46
use nativelink_util::origin_event::get_node_id;
47
use nativelink_util::platform_properties::{PlatformProperties, PlatformPropertyValue};
48
use nativelink_util::shutdown_guard::ShutdownGuard;
49
use tokio::sync::{Notify, mpsc};
50
use tonic::async_trait;
51
use tracing::{debug, error, info, trace, warn};
52
53
/// How many state-manager lookups `kill_revoked_operations` has in flight
54
/// at once while checking which running operations were revoked.
55
const MAX_CONCURRENT_REVOKED_CHECKS: usize = 32;
56
57
/// How many of the action's `priority` properties the worker carries with
58
/// the same value. A priority property never restricts placement; it says
59
/// where the action would rather be.
60
7
fn priority_matches(action: &PlatformProperties, worker: &Worker) -> usize {
61
7
    action
62
7
        .properties
63
7
        .iter()
64
11
        .
filter7
(|(name, value)| {
65
11
            
matches!7
(value, PlatformPropertyValue::Priority(_))
66
4
                && worker.platform_properties.properties.get(*name) == Some(*value)
67
11
        })
68
7
        .count()
69
7
}
70
71
/// How much of the worker's advertised capacity would be left after this
72
/// action, summed over the action's `minimum` properties, in thousandths.
73
/// Lower is a tighter fit. An action with no numbers fits every worker
74
/// equally, and a dimension the worker does not advertise is ignored.
75
7
fn fit_leftover_permille(action: &PlatformProperties, worker: &Worker) -> u64 {
76
7
    let mut leftover = 0u64;
77
11
    for (name, value) in 
&action.properties7
{
78
11
        let PlatformPropertyValue::Minimum(
needed7
) = value else {
79
4
            continue;
80
        };
81
        let (
82
7
            Some(PlatformPropertyValue::Minimum(available)),
83
7
            Some(PlatformPropertyValue::Minimum(total)),
84
        ) = (
85
7
            worker.platform_properties.properties.get(name),
86
7
            worker.total_platform_properties.properties.get(name),
87
        )
88
        else {
89
0
            continue;
90
        };
91
7
        if *total == 0 {
92
0
            continue;
93
7
        }
94
7
        let remaining = available.saturating_sub(*needed);
95
7
        leftover += remaining.saturating_mul(1000) / total;
96
    }
97
7
    leftover
98
7
}
99
100
/// How many property shapes `static_verdicts` holds before it is emptied.
101
const MAX_STATIC_VERDICTS: usize = 4096;
102
103
use uuid::Uuid;
104
105
/// Metrics for tracking scheduler performance.
106
#[derive(Debug, Default)]
107
pub struct SchedulerMetrics {
108
    /// Total number of worker additions.
109
    pub workers_added: AtomicU64,
110
    /// Total number of worker removals.
111
    pub workers_removed: AtomicU64,
112
    /// Total number of `find_worker_for_action` calls.
113
    pub find_worker_calls: AtomicU64,
114
    /// Total number of successful worker matches.
115
    pub find_worker_hits: AtomicU64,
116
    /// Total number of failed worker matches (no worker found).
117
    pub find_worker_misses: AtomicU64,
118
    /// Total time spent in `find_worker_for_action` (nanoseconds).
119
    pub find_worker_time_ns: AtomicU64,
120
    /// Total number of workers iterated during find operations.
121
    pub workers_iterated: AtomicU64,
122
    /// Total number of action dispatches.
123
    pub actions_dispatched: AtomicU64,
124
    /// Total number of keep-alive updates.
125
    pub keep_alive_updates: AtomicU64,
126
    /// Total number of worker timeouts.
127
    pub worker_timeouts: AtomicU64,
128
    /// Dispatches the worker declined, requeued untried.
129
    pub dispatches_declined: AtomicU64,
130
    /// Dispatches that found the worker's channel full, requeued untried.
131
    pub dispatch_channel_full: AtomicU64,
132
    /// Dispatches requeued because the worker never acknowledged them.
133
    pub dispatches_unacknowledged: AtomicU64,
134
}
135
136
use crate::match_outcome::{
137
    MatchOutcome, PropertyShape, UnsatisfiableReason, explain_unsatisfiable,
138
};
139
use crate::platform_property_manager::PlatformPropertyManager;
140
use crate::worker::{ActionInfoWithProps, Worker, WorkerTimestamp, WorkerUpdate};
141
use crate::worker_capability_index::WorkerCapabilityIndex;
142
use crate::worker_registry::SharedWorkerRegistry;
143
use crate::worker_scheduler::{WorkerScheduler, WorkerSummary};
144
145
#[derive(Debug)]
146
struct Workers(LruCache<WorkerId, Worker>);
147
148
impl Deref for Workers {
149
    type Target = LruCache<WorkerId, Worker>;
150
151
1.86k
    fn deref(&self) -> &Self::Target {
152
1.86k
        &self.0
153
1.86k
    }
154
}
155
156
impl DerefMut for Workers {
157
545
    fn deref_mut(&mut self) -> &mut Self::Target {
158
545
        &mut self.0
159
545
    }
160
}
161
162
// Note: This could not be a derive macro because this derive-macro
163
// does not support LruCache and nameless field structs.
164
impl MetricsComponent for Workers {
165
0
    fn publish(
166
0
        &self,
167
0
        _kind: MetricKind,
168
0
        _field_metadata: MetricFieldData,
169
0
    ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> {
170
0
        let _enter = group!("workers").entered();
171
0
        for (worker_id, worker) in self.iter() {
172
0
            let _enter = group!(worker_id).entered();
173
0
            worker.publish(MetricKind::Component, MetricFieldData::default())?;
174
        }
175
0
        Ok(MetricPublishKnownKindData::Component)
176
0
    }
177
}
178
179
/// A collection of workers that are available to run tasks.
180
#[derive(MetricsComponent)]
181
struct ApiWorkerSchedulerImpl {
182
    /// A `LruCache` of workers available based on `allocation_strategy`.
183
    #[metric(group = "workers")]
184
    workers: Workers,
185
186
    /// The worker state manager.
187
    #[metric(group = "worker_state_manager")]
188
    worker_state_manager: Arc<dyn WorkerStateManager>,
189
    /// The allocation strategy for workers.
190
    allocation_strategy: WorkerAllocationStrategy,
191
    /// The `minimum` property compared against each worker's reported free
192
    /// memory before placement; unset means no veto.
193
    live_memory_veto: Option<String>,
194
    /// How a memory kill is turned into a retry with a larger reservation;
195
    /// unset means the kill fails the action.
196
    memory_escalation: Option<MemoryEscalationSpec>,
197
    /// A channel to notify the matching engine that the worker pool has changed.
198
    worker_change_notify: Arc<Notify>,
199
    /// Bumped with every notification, under the same lock as placement, so
200
    /// a matching pass can tell that room opened while it was walking the
201
    /// queue and offer that room to the oldest waiting action first.
202
    capacity_generation: Arc<AtomicU64>,
203
    /// Worker registry for tracking worker liveness.
204
    worker_registry: SharedWorkerRegistry,
205
206
    /// Whether the worker scheduler is shutting down.
207
    shutting_down: bool,
208
209
    /// Index for fast worker capability lookup.
210
    /// Used to accelerate `find_worker_for_action` by filtering candidates
211
    /// based on properties before doing linear scan.
212
    capability_index: WorkerCapabilityIndex,
213
214
    /// Incremented when the set of distinct worker shapes changes, here or
215
    /// on a peer. A worker joining or leaving with a shape the fleet already
216
    /// has does not count: the unsatisfiable tracker keys its clock on this,
217
    /// and a pool that scales up and down with identical workers used to
218
    /// reset that clock every time, so an action no shape could ever run
219
    /// waited forever behind a fleet that never stopped moving.
220
    fleet_generation: u64,
221
    /// The distinct shapes behind `fleet_generation`, local and peer.
222
    fleet_shape_set: BTreeSet<PropertyShape>,
223
224
    /// What the workers connected to other schedulers on the same state
225
    /// registered with, one entry per distinct shape, sorted by shape so
226
    /// that an unchanged fleet compares equal. `None` until the peers have
227
    /// been read, and whenever reading them fails: an action is then never
228
    /// judged unsatisfiable, since a peer might have a worker for it.
229
    peer_fleet: Option<Vec<PlatformProperties>>,
230
231
    /// Whether this scheduler shares its state with peers. A non-shared db is
232
    /// the whole fleet, so peer-absence is authoritative at once; a shared one
233
    /// applies the census warm-up / staleness checks below before it trusts
234
    /// peer-absence to fail an action.
235
    shared: bool,
236
237
    /// One peer-record TTL, used for both the census warm-up and staleness
238
    /// bounds below.
239
    record_ttl: Duration,
240
241
    /// When the peer census last became known (a `None` -> `Some` exchange).
242
    /// Peer-absence is only trusted to fail an action once it has been known
243
    /// for `record_ttl`, so a peer publishing on its own cadence has had time
244
    /// to appear. Cleared whenever the census goes unknown.
245
    peer_fleet_known_since: Option<SystemTime>,
246
247
    /// When the peer census was last refreshed by a successful exchange. If the
248
    /// exchange task dies or stalls this stops advancing, and after `record_ttl`
249
    /// the census is treated as stale and peer-absence is no longer trusted
250
    /// (fail open). Cleared whenever the census goes unknown.
251
    peer_fleet_last_refresh: Option<SystemTime>,
252
253
    /// Woken when a worker joins or leaves this scheduler, so peers hear
254
    /// about it without waiting for the next periodic exchange.
255
    local_fleet_change_notify: Arc<Notify>,
256
257
    /// Whether an action with a given property shape could run on some
258
    /// connected worker when that worker is idle. `None` means it could. This
259
    /// depends only on what workers registered with, so it holds until the
260
    /// fleet changes, at which point the map is emptied.
261
    static_verdicts: HashMap<PropertyShape, Option<Arc<UnsatisfiableReason>>>,
262
}
263
264
/// Whether peer-absence can be trusted to fail an action, given the census
265
/// state and the current time. A free function so its edge cases (warm-up,
266
/// staleness, clock skew, db kind) can be unit-tested without a scheduler.
267
132
fn peer_census_trusted(
268
132
    shared: bool,
269
132
    peer_fleet_known: bool,
270
132
    peer_fleet_known_since: Option<SystemTime>,
271
132
    peer_fleet_last_refresh: Option<SystemTime>,
272
132
    record_ttl: Duration,
273
132
    now: SystemTime,
274
132
) -> bool {
275
    // Unknown (a shared db before its first exchange or after a failed one): a
276
    // peer we cannot see might run the action. Checked before the non-shared
277
    // short-circuit so a deliberately-unknown census is never trusted.
278
132
    if !peer_fleet_known {
279
2
        return false;
280
130
    }
281
    // A non-shared db is the whole fleet; peer-absence is authoritative at
282
    // once, with no warm-up or staleness delay.
283
130
    if !shared {
284
119
        return true;
285
11
    }
286
11
    let (Some(
known_since9
), Some(
last_refresh9
)) = (peer_fleet_known_since, peer_fleet_last_refresh)
287
    else {
288
2
        return false;
289
    };
290
    // Warm: the census must have been known for a full record TTL, so a peer
291
    // publishing on its own ~interval cadence has had time to appear. A clock
292
    // going backwards (Err) is treated as not-yet-warm: don't fail.
293
9
    let warm = now
294
9
        .duration_since(known_since)
295
9
        .is_ok_and(|elapsed| elapsed >= record_ttl);
296
    // Fresh: a successful exchange within the last record TTL. If the exchange
297
    // task died or stalled this goes stale and we stop trusting peer-absence
298
    // (fail open). A future-dated refresh (clock skew) counts as fresh.
299
9
    let fresh = now
300
9
        .duration_since(last_refresh)
301
9
        .map_or(true, |elapsed| 
elapsed8
<=
record_ttl8
);
302
9
    warm && 
fresh8
303
132
}
304
305
impl core::fmt::Debug for ApiWorkerSchedulerImpl {
306
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
307
0
        f.debug_struct("ApiWorkerSchedulerImpl")
308
0
            .field("workers", &self.workers)
309
0
            .field("allocation_strategy", &self.allocation_strategy)
310
0
            .field("worker_change_notify", &self.worker_change_notify)
311
0
            .field(
312
0
                "capability_index_size",
313
0
                &self.capability_index.worker_count(),
314
0
            )
315
0
            .field("worker_registry", &self.worker_registry)
316
0
            .finish_non_exhaustive()
317
0
    }
318
}
319
320
impl ApiWorkerSchedulerImpl {
321
    /// Refreshes the lifetime of the worker with the given timestamp.
322
    ///
323
    /// Instead of sending N keepalive messages (one per operation),
324
    /// we now send a single worker heartbeat. The worker registry tracks worker liveness,
325
    /// and timeout detection checks the worker's `last_seen` instead of per-operation timestamps.
326
    ///
327
    /// Note: This only updates the local worker state. The worker registry is updated
328
    /// separately after releasing the inner lock to reduce contention.
329
50
    fn refresh_lifetime(
330
50
        &mut self,
331
50
        worker_id: &WorkerId,
332
50
        timestamp: WorkerTimestamp,
333
50
        load: Option<WorkerLoad>,
334
50
        keepalive: bool,
335
50
    ) -> Result<(), Error> {
336
50
        let 
worker49
= self.workers.0.peek_mut(worker_id).ok_or_else(||
{1
337
1
            make_input_err!(
338
                "Worker not found in worker map in refresh_lifetime() {}",
339
                worker_id
340
            )
341
1
        })?;
342
        // The scheduler's wall clock can step backwards (NTP, VM time
343
        // sync). The worker is no less alive for it, so a stale-looking
344
        // timestamp keeps the newer one instead of failing: an error here
345
        // propagates up through `touch_liveness` and would discard the
346
        // message that proved liveness — including a finished
347
        // `ExecuteResult`, stranding the operation in `Executing`.
348
49
        if worker.last_update_timestamp > timestamp {
349
2
            warn!(
350
                %worker_id,
351
                last_update_timestamp = worker.last_update_timestamp,
352
                timestamp,
353
                "Scheduler clock stepped backwards; keeping newer liveness timestamp",
354
            );
355
47
        }
356
49
        record_worker_keepalive_gap(timestamp.saturating_sub(worker.last_update_timestamp));
357
49
        worker.last_update_timestamp = worker.last_update_timestamp.max(timestamp);
358
        // Any other message (an acknowledgement, a decline, an execute
359
        // result) proves the worker is alive and nothing more. A decline in
360
        // particular is not the worker saying it is ready to be asked
361
        // again, so the pause it took stands until a real keepalive.
362
49
        if !keepalive {
363
8
            return Ok(());
364
41
        }
365
        // A keepalive without a load (an older worker) leaves the last
366
        // report in place. A report with
367
        // more room than the last one can make a vetoed action eligible
368
        // again; less room never does.
369
41
        let more_room = load.is_some_and(|load| 
{26
370
26
            let more = worker
371
26
                .last_load
372
26
                .as_ref()
373
26
                .is_none_or(|last| 
load.free_memory_kb13
>
last.free_memory_kb13
);
374
26
            worker.last_load = Some(load);
375
26
            more
376
26
        });
377
        // A keepalive is the worker saying it is ready to be asked again,
378
        // so a pause from backpressure lasts one keepalive interval. A
379
        // pause taken for a decline for load is the exception: it waits
380
        // for a keepalive whose free memory, read above, covers what the
381
        // declined action wanted. A worker that reports no load is taken
382
        // at its word. So is a worker that admits when idle and holds
383
        // nothing here: it declined while something of its own was still
384
        // finishing, and once idle it admits whatever it is offered, so
385
        // waiting for free memory it may never report would strand it.
386
41
        let idle_and_admitting = worker.admits_when_idle && 
worker.running_action_infos4
.
is_empty4
();
387
41
        let load_fits = idle_and_admitting
388
39
            || match (worker.pause_needs_kb, worker.last_load) {
389
6
                (Some(needs_kb), Some(load)) => load.free_memory_kb >= needs_kb,
390
33
                _ => true,
391
            };
392
        // A keepalive reporting room for what the worker declined while
393
        // idle makes that size worth offering it again.
394
41
        let idle_decline_lifted = match (worker.idle_declined_kb, load) {
395
2
            (Some(declined_kb), Some(load)) => load.free_memory_kb >= declined_kb,
396
39
            _ => false,
397
        };
398
41
        if idle_decline_lifted {
399
1
            worker.idle_declined_kb = None;
400
40
        }
401
41
        let resumed = worker.is_paused && 
load_fits12
;
402
41
        if resumed {
403
9
            worker.is_paused = false;
404
9
            worker.pause_needs_kb = None;
405
9
            record_worker_state("paused", false);
406
32
        }
407
        // Either is room the fleet did not have a moment ago, so the
408
        // capacity generation moves and the matcher is woken, together:
409
        // a pass in flight offers the room to its parked actions first,
410
        // and a pass not in flight starts. `capacity_changed` by hand,
411
        // since `worker` still borrows the map.
412
41
        if resumed || 
more_room32
||
idle_decline_lifted11
{
413
30
            self.capacity_generation.fetch_add(1, Ordering::Relaxed);
414
30
            self.worker_change_notify.notify_one();
415
30
        
}11
416
417
41
        trace!(
418
            ?worker_id,
419
41
            running_operations = worker.running_action_infos.len(),
420
            "Worker keepalive received"
421
        );
422
41
        record_worker_keepalive();
423
424
41
        Ok(())
425
50
    }
426
427
    /// Adds a worker to the pool.
428
    /// Note: This function will not do any task matching.
429
136
    fn add_worker(&mut self, worker: Worker) -> Result<(), Error> {
430
136
        let worker_id = worker.id.clone();
431
136
        let platform_properties = worker.platform_properties.clone();
432
        // A replacement is one out and one in, so the gauge only moves for a
433
        // genuinely new worker.
434
136
        let replaced = self.workers.put(worker_id.clone(), worker);
435
436
        // Add to capability index for fast matching
437
136
        self.capability_index
438
136
            .add_worker(&worker_id, &platform_properties);
439
136
        self.fleet_changed();
440
136
        self.local_fleet_change_notify.notify_one();
441
442
        // Worker is not cloneable, and we do not want to send the initial connection results until
443
        // we have added it to the map, or we might get some strange race conditions due to the way
444
        // the multi-threaded runtime works.
445
136
        let worker = self.workers.peek_mut(&worker_id).unwrap();
446
136
        let res = worker
447
136
            .send_initial_connection_result(self.live_memory_veto.as_deref())
448
136
            .err_tip(|| "Failed to send initial connection result to worker");
449
136
        if let Err(
err0
) = &res {
450
0
            error!(
451
                ?worker_id,
452
                ?err,
453
                "Worker connection appears to have been closed while adding to pool"
454
            );
455
136
        }
456
136
        if let Some(
replaced2
) = &replaced {
457
2
            if replaced.is_draining {
458
0
                record_worker_state("draining", false);
459
2
            }
460
2
            if replaced.is_paused {
461
0
                record_worker_state("paused", false);
462
2
            }
463
134
        } else {
464
134
            record_worker_connected();
465
134
        }
466
136
        self.capacity_changed();
467
136
        res
468
136
    }
469
470
    /// Removes worker from pool.
471
    /// Note: The caller is responsible for any rescheduling of any tasks that might be
472
    /// running.
473
23
    fn remove_worker(&mut self, worker_id: &WorkerId) -> Option<Worker> {
474
        // Remove from capability index
475
23
        self.capability_index.remove_worker(worker_id);
476
        // Pop before recomputing the shapes, or the departing worker still
477
        // counts and a cached verdict that only it satisfied survives it.
478
23
        let result = self.workers.pop(worker_id);
479
23
        self.fleet_changed();
480
23
        self.local_fleet_change_notify.notify_one();
481
482
23
        self.capacity_changed();
483
23
        result
484
23
    }
485
486
173
    fn fleet_changed(&mut self) {
487
173
        let mut shapes: BTreeSet<PropertyShape> = self
488
173
            .workers
489
173
            .iter()
490
176
            .
map173
(|(_, w)| PropertyShape::from(&w.total_platform_properties))
491
173
            .collect();
492
173
        if let Some(
peer_fleet169
) = &self.peer_fleet {
493
169
            shapes.extend(peer_fleet.iter().map(PropertyShape::from));
494
169
        
}4
495
173
        if shapes == self.fleet_shape_set {
496
26
            return;
497
147
        }
498
147
        self.fleet_shape_set = shapes;
499
147
        self.fleet_generation += 1;
500
147
        self.static_verdicts.clear();
501
173
    }
502
503
    /// Something about the fleet's room to run actions changed: a worker
504
    /// joined, left, paused, resumed, drained or finished an action.
505
235
    fn capacity_changed(&self) {
506
235
        self.capacity_generation.fetch_add(1, Ordering::Relaxed);
507
235
        self.worker_change_notify.notify_one();
508
235
    }
509
510
    /// The distinct properties this scheduler's workers registered with.
511
53
    fn fleet_shapes(&self) -> Vec<PlatformProperties> {
512
53
        let mut seen = HashSet::new();
513
53
        self.workers
514
53
            .iter()
515
53
            .map(|(_, w)| &w.total_platform_properties)
516
53
            .filter(|totals| 
seen42
.
insert42
(
PropertyShape::from42
(
*totals42
)))
517
53
            .cloned()
518
53
            .collect()
519
53
    }
520
521
61
    fn set_peer_fleet(&mut self, peer_fleet: Option<Vec<PlatformProperties>>, now: SystemTime) {
522
        // Peers list their workers in no particular order and may share
523
        // shapes, so sort and dedup before comparing, or an unchanged fleet
524
        // would look changed on every exchange and empty `static_verdicts`.
525
61
        let peer_fleet = peer_fleet.map(|peer_fleet| 
{59
526
59
            let mut keyed: Vec<_> = peer_fleet
527
59
                .into_iter()
528
59
                .map(|totals| (
PropertyShape::from34
(
&totals34
),
totals34
))
529
59
                .collect();
530
59
            keyed.sort_unstable_by(|a, b| 
a.03
.
cmp3
(
&b.03
));
531
59
            keyed.dedup_by(|a, b| 
a.03
==
b.03
);
532
59
            keyed.into_iter().map(|(_, totals)| totals).collect()
533
59
        });
534
        // Freshness bookkeeping: update the warm-up /
535
        // staleness clocks BEFORE the unchanged-shapes early return, so a
536
        // successful exchange that happens to return the same shapes still
537
        // refreshes the staleness clock (otherwise a steady fleet would look
538
        // like a stalled exchange task).
539
61
        if peer_fleet.is_some() {
540
59
            self.peer_fleet_last_refresh = Some(now);
541
            // Start the warm-up only on the unknown -> known transition.
542
59
            if self.peer_fleet.is_none() {
543
6
                self.peer_fleet_known_since = Some(now);
544
53
            }
545
2
        } else {
546
2
            self.peer_fleet_known_since = None;
547
2
            self.peer_fleet_last_refresh = None;
548
2
        }
549
61
        if self.peer_fleet == peer_fleet {
550
47
            return;
551
14
        }
552
        // Unknown peers make every verdict "maybe satisfiable elsewhere",
553
        // and that verdict is cached. Peers becoming known (or unknown
554
        // again) changes what a verdict means even when the union of shapes
555
        // does not move, so the cache goes, though the generation stays:
556
        // clocks already running were right and keep their start.
557
14
        if self.peer_fleet.is_some() != peer_fleet.is_some() {
558
8
            self.static_verdicts.clear();
559
8
        
}6
560
14
        self.peer_fleet = peer_fleet;
561
14
        self.fleet_changed();
562
61
    }
563
564
    /// Sets if the worker is draining or not.
565
5
    fn set_drain_worker(&mut self, worker_id: &WorkerId, is_draining: bool) -> Result<(), Error> {
566
5
        let worker = self
567
5
            .workers
568
5
            .get_mut(worker_id)
569
5
            .err_tip(|| 
format!0
("Worker {worker_id} doesn't exist in the pool"))
?0
;
570
5
        if worker.is_draining != is_draining {
571
3
            record_worker_state("draining", is_draining);
572
3
        
}2
573
5
        worker.is_draining = is_draining;
574
5
        self.capacity_changed();
575
5
        Ok(())
576
5
    }
577
578
    /// Whether peer-absence can be trusted to fail an action right now — a
579
    /// shared census must be known, warm and fresh; a non-shared db is trusted
580
    /// as soon as it is known. See the free `peer_census_trusted`.
581
122
    fn peer_census_trusted(&self, now: SystemTime) -> bool {
582
122
        peer_census_trusted(
583
122
            self.shared,
584
122
            self.peer_fleet.is_some(),
585
122
            self.peer_fleet_known_since,
586
122
            self.peer_fleet_last_refresh,
587
122
            self.record_ttl,
588
122
            now,
589
        )
590
122
    }
591
592
    /// Works out whether any of the `candidates` the capability index picked
593
    /// could run an action with these properties when idle, and if not, why.
594
118
    fn static_verdict(
595
118
        &self,
596
118
        platform_properties: &PlatformProperties,
597
118
        candidates: &HashSet<WorkerId>,
598
118
        full_worker_logging: bool,
599
118
    ) -> Option<Arc<UnsatisfiableReason>> {
600
        // The index only checks that Minimum keys are present, so the values
601
        // are checked here against what each candidate registered with.
602
118
        let satisfiable = candidates.iter().any(|worker_id| 
{114
603
114
            self.workers.peek(worker_id).is_some_and(|w| {
604
114
                platform_properties
605
114
                    .is_satisfied_by(&w.total_platform_properties, full_worker_logging)
606
114
            })
607
114
        });
608
118
        if satisfiable {
609
26
            return None;
610
92
        }
611
        // A peer's worker counts too: the action would run there once that
612
        // worker has room, and this scheduler cannot see how busy it is.
613
        // While the peers are unknown, any of them might have one.
614
92
        let 
peer_fleet88
= self.peer_fleet.as_deref()
?4
;
615
88
        if peer_fleet
616
88
            .iter()
617
88
            .any(|totals| 
platform_properties5
.
is_satisfied_by5
(
totals5
, false))
618
        {
619
5
            return None;
620
83
        }
621
83
        let fleet: Vec<_> = self
622
83
            .workers
623
83
            .iter()
624
83
            .map(|(_, w)| &w.total_platform_properties)
625
83
            .chain(peer_fleet.iter())
626
83
            .collect();
627
83
        Some(Arc::new(explain_unsatisfiable(platform_properties, &fleet)))
628
118
    }
629
630
    /// `static_verdict`, cached per property shape until the fleet changes.
631
179
    fn cached_static_verdict(
632
179
        &mut self,
633
179
        platform_properties: &PlatformProperties,
634
179
        candidates: Option<&HashSet<WorkerId>>,
635
179
        full_worker_logging: bool,
636
179
    ) -> Option<Arc<UnsatisfiableReason>> {
637
179
        let shape = PropertyShape::from(platform_properties);
638
        // A cached verdict skips the per-property logging, so recompute when
639
        // that logging was asked for.
640
179
        if !full_worker_logging && let Some(
verdict61
) =
self.static_verdicts107
.get(&shape) {
641
61
            return verdict.clone();
642
118
        }
643
118
        let verdict = if let Some(
candidates101
) = candidates {
644
101
            self.static_verdict(platform_properties, candidates, full_worker_logging)
645
        } else {
646
17
            let candidates = self
647
17
                .capability_index
648
17
                .find_matching_workers(platform_properties, full_worker_logging);
649
17
            self.static_verdict(platform_properties, &candidates, full_worker_logging)
650
        };
651
118
        if self.static_verdicts.len() >= MAX_STATIC_VERDICTS {
652
0
            self.static_verdicts.clear();
653
118
        }
654
118
        self.static_verdicts.insert(shape, verdict.clone());
655
118
        verdict
656
179
    }
657
658
    /// The memory the action reserves under the `live_memory_veto` property,
659
    /// if the veto is on and the action declares it.
660
288
    fn live_memory_veto_kb(&self, platform_properties: &PlatformProperties) -> Option<u64> {
661
288
        let 
property19
= self.live_memory_veto.as_deref()
?269
;
662
19
        match platform_properties.properties.get(property) {
663
19
            Some(PlatformPropertyValue::Minimum(kb)) => Some(*kb),
664
0
            _ => None,
665
        }
666
288
    }
667
668
    /// Finds an idle worker among `candidates` that can run the action now.
669
288
    fn find_available_worker(
670
288
        &self,
671
288
        platform_properties: &PlatformProperties,
672
288
        candidates: &HashSet<WorkerId>,
673
288
        full_worker_logging: bool,
674
288
    ) -> Option<WorkerId> {
675
        // Check function for availability AND dynamic Minimum property verification.
676
        // The index only does presence checks for Minimum properties since their
677
        // values change dynamically as jobs are assigned to workers.
678
288
        let needed_kb = self.live_memory_veto_kb(platform_properties);
679
305
        let 
worker_matches288
= |(worker_id, w): &(&WorkerId, &Worker)| -> bool {
680
305
            if !w.can_accept_work() {
681
11
                if full_worker_logging {
682
6
                    info!(
683
                        "Worker {worker_id} cannot accept work: is_paused={}, is_draining={}, inflight={}/{}",
684
                        w.is_paused,
685
                        w.is_draining,
686
6
                        w.running_action_infos.len(),
687
                        w.max_inflight_tasks
688
                    );
689
5
                }
690
11
                return false;
691
294
            }
692
693
294
            if let (Some(
needed_kb6
), Some(
declined_kb6
)) = (needed_kb, w.idle_declined_kb)
694
6
                && needed_kb >= declined_kb
695
            {
696
5
                if full_worker_logging {
697
0
                    info!(
698
                        "Worker {worker_id} skipped for this action: it declined {declined_kb} KiB while idle, the action asks {needed_kb} KiB"
699
                    );
700
5
                }
701
5
                return false;
702
289
            }
703
704
            // The ledger only knows what actions declared; the worker's own
705
            // report of what it has left catches the ones that declared too
706
            // little. A worker that reports nothing is never vetoed.
707
            // An action that asks for the worker's whole advertised memory
708
            // (the last escalation step) is not vetoed: a worker seldom
709
            // reports that much free, since its own process memory and
710
            // active page cache still count as used, so the veto could
711
            // hold it back indefinitely, and the ledger already keeps it
712
            // alone there. Nor is a worker that admits when idle and holds
713
            // nothing: it accepts whatever it reads free, so the veto would
714
            // only keep from it the actions it is idle for.
715
14
            if let (Some(property), Some(needed_kb), Some(load)) =
716
289
                (self.live_memory_veto.as_deref(), needed_kb, w.last_load)
717
14
                && !(w.admits_when_idle && 
w.running_action_infos1
.
is_empty1
())
718
13
                && needed_kb > load.free_memory_kb
719
5
                && matches!(
720
5
                    w.total_platform_properties.properties.get(property),
721
5
                    Some(PlatformPropertyValue::Minimum(advertised_kb)) if needed_kb < *advertised_kb
722
                )
723
            {
724
5
                if full_worker_logging {
725
0
                    info!(
726
                        "Worker {worker_id} vetoed for this action: it reports {} KiB free, the action asks {needed_kb} KiB ({property})",
727
                        load.free_memory_kb
728
                    );
729
5
                }
730
5
                return false;
731
284
            }
732
733
            // Verify Minimum properties at runtime (their values are dynamic)
734
284
            platform_properties.is_satisfied_by(&w.platform_properties, full_worker_logging)
735
305
        };
736
737
        // Now check constraints on filtered candidates.
738
        // Iterate in LRU order based on allocation strategy.
739
288
        let workers_iter = self.workers.iter();
740
741
288
        match self.allocation_strategy {
742
            // Use rfind to get the least recently used that satisfies the properties.
743
281
            WorkerAllocationStrategy::LeastRecentlyUsed => workers_iter
744
281
                .rev()
745
298
                .
filter281
(|(worker_id, _)| candidates.contains(worker_id))
746
281
                .find(&worker_matches)
747
281
                .map(|(_, w)| 
w.id136
.
clone136
()),
748
749
            // Use find to get the most recently used that satisfies the properties.
750
0
            WorkerAllocationStrategy::MostRecentlyUsed => workers_iter
751
0
                .filter(|(worker_id, _)| candidates.contains(worker_id))
752
0
                .find(&worker_matches)
753
0
                .map(|(_, w)| w.id.clone()),
754
755
            // Fewest running actions wins; the scan runs from the least
756
            // recently used end so `min_by_key` keeps that one on a tie.
757
3
            WorkerAllocationStrategy::LeastLoaded => workers_iter
758
3
                .rev()
759
4
                .
filter3
(|(worker_id, _)| candidates.contains(worker_id))
760
3
                .filter(&worker_matches)
761
4
                .
min_by_key3
(|(_, w)| w.running_action_infos.len())
762
3
                .map(|(_, w)| w.id.clone()),
763
764
4
            WorkerAllocationStrategy::BestFit => workers_iter
765
4
                .rev()
766
8
                .
filter4
(|(worker_id, _)| candidates.contains(worker_id))
767
4
                .filter(&worker_matches)
768
7
                .
min_by_key4
(|(_, w)| {
769
7
                    (
770
7
                        // More matching priority properties first.
771
7
                        usize::MAX - priority_matches(platform_properties, w),
772
7
                        fit_leftover_permille(platform_properties, w),
773
7
                        w.running_action_infos.len(),
774
7
                    )
775
7
                })
776
4
                .map(|(_, w)| w.id.clone()),
777
        }
778
288
    }
779
780
360
    fn inner_find_worker_for_action(
781
360
        &mut self,
782
360
        platform_properties: &PlatformProperties,
783
360
        full_worker_logging: bool,
784
360
        now: SystemTime,
785
360
    ) -> MatchOutcome {
786
360
        if self.workers.is_empty() {
787
38
            if full_worker_logging {
788
10
                info!("No workers available to match!");
789
28
            }
790
38
            return MatchOutcome::NoWorkersConnected;
791
322
        }
792
793
        // Do a fast check to see if any workers are available at all for work allocation
794
324
        let 
candidates179
= if
self.workers.iter()322
.
any322
(|(_, w)| w.can_accept_work()) {
795
            // Use capability index to get candidate workers that match STATIC properties
796
            // (Exact, Unknown) and have the required property keys (Priority, Minimum).
797
            // This reduces complexity from O(W × P) to O(P × log(W)) for exact properties.
798
288
            let candidates = self
799
288
                .capability_index
800
288
                .find_matching_workers(platform_properties, full_worker_logging);
801
143
            if let Some(worker_id) =
802
288
                self.find_available_worker(platform_properties, &candidates, full_worker_logging)
803
            {
804
143
                return MatchOutcome::Matched(worker_id);
805
145
            }
806
145
            Some(candidates)
807
        } else {
808
34
            if full_worker_logging {
809
3
                info!("All workers are fully allocated");
810
31
            }
811
34
            None
812
        };
813
814
        // Nothing can take the action now. Only then is it worth working out
815
        // whether anything ever could, so a match pays nothing for this.
816
179
        if let Some(
reason115
) = self.cached_static_verdict(
817
179
            platform_properties,
818
179
            candidates.as_ref(),
819
179
            full_worker_logging,
820
179
        ) {
821
            // The intrinsic verdict (nobody could run it) is cached by shape;
822
            // the census freshness check is applied live here, OUTSIDE the
823
            // cache, so a verdict computed while the census was trusted can
824
            // never outlive a staleness transition (nor a warm-up one). On a
825
            // shared db, trust peer-absence to fail only once the census is
826
            // warm and fresh; otherwise a peer we cannot fully account for
827
            // might run it, so wait rather than fail.
828
115
            if !self.peer_census_trusted(now) {
829
0
                if full_worker_logging {
830
0
                    info!("No idle worker matched; peer census not yet trusted, waiting");
831
0
                }
832
0
                return MatchOutcome::WaitingForCapacity;
833
115
            }
834
115
            if full_worker_logging {
835
63
                info!(%reason, "No connected worker can ever run this action");
836
52
            }
837
115
            return MatchOutcome::Unsatisfiable(reason);
838
64
        }
839
64
        if full_worker_logging && 
candidates9
.
is_some9
() {
840
8
            warn!("No workers matched!");
841
56
        }
842
64
        MatchOutcome::WaitingForCapacity
843
360
    }
844
845
    /// The largest memory reservation any connected worker could hold.
846
    /// The largest memory any connected worker advertises under `property`,
847
    /// and that worker's whole CPU under `cpu_property` (`None` when the
848
    /// property is empty or the worker does not advertise it). Reserving
849
    /// both is what runs an action alone there.
850
14
    fn fleet_largest_worker(&self, property: &str, cpu_property: &str) -> (u64, Option<u64>) {
851
28
        let 
minimum14
= |worker: &Worker, name: &str| match worker
852
28
            .total_platform_properties
853
28
            .properties
854
28
            .get(name)
855
        {
856
14
            Some(PlatformPropertyValue::Minimum(value)) => Some(*value),
857
14
            _ => None,
858
28
        };
859
        // A draining worker is leaving: escalating to it would strand the
860
        // action at a size the remaining fleet cannot serve.
861
14
        self.workers
862
14
            .iter()
863
14
            .filter(|(_, worker)| !worker.is_draining)
864
14
            .filter_map(|(_, worker)| {
865
14
                minimum(worker, property).map(|memory_kb| {
866
14
                    let cpu = if cpu_property.is_empty() {
867
0
                        None
868
                    } else {
869
14
                        minimum(worker, cpu_property)
870
                    };
871
14
                    (memory_kb, cpu)
872
14
                })
873
14
            })
874
14
            .max_by_key(|(memory_kb, _)| *memory_kb)
875
14
            .unwrap_or((0, None))
876
14
    }
877
878
    /// Turns a result that the worker's last usage report says was a memory
879
    /// kill into a requeue with a larger reservation, when escalation is
880
    /// configured and there is room to grow. Returns the update to apply
881
    /// and whether it was escalated.
882
46
    fn maybe_escalate(
883
46
        &self,
884
46
        worker_id: &WorkerId,
885
46
        operation_id: &OperationId,
886
46
        update: UpdateOperationType,
887
46
    ) -> (UpdateOperationType, bool) {
888
46
        let Some(
policy14
) = &self.memory_escalation else {
889
32
            return (update, false);
890
        };
891
14
        let UpdateOperationType::UpdateWithActionStage(ActionStage::Completed(result)) = &update
892
        else {
893
0
            return (update, false);
894
        };
895
14
        let Some(pending) = self
896
14
            .workers
897
14
            .peek(worker_id)
898
14
            .and_then(|worker| worker.running_action_infos.get(operation_id))
899
        else {
900
0
            return (update, false);
901
        };
902
14
        let Some(usage) = pending.last_usage.as_ref() else {
903
0
            return (update, false);
904
        };
905
14
        if usage.outcome != ResourceOutcome::KilledMemory as i32 {
906
0
            return (update, false);
907
14
        }
908
        // What it ran under: the worker's echo, else what the scheduler sent.
909
14
        let reserved_kb = usage
910
14
            .reserved
911
14
            .map(|r| r.memory_kb)
912
14
            .filter(|kb| *kb > 0)
913
14
            .or_else(|| 
{0
914
0
                match pending
915
0
                    .action_info
916
0
                    .platform_properties
917
0
                    .properties
918
0
                    .get(&policy.property)
919
                {
920
0
                    Some(PlatformPropertyValue::Minimum(kb)) => Some(*kb),
921
0
                    _ => None,
922
                }
923
0
            })
924
14
            .unwrap_or(0);
925
14
        let (fleet_memory_kb, fleet_cpu) =
926
14
            self.fleet_largest_worker(&policy.property, &policy.cpu_property);
927
        // `max_kb` caps the fleet's ceiling; it cannot raise it past what a
928
        // worker declares, or the escalation lands where nothing can run.
929
14
        let ceiling_kb = if policy.max_kb > 0 {
930
0
            fleet_memory_kb.min(policy.max_kb)
931
        } else {
932
14
            fleet_memory_kb
933
        };
934
14
        if ceiling_kb == 0 {
935
0
            return (update, false);
936
14
        }
937
        // The last step reserves the largest worker whole: its memory and,
938
        // when the fleet advertises one, its CPU, so nothing shares the
939
        // worker with the action. A kill there is the end: no worker in
940
        // the fleet can run the action, and the client hears exactly that.
941
14
        let cpu_held = match pending
942
14
            .action_info
943
14
            .platform_properties
944
14
            .properties
945
14
            .get(&policy.cpu_property)
946
        {
947
0
            Some(PlatformPropertyValue::Minimum(cores)) => *cores,
948
14
            _ => 0,
949
        };
950
14
        let whole_worker_cpu = fleet_cpu.filter(|cores| 
*cores0
> 0);
951
14
        let already_whole =
952
14
            reserved_kb >= ceiling_kb && 
whole_worker_cpu2
.
is_none_or2
(|cores|
cpu_held0
>=
cores0
);
953
14
        if already_whole {
954
2
            warn!(
955
                ?worker_id,
956
                ?operation_id,
957
                reserved_kb,
958
                ceiling_kb,
959
                "Action killed for memory with the largest worker reserved whole; no worker in the fleet can run it"
960
            );
961
2
            let mut failed = result.clone();
962
2
            let no_worker = make_err!(
963
2
                Code::FailedPrecondition,
964
                "Action was killed for memory at {reserved_kb} KiB with the largest worker in the fleet ({ceiling_kb} KiB) reserved for it alone; no worker can run it. It needs a bigger worker or less memory"
965
            );
966
2
            failed.error = Some(match result.error.clone() {
967
2
                Some(worker_error) => no_worker.merge(worker_error),
968
0
                None => no_worker,
969
            });
970
2
            return (
971
2
                UpdateOperationType::UpdateWithActionStage(ActionStage::Completed(failed)),
972
2
                false,
973
2
            );
974
12
        }
975
12
        let base_kb = if reserved_kb > 0 {
976
12
            reserved_kb
977
        } else {
978
0
            usage.peak_memory_kb
979
        };
980
12
        if base_kb == 0 {
981
            // Nothing to grow from: no reservation and an unsampled kill.
982
            // Scaling zero would hand out the smallest steps the ladder or
983
            // the percent allow and burn the budget on them.
984
0
            warn!(
985
                ?worker_id,
986
                ?operation_id,
987
                "Action killed for memory with no reservation and no sampled peak; not escalating"
988
            );
989
0
            return (update, false);
990
12
        }
991
        // With a ladder the next class up; without one, scale. Past the
992
        // top class or the ceiling, the whole worker.
993
12
        let next = if policy.ladder_kb.is_empty() {
994
9
            Some(
995
9
                base_kb
996
9
                    .saturating_mul(policy.percent)
997
9
                    .checked_div(100)
998
9
                    .unwrap_or(base_kb)
999
9
                    .max(base_kb.saturating_add(1)),
1000
9
            )
1001
        } else {
1002
3
            policy
1003
3
                .ladder_kb
1004
3
                .iter()
1005
3
                .copied()
1006
9
                .
filter3
(|kb| *kb > base_kb)
1007
3
                .min()
1008
        };
1009
12
        let value = next.map_or(ceiling_kb, |kb| 
kb11
.
min11
(
ceiling_kb11
));
1010
12
        let cpu = if value >= ceiling_kb {
1011
2
            whole_worker_cpu.map(|cores| (
policy.cpu_property0
.
clone0
(),
cores0
))
1012
        } else {
1013
10
            None
1014
        };
1015
12
        let reason = result.error.clone().unwrap_or_else(|| 
{0
1016
0
            make_err!(
1017
0
                Code::FailedPrecondition,
1018
                "Action was killed for memory at {} KiB",
1019
                usage.peak_memory_kb
1020
            )
1021
0
        });
1022
12
        warn!(
1023
            ?worker_id,
1024
            ?operation_id,
1025
            reserved_kb,
1026
            peak_memory_kb = usage.peak_memory_kb,
1027
            escalated_kb = value,
1028
            ceiling_kb,
1029
            property = %policy.property,
1030
            "Action killed for memory; requeuing with a larger reservation"
1031
        );
1032
12
        (
1033
12
            UpdateOperationType::UpdateWithEscalation(Escalation {
1034
12
                property: policy.property.clone(),
1035
12
                value,
1036
12
                reason,
1037
12
                cpu,
1038
12
            }),
1039
12
            true,
1040
12
        )
1041
46
    }
1042
1043
54
    async fn update_action(
1044
54
        &mut self,
1045
54
        worker_id: &WorkerId,
1046
54
        operation_id: &OperationId,
1047
54
        update: UpdateOperationType,
1048
54
    ) -> Result<(), Error> {
1049
54
        let 
worker52
= self.workers.get_mut(worker_id).err_tip(||
{2
1050
2
            format!("Worker {worker_id} does not exist in SimpleScheduler::update_action")
1051
2
        })?;
1052
1053
        // Ensure the worker is supposed to be running the operation. A
1054
        // result for something this worker no longer holds is stale, not
1055
        // rogue: the operation was requeued after a timeout or a kill and
1056
        // the worker finished it anyway. Refuse it and keep the worker;
1057
        // evicting it here requeued every other action it held for one
1058
        // late message.
1059
52
        if !worker.running_action_infos.contains_key(operation_id) {
1060
1
            warn!(
1061
                %operation_id,
1062
                ?worker_id,
1063
1
                running_actions = worker.running_action_infos.len(),
1064
                "Dropping update for an operation the worker is not running"
1065
            );
1066
1
            return Err(make_err!(
1067
1
                Code::Internal,
1068
1
                "Operation {operation_id} should not be running on worker {worker_id} in SimpleScheduler::update_action"
1069
1
            ));
1070
51
        }
1071
1072
51
        let (
is_finished49
,
due_to_backpressure49
) = match &update {
1073
20
            UpdateOperationType::UpdateWithActionStage(action_stage) => {
1074
20
                (action_stage.is_finished(), false)
1075
            }
1076
0
            UpdateOperationType::KeepAlive => (false, false),
1077
7
            UpdateOperationType::UpdateWithError(err) => {
1078
7
                (true, err.code == Code::ResourceExhausted)
1079
            }
1080
            // A decline pauses like backpressure: the worker said it has no
1081
            // room, so it gets nothing more until it says otherwise.
1082
8
            UpdateOperationType::UpdateWithDecline(_) => (true, true),
1083
            UpdateOperationType::UpdateWithDisconnect
1084
14
            | UpdateOperationType::UpdateWithEscalation(_) => (true, false),
1085
            UpdateOperationType::ExecutionComplete => {
1086
                // The process has exited but the action is still resident on
1087
                // the worker until its result arrives, so nothing is released
1088
                // here; `complete_action` returns the budget and the slot
1089
                // together.
1090
2
                return Ok(());
1091
            }
1092
        };
1093
1094
        // Update the operation in the worker state manager.
1095
49
        let 
update_operation_res48
= if worker.is_kill_requested(operation_id) {
1096
1
            debug!(
1097
                %operation_id,
1098
                ?worker_id,
1099
                "Ignoring update for operation the worker was told to kill"
1100
            );
1101
1
            Ok(())
1102
        } else {
1103
48
            let 
update_operation_res47
= self
1104
48
                .worker_state_manager
1105
48
                .update_operation(operation_id, worker_id, update)
1106
48
                .await
1107
47
                .err_tip(|| "in update_operation on SimpleScheduler::update_action");
1108
47
            if let Err(
err1
) = &update_operation_res {
1109
1
                error!(
1110
                    %operation_id,
1111
                    ?worker_id,
1112
                    ?err,
1113
                    "Failed to update_operation on update_action"
1114
                );
1115
46
            }
1116
47
            update_operation_res
1117
        };
1118
1119
48
        if !is_finished {
1120
0
            return update_operation_res;
1121
48
        }
1122
        // The worker is done with this action even if the state-manager update
1123
        // failed (e.g. the operation was already torn down after its clients
1124
        // timed out). The worker bookkeeping below must still run, or the
1125
        // worker's platform properties leak until it can never match again.
1126
1127
        // Clear this action from the current worker if finished.
1128
48
        let complete_action_res = {
1129
            // Note: We need to run this before dealing with backpressure logic.
1130
48
            let was_paused = worker.is_paused;
1131
48
            let complete_action_res = worker.complete_action(operation_id);
1132
1133
            // Backpressure pauses the worker even when it holds nothing
1134
            // else: an idle worker that refuses work and is not paused is
1135
            // offered the same action again at once, and the pair spin
1136
            // until something changes. The next keepalive clears it.
1137
48
            if due_to_backpressure || (
!worker.can_accept_work()39
&&
worker0
.
has_actions0
()) {
1138
9
                worker.is_paused = true;
1139
39
            }
1140
            // complete_action clears is_paused on its way through, so compare
1141
            // the state either side of it rather than testing the flag after.
1142
            // Testing afterwards counts a re-pause every time and never
1143
            // unwinds, which leaves the gauge climbing forever.
1144
48
            if was_paused != worker.is_paused {
1145
9
                record_worker_state("paused", worker.is_paused);
1146
39
            }
1147
48
            complete_action_res
1148
        };
1149
1150
48
        self.capacity_changed();
1151
1152
48
        update_operation_res.merge(complete_action_res)
1153
53
    }
1154
1155
    /// Notifies the specified worker to run the given action and handles errors by evicting
1156
    /// the worker if the notification fails.
1157
158
    async fn worker_notify_run_action(
1158
158
        &mut self,
1159
158
        worker_id: WorkerId,
1160
158
        operation_id: OperationId,
1161
158
        action_info: ActionInfoWithProps,
1162
158
        dispatched_at: WorkerTimestamp,
1163
158
    ) -> Result<(), Error> {
1164
158
        if let Some(worker) = self.workers.get_mut(&worker_id) {
1165
158
            let notify_worker_result = worker
1166
158
                .notify_update(WorkerUpdate::RunAction(Box::new((
1167
158
                    operation_id.clone(),
1168
158
                    action_info.clone(),
1169
158
                    dispatched_at,
1170
158
                ))))
1171
158
                .await;
1172
1173
158
            if let Err(
notify_worker_result2
) = notify_worker_result {
1174
2
                if notify_worker_result.code == Code::ResourceExhausted {
1175
                    // The worker's queue is full: it has stopped reading, or
1176
                    // it is far behind. Nothing was charged; the action goes
1177
                    // back untried and the worker waits for its next
1178
                    // keepalive before it is offered anything else.
1179
1
                    warn!(
1180
                        ?worker_id,
1181
                        %operation_id,
1182
                        "Worker channel full, requeuing dispatch and pausing the worker"
1183
                    );
1184
1
                    record_dispatch_requeue("channel_full");
1185
1
                    if !worker.is_paused {
1186
1
                        worker.is_paused = true;
1187
1
                        record_worker_state("paused", true);
1188
1
                    
}0
1189
1
                    self.worker_change_notify.notify_one();
1190
1
                    return self
1191
1
                        .worker_state_manager
1192
1
                        .update_operation(
1193
1
                            &operation_id,
1194
1
                            &worker_id,
1195
1
                            UpdateOperationType::UpdateWithDecline(Decline {
1196
1
                                reason: "channel_full".to_string(),
1197
1
                            }),
1198
1
                        )
1199
1
                        .await
1200
1
                        .err_tip(|| "requeuing a dispatch the worker's channel refused");
1201
1
                }
1202
1
                warn!(
1203
                    ?worker_id,
1204
                    ?action_info,
1205
                    ?notify_worker_result,
1206
                    "Worker command failed, removing worker",
1207
                );
1208
1209
                // A slightly nasty way of figuring out that the worker disconnected
1210
                // from send_msg_to_worker without introducing complexity to the
1211
                // code path from here to there.
1212
1
                let is_disconnect = notify_worker_result.code == Code::Internal
1213
1
                    && notify_worker_result.messages.len() == 1
1214
0
                    && notify_worker_result.messages[0] == "Worker Disconnected";
1215
1216
1
                let err = make_err!(
1217
1
                    Code::Internal,
1218
                    "Worker command failed, removing worker {worker_id} -- {notify_worker_result:?}",
1219
                );
1220
1221
1
                let reason = if is_disconnect {
1222
0
                    WorkerDisconnectReason::Disconnected
1223
                } else {
1224
1
                    WorkerDisconnectReason::Error
1225
                };
1226
1
                let evicted = self
1227
1
                    .immediate_evict_worker(&worker_id, err.clone(), is_disconnect, reason)
1228
1
                    .await;
1229
                // The dispatch was never charged to the worker (the send
1230
                // comes first), so the eviction did not see this operation;
1231
                // send it back to the queue here, untried: nothing ran, so
1232
                // no attempt is counted against it.
1233
1
                let requeued = self
1234
1
                    .worker_state_manager
1235
1
                    .update_operation(
1236
1
                        &operation_id,
1237
1
                        &worker_id,
1238
1
                        UpdateOperationType::UpdateWithDecline(Decline {
1239
1
                            reason: "worker_disconnected".to_string(),
1240
1
                        }),
1241
1
                    )
1242
1
                    .await;
1243
1
                return Result::<(), _>::Err(err).merge(evicted).merge(requeued);
1244
156
            }
1245
156
            Ok(())
1246
        } else {
1247
0
            warn!(
1248
                ?worker_id,
1249
                %operation_id,
1250
                ?action_info,
1251
                "Worker not found in worker map in worker_notify_run_action"
1252
            );
1253
            // Ensure the operation is put back to queued state.
1254
0
            self.worker_state_manager
1255
0
                .update_operation(
1256
0
                    &operation_id,
1257
0
                    &worker_id,
1258
0
                    UpdateOperationType::UpdateWithDisconnect,
1259
0
                )
1260
0
                .await
1261
        }
1262
158
    }
1263
1264
    /// Tells the worker to kill an operation it is still running but the
1265
    /// state manager no longer has executing on it. A worker that cannot be
1266
    /// reached is evicted, the same as for a failed run request.
1267
    /// The worker acknowledged a dispatch. A late acknowledgement for an
1268
    /// operation no longer on the worker is nothing to act on.
1269
4
    fn dispatch_accepted(&mut self, worker_id: &WorkerId, operation_id: &OperationId) {
1270
4
        let Some(worker) = self.workers.get_mut(worker_id) else {
1271
0
            return;
1272
        };
1273
4
        if worker.mark_accepted(operation_id) {
1274
3
            return;
1275
1
        }
1276
        // Messages from a worker arrive in order and the acknowledgement
1277
        // comes before the run, so an acknowledgement for an operation not
1278
        // on this worker means the sweep took the dispatch back already: the
1279
        // worker is about to run an action that now belongs elsewhere. Tell
1280
        // it to stop now, not at the next revoked-operation sweep.
1281
1
        info!(
1282
            ?worker_id,
1283
            %operation_id,
1284
            "Acknowledgement for a dispatch already taken back; telling the worker to stop it"
1285
        );
1286
1
        if let Err(
err0
) = worker.kill_unknown_operation(operation_id) {
1287
0
            warn!(?worker_id, %operation_id, ?err, "Could not tell the worker to stop the operation");
1288
1
        }
1289
4
    }
1290
1291
    /// The worker declined a dispatch: the ledger is restored, the action
1292
    /// requeued untried, and the worker paused; for a decline for load the
1293
    /// pause holds until the worker reports that much free memory.
1294
    /// Returns whether there was a dispatch to take back. A decline for an
1295
    /// operation the worker no longer holds (it finished, or the sweep
1296
    /// already requeued it) is nothing to act on, like a late
1297
    /// acknowledgement, and must not pause the worker.
1298
9
    async fn dispatch_declined(
1299
9
        &mut self,
1300
9
        worker_id: &WorkerId,
1301
9
        operation_id: &OperationId,
1302
9
        reason: String,
1303
9
        needs_kb: Option<u64>,
1304
9
    ) -> Result<bool, Error> {
1305
9
        let holds_it = self
1306
9
            .workers
1307
9
            .peek(worker_id)
1308
9
            .is_some_and(|worker| worker.running_action_infos.contains_key(operation_id));
1309
9
        if !holds_it {
1310
1
            debug!(?worker_id, %operation_id, %reason, "Decline for an operation not on this worker");
1311
1
            return Ok(false);
1312
8
        }
1313
8
        info!(?worker_id, %operation_id, %reason, ?needs_kb, "Worker declined dispatch");
1314
8
        self.update_action(
1315
8
            worker_id,
1316
8
            operation_id,
1317
8
            UpdateOperationType::UpdateWithDecline(Decline { reason }),
1318
8
        )
1319
8
        .await
?0
;
1320
8
        if let Some(worker) = self.workers.get_mut(worker_id) {
1321
8
            if !worker.is_paused {
1322
0
                worker.is_paused = true;
1323
0
                record_worker_state("paused", true);
1324
8
            }
1325
            // A worker that admits when idle declined for load because it
1326
            // holds other work, whatever this scheduler last saw it hold: it
1327
            // waits for room like any busy worker, and if this scheduler sees
1328
            // it holding nothing, its pause lifts on the next keepalive.
1329
            // One that does not (an older worker, or one with no cgroup
1330
            // limit) and declines while holding nothing here may not have
1331
            // more room however long it waits, so a pause until a keepalive
1332
            // with enough free could hold it for good. Its pause lifts on
1333
            // the next keepalive instead, and it is not offered that much
1334
            // again until it reports that much free, or the action would
1335
            // bounce between it and the queue untried.
1336
8
            if !worker.admits_when_idle && 
worker.running_action_infos6
.
is_empty6
() {
1337
3
                worker.pause_needs_kb = None;
1338
3
                if let Some(
needs_kb1
) = needs_kb {
1339
                    worker.idle_declined_kb = Some(
1340
1
                        worker
1341
1
                            .idle_declined_kb
1342
1
                            .map_or(needs_kb, |declined_kb| 
declined_kb0
.
min0
(
needs_kb0
)),
1343
                    );
1344
2
                }
1345
5
            } else {
1346
5
                worker.pause_needs_kb = needs_kb;
1347
5
            }
1348
0
        }
1349
8
        Ok(true)
1350
9
    }
1351
1352
    /// Dispatches this worker has not acknowledged within the timeout, as
1353
    /// of its latest keepalive.
1354
8
    fn unacknowledged_dispatches(
1355
8
        &self,
1356
8
        worker_id: &WorkerId,
1357
8
        timestamp: WorkerTimestamp,
1358
8
        timeout_s: u64,
1359
8
    ) -> Vec<OperationId> {
1360
8
        self.workers
1361
8
            .peek(worker_id)
1362
8
            .map(|worker| worker.unacknowledged(timestamp.saturating_sub(timeout_s)))
1363
8
            .unwrap_or_default()
1364
8
    }
1365
1366
5
    async fn worker_notify_kill_operation(
1367
5
        &mut self,
1368
5
        worker_id: &WorkerId,
1369
5
        operation_id: OperationId,
1370
5
    ) -> Result<(), Error> {
1371
5
        let Some(worker) = self.workers.get_mut(worker_id) else {
1372
            // Gone between the snapshot and now; its actions were requeued.
1373
0
            return Ok(());
1374
        };
1375
        // Already told, or finished in the meantime; nothing more to send.
1376
5
        if !worker.running_action_infos.contains_key(&operation_id)
1377
5
            || worker.is_kill_requested(&operation_id)
1378
        {
1379
0
            return Ok(());
1380
5
        }
1381
5
        info!(
1382
            ?worker_id,
1383
            %operation_id,
1384
            "Killing operation the state manager no longer has executing on this worker"
1385
        );
1386
5
        if let Err(
err2
) = worker
1387
5
            .notify_update(WorkerUpdate::KillOperation(operation_id.clone()))
1388
5
            .await
1389
        {
1390
            // A full channel is a worker that is behind, not one that is
1391
            // gone. It keeps its place; the request is forgotten so the
1392
            // next sweep sends the kill again.
1393
2
            if err.code == Code::ResourceExhausted {
1394
1
                worker.clear_kill_request(&operation_id);
1395
1
                warn!(
1396
                    ?worker_id,
1397
                    %operation_id,
1398
                    "Worker channel full, kill deferred to the next sweep"
1399
                );
1400
1
                return Ok(());
1401
1
            }
1402
1
            warn!(
1403
                ?worker_id,
1404
                %operation_id,
1405
                ?err,
1406
                "Worker command failed, removing worker"
1407
            );
1408
1
            let err = make_err!(
1409
1
                Code::Internal,
1410
                "Worker command failed, removing worker {worker_id} -- {err:?}",
1411
            );
1412
1
            return Result::<(), _>::Err(err.clone()).merge(
1413
1
                self.immediate_evict_worker(
1414
1
                    worker_id,
1415
1
                    err,
1416
1
                    true,
1417
1
                    WorkerDisconnectReason::Disconnected,
1418
1
                )
1419
1
                .await,
1420
            );
1421
3
        }
1422
3
        Ok(())
1423
5
    }
1424
1425
    /// Evicts the worker from the pool and puts items back into the queue if anything was being executed on it.
1426
23
    async fn immediate_evict_worker(
1427
23
        &mut self,
1428
23
        worker_id: &WorkerId,
1429
23
        err: Error,
1430
23
        is_disconnect: bool,
1431
23
        reason: WorkerDisconnectReason,
1432
23
    ) -> Result<(), Error> {
1433
23
        let mut result = Ok(());
1434
23
        if let Some(mut worker) = self.remove_worker(worker_id) {
1435
            // Log every eviction here rather than in each caller, so a worker
1436
            // can never leave the pool unexplained. Without this, a worker that
1437
            // vanished mid-build left nothing on the scheduler side to
1438
            // attribute it to, and the only visible symptom was the worker
1439
            // reconnecting with a fresh id.
1440
23
            info!(
1441
                ?worker_id,
1442
                is_disconnect,
1443
23
                running_actions = worker.running_action_infos.len(),
1444
23
                reason = %err.message_string(),
1445
                "Evicting worker from pool"
1446
            );
1447
23
            record_worker_disconnected(reason, worker.is_draining, worker.is_paused);
1448
            // We don't care if we fail to send message to worker, this is only a best attempt.
1449
23
            drop(worker.notify_update(WorkerUpdate::Disconnect).await);
1450
            // Whatever took the worker away (its stream ended, it timed
1451
            // out, a command to it failed, an operator removed it), the
1452
            // action running on it did nothing wrong: it is a worker loss
1453
            // with its own budget, not an attempt the action spent.
1454
23
            let update = UpdateOperationType::UpdateWithDisconnect;
1455
23
            for (
operation_id19
, _) in worker.running_action_infos.drain() {
1456
19
                result = result.merge(
1457
19
                    self.worker_state_manager
1458
19
                        .update_operation(&operation_id, worker_id, update.clone())
1459
19
                        .await,
1460
                );
1461
            }
1462
0
        }
1463
        // Note: Calling this many time is very cheap, it'll only trigger `do_try_match` once.
1464
        // TODO(palfrey) This should be moved to inside the Workers struct.
1465
23
        self.capacity_changed();
1466
23
        result
1467
23
    }
1468
}
1469
1470
#[derive(Debug, MetricsComponent)]
1471
pub struct ApiWorkerScheduler {
1472
    #[metric]
1473
    inner: Mutex<ApiWorkerSchedulerImpl>,
1474
    /// See `ApiWorkerSchedulerImpl::capacity_generation`; read here without
1475
    /// the lock.
1476
    capacity_generation: Arc<AtomicU64>,
1477
    #[metric(group = "platform_property_manager")]
1478
    platform_property_manager: Arc<PlatformPropertyManager>,
1479
1480
    #[metric(
1481
        help = "Timeout of how long to evict workers if no response in this given amount of time in seconds."
1482
    )]
1483
    worker_timeout_s: u64,
1484
    #[metric(
1485
        help = "How long a sent kill may go unacknowledged before the worker is evicted, in seconds."
1486
    )]
1487
    unacknowledged_kill_timeout_s: u64,
1488
    #[metric(
1489
        help = "How long a dispatch may go unacknowledged before it is requeued untried, in seconds; 0 is off."
1490
    )]
1491
    dispatch_ack_timeout_s: u64,
1492
    /// Shared worker registry for checking worker liveness.
1493
    worker_registry: SharedWorkerRegistry,
1494
1495
    /// Performance metrics for observability.
1496
    metrics: Arc<SchedulerMetrics>,
1497
1498
    /// Channel for publishing origin events such as worker-observed action
1499
    /// resource usage. `None` when origin events are disabled.
1500
    maybe_origin_event_tx: Option<mpsc::Sender<OriginEvent>>,
1501
1502
    /// Woken when a worker joins or leaves this scheduler.
1503
    local_fleet_change_notify: Arc<Notify>,
1504
}
1505
1506
impl ApiWorkerScheduler {
1507
    #[expect(clippy::too_many_arguments)]
1508
116
    pub fn new(
1509
116
        worker_state_manager: Arc<dyn WorkerStateManager>,
1510
116
        platform_property_manager: Arc<PlatformPropertyManager>,
1511
116
        allocation_strategy: WorkerAllocationStrategy,
1512
116
        live_memory_veto: Option<String>,
1513
116
        memory_escalation: Option<MemoryEscalationSpec>,
1514
116
        worker_change_notify: Arc<Notify>,
1515
116
        worker_timeout_s: u64,
1516
116
        unacknowledged_kill_timeout_s: u64,
1517
116
        dispatch_ack_timeout_s: u64,
1518
116
        worker_registry: SharedWorkerRegistry,
1519
116
        maybe_origin_event_tx: Option<mpsc::Sender<OriginEvent>>,
1520
116
        has_peers: bool,
1521
116
        record_ttl: Duration,
1522
116
    ) -> Arc<Self> {
1523
116
        debug_assert!(
1524
0
            !record_ttl.is_zero(),
1525
            "record_ttl must be non-zero; a zero TTL degenerates the census warm-up/staleness checks"
1526
        );
1527
116
        let local_fleet_change_notify = Arc::new(Notify::new());
1528
116
        let capacity_generation = Arc::new(AtomicU64::new(0));
1529
116
        Arc::new(Self {
1530
116
            inner: Mutex::new(ApiWorkerSchedulerImpl {
1531
116
                workers: Workers(LruCache::unbounded()),
1532
116
                worker_state_manager,
1533
116
                allocation_strategy,
1534
116
                live_memory_veto,
1535
116
                memory_escalation,
1536
116
                worker_change_notify,
1537
116
                capacity_generation: capacity_generation.clone(),
1538
116
                worker_registry: worker_registry.clone(),
1539
116
                shutting_down: false,
1540
116
                capability_index: WorkerCapabilityIndex::new(),
1541
116
                fleet_generation: 0,
1542
116
                fleet_shape_set: BTreeSet::new(),
1543
116
                // Without peers there is nothing to wait for.
1544
116
                peer_fleet: (!has_peers).then(Vec::new),
1545
116
                shared: has_peers,
1546
116
                record_ttl,
1547
116
                peer_fleet_known_since: None,
1548
116
                peer_fleet_last_refresh: None,
1549
116
                local_fleet_change_notify: local_fleet_change_notify.clone(),
1550
116
                static_verdicts: HashMap::new(),
1551
116
            }),
1552
116
            platform_property_manager,
1553
116
            worker_timeout_s,
1554
116
            unacknowledged_kill_timeout_s,
1555
116
            dispatch_ack_timeout_s,
1556
116
            worker_registry,
1557
116
            metrics: Arc::new(SchedulerMetrics::default()),
1558
116
            maybe_origin_event_tx,
1559
116
            local_fleet_change_notify,
1560
116
            capacity_generation,
1561
116
        })
1562
116
    }
1563
1564
    /// Whether no worker is connected here and, as far as a trusted census
1565
    /// says, to any peer either. False while the census cannot be trusted,
1566
    /// so a peer we cannot see is assumed to have workers.
1567
10
    pub async fn no_worker_anywhere(&self, now: SystemTime) -> bool {
1568
10
        let inner = self.inner.lock().await;
1569
10
        inner.workers.is_empty()
1570
7
            && inner.peer_census_trusted(now)
1571
7
            && inner.peer_fleet.as_ref().is_some_and(Vec::is_empty)
1572
10
    }
1573
1574
    /// The capacity generation as of now; see `find_worker_for_action_observed`.
1575
627
    pub fn capacity_generation(&self) -> u64 {
1576
627
        self.capacity_generation.load(Ordering::Relaxed)
1577
627
    }
1578
1579
    /// Returns a reference to the worker registry.
1580
0
    pub const fn worker_registry(&self) -> &SharedWorkerRegistry {
1581
0
        &self.worker_registry
1582
0
    }
1583
1584
    /// `dispatched_at` is the scheduler's clock at the send; the
1585
    /// acknowledgement timeout counts from it.
1586
158
    pub async fn worker_notify_run_action(
1587
158
        &self,
1588
158
        worker_id: WorkerId,
1589
158
        operation_id: OperationId,
1590
158
        action_info: ActionInfoWithProps,
1591
158
        dispatched_at: WorkerTimestamp,
1592
158
    ) -> Result<(), Error> {
1593
158
        self.metrics
1594
158
            .actions_dispatched
1595
158
            .fetch_add(1, Ordering::Relaxed);
1596
158
        let mut inner = self.inner.lock().await;
1597
158
        inner
1598
158
            .worker_notify_run_action(worker_id, operation_id, action_info, dispatched_at)
1599
158
            .await
1600
158
    }
1601
1602
    /// A keepalive, or another message standing in for one. Only a
1603
    /// keepalive carries a load and lifts a pause; anything else refreshes
1604
    /// the timestamp and runs the acknowledgement sweep.
1605
50
    async fn refresh_worker(
1606
50
        &self,
1607
50
        worker_id: &WorkerId,
1608
50
        timestamp: WorkerTimestamp,
1609
50
        load: Option<WorkerLoad>,
1610
50
        keepalive: bool,
1611
50
    ) -> Result<(), Error> {
1612
        {
1613
50
            let mut inner = self.inner.lock().await;
1614
50
            inner
1615
50
                .refresh_lifetime(worker_id, timestamp, load, keepalive)
1616
50
                .err_tip(|| "Error refreshing lifetime in worker_keep_alive_received()")
?1
;
1617
49
            if self.dispatch_ack_timeout_s > 0 {
1618
8
                let stale = inner.unacknowledged_dispatches(
1619
8
                    worker_id,
1620
8
                    timestamp,
1621
8
                    self.dispatch_ack_timeout_s,
1622
8
                );
1623
8
                for 
operation_id2
in stale {
1624
2
                    warn!(
1625
                        ?worker_id,
1626
                        %operation_id,
1627
                        timeout_s = self.dispatch_ack_timeout_s,
1628
                        "Dispatch never acknowledged, requeuing untried"
1629
                    );
1630
2
                    match inner
1631
2
                        .dispatch_declined(
1632
2
                            worker_id,
1633
2
                            &operation_id,
1634
2
                            "unacknowledged".to_string(),
1635
2
                            None,
1636
2
                        )
1637
2
                        .await
1638
                    {
1639
2
                        Ok(true) => {
1640
2
                            self.metrics
1641
2
                                .dispatches_unacknowledged
1642
2
                                .fetch_add(1, Ordering::Relaxed);
1643
2
                            record_dispatch_requeue("unacknowledged");
1644
2
                        }
1645
0
                        Ok(false) => {}
1646
0
                        Err(err) => {
1647
0
                            warn!(?worker_id, %operation_id, ?err, "Could not requeue unacknowledged dispatch");
1648
                        }
1649
                    }
1650
                }
1651
41
            }
1652
        }
1653
49
        let now = UNIX_EPOCH + Duration::from_secs(timestamp);
1654
49
        self.worker_registry
1655
49
            .update_worker_heartbeat(worker_id, now)
1656
49
            .await;
1657
49
        Ok(())
1658
50
    }
1659
1660
17
    pub async fn running_action_info(
1661
17
        &self,
1662
17
        worker_id: &WorkerId,
1663
17
        operation_id: &OperationId,
1664
17
    ) -> Option<ActionInfoWithProps> {
1665
17
        let inner = self.inner.lock().await;
1666
17
        inner
1667
17
            .workers
1668
17
            .peek(worker_id)
1669
17
            .and_then(|worker| worker.running_action_infos.get(operation_id))
1670
17
            .map(|pending_action_info| pending_action_info.action_info.clone())
1671
17
    }
1672
1673
    /// Returns the scheduler metrics for observability.
1674
    #[must_use]
1675
0
    pub const fn get_metrics(&self) -> &Arc<SchedulerMetrics> {
1676
0
        &self.metrics
1677
0
    }
1678
1679
    /// A counter that changes every time a worker joins or leaves.
1680
603
    pub async fn fleet_generation(&self) -> u64 {
1681
603
        self.inner.lock().await.fleet_generation
1682
603
    }
1683
1684
    /// The distinct properties this scheduler's workers registered with.
1685
53
    pub async fn fleet_shapes(&self) -> Vec<PlatformProperties> {
1686
53
        self.inner.lock().await.fleet_shapes()
1687
53
    }
1688
1689
    /// Records what the workers connected to peer schedulers can run, or
1690
    /// `None` when that is not known.
1691
61
    pub async fn set_peer_fleet(
1692
61
        &self,
1693
61
        peer_fleet: Option<Vec<PlatformProperties>>,
1694
61
        now: SystemTime,
1695
61
    ) {
1696
61
        self.inner.lock().await.set_peer_fleet(peer_fleet, now);
1697
61
    }
1698
1699
    /// Woken when a worker joins or leaves this scheduler.
1700
    #[must_use]
1701
100
    pub const fn local_fleet_change_notify(&self) -> &Arc<Notify> {
1702
100
        &self.local_fleet_change_notify
1703
100
    }
1704
1705
    /// Attempts to find a worker that is capable of running this action.
1706
    // TODO(palfrey) This algorithm is not very efficient. Simple testing using a tree-like
1707
    // structure showed worse performance on a 10_000 worker * 7 properties * 1000 queued tasks
1708
    // simulation of worst cases in a single threaded environment.
1709
8
    pub async fn find_worker_for_action(
1710
8
        &self,
1711
8
        platform_properties: &PlatformProperties,
1712
8
        full_worker_logging: bool,
1713
8
        now: SystemTime,
1714
8
    ) -> MatchOutcome {
1715
8
        self.find_worker_for_action_observed(platform_properties, full_worker_logging, now)
1716
8
            .await
1717
            .0
1718
8
    }
1719
1720
    /// `find_worker_for_action`, also returning the capacity generation read
1721
    /// under the same lock as the placement. A pass compares it with the one
1722
    /// it saw last: a change means a worker gained room since, and an older
1723
    /// action that found none earlier in the pass should be offered that room
1724
    /// before this one takes it.
1725
360
    pub async fn find_worker_for_action_observed(
1726
360
        &self,
1727
360
        platform_properties: &PlatformProperties,
1728
360
        full_worker_logging: bool,
1729
360
        now: SystemTime,
1730
360
    ) -> (MatchOutcome, u64) {
1731
360
        let start = Instant::now();
1732
360
        self.metrics
1733
360
            .find_worker_calls
1734
360
            .fetch_add(1, Ordering::Relaxed);
1735
1736
360
        let mut inner = self.inner.lock().await;
1737
360
        let worker_count = inner.workers.len() as u64;
1738
360
        let result =
1739
360
            inner.inner_find_worker_for_action(platform_properties, full_worker_logging, now);
1740
360
        let generation = inner.capacity_generation.load(Ordering::Relaxed);
1741
1742
        // Track workers iterated (worst case is all workers)
1743
360
        self.metrics
1744
360
            .workers_iterated
1745
360
            .fetch_add(worker_count, Ordering::Relaxed);
1746
1747
360
        if 
matches!217
(result, MatchOutcome::Matched(_)) {
1748
143
            self.metrics
1749
143
                .find_worker_hits
1750
143
                .fetch_add(1, Ordering::Relaxed);
1751
217
        } else {
1752
217
            self.metrics
1753
217
                .find_worker_misses
1754
217
                .fetch_add(1, Ordering::Relaxed);
1755
217
        }
1756
1757
        #[allow(clippy::cast_possible_truncation)]
1758
360
        self.metrics
1759
360
            .find_worker_time_ns
1760
360
            .fetch_add(start.elapsed().as_nanos() as u64, Ordering::Relaxed);
1761
360
        (result, generation)
1762
360
    }
1763
1764
    /// Checks to see if the worker exists in the worker pool. Should only be used in unit tests.
1765
    #[must_use]
1766
14
    pub async fn contains_worker_for_test(&self, worker_id: &WorkerId) -> bool {
1767
14
        let inner = self.inner.lock().await;
1768
14
        inner.workers.contains(worker_id)
1769
14
    }
1770
1771
    /// A unit test function used to send the keep alive message to the worker from the server.
1772
0
    pub async fn send_keep_alive_to_worker_for_test(
1773
0
        &self,
1774
0
        worker_id: &WorkerId,
1775
1
    ) -> Result<(), Error> {
1776
1
        let mut inner = self.inner.lock().await;
1777
1
        let worker = inner.workers.get_mut(worker_id).ok_or_else(|| 
{0
1778
0
            make_input_err!("WorkerId '{}' does not exist in workers map", worker_id)
1779
0
        })?;
1780
1
        worker.keep_alive()
1781
1
    }
1782
}
1783
1784
#[async_trait]
1785
impl WorkerScheduler for ApiWorkerScheduler {
1786
27
    fn get_platform_property_manager(&self) -> &PlatformPropertyManager {
1787
27
        self.platform_property_manager.as_ref()
1788
27
    }
1789
1790
    async fn record_action_resource_usage(
1791
        &self,
1792
        worker_id: &WorkerId,
1793
        operation_id: &OperationId,
1794
        mut resource_usage: ActionResourceUsage,
1795
15
    ) -> Result<(), Error> {
1796
        // The worker API talks to this `ApiWorkerScheduler` (it is the
1797
        // `WorkerScheduler` returned by `SimpleScheduler::new`), so the
1798
        // resource-usage origin event must be published here. Previously the
1799
        // only override lived on `SimpleScheduler`, which this path never
1800
        // reaches, so the event was silently dropped by the trait's no-op
1801
        // default and `observed_worker_peak_memory_mib` was never recorded.
1802
        // Sampling is optional, so only record when the worker actually took a
1803
        // reading. A zero here means "not sampled", not "used no memory".
1804
        {
1805
            // The result follows this report; what it says decides whether
1806
            // that result is a kill to escalate.
1807
            let mut inner = self.inner.lock().await;
1808
            if let Some(pending) = inner
1809
                .workers
1810
                .get_mut(worker_id)
1811
15
                .and_then(|worker| worker.running_action_infos.get_mut(operation_id))
1812
            {
1813
                pending.last_usage = Some(resource_usage.clone());
1814
            }
1815
        }
1816
        let maybe_action_info = self.running_action_info(worker_id, operation_id).await;
1817
        let action_mnemonic = maybe_action_info
1818
            .as_ref()
1819
15
            .and_then(|action_info| action_info.origin_metadata.bazel_metadata.as_ref())
1820
1
            .map_or_else(String::new, |bazel_metadata| {
1821
1
                bazel_metadata.action_mnemonic.clone()
1822
1
            });
1823
1824
        let outcome =
1825
            ResourceOutcome::try_from(resource_usage.outcome).unwrap_or(ResourceOutcome::Unknown);
1826
        if !matches!(
1827
            outcome,
1828
            ResourceOutcome::Unknown | ResourceOutcome::Completed
1829
        ) {
1830
            warn!(
1831
                ?worker_id,
1832
                ?operation_id,
1833
                outcome = outcome.as_str_name(),
1834
                enforced = resource_usage.enforced,
1835
                action_mnemonic,
1836
                peak_memory_kb = resource_usage.peak_memory_kb,
1837
                wall_time_ms = resource_usage.wall_time_ms,
1838
                reserved = ?resource_usage.reserved,
1839
                "Action ended by a kill"
1840
            );
1841
        }
1842
1843
        if resource_usage.sampled {
1844
            if resource_usage.peak_memory_kb > 0 {
1845
                record_execution_peak_memory(
1846
                    resource_usage.peak_memory_kb,
1847
                    "",
1848
                    &action_mnemonic,
1849
                    outcome.as_str_name(),
1850
                );
1851
            }
1852
            if resource_usage.cpu_time_ms > 0 {
1853
                record_execution_cpu_time(
1854
                    resource_usage.cpu_time_ms,
1855
                    "",
1856
                    &action_mnemonic,
1857
                    outcome.as_str_name(),
1858
                );
1859
            }
1860
        }
1861
1862
        let Some(origin_event_tx) = self.maybe_origin_event_tx.as_ref() else {
1863
            return Ok(());
1864
        };
1865
        let Some(action_info) = maybe_action_info else {
1866
            return Ok(());
1867
        };
1868
1869
        if resource_usage.operation_id.is_empty() {
1870
            resource_usage.operation_id = operation_id.to_string();
1871
        }
1872
        if resource_usage.worker_id.is_empty() {
1873
            resource_usage.worker_id = worker_id.to_string();
1874
        }
1875
1876
        let event = Event {
1877
            event: Some(event::Event::Response(ResponseEvent {
1878
                event: Some(response_event::Event::ActionResourceUsage(resource_usage)),
1879
            })),
1880
        };
1881
        let origin_event = OriginEvent {
1882
            version: 0,
1883
            event_id: Uuid::now_v6(&get_node_id(Some(&event)))
1884
                .hyphenated()
1885
                .to_string(),
1886
            parent_event_id: action_info
1887
                .scheduler_start_execute_event_id
1888
                .clone()
1889
                .unwrap_or_default(),
1890
            bazel_request_metadata: action_info.origin_metadata.bazel_metadata.clone(),
1891
            identity: action_info.origin_metadata.identity,
1892
            event: Some(event),
1893
        };
1894
        // Awaited send (not try_send): apply backpressure when the publisher
1895
        // queue is full instead of silently dropping the resource-usage event,
1896
        // which is what drives action-level resource sizing in the UI.
1897
        if let Err(err) = origin_event_tx.send(origin_event).await {
1898
            warn!(?err, "Failed to publish action resource usage origin event");
1899
        }
1900
        Ok(())
1901
15
    }
1902
1903
136
    async fn add_worker(&self, worker: Worker) -> Result<(), Error> {
1904
        let worker_id = worker.id.clone();
1905
        let worker_timestamp = worker.last_update_timestamp;
1906
        let mut inner = self.inner.lock().await;
1907
        if inner.shutting_down {
1908
            warn!("Rejected worker add during shutdown: {}", worker_id);
1909
            return Err(make_err!(
1910
                Code::Unavailable,
1911
                "Received request to add worker while shutting down"
1912
            ));
1913
        }
1914
        let result = inner
1915
            .add_worker(worker)
1916
            .err_tip(|| "Error while adding worker, removing from pool");
1917
        if let Err(err) = result {
1918
            return Result::<(), _>::Err(err.clone()).merge(
1919
                inner
1920
                    .immediate_evict_worker(&worker_id, err, false, WorkerDisconnectReason::Error)
1921
                    .await,
1922
            );
1923
        }
1924
1925
        let now = UNIX_EPOCH + Duration::from_secs(worker_timestamp);
1926
        self.worker_registry.register_worker(&worker_id, now).await;
1927
1928
        self.metrics.workers_added.fetch_add(1, Ordering::Relaxed);
1929
        Ok(())
1930
136
    }
1931
1932
    async fn update_action(
1933
        &self,
1934
        worker_id: &WorkerId,
1935
        operation_id: &OperationId,
1936
        update: UpdateOperationType,
1937
46
    ) -> Result<(), Error> {
1938
        let mut inner = self.inner.lock().await;
1939
        let (update, escalated) = inner.maybe_escalate(worker_id, operation_id, update);
1940
        if escalated {
1941
            record_dispatch_requeue("memory_escalation");
1942
        }
1943
        inner.update_action(worker_id, operation_id, update).await
1944
46
    }
1945
1946
    async fn worker_dispatch_accepted(
1947
        &self,
1948
        worker_id: &WorkerId,
1949
        operation_id: &OperationId,
1950
4
    ) -> Result<(), Error> {
1951
        let mut inner = self.inner.lock().await;
1952
        inner.dispatch_accepted(worker_id, operation_id);
1953
        Ok(())
1954
4
    }
1955
1956
    async fn worker_dispatch_declined(
1957
        &self,
1958
        worker_id: &WorkerId,
1959
        operation_id: &OperationId,
1960
        reason: String,
1961
        needs_kb: Option<u64>,
1962
7
    ) -> Result<(), Error> {
1963
        let mut inner = self.inner.lock().await;
1964
        // Counted once the action is back in the queue, not per message.
1965
        if inner
1966
            .dispatch_declined(worker_id, operation_id, reason, needs_kb)
1967
            .await?
1968
        {
1969
            self.metrics
1970
                .dispatches_declined
1971
                .fetch_add(1, Ordering::Relaxed);
1972
            record_dispatch_requeue("declined");
1973
        }
1974
        Ok(())
1975
7
    }
1976
1977
    async fn worker_keep_alive_received(
1978
        &self,
1979
        worker_id: &WorkerId,
1980
        timestamp: WorkerTimestamp,
1981
        load: Option<WorkerLoad>,
1982
41
    ) -> Result<(), Error> {
1983
        self.refresh_worker(worker_id, timestamp, load, true).await
1984
41
    }
1985
1986
    async fn worker_liveness_refreshed(
1987
        &self,
1988
        worker_id: &WorkerId,
1989
        timestamp: WorkerTimestamp,
1990
9
    ) -> Result<(), Error> {
1991
        self.refresh_worker(worker_id, timestamp, None, false).await
1992
9
    }
1993
1994
15
    async fn remove_worker(&self, worker_id: &WorkerId) -> Result<(), Error> {
1995
        self.worker_registry.remove_worker(worker_id).await;
1996
1997
        let mut inner = self.inner.lock().await;
1998
        inner
1999
            .immediate_evict_worker(
2000
                worker_id,
2001
                make_err!(Code::Internal, "Received request to remove worker"),
2002
                false,
2003
                WorkerDisconnectReason::Removed,
2004
            )
2005
            .await
2006
15
    }
2007
2008
2
    async fn worker_disconnected(&self, worker_id: &WorkerId) -> Result<(), Error> {
2009
        self.worker_registry.remove_worker(worker_id).await;
2010
2011
        let mut inner = self.inner.lock().await;
2012
        inner
2013
            .immediate_evict_worker(
2014
                worker_id,
2015
                make_err!(Code::Unavailable, "Worker stream ended without going away"),
2016
                true,
2017
                WorkerDisconnectReason::Disconnected,
2018
            )
2019
            .await
2020
2
    }
2021
2022
0
    async fn shutdown(&self, shutdown_guard: ShutdownGuard) {
2023
        let mut inner = self.inner.lock().await;
2024
        inner.shutting_down = true; // should reject further worker registration
2025
        while let Some(worker_id) = inner
2026
            .workers
2027
            .peek_lru()
2028
0
            .map(|(worker_id, _worker)| worker_id.clone())
2029
        {
2030
            if let Err(err) = inner
2031
                .immediate_evict_worker(
2032
                    &worker_id,
2033
                    make_err!(Code::Internal, "Scheduler shutdown"),
2034
                    true,
2035
                    WorkerDisconnectReason::Shutdown,
2036
                )
2037
                .await
2038
            {
2039
                error!(?err, "Error evicting worker on shutdown.");
2040
            }
2041
        }
2042
        drop(shutdown_guard);
2043
0
    }
2044
2045
9
    async fn remove_timedout_workers(&self, now_timestamp: WorkerTimestamp) -> Result<(), Error> {
2046
        // Check worker liveness using both the local timestamp (from LRU)
2047
        // and the worker registry. A worker is alive if either source says it's alive.
2048
        let timeout = Duration::from_secs(self.worker_timeout_s);
2049
        let now = UNIX_EPOCH + Duration::from_secs(now_timestamp);
2050
        let timeout_threshold = now_timestamp.saturating_sub(self.worker_timeout_s);
2051
2052
        let workers_to_check: Vec<(WorkerId, bool, bool)> = {
2053
            let inner = self.inner.lock().await;
2054
            inner
2055
                .workers
2056
                .iter()
2057
10
                .map(|(worker_id, worker)| {
2058
10
                    let local_alive = worker.last_update_timestamp > timeout_threshold;
2059
10
                    let kill_overdue = worker.running_action_infos.values().any(|info| 
{5
2060
5
                        info.kill_requested_at.is_some_and(|at| 
{2
2061
2
                            now_timestamp.saturating_sub(at) > self.unacknowledged_kill_timeout_s
2062
2
                        })
2063
5
                    });
2064
10
                    (worker_id.clone(), local_alive, kill_overdue)
2065
10
                })
2066
                .collect()
2067
        };
2068
2069
        let mut worker_ids_to_remove = Vec::new();
2070
        for (worker_id, local_alive, kill_overdue) in workers_to_check {
2071
            // A healthy worker acknowledges a kill in moments; one that
2072
            // cannot is wedged, and its keepalives keep the liveness checks
2073
            // below from ever firing while the dead operation holds its
2074
            // slot (the nativelink#2672 symptom). Evicting requeues its
2075
            // other operations.
2076
            if kill_overdue {
2077
                warn!(
2078
                    ?worker_id,
2079
                    unacknowledged_kill_timeout_s = self.unacknowledged_kill_timeout_s,
2080
                    "Worker did not acknowledge a kill in time, removing from pool"
2081
                );
2082
                worker_ids_to_remove.push((worker_id, true));
2083
                continue;
2084
            }
2085
2086
            if local_alive {
2087
                continue;
2088
            }
2089
2090
            let registry_alive = self
2091
                .worker_registry
2092
                .is_worker_alive(&worker_id, timeout, now)
2093
                .await;
2094
2095
            if !registry_alive {
2096
                trace!(
2097
                    ?worker_id,
2098
                    local_alive,
2099
                    registry_alive,
2100
                    timeout_threshold,
2101
                    "Worker timed out - neither local nor registry shows alive"
2102
                );
2103
                worker_ids_to_remove.push((worker_id, false));
2104
            }
2105
        }
2106
2107
        if worker_ids_to_remove.is_empty() {
2108
            return Ok(());
2109
        }
2110
2111
        let mut inner = self.inner.lock().await;
2112
        let mut result = Ok(());
2113
2114
        for (worker_id, kill_overdue) in &worker_ids_to_remove {
2115
            let err = if *kill_overdue {
2116
                make_err!(
2117
                    Code::Internal,
2118
                    "Worker {worker_id} did not acknowledge a kill within {}s, removing from pool",
2119
                    self.unacknowledged_kill_timeout_s
2120
                )
2121
            } else {
2122
                warn!(?worker_id, "Worker timed out, removing from pool");
2123
                make_err!(
2124
                    Code::Internal,
2125
                    "Worker {worker_id} timed out, removing from pool"
2126
                )
2127
            };
2128
            let reason = if *kill_overdue {
2129
                WorkerDisconnectReason::KillUnacknowledged
2130
            } else {
2131
                WorkerDisconnectReason::Timeout
2132
            };
2133
            result = result.merge(
2134
                inner
2135
                    .immediate_evict_worker(worker_id, err, false, reason)
2136
                    .await,
2137
            );
2138
        }
2139
2140
        result
2141
9
    }
2142
2143
6
    async fn worker_snapshot(&self) -> Vec<WorkerSummary> {
2144
12
        fn as_strings(properties: &PlatformProperties) -> HashMap<String, String> {
2145
12
            properties
2146
12
                .properties
2147
12
                .iter()
2148
12
                .map(|(name, value)| 
{4
2149
4
                    let value = match value {
2150
4
                        PlatformPropertyValue::Minimum(v) => v.to_string(),
2151
0
                        PlatformPropertyValue::Exact(v)
2152
0
                        | PlatformPropertyValue::Priority(v)
2153
0
                        | PlatformPropertyValue::Ignore(v)
2154
0
                        | PlatformPropertyValue::Unknown(v) => v.clone(),
2155
                    };
2156
4
                    (name.clone(), value)
2157
4
                })
2158
12
                .collect()
2159
12
        }
2160
        let inner = self.inner.lock().await;
2161
        inner
2162
            .workers
2163
            .iter()
2164
            .map(|(_, worker)| WorkerSummary {
2165
6
                id: worker.id.to_string(),
2166
6
                running_actions: u32::try_from(worker.running_action_infos.len())
2167
6
                    .unwrap_or(u32::MAX),
2168
6
                max_inflight_tasks: worker.max_inflight_tasks,
2169
6
                is_paused: worker.is_paused,
2170
6
                is_draining: worker.is_draining,
2171
6
                last_update_timestamp: worker.last_update_timestamp,
2172
6
                platform_properties: as_strings(&worker.total_platform_properties),
2173
6
                available_platform_properties: as_strings(&worker.platform_properties),
2174
6
                free_memory_kb: worker.last_load.map(|load| load.free_memory_kb),
2175
6
            })
2176
            .collect()
2177
6
    }
2178
2179
5
    async fn set_drain_worker(&self, worker_id: &WorkerId, is_draining: bool) -> Result<(), Error> {
2180
        let mut inner = self.inner.lock().await;
2181
        inner.set_drain_worker(worker_id, is_draining)
2182
5
    }
2183
2184
9
    async fn kill_revoked_operations(&self) -> Result<(), Error> {
2185
        let (worker_state_manager, running) = {
2186
            let inner = self.inner.lock().await;
2187
            let running: Vec<(WorkerId, OperationId)> = inner
2188
                .workers
2189
                .iter()
2190
9
                .flat_map(|(worker_id, worker)| {
2191
9
                    worker
2192
9
                        .running_action_infos
2193
9
                        .iter()
2194
                        // An operation already told to die is never swept
2195
                        // again; the kill itself is not re-sent. A worker
2196
                        // that received the kill but never reports back is
2197
                        // evicted by remove_timedout_workers once the kill
2198
                        // has gone unacknowledged longer than
2199
                        // `unacknowledged_kill_timeout_s`; a dead worker by
2200
                        // the ordinary keepalive timeout.
2201
41
                        .
filter9
(|(_, pending_action_info)| {
2202
41
                            pending_action_info.kill_requested_at.is_none()
2203
41
                        })
2204
40
                        .
map9
(|(operation_id, _)| (worker_id.clone(), operation_id.clone()))
2205
9
                })
2206
                .collect();
2207
            (inner.worker_state_manager.clone(), running)
2208
        };
2209
2210
        // On store-backed deployments each check is a network round-trip,
2211
        // so run them lock-free with bounded concurrency.
2212
        let revoked: Vec<(WorkerId, OperationId)> = futures::stream::iter(running)
2213
40
            .map(|(worker_id, operation_id)| {
2214
40
                let worker_state_manager = worker_state_manager.clone();
2215
40
                async move {
2216
40
                    match worker_state_manager
2217
40
                        .is_executing_on_worker(&operation_id, &worker_id)
2218
40
                        .await
2219
                    {
2220
35
                        Ok(true) => None,
2221
5
                        Ok(false) => Some((worker_id, operation_id)),
2222
                        // Only kill on positive evidence; try again next pass.
2223
0
                        Err(err) => {
2224
0
                            warn!(
2225
                                ?worker_id,
2226
                                %operation_id,
2227
                                ?err,
2228
                                "Could not check whether operation is still executing on worker"
2229
                            );
2230
0
                            None
2231
                        }
2232
                    }
2233
40
                }
2234
40
            })
2235
            .buffer_unordered(MAX_CONCURRENT_REVOKED_CHECKS)
2236
            .filter_map(future::ready)
2237
            .collect()
2238
            .await;
2239
2240
        if revoked.is_empty() {
2241
            return Ok(());
2242
        }
2243
2244
        // Re-check lock-free so the scheduler mutex is never held across
2245
        // store I/O; the checks above may be stale by the time we get here.
2246
        // The remaining TOCTOU window is fine: worker_notify_kill_operation
2247
        // re-guards with contains_key + is_kill_requested under the lock.
2248
        let mut confirmed = Vec::new();
2249
        for (worker_id, operation_id) in revoked {
2250
            // A result of Ok(true) or Err(_) means the operation was
2251
            // reassigned to this worker after the first check, or the state
2252
            // manager went quiet: nothing to kill on this pass.
2253
            let revoked = worker_state_manager
2254
                .is_executing_on_worker(&operation_id, &worker_id)
2255
                .await
2256
5
                .is_ok_and(|executing| !executing);
2257
            if revoked {
2258
                confirmed.push((worker_id, operation_id));
2259
            }
2260
        }
2261
2262
        if confirmed.is_empty() {
2263
            return Ok(());
2264
        }
2265
2266
        let mut inner = self.inner.lock().await;
2267
        let mut result = Ok(());
2268
        for (worker_id, operation_id) in confirmed {
2269
            result = result.merge(
2270
                inner
2271
                    .worker_notify_kill_operation(&worker_id, operation_id)
2272
                    .await,
2273
            );
2274
        }
2275
        result
2276
9
    }
2277
}
2278
2279
impl RootMetricsComponent for ApiWorkerScheduler {}
2280
2281
#[cfg(test)]
2282
mod peer_census_trusted_tests {
2283
    use super::{Duration, SystemTime, UNIX_EPOCH, peer_census_trusted};
2284
2285
    const TTL: Duration = Duration::from_secs(15);
2286
23
    fn at(secs: u64) -> SystemTime {
2287
23
        UNIX_EPOCH + Duration::from_secs(1000 + secs)
2288
23
    }
2289
2290
    #[test]
2291
1
    fn unknown_census_is_never_trusted() {
2292
        // Even a non-shared db, if explicitly unknown, is not trusted.
2293
1
        assert!(!peer_census_trusted(false, false, None, None, TTL, at(0)));
2294
1
        assert!(!peer_census_trusted(
2295
1
            true,
2296
1
            false,
2297
1
            Some(at(0)),
2298
1
            Some(at(0)),
2299
1
            TTL,
2300
1
            at(100)
2301
1
        ));
2302
1
    }
2303
2304
    #[test]
2305
1
    fn non_shared_known_is_trusted_immediately() {
2306
        // The whole fleet is local; no warm-up or staleness applies.
2307
1
        assert!(peer_census_trusted(false, true, None, None, TTL, at(0)));
2308
1
    }
2309
2310
    #[test]
2311
1
    fn shared_known_needs_both_timestamps() {
2312
1
        assert!(!peer_census_trusted(true, true, None, None, TTL, at(100)));
2313
1
        assert!(!peer_census_trusted(
2314
1
            true,
2315
1
            true,
2316
1
            Some(at(0)),
2317
1
            None,
2318
1
            TTL,
2319
1
            at(100)
2320
1
        ));
2321
1
    }
2322
2323
    #[test]
2324
1
    fn shared_trusts_only_when_warm_and_fresh() {
2325
        // Known at 0, refreshed at 100, evaluated at 100: warm and fresh.
2326
1
        assert!(peer_census_trusted(
2327
            true,
2328
            true,
2329
1
            Some(at(0)),
2330
1
            Some(at(100)),
2331
            TTL,
2332
1
            at(100)
2333
        ));
2334
        // Exactly one TTL since known counts as warm.
2335
1
        assert!(peer_census_trusted(
2336
            true,
2337
            true,
2338
1
            Some(at(0)),
2339
1
            Some(at(15)),
2340
            TTL,
2341
1
            at(15)
2342
        ));
2343
1
    }
2344
2345
    #[test]
2346
1
    fn shared_not_warm_is_not_trusted() {
2347
        // Known only half a TTL ago: a peer may not have published yet.
2348
1
        assert!(!peer_census_trusted(
2349
1
            true,
2350
1
            true,
2351
1
            Some(at(0)),
2352
1
            Some(at(7)),
2353
1
            TTL,
2354
1
            at(7)
2355
1
        ));
2356
1
    }
2357
2358
    #[test]
2359
1
    fn shared_stale_refresh_is_not_trusted() {
2360
        // Warm, but the last successful exchange was more than a TTL ago (the
2361
        // exchange task died/stalled): fail open.
2362
1
        assert!(!peer_census_trusted(
2363
1
            true,
2364
1
            true,
2365
1
            Some(at(0)),
2366
1
            Some(at(50)),
2367
1
            TTL,
2368
1
            at(100)
2369
1
        ));
2370
1
    }
2371
2372
    #[test]
2373
1
    fn future_dated_refresh_counts_as_fresh() {
2374
        // Clock skew: a refresh timestamped ahead of `now` is treated as fresh
2375
        // (the safe direction). Still requires warm.
2376
1
        assert!(peer_census_trusted(
2377
            true,
2378
            true,
2379
1
            Some(at(0)),
2380
1
            Some(at(120)),
2381
            TTL,
2382
1
            at(100)
2383
        ));
2384
1
    }
2385
}