Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-scheduler/src/simple_scheduler_state_manager.rs
Line
Count
Source
1
// Copyright 2024 Trace Machina, Inc. 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::Bound;
16
use core::time::Duration;
17
use std::collections::HashMap;
18
use std::string::ToString;
19
use std::sync::{Arc, Weak};
20
use std::time::SystemTime;
21
22
use async_lock::Mutex;
23
use async_trait::async_trait;
24
use futures::future::BoxFuture;
25
use futures::{StreamExt, TryStreamExt, stream};
26
use nativelink_error::{Code, Error, ResultExt, make_err};
27
use nativelink_metric::MetricsComponent;
28
use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::AttemptEndReason;
29
use nativelink_util::action_messages::{
30
    ActionInfo, ActionResult, ActionStage, ActionState, ActionUniqueQualifier, ExecutionMetadata,
31
    LOST_RESULT_ERROR_TIP, OperationId, STORE_UNREACHABLE_ERROR_TIP, WorkerId,
32
};
33
use nativelink_util::instant_wrapper::InstantWrapper;
34
use nativelink_util::metrics::{
35
    AgeBuckets, EXECUTION_METRICS, EXECUTION_RESULT, EXECUTION_STAGE, ExecutionResult,
36
    ExecutionStage, record_awaited_action_orphan, record_dispatch_requeue, record_queue_retired,
37
};
38
use nativelink_util::operation_state_manager::{
39
    ActionStateResult, ActionStateResultStream, ClientStateManager, DispatchFacts, Escalation,
40
    MatchingEngineStateManager, OperationFilter, OperationStageFlags, OrderDirection,
41
    UpdateOperationType, WorkerStateManager,
42
};
43
use nativelink_util::origin_event::OriginMetadata;
44
use nativelink_util::platform_properties::PlatformProperties;
45
use opentelemetry::KeyValue;
46
use tracing::{debug, info, trace, warn};
47
48
use super::awaited_action_db::{
49
    AwaitedAction, AwaitedActionDb, AwaitedActionSubscriber, PeerScheduler,
50
    SortedAwaitedActionState,
51
};
52
use crate::worker_registry::{ORPHANED_ACTION_TIMEOUT, SharedWorkerRegistry, WorkerLiveness};
53
54
/// An attempt the executing sweep ended, for the scheduler to report.
55
#[derive(Debug, Clone, PartialEq, Eq)]
56
pub struct SweptAttempt {
57
    pub operation_id: OperationId,
58
    pub worker_id: WorkerId,
59
    pub reason: AttemptEndReason,
60
    pub detail: String,
61
}
62
63
/// What one executing sweep did.
64
#[derive(Debug, Default, Clone, PartialEq, Eq)]
65
pub struct ExecutingSweepReport {
66
    /// Retired as overdue, through `timeout_operation_id`.
67
    pub retired: u64,
68
    /// Sent back to the queue because their worker no longer ran them.
69
    pub requeued: u64,
70
    /// Could not be moved this sweep; they are tried again on the next.
71
    pub failed: u64,
72
    /// How long each executing action the sweep read had been executing,
73
    /// counted before any was retired or requeued.
74
    pub ages: AgeBuckets,
75
    /// The attempts it retired or requeued.
76
    pub ended: Vec<SweptAttempt>,
77
}
78
79
/// Why the scheduler times out an executing action.
80
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81
pub enum TimeoutCause {
82
    /// The action's `Action.timeout` passed, plus the grace a live worker
83
    /// has to report its own result.
84
    ActionTimeout { timeout: Duration, grace: Duration },
85
    /// The worker sent no update on the action for this long.
86
    NoWorkerUpdate(Duration),
87
}
88
89
impl core::fmt::Display for TimeoutCause {
90
13
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91
13
        match self {
92
1
            Self::ActionTimeout { timeout, grace } => write!(
93
1
                f,
94
                "exceeded its Action.timeout of {} seconds, and its worker had not reported within {} seconds after that",
95
1
                timeout.as_secs_f32(),
96
1
                grace.as_secs_f32(),
97
            ),
98
12
            Self::NoWorkerUpdate(ceiling) => write!(
99
12
                f,
100
                "timed out after {} seconds with no update from its worker",
101
12
                ceiling.as_secs_f32(),
102
            ),
103
        }
104
13
    }
105
}
106
107
/// Maximum number of times an update to the database
108
/// can fail before giving up.
109
const MAX_UPDATE_RETRIES: usize = 5;
110
111
/// Requeues the same action asking for more: the next dispatch carries the
112
/// raised reservation and the worker holds it to that. The last step also
113
/// asks for the worker's whole CPU, so it runs alone.
114
23
fn raise_reservation(awaited_action: &mut AwaitedAction, escalation: &Escalation) {
115
23
    let mut action_info = (**awaited_action.action_info()).clone();
116
23
    action_info
117
23
        .platform_properties
118
23
        .insert(escalation.property.clone(), escalation.value.to_string());
119
23
    if let Some((
cpu_property0
,
cpu_value0
)) = &escalation.cpu {
120
0
        action_info
121
0
            .platform_properties
122
0
            .insert(cpu_property.clone(), cpu_value.to_string());
123
23
    }
124
23
    awaited_action.set_action_info(Arc::new(action_info));
125
23
}
126
127
/// Base delay for exponential backoff on version conflicts (in ms).
128
const BASE_RETRY_DELAY_MS: u64 = 10;
129
130
/// Maximum jitter to add to retry delay (in ms).
131
const MAX_RETRY_JITTER_MS: u64 = 20;
132
133
/// How long to wait before the `retry_count`th attempt at an update that
134
/// lost a version conflict: exponential in the attempt, plus jitter.
135
/// The store errors an operation update retries within its budget: the
136
/// record moved (`Aborted`), or the store did not take the write for a
137
/// reason that passes (unreachable, slow, out of room, or a server reply the
138
/// client could not classify, which is what a Redis `-ERR` arrives as).
139
/// Everything else, `Internal` included, is final at once: an internal
140
/// error is the scheduler's own, not the store's weather.
141
21
const fn is_transient_store_error(code: Code) -> bool {
142
1
    matches!(
143
21
        code,
144
        Code::Aborted
145
            | Code::Unavailable
146
            | Code::DeadlineExceeded
147
            | Code::ResourceExhausted
148
            | Code::Unknown
149
    )
150
21
}
151
152
20
fn version_conflict_backoff(retry_count: usize) -> Duration {
153
20
    let base_delay = BASE_RETRY_DELAY_MS * (1 << retry_count.saturating_sub(2).min(4));
154
20
    let jitter = SystemTime::now()
155
20
        .duration_since(std::time::UNIX_EPOCH)
156
20
        .map_or(0, |d| {
157
20
            u64::try_from(d.as_nanos()).expect("u64 error") % MAX_RETRY_JITTER_MS
158
20
        });
159
20
    Duration::from_millis(base_delay + jitter)
160
20
}
161
162
/// Records the execution metrics for an action whose new state was just
163
/// stored. `is_retry` is set when a failed attempt re-queued the action.
164
21.9k
fn record_execution_metrics(awaited_action: &AwaitedAction, is_retry: bool) {
165
21.9k
    let action_state = awaited_action.state();
166
21.9k
    let instance_name = awaited_action
167
21.9k
        .action_info()
168
21.9k
        .unique_qualifier
169
21.9k
        .instance_name()
170
21.9k
        .as_str();
171
21.9k
    let worker_id = awaited_action
172
21.9k
        .worker_id()
173
21.9k
        .map(std::string::ToString::to_string);
174
21.9k
    let priority = Some(awaited_action.action_info().priority);
175
176
    // Build base attributes for metrics
177
21.9k
    let mut attrs = nativelink_util::metrics::make_execution_attributes(
178
21.9k
        instance_name,
179
21.9k
        worker_id.as_deref(),
180
21.9k
        priority,
181
    );
182
183
    // Add stage attribute
184
21.9k
    let execution_stage: ExecutionStage = (&action_state.stage).into();
185
21.9k
    attrs.push(KeyValue::new(EXECUTION_STAGE, execution_stage));
186
187
    // Record stage transition
188
21.9k
    EXECUTION_METRICS.execution_stage_transitions.add(1, &attrs);
189
190
    // For completed actions, record the completion count with result
191
21.9k
    match &action_state.stage {
192
10.1k
        ActionStage::Completed(action_result) => {
193
10.1k
            let result = if action_result.exit_code == 0 {
194
10.0k
                ExecutionResult::Success
195
            } else {
196
87
                ExecutionResult::Failure
197
            };
198
10.1k
            attrs.push(KeyValue::new(EXECUTION_RESULT, result));
199
10.1k
            EXECUTION_METRICS.execution_completed_count.add(1, &attrs);
200
10.1k
            nativelink_util::metrics::record_completed_execution_metrics(
201
10.1k
                action_result,
202
10.1k
                instance_name,
203
10.1k
                worker_id.as_deref(),
204
10.1k
                priority,
205
            );
206
        }
207
0
        ActionStage::CompletedFromCache(_) => {
208
0
            attrs.push(KeyValue::new(EXECUTION_RESULT, ExecutionResult::CacheHit));
209
0
            EXECUTION_METRICS.execution_completed_count.add(1, &attrs);
210
0
        }
211
11.7k
        _ => {}
212
    }
213
214
    // A failed attempt that re-queued the action counts as a retry.
215
21.9k
    if is_retry {
216
734
        let retry_attrs = nativelink_util::metrics::make_execution_attributes(
217
734
            instance_name,
218
734
            worker_id.as_deref(),
219
734
            priority,
220
734
        );
221
734
        EXECUTION_METRICS.execution_retry_count.add(1, &retry_attrs);
222
21.1k
    }
223
21.9k
}
224
225
/// Whether an error reading a listed queue entry means the entry itself is
226
/// lost: its record is gone from the store or cannot be decoded. Such an
227
/// entry is skipped and counted. Any other error (the store unreachable, a
228
/// listing page that failed to load) is about the store, not the entry, and
229
/// is propagated so the caller retries instead of reporting a clean pass.
230
14
pub(crate) const fn is_lost_record(err: &Error) -> bool {
231
14
    
matches!6
(err.code, Code::NotFound | Code::InvalidArgument)
232
14
}
233
234
/// Simple struct that implements the `ActionStateResult` trait and always returns an error.
235
struct ErrorActionStateResult(Error);
236
237
#[async_trait]
238
impl ActionStateResult for ErrorActionStateResult {
239
1
    async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
240
        Err(self.0.clone())
241
1
    }
242
243
0
    async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
244
        Err(self.0.clone())
245
0
    }
246
247
1
    async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> {
248
        Err(self.0.clone())
249
1
    }
250
}
251
252
struct ClientActionStateResult<U, T, I, NowFn>
253
where
254
    U: AwaitedActionSubscriber,
255
    T: AwaitedActionDb,
256
    I: InstantWrapper,
257
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
258
{
259
    inner: MatchingEngineActionStateResult<U, T, I, NowFn>,
260
}
261
262
impl<U, T, I, NowFn> ClientActionStateResult<U, T, I, NowFn>
263
where
264
    U: AwaitedActionSubscriber,
265
    T: AwaitedActionDb,
266
    I: InstantWrapper,
267
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
268
{
269
17.2k
    const fn new(
270
17.2k
        sub: U,
271
17.2k
        simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>,
272
17.2k
        no_event_action_timeout: Duration,
273
17.2k
        now_fn: NowFn,
274
17.2k
    ) -> Self {
275
17.2k
        Self {
276
17.2k
            inner: MatchingEngineActionStateResult::new(
277
17.2k
                sub,
278
17.2k
                simple_scheduler_state_manager,
279
17.2k
                no_event_action_timeout,
280
17.2k
                now_fn,
281
17.2k
            ),
282
17.2k
        }
283
17.2k
    }
284
}
285
286
#[async_trait]
287
impl<U, T, I, NowFn> ActionStateResult for ClientActionStateResult<U, T, I, NowFn>
288
where
289
    U: AwaitedActionSubscriber,
290
    T: AwaitedActionDb,
291
    I: InstantWrapper,
292
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
293
{
294
16.3k
    async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
295
        self.inner.as_state().await
296
16.3k
    }
297
298
47.5k
    async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
299
        self.inner.changed().await
300
47.5k
    }
301
302
7
    async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> {
303
        self.inner.as_action_info().await
304
7
    }
305
306
4
    async fn client_last_seen(&self) -> Result<Option<SystemTime>, Error> {
307
        self.inner.client_last_seen().await
308
4
    }
309
}
310
311
struct MatchingEngineActionStateResult<U, T, I, NowFn>
312
where
313
    U: AwaitedActionSubscriber,
314
    T: AwaitedActionDb,
315
    I: InstantWrapper,
316
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
317
{
318
    awaited_action_sub: U,
319
    simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>,
320
    no_event_action_timeout: Duration,
321
    now_fn: NowFn,
322
}
323
impl<U, T, I, NowFn> MatchingEngineActionStateResult<U, T, I, NowFn>
324
where
325
    U: AwaitedActionSubscriber,
326
    T: AwaitedActionDb,
327
    I: InstantWrapper,
328
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
329
{
330
6.68M
    const fn new(
331
6.68M
        awaited_action_sub: U,
332
6.68M
        simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>,
333
6.68M
        no_event_action_timeout: Duration,
334
6.68M
        now_fn: NowFn,
335
6.68M
    ) -> Self {
336
6.68M
        Self {
337
6.68M
            awaited_action_sub,
338
6.68M
            simple_scheduler_state_manager,
339
6.68M
            no_event_action_timeout,
340
6.68M
            now_fn,
341
6.68M
        }
342
6.68M
    }
343
}
344
345
#[async_trait]
346
impl<U, T, I, NowFn> ActionStateResult for MatchingEngineActionStateResult<U, T, I, NowFn>
347
where
348
    U: AwaitedActionSubscriber,
349
    T: AwaitedActionDb,
350
    I: InstantWrapper,
351
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
352
{
353
27.3k
    async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
354
        let awaited_action = self
355
            .awaited_action_sub
356
            .borrow()
357
            .await
358
            .err_tip(|| "In MatchingEngineActionStateResult::as_state")?;
359
        Ok((
360
            awaited_action.state().clone(),
361
            awaited_action.maybe_origin_metadata().cloned(),
362
        ))
363
27.3k
    }
364
365
4
    async fn client_last_seen(&self) -> Result<Option<SystemTime>, Error> {
366
        let awaited_action = self
367
            .awaited_action_sub
368
            .borrow()
369
            .await
370
            .err_tip(|| "In MatchingEngineActionStateResult::client_last_seen")?;
371
        Ok(Some(awaited_action.last_client_keepalive_timestamp()))
372
4
    }
373
374
6.66M
    async fn dispatch_facts(&self) -> Result<DispatchFacts, Error> {
375
        let awaited_action = self
376
            .awaited_action_sub
377
            .borrow()
378
            .await
379
            .err_tip(|| "In MatchingEngineActionStateResult::dispatch_facts")?;
380
        Ok(DispatchFacts {
381
            avoid_worker_ids: awaited_action.avoid_worker_ids().to_vec(),
382
            avoid_node_names: awaited_action.avoid_node_names().to_vec(),
383
            queued_since: Some(awaited_action.state().last_transition_timestamp),
384
        })
385
6.66M
    }
386
387
47.5k
    async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
388
        let mut timeout_attempts = 0;
389
        loop {
390
            tokio::select! {
391
                awaited_action_result = self.awaited_action_sub.changed() => {
392
                    return awaited_action_result
393
                        .err_tip(|| "In MatchingEngineActionStateResult::changed")
394
41.1k
                        .map(|v| (v.state().clone(), v.maybe_origin_metadata().cloned()));
395
                }
396
                () = (self.now_fn)().sleep(self.no_event_action_timeout) => {
397
                    // Timeout happened, do additional checks below.
398
                }
399
            }
400
401
            let awaited_action = self
402
                .awaited_action_sub
403
                .borrow()
404
                .await
405
                .err_tip(|| "In MatchingEngineActionStateResult::changed")?;
406
407
            if matches!(awaited_action.state().stage, ActionStage::Queued) {
408
                // Actions in queued state do not get periodically updated,
409
                // so we don't need to timeout them.
410
                continue;
411
            }
412
413
            let simple_scheduler_state_manager = self
414
                .simple_scheduler_state_manager
415
                .upgrade()
416
0
                .err_tip(|| format!("Failed to upgrade weak reference to SimpleSchedulerStateManager in MatchingEngineActionStateResult::changed at attempt: {timeout_attempts}"))?;
417
418
            // Check if worker is alive via registry before timing out.
419
            let Some(cause) = simple_scheduler_state_manager
420
                .timeout_cause(&awaited_action)
421
                .await
422
            else {
423
                // Worker is alive, continue waiting for updates
424
                trace!(
425
                    operation_id = %awaited_action.operation_id(),
426
                    "Operation timeout check passed, worker is alive"
427
                );
428
                continue;
429
            };
430
431
            warn!(
432
                ?awaited_action,
433
                "OperationId {} / {} {cause}, issuing a retry",
434
                awaited_action.operation_id(),
435
                awaited_action.state().client_operation_id,
436
            );
437
438
            simple_scheduler_state_manager
439
                .timeout_operation_id(awaited_action.operation_id())
440
                .await
441
                .err_tip(|| "In MatchingEngineActionStateResult::changed")?;
442
443
            if timeout_attempts >= MAX_UPDATE_RETRIES {
444
                return Err(make_err!(
445
                    Code::Internal,
446
                    "Failed to update action after {} retries with no error set in MatchingEngineActionStateResult::changed - {} {:?}",
447
                    MAX_UPDATE_RETRIES,
448
                    awaited_action.operation_id(),
449
                    awaited_action.state().stage,
450
                ));
451
            }
452
            timeout_attempts += 1;
453
        }
454
47.5k
    }
455
456
6.66M
    async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> {
457
        let awaited_action = self
458
            .awaited_action_sub
459
            .borrow()
460
            .await
461
            .err_tip(|| "In MatchingEngineActionStateResult::as_action_info")?;
462
        Ok((
463
            awaited_action.action_info().clone(),
464
            awaited_action.maybe_origin_metadata().cloned(),
465
        ))
466
6.66M
    }
467
}
468
469
/// How often an action is queued again, by cause, before the scheduler
470
/// completes it with `FailedPrecondition`. Each cause has its own budget:
471
/// an action's own failures, the workers it lost through no fault of its
472
/// own, and the memory escalations it was granted.
473
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
474
pub struct RetryLimits {
475
    /// Internal errors and timeouts the action produced.
476
    pub max_job_retries: usize,
477
    /// Workers lost while the action ran.
478
    pub max_worker_loss_retries: usize,
479
    /// Memory escalations; 0 leaves only the fleet's ceiling to bound them.
480
    pub max_escalations: usize,
481
    /// The worker loss from which a lost worker escalates the action's
482
    /// memory (RFC-0018); 0 never does.
483
    pub escalate_after_worker_losses: usize,
484
    /// Command timeouts queued again for another worker; 0 keeps the
485
    /// first timeout final.
486
    pub max_timeout_retries: usize,
487
}
488
489
impl RetryLimits {
490
    /// Lost workers allowed by default; the action did nothing wrong.
491
    pub const DEFAULT_WORKER_LOSS_RETRIES: usize = 10;
492
    /// Command timeouts tried again elsewhere by default: one second
493
    /// opinion, since a worker gone slow times out whatever it is given.
494
    pub const DEFAULT_TIMEOUT_RETRIES: usize = 1;
495
}
496
497
/// Only the job budget stated: the other two take their defaults, which is
498
/// what older callers meant.
499
impl From<usize> for RetryLimits {
500
0
    fn from(max_job_retries: usize) -> Self {
501
0
        Self {
502
0
            max_job_retries,
503
0
            max_worker_loss_retries: Self::DEFAULT_WORKER_LOSS_RETRIES,
504
0
            max_escalations: 0,
505
0
            escalate_after_worker_losses: 0,
506
0
            max_timeout_retries: Self::DEFAULT_TIMEOUT_RETRIES,
507
0
        }
508
0
    }
509
}
510
511
/// `SimpleSchedulerStateManager` is responsible for maintaining the state of the scheduler.
512
/// Scheduler state includes the actions that are queued, active, and recently completed.
513
/// It also includes the workers that are available to execute actions based on allocation
514
/// strategy.
515
516
#[derive(MetricsComponent, Debug)]
517
pub struct SimpleSchedulerStateManager<T, I, NowFn>
518
where
519
    T: AwaitedActionDb,
520
    I: InstantWrapper,
521
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
522
{
523
    /// Database for storing the state of all actions.
524
    #[metric(group = "action_db")]
525
    action_db: T,
526
527
    /// Maximum number of times a job can be retried.
528
    // TODO(palfrey) This should be a scheduler decorator instead
529
    // of always having it on every SimpleScheduler.
530
    #[metric(help = "Maximum number of times a job can be retried")]
531
    max_job_retries: usize,
532
533
    #[metric(help = "Maximum number of times a job is queued again after losing its worker")]
534
    max_worker_loss_retries: usize,
535
536
    #[metric(
537
        help = "Maximum number of memory escalations for a job; 0 leaves the ceiling to bound them"
538
    )]
539
    max_escalations: usize,
540
541
    #[metric(
542
        help = "Worker losses before a requeue also raises the memory reservation; 0 never does"
543
    )]
544
    escalate_after_worker_losses: usize,
545
546
    #[metric(
547
        help = "Maximum number of times a job is queued again for another worker after its command timed out"
548
    )]
549
    max_timeout_retries: usize,
550
551
    /// Duration after which an action is considered to be timed out if
552
    /// no event is received.
553
    #[metric(
554
        help = "Duration after which an action is considered to be timed out if no event is received"
555
    )]
556
    no_event_action_timeout: Duration,
557
558
    /// Mark operation as timed out if the worker has not updated in this duration.
559
    /// This is used to prevent operations from being stuck in the queue forever
560
    /// if it is not being processed by any worker.
561
    client_action_timeout: Duration,
562
563
    /// Maximum time an action can stay in Executing state without any worker
564
    /// update, regardless of worker keepalive status. `Duration::ZERO` disables.
565
    max_executing_timeout: Duration,
566
567
    // A lock to ensure only one timeout operation is running at a time
568
    // on this service.
569
    timeout_operation_mux: Mutex<()>,
570
571
    /// Weak reference to self.
572
    // We use a weak reference to reduce the risk of a memory leak from
573
    // future changes. If this becomes some kind of performance issue,
574
    // we can consider using a strong reference.
575
    weak_self: Weak<Self>,
576
577
    /// Function to get the current time.
578
    now_fn: NowFn,
579
580
    /// Worker registry for checking worker liveness.
581
    worker_registry: Option<SharedWorkerRegistry>,
582
}
583
584
impl<T, I, NowFn> SimpleSchedulerStateManager<T, I, NowFn>
585
where
586
    T: AwaitedActionDb,
587
    I: InstantWrapper,
588
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
589
{
590
379
    pub fn new(
591
379
        retry_limits: impl Into<RetryLimits>,
592
379
        no_event_action_timeout: Duration,
593
379
        client_action_timeout: Duration,
594
379
        max_executing_timeout: Duration,
595
379
        action_db: T,
596
379
        now_fn: NowFn,
597
379
        worker_registry: Option<SharedWorkerRegistry>,
598
379
    ) -> Arc<Self> {
599
        let RetryLimits {
600
379
            max_job_retries,
601
379
            max_worker_loss_retries,
602
379
            max_escalations,
603
379
            escalate_after_worker_losses,
604
379
            max_timeout_retries,
605
379
        } = retry_limits.into();
606
379
        Arc::new_cyclic(|weak_self| Self {
607
379
            action_db,
608
379
            max_job_retries,
609
379
            max_worker_loss_retries,
610
379
            max_escalations,
611
379
            escalate_after_worker_losses,
612
379
            max_timeout_retries,
613
379
            no_event_action_timeout,
614
379
            client_action_timeout,
615
379
            max_executing_timeout,
616
379
            timeout_operation_mux: Mutex::new(()),
617
379
            weak_self: weak_self.clone(),
618
379
            now_fn,
619
379
            worker_registry,
620
379
        })
621
379
    }
622
623
    /// Retires queued actions whose client has stopped listening.
624
    ///
625
    /// A queued action has no worker, so neither of the other two timeout
626
    /// paths can reach it. `MatchingEngineActionStateResult::changed` skips
627
    /// the `Queued` stage outright, and `should_timeout_operation` only
628
    /// considers `Executing`. That leaves the matching pass as the only
629
    /// thing that retires them, and it only does so for actions it happens
630
    /// to visit.
631
    ///
632
    /// When it does not, they accumulate with no expiry and are handed to
633
    /// the matcher again on every pass. Every pass reads the whole queued
634
    /// set, so enough of them make each read slow and each pass cost more
635
    /// than it places: workers sit idle, nothing executes or completes, and
636
    /// clients eventually report a remote execution failure.
637
    ///
638
    /// Returns how many were retired.
639
    /// Retires executing actions that nothing else will reach.
640
    ///
641
    /// The timeout check in `MatchingEngineActionStateResult::changed` runs
642
    /// only while a client is waiting on the action, once per
643
    /// `no_event_action_timeout`. An action whose client has gone, or whose
644
    /// result the worker sent but the store did not take, stays `Executing`
645
    /// with its slot's worth of fleet capacity accounted to it until
646
    /// something lists it. This does, on the scheduler's own clock:
647
    ///
648
    /// - an action `timeout_cause` says is overdue is retired through
649
    ///   `timeout_operation_id`, which re-checks under its lock;
650
    /// - an action whose worker `running_on_worker` knows and reports as no
651
    ///   longer running it has lost its result. It goes back to the queue
652
    ///   untried once that has been so for `lost_grace`, which covers the
653
    ///   moment between the store write of an assignment and the dispatch
654
    ///   that follows it. `lost_since` carries the first sighting between
655
    ///   sweeps and is pruned of actions no longer in that state.
656
    ///
657
    /// `running_on_worker` answers `None` for a worker this scheduler is not
658
    /// connected to: a peer's worker, or one already gone, which the
659
    /// timeout path handles. A record the listing names but cannot read is
660
    /// skipped, so one bad entry does not stop the sweep.
661
8
    pub async fn sweep_executing_actions(
662
8
        &self,
663
8
        running_on_worker: &(
664
8
             dyn Fn(WorkerId, OperationId) -> BoxFuture<'static, Option<bool>> + Send + Sync
665
8
         ),
666
8
        lost_since: &mut HashMap<OperationId, SystemTime>,
667
8
        lost_grace: Duration,
668
8
    ) -> Result<ExecutingSweepReport, Error> {
669
8
        let now = (self.now_fn)().now();
670
8
        let stream = self
671
8
            .action_db
672
8
            .get_range_of_actions(
673
8
                SortedAwaitedActionState::Executing,
674
8
                Bound::Unbounded,
675
8
                Bound::Unbounded,
676
8
                true,
677
8
            )
678
8
            .await
679
8
            .err_tip(|| "In sweep_executing_actions")
?0
;
680
8
        tokio::pin!(stream);
681
682
8
        let mut report = ExecutingSweepReport::default();
683
8
        let mut still_lost = HashMap::new();
684
16
        while let Some(
subscriber8
) = stream.next().await {
685
8
            let awaited_action = match subscriber {
686
8
                Ok(subscriber) => subscriber.borrow().await,
687
0
                Err(err) => Err(err),
688
            };
689
8
            let awaited_action = match awaited_action {
690
8
                Ok(awaited_action) => awaited_action,
691
0
                Err(err) => {
692
0
                    if !is_lost_record(&err) {
693
0
                        return Err(err).err_tip(|| "In sweep_executing_actions");
694
0
                    }
695
0
                    warn!(
696
                        ?err,
697
                        "Executing operation listed but its record cannot be read; skipping"
698
                    );
699
0
                    record_awaited_action_orphan("executing_sweep");
700
0
                    continue;
701
                }
702
            };
703
8
            if !
matches!0
(awaited_action.state().stage, ActionStage::Executing) {
704
0
                continue;
705
8
            }
706
8
            report
707
8
                .ages
708
8
                .observe_since(awaited_action.state().last_transition_timestamp, now);
709
8
            let operation_id = awaited_action.operation_id().clone();
710
711
8
            if let Some(
cause1
) = self.timeout_cause(&awaited_action).await {
712
1
                match self.timeout_operation_id(&operation_id).await {
713
0
                    Ok(false) => {}
714
                    Ok(true) => {
715
1
                        report.retired += 1;
716
1
                        if let Some(worker_id) = awaited_action.worker_id() {
717
1
                            report.ended.push(SweptAttempt {
718
1
                                operation_id: operation_id.clone(),
719
1
                                worker_id: worker_id.clone(),
720
1
                                reason: AttemptEndReason::Timeout,
721
1
                                detail: format!("Operation {cause}"),
722
1
                            });
723
1
                        
}0
724
                    }
725
                    // A conflicting write means someone else moved it, which
726
                    // is what we wanted; anything else is logged and the
727
                    // sweep goes on to the rest.
728
0
                    Err(err) if err.code == Code::Aborted => {}
729
0
                    Err(err) => {
730
0
                        warn!(%operation_id, ?err, "Could not time out an executing operation");
731
0
                        report.failed += 1;
732
                    }
733
                }
734
1
                continue;
735
7
            }
736
737
7
            let Some(worker_id) = awaited_action.worker_id() else {
738
0
                continue;
739
            };
740
            // Freshly assigned: the dispatch that puts it on the worker may
741
            // not have happened yet.
742
7
            if now
743
7
                .duration_since(awaited_action.state().last_transition_timestamp)
744
7
                .is_ok_and(|since| since < lost_grace)
745
            {
746
1
                continue;
747
6
            }
748
6
            if running_on_worker(worker_id.clone(), operation_id.clone()).await == Some(false) {
749
4
                let first_seen = *lost_since.get(&operation_id).unwrap_or(&now);
750
4
                if now
751
4
                    .duration_since(first_seen)
752
4
                    .is_ok_and(|lost| lost >= lost_grace)
753
                {
754
2
                    warn!(
755
                        %operation_id,
756
                        ?worker_id,
757
2
                        lost_for_s = lost_grace.as_secs(),
758
                        "Executing operation is not running on its worker; its result was lost, requeuing"
759
                    );
760
                    // Through the error path, so the loss spends one of the
761
                    // action's `max_job_retries` and a result the store keeps
762
                    // refusing ends in a failure the client sees, not a loop.
763
                    // This is a lost execution result, not a worker-reported
764
                    // Unavailable store read that uses the store-loss budget.
765
2
                    match self
766
2
                        .inner_update_operation(
767
2
                            &operation_id,
768
2
                            Some(worker_id),
769
2
                            None,
770
2
                            UpdateOperationType::UpdateWithError(make_err!(
771
2
                                Code::Internal,
772
2
                                "{LOST_RESULT_ERROR_TIP}"
773
2
                            )),
774
                        )
775
2
                        .await
776
                    {
777
2
                        Ok(()) => {
778
2
                            record_dispatch_requeue("lost_result");
779
2
                            report.requeued += 1;
780
2
                            report.ended.push(SweptAttempt {
781
2
                                operation_id: operation_id.clone(),
782
2
                                worker_id: worker_id.clone(),
783
2
                                reason: AttemptEndReason::LostResult,
784
2
                                detail: String::new(),
785
2
                            });
786
2
                        }
787
0
                        Err(err) if err.code == Code::Aborted => {}
788
0
                        Err(err) => {
789
0
                            warn!(%operation_id, ?err, "Could not requeue an operation whose result was lost");
790
0
                            report.failed += 1;
791
0
                            still_lost.insert(operation_id.clone(), first_seen);
792
                        }
793
                    }
794
2
                } else {
795
2
                    still_lost.insert(operation_id.clone(), first_seen);
796
2
                }
797
2
            }
798
        }
799
8
        *lost_since = still_lost;
800
8
        Ok(report)
801
8
    }
802
803
118
    pub async fn sweep_abandoned_queued_actions(&self) -> Result<u64, Error> {
804
118
        let now = (self.now_fn)().now();
805
118
        let stream = self
806
118
            .action_db
807
118
            .get_range_of_actions(
808
118
                SortedAwaitedActionState::Queued,
809
118
                Bound::Unbounded,
810
118
                Bound::Unbounded,
811
118
                true,
812
118
            )
813
118
            .await
814
118
            .err_tip(|| "In sweep_abandoned_queued_actions")
?0
;
815
118
        tokio::pin!(stream);
816
817
118
        let mut retired = 0u64;
818
4.22k
        while let Some(
subscriber4.11k
) = stream.next().await {
819
            // The queue listed it but the record is gone (evicted) or cannot
820
            // be decoded. One such entry must not stop the sweep from
821
            // retiring everything behind it, which is how a pile of
822
            // abandoned actions becomes permanent. Anything else, such as
823
            // the listing's next page failing to load, is a failed sweep:
824
            // the entries it did not reach are still there.
825
4.11k
            let 
awaited_action4.11k
= match subscriber {
826
4.11k
                Ok(subscriber) => subscriber.borrow().await,
827
2
                Err(err) => Err(err),
828
            };
829
4.10k
            let awaited_action = match 
awaited_action3
{
830
4.10k
                Ok(awaited_action) => awaited_action,
831
3
                Err(
err2
) if is_lost_record(&err
)2
=> {
832
2
                    warn!(
833
                        ?err,
834
                        "Queued operation listed but its record cannot be read; skipping"
835
                    );
836
2
                    record_awaited_action_orphan("sweep");
837
2
                    continue;
838
                }
839
1
                Err(err) => return Err(err).err_tip(|| "In sweep_abandoned_queued_actions"),
840
            };
841
842
            // Only queued actions belong to this sweep, and only once the
843
            // client has been gone longer than it is allowed to be.
844
4.10k
            if !
matches!0
(awaited_action.state().stage, ActionStage::Queued) {
845
0
                continue;
846
4.10k
            }
847
4.10k
            if awaited_action.last_client_keepalive_timestamp() + self.client_action_timeout >= now
848
            {
849
4.09k
                continue;
850
11
            }
851
852
11
            let mut state = awaited_action.state().as_ref().clone();
853
11
            state.stage = ActionStage::Completed(ActionResult {
854
11
                error: Some(make_err!(
855
11
                    Code::DeadlineExceeded,
856
11
                    "Operation timed out {} seconds of having no more clients listening",
857
11
                    self.client_action_timeout.as_secs_f32(),
858
11
                )),
859
11
                ..ActionResult::default()
860
11
            });
861
11
            state.last_transition_timestamp = now;
862
863
11
            let mut new_awaited_action = awaited_action;
864
11
            new_awaited_action.worker_set_state(Arc::new(state), now);
865
            // A conflict means something else is already changing this
866
            // action, which is the outcome this sweep wants anyway. Leave it
867
            // for the next pass rather than fighting for it.
868
11
            match self
869
11
                .action_db
870
11
                .update_awaited_action(new_awaited_action)
871
11
                .await
872
            {
873
11
                Ok(()) => retired += 1,
874
0
                Err(err) if err.code == Code::Aborted => {}
875
0
                Err(err) => {
876
0
                    return Err(err).err_tip(|| "In sweep_abandoned_queued_actions");
877
                }
878
            }
879
        }
880
881
116
        if retired > 0 {
882
5
            warn!(
883
                retired,
884
3
                timeout_secs = self.client_action_timeout.as_secs_f32(),
885
                "Retired queued operations that had no clients listening"
886
            );
887
111
        }
888
116
        record_queue_retired(retired);
889
116
        Ok(retired)
890
117
    }
891
892
    /// See `AwaitedActionDb::exchange_fleet_capabilities`.
893
1.30k
    pub async fn exchange_fleet_capabilities(
894
1.30k
        &self,
895
1.30k
        scheduler_id: &str,
896
1.30k
        local: Vec<PlatformProperties>,
897
1.30k
        ttl: Duration,
898
1.30k
    ) -> Result<Vec<PeerScheduler>, Error> {
899
1.30k
        self.action_db
900
1.30k
            .exchange_fleet_capabilities(scheduler_id, local, ttl)
901
1.30k
            .await
902
1.30k
    }
903
904
34
    async fn inner_fail_queued_operation(
905
34
        &self,
906
34
        operation_id: &OperationId,
907
34
        err: Error,
908
34
    ) -> Result<bool, Error> {
909
34
        let Some(
subscriber33
) = self
910
34
            .action_db
911
34
            .get_by_operation_id(operation_id)
912
34
            .await
913
34
            .err_tip(|| "In SimpleSchedulerStateManager::fail_queued_operation")
?0
914
        else {
915
1
            return Ok(false);
916
        };
917
        // The action may be racing an assignment or a client update, so on
918
        // a version conflict reload it and, if it is still queued, try again.
919
33
        let mut last_err = None;
920
33
        for attempt in 1..=MAX_UPDATE_RETRIES {
921
33
            if attempt > 1 {
922
0
                tokio::time::sleep(version_conflict_backoff(attempt)).await;
923
33
            }
924
33
            let awaited_action = match subscriber.borrow().await {
925
33
                Ok(awaited_action) => awaited_action,
926
                // Store-backed dbs hand out a subscriber for any id and only
927
                // discover the operation is gone on read.
928
0
                Err(err) if err.code == Code::NotFound => return Ok(false),
929
0
                Err(err) => {
930
0
                    return Err(err)
931
0
                        .err_tip(|| "In SimpleSchedulerStateManager::fail_queued_operation");
932
                }
933
            };
934
33
            if !
matches!2
(awaited_action.state().stage, ActionStage::Queued) {
935
2
                return Ok(false);
936
31
            }
937
938
31
            let now = (self.now_fn)().now();
939
31
            let mut state = awaited_action.state().as_ref().clone();
940
31
            state.stage = ActionStage::Completed(ActionResult {
941
31
                error: Some(err.clone()),
942
31
                ..ActionResult::default()
943
31
            });
944
31
            state.last_transition_timestamp = now;
945
946
31
            let mut new_awaited_action = awaited_action;
947
31
            new_awaited_action.worker_set_state(Arc::new(state), now);
948
31
            match self
949
31
                .action_db
950
31
                .update_awaited_action(new_awaited_action.clone())
951
31
                .await
952
            {
953
                Ok(()) => {
954
31
                    info!(%operation_id, ?err, "Failed a queued action no worker can run");
955
31
                    record_execution_metrics(&new_awaited_action, false);
956
31
                    return Ok(true);
957
                }
958
0
                Err(err) if err.code == Code::Aborted => last_err = Some(err),
959
0
                Err(err) => {
960
0
                    return Err(err)
961
0
                        .err_tip(|| "In SimpleSchedulerStateManager::fail_queued_operation");
962
                }
963
            }
964
        }
965
0
        warn!(
966
            %operation_id,
967
            ?last_err,
968
            "Could not fail an unsatisfiable action after repeated version conflicts, will try again on the next matching pass"
969
        );
970
0
        Ok(false)
971
34
    }
972
973
13
    pub async fn should_timeout_operation(&self, awaited_action: &AwaitedAction) -> bool {
974
13
        self.timeout_cause(awaited_action).await.is_some()
975
13
    }
976
977
    /// Why the scheduler should time out an executing action, or `None` if
978
    /// it should not.
979
2.73k
    pub async fn timeout_cause(&self, awaited_action: &AwaitedAction) -> Option<TimeoutCause> {
980
2.73k
        if !
matches!76
(awaited_action.state().stage, ActionStage::Executing) {
981
76
            return None;
982
2.65k
        }
983
984
2.65k
        let now = (self.now_fn)().now();
985
986
        // Honor the per-action `Action.timeout` from the RBE protocol as a
987
        // backend wall-clock deadline, in every liveness case below. The
988
        // worker enforces the same timeout, but its clock starts when the
989
        // command starts, after the inputs are fetched, which the scheduler
990
        // cannot observe. Enforcing it at `Action.timeout` from assignment
991
        // would pre-empt every live worker's own DEADLINE_EXCEEDED result, so
992
        // allow `no_event_action_timeout` more, measured from the later of the
993
        // assignment and the worker's last update: a live worker then reports
994
        // first, and a wedged one (hung input fetch, unkillable child, broken
995
        // external timeout, orphan) is still ended here.
996
2.65k
        let action_timeout = awaited_action.action_info().timeout;
997
2.65k
        if action_timeout > Duration::ZERO {
998
2.63k
            let anchor = awaited_action
999
2.63k
                .state()
1000
2.63k
                .last_transition_timestamp
1001
2.63k
                .max(awaited_action.last_worker_updated_timestamp());
1002
2.63k
            if let Ok(elapsed) = now.duration_since(anchor)
1003
2.63k
                && elapsed > action_timeout.saturating_add(self.no_event_action_timeout)
1004
            {
1005
5
                return Some(TimeoutCause::ActionTimeout {
1006
5
                    timeout: action_timeout,
1007
5
                    grace: self.no_event_action_timeout,
1008
5
                });
1009
2.63k
            }
1010
16
        }
1011
1012
2.65k
        let liveness = match (&self.worker_registry, awaited_action.worker_id()) {
1013
2.64k
            (Some(worker_registry), Some(worker_id)) => {
1014
2.64k
                worker_registry
1015
2.64k
                    .check_liveness(worker_id, self.no_event_action_timeout, now)
1016
2.64k
                    .await
1017
            }
1018
            // No registry, or not assigned yet: fall back to the
1019
            // timestamp-only check.
1020
2
            _ => WorkerLiveness::Stale,
1021
        };
1022
1023
2.65k
        let last_update = awaited_action.last_worker_updated_timestamp();
1024
2.65k
        let silent_for = |ceiling: Duration| 
{249
1025
249
            now.duration_since(last_update)
1026
249
                .is_ok_and(|elapsed| elapsed > ceiling)
1027
249
                .then_some(TimeoutCause::NoWorkerUpdate(ceiling))
1028
249
        };
1029
1030
2.65k
        match liveness {
1031
            // Ours and heartbeating: besides Action.timeout above, only the
1032
            // stuck-but-alive ceiling applies, and disabling that leaves an
1033
            // action with no Action.timeout no ceiling on a live worker.
1034
            WorkerLiveness::Alive => {
1035
2.41k
                if self.max_executing_timeout > Duration::ZERO {
1036
10
                    silent_for(self.max_executing_timeout)
1037
                } else {
1038
2.40k
                    None
1039
                }
1040
            }
1041
1042
            // Usually a peer's healthy worker, so worker_timeout_s must not
1043
            // apply. It can also be an orphan no instance will ever reap, and
1044
            // max_action_executing_timeout_s defaults to disabled, so fall
1045
            // back to a ceiling rather than never timing out.
1046
            WorkerLiveness::Unknown => {
1047
216
                if self.max_executing_timeout > Duration::ZERO {
1048
3
                    silent_for(self.max_executing_timeout)
1049
                } else {
1050
213
                    silent_for(ORPHANED_ACTION_TIMEOUT)
1051
                }
1052
            }
1053
1054
            // Registered here and gone quiet: ours, and it looks dead.
1055
23
            WorkerLiveness::Stale => silent_for(self.no_event_action_timeout),
1056
        }
1057
2.73k
    }
1058
1059
6.67M
    async fn apply_filter_predicate(
1060
6.67M
        &self,
1061
6.67M
        awaited_action: &AwaitedAction,
1062
6.67M
        subscriber: &T::Subscriber,
1063
6.67M
        filter: &OperationFilter,
1064
6.67M
    ) -> bool {
1065
        // Note: The caller must filter `client_operation_id`.
1066
1067
6.67M
        let mut maybe_reloaded_awaited_action: Option<AwaitedAction> = None;
1068
6.67M
        let now = (self.now_fn)().now();
1069
1070
        // Check if client has timed out
1071
6.67M
        if awaited_action.last_client_keepalive_timestamp() + self.client_action_timeout < now {
1072
            // This may change if the version is out of date.
1073
2.49k
            let mut timed_out = true;
1074
2.49k
            if !awaited_action.state().stage.is_finished() {
1075
2.49k
                let mut state = awaited_action.state().as_ref().clone();
1076
2.49k
                warn!(operation_id = ?
awaited_action7
.
operation_id7
(), timeout_secs =
self.client_action_timeout7
.
as_secs_f327
(), "Operation timed out having no more clients listening");
1077
2.49k
                state.stage = ActionStage::Completed(ActionResult {
1078
2.49k
                    error: Some(make_err!(
1079
2.49k
                        Code::DeadlineExceeded,
1080
2.49k
                        "Operation timed out {} seconds of having no more clients listening",
1081
2.49k
                        self.client_action_timeout.as_secs_f32(),
1082
2.49k
                    )),
1083
2.49k
                    ..ActionResult::default()
1084
2.49k
                });
1085
2.49k
                state.last_transition_timestamp = now;
1086
2.49k
                let state = Arc::new(state);
1087
                // We may be competing with an client timestamp update, so try
1088
                // this a few times.
1089
2.49k
                for attempt in 
1..=MAX_UPDATE_RETRIES2.49k
{
1090
2.49k
                    let mut new_awaited_action = match &maybe_reloaded_awaited_action {
1091
2.49k
                        None => awaited_action.clone(),
1092
4
                        Some(reloaded_awaited_action) => reloaded_awaited_action.clone(),
1093
                    };
1094
2.49k
                    new_awaited_action.worker_set_state(state.clone(), (self.now_fn)().now());
1095
2.49k
                    let 
err5
= match self
1096
2.49k
                        .action_db
1097
2.49k
                        .update_awaited_action(new_awaited_action)
1098
2.49k
                        .await
1099
                    {
1100
2.49k
                        Ok(()) => break,
1101
5
                        Err(err) => err,
1102
                    };
1103
                    // Reload from the database if the action was outdated.
1104
5
                    let maybe_awaited_action =
1105
5
                        if attempt == MAX_UPDATE_RETRIES || 
err.code != Code::Aborted4
{
1106
1
                            None
1107
                        } else {
1108
4
                            subscriber.borrow().await.ok()
1109
                        };
1110
5
                    if let Some(
reloaded_awaited_action4
) = maybe_awaited_action {
1111
4
                        maybe_reloaded_awaited_action = Some(reloaded_awaited_action);
1112
4
                    } else {
1113
1
                        warn!(
1114
                            "Failed to update action to timed out state after client keepalive timeout. This is ok if multiple schedulers tried to set the state at the same time: {err}",
1115
                        );
1116
1
                        break;
1117
                    }
1118
                    // Re-check the predicate after reload.
1119
4
                    if maybe_reloaded_awaited_action
1120
4
                        .as_ref()
1121
4
                        .is_some_and(|awaited_action| {
1122
4
                            awaited_action.last_client_keepalive_timestamp()
1123
4
                                + self.client_action_timeout
1124
4
                                >= (self.now_fn)().now()
1125
4
                        })
1126
                    {
1127
0
                        timed_out = false;
1128
0
                        break;
1129
4
                    } else if maybe_reloaded_awaited_action
1130
4
                        .as_ref()
1131
4
                        .is_some_and(|awaited_action| awaited_action.state().stage.is_finished())
1132
                    {
1133
0
                        break;
1134
4
                    }
1135
                }
1136
0
            }
1137
2.49k
            if timed_out {
1138
2.49k
                return false;
1139
0
            }
1140
6.66M
        }
1141
        // If the action was reloaded, then use that for the rest of the checks
1142
        // instead of the input parameter.
1143
6.66M
        let awaited_action = maybe_reloaded_awaited_action
1144
6.66M
            .as_ref()
1145
6.66M
            .unwrap_or(awaited_action);
1146
1147
6.66M
        if let Some(
operation_id0
) = &filter.operation_id
1148
0
            && operation_id != awaited_action.operation_id()
1149
        {
1150
0
            return false;
1151
6.66M
        }
1152
1153
6.66M
        if filter.worker_id.is_some() && 
filter.worker_id.as_ref()0
!= awaited_action.worker_id() {
1154
0
            return false;
1155
6.66M
        }
1156
1157
        {
1158
6.66M
            if let Some(
filter_unique_key0
) = &filter.unique_key {
1159
0
                match &awaited_action.action_info().unique_qualifier {
1160
0
                    ActionUniqueQualifier::Cacheable(unique_key) => {
1161
0
                        if filter_unique_key != unique_key {
1162
0
                            return false;
1163
0
                        }
1164
                    }
1165
                    ActionUniqueQualifier::Uncacheable(_) => {
1166
0
                        return false;
1167
                    }
1168
                }
1169
6.66M
            }
1170
6.66M
            if let Some(
action_digest0
) = filter.action_digest
1171
0
                && action_digest != awaited_action.action_info().digest()
1172
            {
1173
0
                return false;
1174
6.66M
            }
1175
        }
1176
1177
        {
1178
6.66M
            let last_worker_update_timestamp = awaited_action.last_worker_updated_timestamp();
1179
6.66M
            if let Some(
worker_update_before0
) = filter.worker_update_before
1180
0
                && worker_update_before < last_worker_update_timestamp
1181
            {
1182
0
                return false;
1183
6.66M
            }
1184
6.66M
            if let Some(
completed_before0
) = filter.completed_before
1185
0
                && awaited_action.state().stage.is_finished()
1186
0
                && completed_before < last_worker_update_timestamp
1187
            {
1188
0
                return false;
1189
6.66M
            }
1190
6.66M
            if filter.stages != OperationStageFlags::Any {
1191
6.66M
                let stage_flag = match awaited_action.state().stage {
1192
0
                    ActionStage::Unknown => OperationStageFlags::Any,
1193
0
                    ActionStage::CacheCheck => OperationStageFlags::CacheCheck,
1194
6.66M
                    ActionStage::Queued => OperationStageFlags::Queued,
1195
8
                    ActionStage::Executing => OperationStageFlags::Executing,
1196
                    ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => {
1197
0
                        OperationStageFlags::Completed
1198
                    }
1199
                };
1200
6.66M
                if !filter.stages.intersects(stage_flag) {
1201
0
                    return false;
1202
6.66M
                }
1203
211
            }
1204
        }
1205
1206
6.66M
        true
1207
6.67M
    }
1208
1209
    /// Let the scheduler know that an operation has timed out from
1210
    /// the client side (ie: worker has not updated in a while).
1211
    /// Returns whether the operation was timed out; false when the check
1212
    /// under the lock finds it no longer needs to be.
1213
12
    async fn timeout_operation_id(&self, operation_id: &OperationId) -> Result<bool, Error> {
1214
        // Ensure that only one timeout operation is running at a time.
1215
        // Failing to do this could result in the same operation being
1216
        // timed out multiple times at the same time.
1217
        // Note: We could implement this on a per-operation_id basis, but it is quite
1218
        // complex to manage the locks.
1219
12
        let _lock = self.timeout_operation_mux.lock().await;
1220
1221
12
        let awaited_action_subscriber = self
1222
12
            .action_db
1223
12
            .get_by_operation_id(operation_id)
1224
12
            .await
1225
12
            .err_tip(|| "In SimpleSchedulerStateManager::timeout_operation_id")
?0
1226
12
            .err_tip(|| 
{0
1227
0
                format!("Operation id {operation_id} does not exist in SimpleSchedulerStateManager::timeout_operation_id")
1228
0
            })?;
1229
1230
12
        let awaited_action = awaited_action_subscriber
1231
12
            .borrow()
1232
12
            .await
1233
12
            .err_tip(|| "In SimpleSchedulerStateManager::timeout_operation_id")
?0
;
1234
1235
        // Re-check under the lock against freshly loaded state, and delegate
1236
        // rather than re-deriving the rule: the two copies had drifted.
1237
12
        let Some(
cause8
) = self.timeout_cause(&awaited_action).await else {
1238
4
            trace!(
1239
                %operation_id,
1240
0
                worker_id = ?awaited_action.worker_id(),
1241
                "Operation no longer needs timing out, skipping"
1242
            );
1243
4
            return Ok(false);
1244
        };
1245
1246
8
        warn!(
1247
            %operation_id,
1248
2
            worker_id = ?awaited_action.worker_id(),
1249
            %cause,
1250
            "Timing out operation"
1251
        );
1252
1253
8
        self.assign_operation(
1254
8
            operation_id,
1255
8
            Err(make_err!(Code::DeadlineExceeded, "Operation {cause}")),
1256
8
        )
1257
8
        .await
1258
8
        .map(|()| true)
1259
12
    }
1260
1261
21.9k
    async fn inner_update_operation(
1262
21.9k
        &self,
1263
21.9k
        operation_id: &OperationId,
1264
21.9k
        maybe_worker_id: Option<&WorkerId>,
1265
21.9k
        worker_node: Option<&str>,
1266
21.9k
        update: UpdateOperationType,
1267
21.9k
    ) -> Result<(), Error> {
1268
21.9k
        let update_type_str = match &update {
1269
0
            UpdateOperationType::KeepAlive => "KeepAlive",
1270
21.1k
            UpdateOperationType::UpdateWithActionStage(stage) => match stage {
1271
0
                ActionStage::Queued => "Stage:Queued",
1272
11.0k
                ActionStage::Executing => "Stage:Executing",
1273
10.1k
                ActionStage::Completed(_) => "Stage:Completed",
1274
0
                ActionStage::CompletedFromCache(_) => "Stage:CompletedFromCache",
1275
0
                ActionStage::CacheCheck => "Stage:CacheCheck",
1276
0
                ActionStage::Unknown => "Stage:Unknown",
1277
            },
1278
283
            UpdateOperationType::UpdateWithError(_) => "Error",
1279
445
            UpdateOperationType::UpdateWithDisconnect(_) => "Disconnect",
1280
1
            UpdateOperationType::ExecutionComplete => "ExecutionComplete",
1281
12
            UpdateOperationType::UpdateWithDecline(_) => "Decline",
1282
15
            UpdateOperationType::UpdateWithEscalation(_) => "Escalation",
1283
        };
1284
1285
21.9k
        debug!(
1286
            %operation_id,
1287
            ?maybe_worker_id,
1288
            update_type = %update_type_str,
1289
            "inner_update_operation START"
1290
        );
1291
1292
21.9k
        let mut last_err = None;
1293
21.9k
        let mut retry_count = 0;
1294
21.9k
        for _ in 0..MAX_UPDATE_RETRIES {
1295
21.9k
            retry_count += 1;
1296
21.9k
            if retry_count > 1 {
1297
20
                let delay = version_conflict_backoff(retry_count);
1298
1299
20
                warn!(
1300
                    %operation_id,
1301
                    ?maybe_worker_id,
1302
                    retry_count,
1303
6
                    delay_ms = delay.as_millis(),
1304
                    update_type = %update_type_str,
1305
                    "Retrying operation update after a version conflict or a transient store error (with backoff)"
1306
                );
1307
1308
20
                tokio::time::sleep(delay).await;
1309
21.9k
            }
1310
21.9k
            let maybe_awaited_action_subscriber = self
1311
21.9k
                .action_db
1312
21.9k
                .get_by_operation_id(operation_id)
1313
21.9k
                .await
1314
21.9k
                .err_tip(|| "In SimpleSchedulerStateManager::update_operation")
?0
;
1315
21.9k
            let Some(
awaited_action_subscriber21.9k
) = maybe_awaited_action_subscriber else {
1316
                // No action found. It is ok if the action was not found. It
1317
                // probably means that the action was dropped, but worker was
1318
                // still processing it.
1319
2
                warn!(
1320
                    %operation_id,
1321
                    "Unable to update action due to it being missing, probably dropped"
1322
                );
1323
2
                return Ok(());
1324
            };
1325
1326
21.9k
            let 
mut awaited_action21.9k
= awaited_action_subscriber
1327
21.9k
                .borrow()
1328
21.9k
                .await
1329
21.9k
                .err_tip(|| "In SimpleSchedulerStateManager::update_operation")
?7
;
1330
1331
            // Make sure the worker id matches the awaited action worker id.
1332
            // This might happen if the worker sending the update is not the
1333
            // worker that was assigned.
1334
21.9k
            if awaited_action.worker_id().is_some()
1335
10.8k
                && maybe_worker_id.is_some()
1336
10.8k
                && maybe_worker_id != awaited_action.worker_id()
1337
            {
1338
                // If another worker is already assigned to the action, another
1339
                // worker probably picked up the action. We should not update the
1340
                // action in this case and abort this operation.
1341
2
                let err = make_err!(
1342
2
                    Code::Aborted,
1343
                    "Worker ids do not match - {:?} != {:?} for {:?}",
1344
                    maybe_worker_id,
1345
2
                    awaited_action.worker_id(),
1346
                    awaited_action,
1347
                );
1348
2
                info!(
1349
                    "Worker ids do not match - {:?} != {:?} for {:?}. This is probably due to another worker picking up the action.",
1350
                    maybe_worker_id,
1351
2
                    awaited_action.worker_id(),
1352
                    awaited_action,
1353
                );
1354
2
                return Err(err);
1355
21.9k
            }
1356
1357
            // Make sure we don't update an action that is already completed.
1358
21.9k
            if awaited_action.state().stage.is_finished() {
1359
6
                match &update {
1360
                    UpdateOperationType::UpdateWithDisconnect(_)
1361
                    | UpdateOperationType::KeepAlive => {
1362
                        // No need to error a keep-alive when it's completed, it's just
1363
                        // unnecessary log noise.
1364
0
                        return Ok(());
1365
                    }
1366
                    _ => {
1367
6
                        return Err(make_err!(
1368
6
                            Code::Internal,
1369
6
                            "Action {operation_id} is already completed with state {:?} - maybe_worker_id: {:?}",
1370
6
                            awaited_action.state().stage,
1371
6
                            maybe_worker_id,
1372
6
                        ));
1373
                    }
1374
                }
1375
21.9k
            }
1376
1377
            // A disconnect for an operation no worker holds any more (the
1378
            // executing sweep requeued it) must not spend a worker loss.
1379
21.9k
            if 
matches!21.4k
(update, UpdateOperationType::UpdateWithDisconnect(_))
1380
440
                && awaited_action.worker_id().is_none()
1381
            {
1382
0
                return Ok(());
1383
21.9k
            }
1384
1385
21.9k
            let mut is_retry = false;
1386
21.9k
            let 
stage21.9k
= match &update {
1387
                UpdateOperationType::KeepAlive => {
1388
0
                    awaited_action.worker_keep_alive((self.now_fn)().now());
1389
0
                    match self
1390
0
                        .action_db
1391
0
                        .update_awaited_action(awaited_action)
1392
0
                        .await
1393
0
                        .err_tip(|| "Failed to send KeepAlive in SimpleSchedulerStateManager::update_operation") {
1394
                        // Try again if there was a version mismatch.
1395
0
                        Err(err) if err.code == Code::Aborted => {
1396
0
                            last_err = Some(err);
1397
0
                            continue;
1398
                        }
1399
0
                        result => return result,
1400
                    }
1401
                }
1402
21.1k
                UpdateOperationType::UpdateWithActionStage(stage) => {
1403
21.1k
                    if stage == &ActionStage::Executing
1404
11.0k
                        && awaited_action.state().stage == ActionStage::Executing
1405
                    {
1406
7
                        warn!(state = ?awaited_action.state(), "Action already assigned");
1407
7
                        return Err(make_err!(Code::Aborted, "Action already assigned"));
1408
21.1k
                    }
1409
                    // A command that ran past its timeout on a worker comes
1410
                    // back as a result. Once, it is queued again for another
1411
                    // worker: a worker whose host has gone slow times out
1412
                    // everything it is given, and only a second worker tells
1413
                    // that from an action that is slow by itself. A timeout
1414
                    // there, or with no budget, is final as before.
1415
10.1k
                    if let (ActionStage::Completed(result), Some(worker_id)) =
1416
21.1k
                        (stage, maybe_worker_id)
1417
10.1k
                        && result
1418
10.1k
                            .error
1419
10.1k
                            .as_ref()
1420
10.1k
                            .is_some_and(|err| 
err.code31
==
Code::DeadlineExceeded31
)
1421
28
                        && awaited_action.timeouts < self.max_timeout_retries
1422
                    {
1423
5
                        awaited_action.timeouts += 1;
1424
5
                        awaited_action.avoid_worker(worker_id.clone());
1425
5
                        if let Some(
node2
) = worker_node {
1426
2
                            awaited_action.avoid_node(node);
1427
3
                        }
1428
5
                        warn!(
1429
                            %operation_id,
1430
                            %worker_id,
1431
                            timeouts = awaited_action.timeouts,
1432
                            max_timeout_retries = self.max_timeout_retries,
1433
                            "Command timed out on the worker; queued again for another worker"
1434
                        );
1435
5
                        is_retry = true;
1436
5
                        ActionStage::Queued
1437
                    } else {
1438
21.1k
                        stage.clone()
1439
                    }
1440
                }
1441
282
                UpdateOperationType::UpdateWithError(err) => {
1442
                    // Don't count a backpressure failure as an attempt for an action.
1443
282
                    let due_to_backpressure = err.code == Code::ResourceExhausted;
1444
                    // Nor a store the worker could not reach once its retries
1445
                    // ran out: a CAS pod being replaced is not the action's
1446
                    // doing. The worker marks those; a bare Unavailable is
1447
                    // also what this scheduler raises for a lost result, which
1448
                    // must keep spending max_job_retries. A store that never
1449
                    // comes back is bounded like a lost worker.
1450
282
                    let store_unreachable =
1451
282
                        err.message_string().contains(STORE_UNREACHABLE_ERROR_TIP);
1452
                    // A result that never reached the scheduler is not the
1453
                    // worker's doing either: it ran the action. The retry
1454
                    // spends max_job_retries, as before, but may go straight
1455
                    // back to that worker; avoiding it would make a lone
1456
                    // worker wait out the fallback for its own lost result.
1457
282
                    let lost_result = err.message_string().contains(LOST_RESULT_ERROR_TIP);
1458
282
                    if store_unreachable {
1459
3
                        awaited_action.store_losses += 1;
1460
279
                    } else if !due_to_backpressure {
1461
278
                        awaited_action.attempts += 1;
1462
278
                    
}1
1463
1464
282
                    if store_unreachable
1465
3
                        && awaited_action.store_losses > self.max_worker_loss_retries
1466
                    {
1467
1
                        ActionStage::Completed(ActionResult {
1468
1
                            execution_metadata: ExecutionMetadata {
1469
1
                                worker: maybe_worker_id.map_or_else(String::default, ToString::to_string),
1470
1
                                ..ExecutionMetadata::default()
1471
1
                            },
1472
1
                            error: Some(make_err!(
1473
1
                                Code::FailedPrecondition,
1474
1
                                "The worker running this action could not reach its store {} times, more than max_worker_loss_retries ({}); the store this action's inputs live on is down, not the action. {}",
1475
1
                                awaited_action.store_losses,
1476
1
                                self.max_worker_loss_retries,
1477
1
                                format!("for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"),
1478
1
                            ).merge(err.clone())),
1479
1
                            ..ActionResult::default()
1480
1
                        })
1481
281
                    } else if awaited_action.attempts > self.max_job_retries {
1482
                        // FailedPrecondition, not Internal: Bazel resubmits an
1483
                        // Internal execute failure as a fresh operation with a
1484
                        // fresh attempt counter, so the cap never reached the
1485
                        // client and a crashing action cycled through the pool
1486
                        // for as long as the build ran.
1487
3
                        ActionStage::Completed(ActionResult {
1488
3
                            execution_metadata: ExecutionMetadata {
1489
3
                                worker: maybe_worker_id.map_or_else(String::default, ToString::to_string),
1490
3
                                ..ExecutionMetadata::default()
1491
3
                            },
1492
3
                            error: Some(make_err!(
1493
3
                                Code::FailedPrecondition,
1494
3
                                "Job cancelled because it attempted to execute too many times {} > {} times {}",
1495
3
                                awaited_action.attempts,
1496
3
                                self.max_job_retries,
1497
3
                                format!("for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"),
1498
3
                            ).merge(err.clone())),
1499
3
                            ..ActionResult::default()
1500
3
                        })
1501
                    } else {
1502
278
                        if let Some(
worker_id270
) = maybe_worker_id
1503
270
                            && !store_unreachable
1504
268
                            && !due_to_backpressure
1505
267
                            && !lost_result
1506
                        {
1507
                            // The action failed on this worker; a retry
1508
                            // prefers another while one can take it.
1509
265
                            awaited_action.avoid_worker(worker_id.clone());
1510
265
                            if let Some(
node0
) = worker_node {
1511
0
                                awaited_action.avoid_node(node);
1512
265
                            }
1513
13
                        }
1514
278
                        is_retry = true;
1515
278
                        ActionStage::Queued
1516
                    }
1517
                }
1518
440
                UpdateOperationType::UpdateWithDisconnect(loss) => {
1519
                    // A lost worker (OOM-killed as a whole, pod evicted,
1520
                    // network drop, timed out) is usually not this action's
1521
                    // doing, so it has its own budget rather than spending
1522
                    // the action's `max_job_retries`. The budget is what
1523
                    // stops an action that takes a worker down every time
1524
                    // it runs from doing so forever.
1525
440
                    awaited_action.worker_losses += 1;
1526
1527
440
                    if awaited_action.worker_losses > self.max_worker_loss_retries {
1528
3
                        ActionStage::Completed(ActionResult {
1529
3
                            execution_metadata: ExecutionMetadata {
1530
3
                                worker: maybe_worker_id
1531
3
                                    .map_or_else(String::default, ToString::to_string),
1532
3
                                ..ExecutionMetadata::default()
1533
3
                            },
1534
3
                            error: Some(make_err!(
1535
3
                                Code::FailedPrecondition,
1536
3
                                "The worker running this action was lost {} times, more than max_worker_loss_retries ({}); the action is likely what takes the worker down (OOM-killed pod, eviction). Give it a memory reservation or raise the pool's memory limit before retrying. {}",
1537
3
                                awaited_action.worker_losses,
1538
3
                                self.max_worker_loss_retries,
1539
3
                                format!(
1540
3
                                    "for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"
1541
3
                                ),
1542
3
                            )),
1543
3
                            ..ActionResult::default()
1544
3
                        })
1545
                    } else {
1546
                        // A loss that may have been this action's memory
1547
                        // (RFC-0018): escalate when the worker saw it over
1548
                        // its reservation, or when this is not the first
1549
                        // worker it took down, while the escalation budget
1550
                        // lasts. Past it, the loss budget ends the action.
1551
437
                        if let Some(
loss21
) = loss
1552
21
                            && (loss.sampled_over
1553
13
                                || (self.escalate_after_worker_losses > 0
1554
9
                                    && awaited_action.worker_losses
1555
9
                                        >= self.escalate_after_worker_losses))
1556
12
                            && (self.max_escalations == 0
1557
6
                                || awaited_action.escalations < self.max_escalations)
1558
                        {
1559
9
                            awaited_action.escalations += 1;
1560
9
                            warn!(
1561
                                %operation_id,
1562
                                ?maybe_worker_id,
1563
                                worker_losses = awaited_action.worker_losses,
1564
                                sampled_over = loss.sampled_over,
1565
                                property = %loss.escalation.property,
1566
                                escalated = loss.escalation.value,
1567
                                "Worker lost under this action; requeuing with a larger reservation"
1568
                            );
1569
9
                            raise_reservation(&mut awaited_action, &loss.escalation);
1570
9
                            record_dispatch_requeue("loss_escalation");
1571
428
                        }
1572
437
                        is_retry = true;
1573
437
                        ActionStage::Queued
1574
                    }
1575
                }
1576
15
                UpdateOperationType::UpdateWithEscalation(escalation) => {
1577
                    // The scheduler's own sizing decision, not a failure of
1578
                    // the action: it has its own budget and does not spend
1579
                    // `max_job_retries`. The fleet's ceiling bounds it
1580
                    // anyway; the budget catches a fleet whose ceiling is
1581
                    // far above where the action started.
1582
15
                    awaited_action.escalations += 1;
1583
15
                    if self.max_escalations > 0 && 
awaited_action.escalations > self.max_escalations5
1584
                    {
1585
1
                        ActionStage::Completed(ActionResult {
1586
1
                            execution_metadata: ExecutionMetadata {
1587
1
                                worker: maybe_worker_id
1588
1
                                    .map_or_else(String::default, ToString::to_string),
1589
1
                                ..ExecutionMetadata::default()
1590
1
                            },
1591
1
                            error: Some(
1592
1
                                make_err!(
1593
1
                                    Code::FailedPrecondition,
1594
1
                                    "Job cancelled after being killed for memory at every reservation tried ({} escalations, more than max_steps {}); the last reservation was {} {} {}",
1595
1
                                    awaited_action.escalations,
1596
1
                                    self.max_escalations,
1597
1
                                    escalation.value,
1598
1
                                    escalation.property,
1599
1
                                    format!("for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"),
1600
1
                                )
1601
1
                                .merge(escalation.reason.clone()),
1602
1
                            ),
1603
1
                            ..ActionResult::default()
1604
1
                        })
1605
                    } else {
1606
                        // The same action, asking for more: the next
1607
                        // dispatch carries the raised reservation and the
1608
                        // worker holds it to that. The last step also asks
1609
                        // for the worker's whole CPU, so it runs alone.
1610
14
                        raise_reservation(&mut awaited_action, escalation);
1611
14
                        is_retry = true;
1612
14
                        ActionStage::Queued
1613
                    }
1614
                }
1615
                // We shouldn't get here, but we just ignore it if we do.
1616
12
                UpdateOperationType::UpdateWithDecline(decline) => {
1617
                    // Nothing ran, so nothing is charged against the retry
1618
                    // cap and the retry metric does not move; the action
1619
                    // goes back where it was.
1620
12
                    debug!(reason = %decline.reason, "Dispatch declined, requeuing untried");
1621
12
                    ActionStage::Queued
1622
                }
1623
                UpdateOperationType::ExecutionComplete => {
1624
0
                    warn!("inner_update_operation got an ExecutionComplete, that's unexpected.");
1625
0
                    return Ok(());
1626
                }
1627
            };
1628
21.9k
            let now = (self.now_fn)().now();
1629
21.9k
            if 
matches!21.1k
(stage, ActionStage::Queued) {
1630
746
                // If the action is queued, we need to unset the worker id regardless of
1631
746
                // which worker sent the update.
1632
746
                awaited_action.set_worker_id(None, now);
1633
21.1k
            } else {
1634
21.1k
                awaited_action.set_worker_id(maybe_worker_id.cloned(), now);
1635
21.1k
            }
1636
21.9k
            awaited_action.worker_set_state(
1637
21.9k
                Arc::new(ActionState {
1638
21.9k
                    stage,
1639
21.9k
                    // Client id is not known here, it is the responsibility of
1640
21.9k
                    // the the subscriber impl to replace this with the
1641
21.9k
                    // correct client id.
1642
21.9k
                    client_operation_id: operation_id.clone(),
1643
21.9k
                    action_digest: awaited_action.action_info().digest(),
1644
21.9k
                    last_transition_timestamp: now,
1645
21.9k
                }),
1646
21.9k
                now,
1647
            );
1648
1649
21.9k
            let update_action_result = self
1650
21.9k
                .action_db
1651
21.9k
                .update_awaited_action(awaited_action.clone())
1652
21.9k
                .await
1653
21.9k
                .err_tip(|| "In SimpleSchedulerStateManager::update_operation");
1654
21.9k
            if let Err(
err21
) = update_action_result {
1655
                // Aborted says the record moved under us: re-read and apply
1656
                // again. A store that is unreachable, slow, full or answering
1657
                // with a server error is transient in the same way, and the
1658
                // cost of not retrying is not symmetric: the worker has
1659
                // already reported this result and will not report it again,
1660
                // so a dropped write parks the action until the executing
1661
                // ceiling (found by the simulation harness, 2026-11-04). The
1662
                // same bounded budget and backoff cover both; a store that
1663
                // stays down fails the update as before.
1664
21
                if is_transient_store_error(err.code) {
1665
20
                    debug!(
1666
                        %operation_id,
1667
                        retry_count,
1668
                        update_type = %update_type_str,
1669
                        code = ?err.code,
1670
                        "Store write did not land (transient), will retry"
1671
                    );
1672
20
                    last_err = Some(err);
1673
20
                    continue;
1674
1
                }
1675
1
                warn!(
1676
                    %operation_id,
1677
                    update_type = %update_type_str,
1678
                    ?err,
1679
                    "inner_update_operation FAILED (non-retryable)"
1680
                );
1681
1
                return Err(err);
1682
21.8k
            }
1683
1684
21.8k
            record_execution_metrics(&awaited_action, is_retry);
1685
1686
21.8k
            debug!(
1687
                %operation_id,
1688
                retry_count,
1689
                update_type = %update_type_str,
1690
                "inner_update_operation SUCCESS"
1691
            );
1692
21.8k
            return Ok(());
1693
        }
1694
1695
0
        warn!(
1696
            %operation_id,
1697
            update_type = %update_type_str,
1698
            retry_count = MAX_UPDATE_RETRIES,
1699
            "inner_update_operation EXHAUSTED all retries"
1700
        );
1701
0
        Err(last_err.unwrap_or_else(|| {
1702
0
            make_err!(
1703
0
                Code::Internal,
1704
                "Failed to update action after {} retries with no error set",
1705
                MAX_UPDATE_RETRIES,
1706
            )
1707
0
        }))
1708
21.9k
    }
1709
1710
16.5k
    async fn inner_add_operation(
1711
16.5k
        &self,
1712
16.5k
        new_client_operation_id: OperationId,
1713
16.5k
        action_info: Arc<ActionInfo>,
1714
16.5k
    ) -> Result<T::Subscriber, Error> {
1715
16.5k
        self.action_db
1716
16.5k
            .add_action(
1717
16.5k
                new_client_operation_id,
1718
16.5k
                action_info,
1719
16.5k
                self.no_event_action_timeout,
1720
16.5k
            )
1721
16.5k
            .await
1722
16.5k
            .err_tip(|| "In SimpleSchedulerStateManager::add_operation")
1723
16.5k
    }
1724
1725
37.4k
    async fn inner_filter_operations<'a, F>(
1726
37.4k
        &'a self,
1727
37.4k
        filter: OperationFilter,
1728
37.4k
        to_action_state_result: F,
1729
37.4k
    ) -> Result<ActionStateResultStream<'a>, Error>
1730
37.4k
    where
1731
37.4k
        F: Fn(T::Subscriber) -> Box<dyn ActionStateResult> + Send + Sync + 'a,
1732
37.4k
    {
1733
37.0k
        const fn sorted_awaited_action_state_for_flags(
1734
37.0k
            stage: OperationStageFlags,
1735
37.0k
        ) -> Option<SortedAwaitedActionState> {
1736
37.0k
            match stage {
1737
0
                OperationStageFlags::CacheCheck => Some(SortedAwaitedActionState::CacheCheck),
1738
36.8k
                OperationStageFlags::Queued => Some(SortedAwaitedActionState::Queued),
1739
184
                OperationStageFlags::Executing => Some(SortedAwaitedActionState::Executing),
1740
0
                OperationStageFlags::Completed => Some(SortedAwaitedActionState::Completed),
1741
8
                _ => None,
1742
            }
1743
37.0k
        }
1744
1745
37.4k
        if let Some(
operation_id0
) = &filter.operation_id {
1746
0
            let maybe_subscriber = self
1747
0
                .action_db
1748
0
                .get_by_operation_id(operation_id)
1749
0
                .await
1750
0
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?;
1751
0
            let Some(subscriber) = maybe_subscriber else {
1752
0
                return Ok(Box::pin(stream::empty()));
1753
            };
1754
0
            let awaited_action = subscriber
1755
0
                .borrow()
1756
0
                .await
1757
0
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?;
1758
0
            if !self
1759
0
                .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1760
0
                .await
1761
            {
1762
0
                return Ok(Box::pin(stream::empty()));
1763
0
            }
1764
0
            return Ok(Box::pin(stream::once(async move {
1765
0
                to_action_state_result(subscriber)
1766
0
            })));
1767
37.4k
        }
1768
37.4k
        if let Some(
client_operation_id413
) = &filter.client_operation_id {
1769
413
            let maybe_subscriber = self
1770
413
                .action_db
1771
413
                .get_awaited_action_by_id(client_operation_id)
1772
413
                .await
1773
413
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1774
413
            let Some(
subscriber207
) = maybe_subscriber else {
1775
206
                return Ok(Box::pin(stream::empty()));
1776
            };
1777
207
            let awaited_action = subscriber
1778
207
                .borrow()
1779
207
                .await
1780
207
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1781
207
            if !self
1782
207
                .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1783
207
                .await
1784
            {
1785
0
                return Ok(Box::pin(stream::empty()));
1786
207
            }
1787
207
            return Ok(Box::pin(stream::once(async move {
1788
207
                to_action_state_result(subscriber)
1789
207
            })));
1790
37.0k
        }
1791
1792
37.0k
        let Some(sorted_awaited_action_state) =
1793
37.0k
            sorted_awaited_action_state_for_flags(filter.stages)
1794
        else {
1795
8
            let 
mut all_items7
:
Vec<_>7
= self
1796
8
                .action_db
1797
8
                .get_all_awaited_actions()
1798
8
                .await
1799
8
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
1800
24
                .
filter_map8
(|listed| async move
{12
listed_or_lost12
(
listed12
) })
1801
10
                .
try_filter_map8
(|awaited_action_subscriber| async move {
1802
10
                    borrow_listed(awaited_action_subscriber).await
1803
20
                })
1804
10
                .
try_filter_map8
(|(subscriber, awaited_action)| {
1805
10
                    let filter = filter.clone();
1806
10
                    async move {
1807
10
                        Ok(self
1808
10
                            .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1809
10
                            .await
1810
10
                            .then_some((subscriber, awaited_action.sort_key())))
1811
10
                    }
1812
10
                })
1813
8
                .try_collect()
1814
8
                .await
1815
8
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?1
;
1816
1817
            #[allow(clippy::unnecessary_sort_by)]
1818
0
            match filter.order_by_priority_direction {
1819
0
                Some(OrderDirection::Asc) => all_items.sort_unstable_by(|(_, a), (_, b)| a.cmp(b)),
1820
0
                Some(OrderDirection::Desc) => all_items.sort_unstable_by(|(_, a), (_, b)| b.cmp(a)),
1821
7
                None => {}
1822
            }
1823
7
            return Ok(Box::pin(stream::iter(
1824
7
                all_items
1825
7
                    .into_iter()
1826
7
                    .map(move |(subscriber, _)| 
to_action_state_result3
(
subscriber3
)),
1827
            )));
1828
        };
1829
1830
37.0k
        let desc = 
matches!517
(
1831
36.5k
            filter.order_by_priority_direction,
1832
            Some(OrderDirection::Desc)
1833
        );
1834
37.0k
        let 
stream37.0k
= self
1835
37.0k
            .action_db
1836
37.0k
            .get_range_of_actions(
1837
37.0k
                sorted_awaited_action_state,
1838
37.0k
                Bound::Unbounded,
1839
37.0k
                Bound::Unbounded,
1840
37.0k
                desc,
1841
37.0k
            )
1842
37.0k
            .await
1843
37.0k
            .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?4
1844
13.3M
            .
filter_map37.0k
(|listed| async move
{6.67M
listed_or_lost6.67M
(
listed6.67M
) })
1845
6.67M
            .
try_filter_map37.0k
(|awaited_action_subscriber| async move {
1846
6.67M
                borrow_listed(awaited_action_subscriber).await
1847
13.3M
            })
1848
6.67M
            .
try_filter_map37.0k
(move |(subscriber, awaited_action)| {
1849
6.67M
                let filter = filter.clone();
1850
6.67M
                async move {
1851
6.67M
                    Ok(self
1852
6.67M
                        .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1853
6.67M
                        .await
1854
6.67M
                        .then_some(subscriber))
1855
6.67M
                }
1856
6.67M
            })
1857
6.66M
            .
map37.0k
(move |result| -> Box<dyn ActionStateResult> {
1858
6.66M
                result.map_or_else(
1859
2
                    |e| -> Box<dyn ActionStateResult> { Box::new(ErrorActionStateResult(e)) },
1860
6.66M
                    |v| -> Box<dyn ActionStateResult> { to_action_state_result(v) },
1861
                )
1862
6.66M
            });
1863
37.0k
        Ok(Box::pin(stream))
1864
37.4k
    }
1865
}
1866
1867
/// A listed entry the store could not even hand over: the row was gone
1868
/// (`NotFound`) or would not decode (`InvalidArgument`), which the Redis
1869
/// listing reports as an error item before any subscriber exists. Skipped
1870
/// and counted like a record that fails to read below. Anything else is the
1871
/// store failing and stays an error for the reader.
1872
6.67M
fn listed_or_lost<S>(listed: Result<S, Error>) -> Option<Result<S, Error>> {
1873
5
    match listed {
1874
5
        Err(
err3
) if is_lost_record(&err
)3
=> {
1875
3
            warn!(
1876
                ?err,
1877
                "Operation listed but its row cannot be read; skipping it in the listing"
1878
            );
1879
3
            record_awaited_action_orphan("listing");
1880
3
            None
1881
        }
1882
6.67M
        listed => Some(listed),
1883
    }
1884
6.67M
}
1885
1886
/// Reads the record behind a listed subscriber. A record the index listed
1887
/// but the store no longer has (it expired between the search and the
1888
/// read, or eviction took it) is skipped and counted, not returned as an
1889
/// error: one such row must not end a listing that the provisioner and the
1890
/// admin API read every few seconds. Any other error is the store failing
1891
/// and is passed on. The sweep and the matcher skip the same way.
1892
6.67M
async fn borrow_listed<S: AwaitedActionSubscriber>(
1893
6.67M
    subscriber: S,
1894
6.67M
) -> Result<Option<(S, AwaitedAction)>, Error> {
1895
6.67M
    match subscriber.borrow().await {
1896
6.67M
        Ok(awaited_action) => Ok(Some((subscriber, awaited_action))),
1897
2
        Err(
err1
) if is_lost_record(&err
)1
=> {
1898
1
            warn!(
1899
                ?err,
1900
                "Operation listed but its record cannot be read; skipping it in the listing"
1901
            );
1902
1
            record_awaited_action_orphan("listing");
1903
1
            Ok(None)
1904
        }
1905
        // The store itself failed; that is the listing's error to report.
1906
1
        Err(err) => Err(err).err_tip(|| "In SimpleSchedulerStateManager::filter_operations"),
1907
    }
1908
6.67M
}
1909
1910
#[async_trait]
1911
impl<T, I, NowFn> ClientStateManager for SimpleSchedulerStateManager<T, I, NowFn>
1912
where
1913
    T: AwaitedActionDb,
1914
    I: InstantWrapper,
1915
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
1916
{
1917
    async fn add_action(
1918
        &self,
1919
        client_operation_id: OperationId,
1920
        action_info: Arc<ActionInfo>,
1921
16.5k
    ) -> Result<Box<dyn ActionStateResult>, Error> {
1922
        let sub = self
1923
            .inner_add_operation(client_operation_id, action_info.clone())
1924
            .await?;
1925
1926
        Ok(Box::new(ClientActionStateResult::new(
1927
            sub,
1928
            self.weak_self.clone(),
1929
            self.no_event_action_timeout,
1930
            self.now_fn.clone(),
1931
        )))
1932
16.5k
    }
1933
1934
    async fn filter_operations<'a>(
1935
        &'a self,
1936
        filter: OperationFilter,
1937
1.10k
    ) -> Result<ActionStateResultStream<'a>, Error> {
1938
718
        self.inner_filter_operations(filter, move |rx| {
1939
718
            Box::new(ClientActionStateResult::new(
1940
718
                rx,
1941
718
                self.weak_self.clone(),
1942
718
                self.no_event_action_timeout,
1943
718
                self.now_fn.clone(),
1944
718
            ))
1945
718
        })
1946
        .await
1947
1.10k
    }
1948
}
1949
1950
#[async_trait]
1951
impl<T, I, NowFn> WorkerStateManager for SimpleSchedulerStateManager<T, I, NowFn>
1952
where
1953
    T: AwaitedActionDb,
1954
    I: InstantWrapper,
1955
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
1956
{
1957
    async fn update_operation(
1958
        &self,
1959
        operation_id: &OperationId,
1960
        worker_id: &WorkerId,
1961
        update: UpdateOperationType,
1962
447
    ) -> Result<(), Error> {
1963
        self.inner_update_operation(operation_id, Some(worker_id), None, update)
1964
            .await
1965
447
    }
1966
1967
    async fn update_operation_on_node(
1968
        &self,
1969
        operation_id: &OperationId,
1970
        worker_id: &WorkerId,
1971
        worker_node: Option<&str>,
1972
        update: UpdateOperationType,
1973
10.4k
    ) -> Result<(), Error> {
1974
        self.inner_update_operation(operation_id, Some(worker_id), worker_node, update)
1975
            .await
1976
10.4k
    }
1977
1978
    async fn is_executing_on_worker(
1979
        &self,
1980
        operation_id: &OperationId,
1981
        worker_id: &WorkerId,
1982
8.89k
    ) -> Result<bool, Error> {
1983
        let Some(subscriber) = self
1984
            .action_db
1985
            .get_by_operation_id(operation_id)
1986
            .await
1987
            .err_tip(|| "In SimpleSchedulerStateManager::is_executing_on_worker")?
1988
        else {
1989
            return Ok(false);
1990
        };
1991
        let awaited_action = match subscriber.borrow().await {
1992
            Ok(awaited_action) => awaited_action,
1993
            // Store-backed dbs hand out a subscriber for any id and only
1994
            // discover the operation is gone on read.
1995
            Err(err) if err.code == Code::NotFound => return Ok(false),
1996
            Err(err) => {
1997
                return Err(err)
1998
                    .err_tip(|| "In SimpleSchedulerStateManager::is_executing_on_worker");
1999
            }
2000
        };
2001
        Ok(
2002
            matches!(awaited_action.state().stage, ActionStage::Executing)
2003
                && awaited_action.worker_id() == Some(worker_id),
2004
        )
2005
8.89k
    }
2006
}
2007
2008
#[async_trait]
2009
impl<T, I, NowFn> MatchingEngineStateManager for SimpleSchedulerStateManager<T, I, NowFn>
2010
where
2011
    T: AwaitedActionDb,
2012
    I: InstantWrapper,
2013
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
2014
{
2015
    async fn filter_operations<'a>(
2016
        &'a self,
2017
        filter: OperationFilter,
2018
36.3k
    ) -> Result<ActionStateResultStream<'a>, Error> {
2019
6.66M
        self.inner_filter_operations(filter, |rx| {
2020
6.66M
            Box::new(MatchingEngineActionStateResult::new(
2021
6.66M
                rx,
2022
6.66M
                self.weak_self.clone(),
2023
6.66M
                self.no_event_action_timeout,
2024
6.66M
                self.now_fn.clone(),
2025
6.66M
            ))
2026
6.66M
        })
2027
        .await
2028
36.3k
    }
2029
2030
    async fn assign_operation(
2031
        &self,
2032
        operation_id: &OperationId,
2033
        worker_id_or_reason_for_unassign: Result<&WorkerId, Error>,
2034
11.0k
    ) -> Result<(), Error> {
2035
        let (maybe_worker_id, update) = match worker_id_or_reason_for_unassign {
2036
            Ok(worker_id) => (
2037
                Some(worker_id),
2038
                UpdateOperationType::UpdateWithActionStage(ActionStage::Executing),
2039
            ),
2040
            Err(err) => (None, UpdateOperationType::UpdateWithError(err)),
2041
        };
2042
        self.inner_update_operation(operation_id, maybe_worker_id, None, update)
2043
            .await
2044
11.0k
    }
2045
2046
    async fn fail_queued_operation(
2047
        &self,
2048
        operation_id: &OperationId,
2049
        err: Error,
2050
34
    ) -> Result<bool, Error> {
2051
        self.inner_fail_queued_operation(operation_id, err).await
2052
34
    }
2053
}