Coverage Report

Created: 2026-09-07 09:23

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 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::ops::Bound;
16
use core::time::Duration;
17
use std::string::ToString;
18
use std::sync::{Arc, Weak};
19
20
use async_lock::Mutex;
21
use async_trait::async_trait;
22
use futures::{StreamExt, TryStreamExt, stream};
23
use nativelink_error::{Code, Error, ResultExt, make_err};
24
use nativelink_metric::MetricsComponent;
25
use nativelink_util::action_messages::{
26
    ActionInfo, ActionResult, ActionStage, ActionState, ActionUniqueQualifier, ExecutionMetadata,
27
    OperationId, WorkerId,
28
};
29
use nativelink_util::instant_wrapper::InstantWrapper;
30
use nativelink_util::metrics::{
31
    EXECUTION_METRICS, EXECUTION_RESULT, EXECUTION_STAGE, ExecutionResult, ExecutionStage,
32
};
33
use nativelink_util::operation_state_manager::{
34
    ActionStateResult, ActionStateResultStream, ClientStateManager, MatchingEngineStateManager,
35
    OperationFilter, OperationStageFlags, OrderDirection, UpdateOperationType, WorkerStateManager,
36
};
37
use nativelink_util::origin_event::OriginMetadata;
38
use opentelemetry::KeyValue;
39
use tracing::{debug, info, trace, warn};
40
41
use super::awaited_action_db::{
42
    AwaitedAction, AwaitedActionDb, AwaitedActionSubscriber, SortedAwaitedActionState,
43
};
44
use crate::worker_registry::{ORPHANED_ACTION_TIMEOUT, SharedWorkerRegistry, WorkerLiveness};
45
46
/// Maximum number of times an update to the database
47
/// can fail before giving up.
48
const MAX_UPDATE_RETRIES: usize = 5;
49
50
/// Base delay for exponential backoff on version conflicts (in ms).
51
const BASE_RETRY_DELAY_MS: u64 = 10;
52
53
/// Maximum jitter to add to retry delay (in ms).
54
const MAX_RETRY_JITTER_MS: u64 = 20;
55
56
/// Simple struct that implements the `ActionStateResult` trait and always returns an error.
57
struct ErrorActionStateResult(Error);
58
59
#[async_trait]
60
impl ActionStateResult for ErrorActionStateResult {
61
0
    async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
62
        Err(self.0.clone())
63
0
    }
64
65
0
    async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
66
        Err(self.0.clone())
67
0
    }
68
69
0
    async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> {
70
        Err(self.0.clone())
71
0
    }
72
}
73
74
struct ClientActionStateResult<U, T, I, NowFn>
75
where
76
    U: AwaitedActionSubscriber,
77
    T: AwaitedActionDb,
78
    I: InstantWrapper,
79
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
80
{
81
    inner: MatchingEngineActionStateResult<U, T, I, NowFn>,
82
}
83
84
impl<U, T, I, NowFn> ClientActionStateResult<U, T, I, NowFn>
85
where
86
    U: AwaitedActionSubscriber,
87
    T: AwaitedActionDb,
88
    I: InstantWrapper,
89
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
90
{
91
547
    const fn new(
92
547
        sub: U,
93
547
        simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>,
94
547
        no_event_action_timeout: Duration,
95
547
        now_fn: NowFn,
96
547
    ) -> Self {
97
547
        Self {
98
547
            inner: MatchingEngineActionStateResult::new(
99
547
                sub,
100
547
                simple_scheduler_state_manager,
101
547
                no_event_action_timeout,
102
547
                now_fn,
103
547
            ),
104
547
        }
105
547
    }
106
}
107
108
#[async_trait]
109
impl<U, T, I, NowFn> ActionStateResult for ClientActionStateResult<U, T, I, NowFn>
110
where
111
    U: AwaitedActionSubscriber,
112
    T: AwaitedActionDb,
113
    I: InstantWrapper,
114
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
115
{
116
7
    async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
117
        self.inner.as_state().await
118
7
    }
119
120
56
    async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
121
        self.inner.changed().await
122
56
    }
123
124
0
    async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> {
125
        self.inner.as_action_info().await
126
0
    }
127
}
128
129
struct MatchingEngineActionStateResult<U, T, I, NowFn>
130
where
131
    U: AwaitedActionSubscriber,
132
    T: AwaitedActionDb,
133
    I: InstantWrapper,
134
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
135
{
136
    awaited_action_sub: U,
137
    simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>,
138
    no_event_action_timeout: Duration,
139
    now_fn: NowFn,
140
}
141
impl<U, T, I, NowFn> MatchingEngineActionStateResult<U, T, I, NowFn>
142
where
143
    U: AwaitedActionSubscriber,
144
    T: AwaitedActionDb,
145
    I: InstantWrapper,
146
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
147
{
148
619
    const fn new(
149
619
        awaited_action_sub: U,
150
619
        simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>,
151
619
        no_event_action_timeout: Duration,
152
619
        now_fn: NowFn,
153
619
    ) -> Self {
154
619
        Self {
155
619
            awaited_action_sub,
156
619
            simple_scheduler_state_manager,
157
619
            no_event_action_timeout,
158
619
            now_fn,
159
619
        }
160
619
    }
161
}
162
163
#[async_trait]
164
impl<U, T, I, NowFn> ActionStateResult for MatchingEngineActionStateResult<U, T, I, NowFn>
165
where
166
    U: AwaitedActionSubscriber,
167
    T: AwaitedActionDb,
168
    I: InstantWrapper,
169
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
170
{
171
55
    async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
172
        let awaited_action = self
173
            .awaited_action_sub
174
            .borrow()
175
            .await
176
            .err_tip(|| "In MatchingEngineActionStateResult::as_state")?;
177
        Ok((
178
            awaited_action.state().clone(),
179
            awaited_action.maybe_origin_metadata().cloned(),
180
        ))
181
55
    }
182
183
56
    async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
184
        let mut timeout_attempts = 0;
185
        loop {
186
            tokio::select! {
187
                awaited_action_result = self.awaited_action_sub.changed() => {
188
                    return awaited_action_result
189
                        .err_tip(|| "In MatchingEngineActionStateResult::changed")
190
52
                        .map(|v| (v.state().clone(), v.maybe_origin_metadata().cloned()));
191
                }
192
                () = (self.now_fn)().sleep(self.no_event_action_timeout) => {
193
                    // Timeout happened, do additional checks below.
194
                }
195
            }
196
197
            let awaited_action = self
198
                .awaited_action_sub
199
                .borrow()
200
                .await
201
                .err_tip(|| "In MatchingEngineActionStateResult::changed")?;
202
203
            if matches!(awaited_action.state().stage, ActionStage::Queued) {
204
                // Actions in queued state do not get periodically updated,
205
                // so we don't need to timeout them.
206
                continue;
207
            }
208
209
            let simple_scheduler_state_manager = self
210
                .simple_scheduler_state_manager
211
                .upgrade()
212
0
                .err_tip(|| format!("Failed to upgrade weak reference to SimpleSchedulerStateManager in MatchingEngineActionStateResult::changed at attempt: {timeout_attempts}"))?;
213
214
            // Check if worker is alive via registry before timing out.
215
            let should_timeout = simple_scheduler_state_manager
216
                .should_timeout_operation(&awaited_action)
217
                .await;
218
219
            if !should_timeout {
220
                // Worker is alive, continue waiting for updates
221
                trace!(
222
                    operation_id = %awaited_action.operation_id(),
223
                    "Operation timeout check passed, worker is alive"
224
                );
225
                continue;
226
            }
227
228
            warn!(
229
                ?awaited_action,
230
                "OperationId {} / {} timed out after {} seconds issuing a retry",
231
                awaited_action.operation_id(),
232
                awaited_action.state().client_operation_id,
233
                self.no_event_action_timeout.as_secs_f32(),
234
            );
235
236
            simple_scheduler_state_manager
237
                .timeout_operation_id(awaited_action.operation_id())
238
                .await
239
                .err_tip(|| "In MatchingEngineActionStateResult::changed")?;
240
241
            if timeout_attempts >= MAX_UPDATE_RETRIES {
242
                return Err(make_err!(
243
                    Code::Internal,
244
                    "Failed to update action after {} retries with no error set in MatchingEngineActionStateResult::changed - {} {:?}",
245
                    MAX_UPDATE_RETRIES,
246
                    awaited_action.operation_id(),
247
                    awaited_action.state().stage,
248
                ));
249
            }
250
            timeout_attempts += 1;
251
        }
252
56
    }
253
254
70
    async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> {
255
        let awaited_action = self
256
            .awaited_action_sub
257
            .borrow()
258
            .await
259
            .err_tip(|| "In MatchingEngineActionStateResult::as_action_info")?;
260
        Ok((
261
            awaited_action.action_info().clone(),
262
            awaited_action.maybe_origin_metadata().cloned(),
263
        ))
264
70
    }
265
}
266
267
/// `SimpleSchedulerStateManager` is responsible for maintaining the state of the scheduler.
268
/// Scheduler state includes the actions that are queued, active, and recently completed.
269
/// It also includes the workers that are available to execute actions based on allocation
270
/// strategy.
271
#[derive(MetricsComponent, Debug)]
272
pub struct SimpleSchedulerStateManager<T, I, NowFn>
273
where
274
    T: AwaitedActionDb,
275
    I: InstantWrapper,
276
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
277
{
278
    /// Database for storing the state of all actions.
279
    #[metric(group = "action_db")]
280
    action_db: T,
281
282
    /// Maximum number of times a job can be retried.
283
    // TODO(palfrey) This should be a scheduler decorator instead
284
    // of always having it on every SimpleScheduler.
285
    #[metric(help = "Maximum number of times a job can be retried")]
286
    max_job_retries: usize,
287
288
    /// Duration after which an action is considered to be timed out if
289
    /// no event is received.
290
    #[metric(
291
        help = "Duration after which an action is considered to be timed out if no event is received"
292
    )]
293
    no_event_action_timeout: Duration,
294
295
    /// Mark operation as timed out if the worker has not updated in this duration.
296
    /// This is used to prevent operations from being stuck in the queue forever
297
    /// if it is not being processed by any worker.
298
    client_action_timeout: Duration,
299
300
    /// Maximum time an action can stay in Executing state without any worker
301
    /// update, regardless of worker keepalive status. `Duration::ZERO` disables.
302
    max_executing_timeout: Duration,
303
304
    // A lock to ensure only one timeout operation is running at a time
305
    // on this service.
306
    timeout_operation_mux: Mutex<()>,
307
308
    /// Weak reference to self.
309
    // We use a weak reference to reduce the risk of a memory leak from
310
    // future changes. If this becomes some kind of performance issue,
311
    // we can consider using a strong reference.
312
    weak_self: Weak<Self>,
313
314
    /// Function to get the current time.
315
    now_fn: NowFn,
316
317
    /// Worker registry for checking worker liveness.
318
    worker_registry: Option<SharedWorkerRegistry>,
319
}
320
321
impl<T, I, NowFn> SimpleSchedulerStateManager<T, I, NowFn>
322
where
323
    T: AwaitedActionDb,
324
    I: InstantWrapper,
325
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
326
{
327
43
    pub fn new(
328
43
        max_job_retries: usize,
329
43
        no_event_action_timeout: Duration,
330
43
        client_action_timeout: Duration,
331
43
        max_executing_timeout: Duration,
332
43
        action_db: T,
333
43
        now_fn: NowFn,
334
43
        worker_registry: Option<SharedWorkerRegistry>,
335
43
    ) -> Arc<Self> {
336
43
        Arc::new_cyclic(|weak_self| Self {
337
43
            action_db,
338
43
            max_job_retries,
339
43
            no_event_action_timeout,
340
43
            client_action_timeout,
341
43
            max_executing_timeout,
342
43
            timeout_operation_mux: Mutex::new(()),
343
43
            weak_self: weak_self.clone(),
344
43
            now_fn,
345
43
            worker_registry,
346
43
        })
347
43
    }
348
349
    /// Retires queued actions whose client has stopped listening.
350
    ///
351
    /// A queued action has no worker, so neither of the other two timeout
352
    /// paths can reach it. `MatchingEngineActionStateResult::changed` skips
353
    /// the `Queued` stage outright, and `should_timeout_operation` only
354
    /// considers `Executing`. That leaves the matching pass as the only
355
    /// thing that retires them, and it only does so for actions it happens
356
    /// to visit.
357
    ///
358
    /// When it does not, they accumulate with no expiry and are handed to
359
    /// the matcher again on every pass, ahead of live work because they are
360
    /// the oldest. Enough of them starve dispatch entirely: workers sit
361
    /// idle, nothing executes or completes, and clients eventually report a
362
    /// remote execution failure.
363
    ///
364
    /// Returns how many were retired.
365
3
    pub async fn sweep_abandoned_queued_actions(&self) -> Result<u64, Error> {
366
3
        let now = (self.now_fn)().now();
367
3
        let stream = self
368
3
            .action_db
369
3
            .get_range_of_actions(
370
3
                SortedAwaitedActionState::Queued,
371
3
                Bound::Unbounded,
372
3
                Bound::Unbounded,
373
3
                true,
374
3
            )
375
3
            .await
376
3
            .err_tip(|| "In sweep_abandoned_queued_actions")
?0
;
377
3
        tokio::pin!(stream);
378
379
3
        let mut retired = 0u64;
380
5
        while let Some(
subscriber2
) = stream.next().await {
381
2
            let subscriber = subscriber.err_tip(|| "In sweep_abandoned_queued_actions")
?0
;
382
2
            let awaited_action = subscriber
383
2
                .borrow()
384
2
                .await
385
2
                .err_tip(|| "In sweep_abandoned_queued_actions")
?0
;
386
387
            // Only queued actions belong to this sweep, and only once the
388
            // client has been gone longer than it is allowed to be.
389
2
            if !
matches!0
(awaited_action.state().stage, ActionStage::Queued) {
390
0
                continue;
391
2
            }
392
2
            if awaited_action.last_client_keepalive_timestamp() + self.client_action_timeout >= now
393
            {
394
1
                continue;
395
1
            }
396
397
1
            let mut state = awaited_action.state().as_ref().clone();
398
1
            state.stage = ActionStage::Completed(ActionResult {
399
1
                error: Some(make_err!(
400
1
                    Code::DeadlineExceeded,
401
1
                    "Operation timed out {} seconds of having no more clients listening",
402
1
                    self.client_action_timeout.as_secs_f32(),
403
1
                )),
404
1
                ..ActionResult::default()
405
1
            });
406
1
            state.last_transition_timestamp = now;
407
408
1
            let mut new_awaited_action = awaited_action;
409
1
            new_awaited_action.worker_set_state(Arc::new(state), now);
410
            // A conflict means something else is already changing this
411
            // action, which is the outcome this sweep wants anyway. Leave it
412
            // for the next pass rather than fighting for it.
413
1
            match self
414
1
                .action_db
415
1
                .update_awaited_action(new_awaited_action)
416
1
                .await
417
            {
418
1
                Ok(()) => retired += 1,
419
0
                Err(err) if err.code == Code::Aborted => {}
420
0
                Err(err) => {
421
0
                    return Err(err).err_tip(|| "In sweep_abandoned_queued_actions");
422
                }
423
            }
424
        }
425
426
3
        if retired > 0 {
427
1
            warn!(
428
                retired,
429
1
                timeout_secs = self.client_action_timeout.as_secs_f32(),
430
                "Retired queued operations that had no clients listening"
431
            );
432
2
        }
433
3
        Ok(retired)
434
3
    }
435
436
11
    pub async fn should_timeout_operation(&self, awaited_action: &AwaitedAction) -> bool {
437
11
        if !
matches!0
(awaited_action.state().stage, ActionStage::Executing) {
438
0
            return false;
439
11
        }
440
441
11
        let now = (self.now_fn)().now();
442
443
        // Honor the per-action `Action.timeout` from the RBE protocol as a
444
        // backend wall-clock deadline. Without this, the only enforcement is
445
        // the Bazel client's --test_timeout, which surfaces as TIMEOUT/NO
446
        // STATUS instead of a backend signal pointing at the worker.
447
11
        let action_timeout = awaited_action.action_info().timeout;
448
11
        if action_timeout > Duration::ZERO {
449
4
            let executing_started_at = awaited_action.state().last_transition_timestamp;
450
4
            if let Ok(elapsed) = now.duration_since(executing_started_at)
451
4
                && elapsed > action_timeout
452
            {
453
1
                return true;
454
3
            }
455
7
        }
456
457
10
        let liveness = match (&self.worker_registry, awaited_action.worker_id()) {
458
9
            (Some(worker_registry), Some(worker_id)) => {
459
9
                worker_registry
460
9
                    .check_liveness(worker_id, self.no_event_action_timeout, now)
461
9
                    .await
462
            }
463
            // No registry, or not assigned yet: fall back to the
464
            // timestamp-only check.
465
1
            _ => WorkerLiveness::Stale,
466
        };
467
468
10
        match liveness {
469
            // Ours and heartbeating: only the stuck-but-alive ceiling applies,
470
            // and disabling that means no ceiling on a live worker.
471
            WorkerLiveness::Alive => {
472
2
                if self.max_executing_timeout > Duration::ZERO {
473
2
                    let last_update = awaited_action.last_worker_updated_timestamp();
474
2
                    if let Ok(elapsed) = now.duration_since(last_update) {
475
2
                        return elapsed > self.max_executing_timeout;
476
0
                    }
477
0
                }
478
0
                false
479
            }
480
481
            // Usually a peer's healthy worker, so worker_timeout_s must not
482
            // apply. It can also be an orphan no instance will ever reap, and
483
            // max_action_executing_timeout_s defaults to disabled, so fall
484
            // back to a ceiling rather than never timing out.
485
            WorkerLiveness::Unknown => {
486
4
                let ceiling = if self.max_executing_timeout > Duration::ZERO {
487
2
                    self.max_executing_timeout
488
                } else {
489
2
                    ORPHANED_ACTION_TIMEOUT
490
                };
491
4
                let last_update = awaited_action.last_worker_updated_timestamp();
492
4
                match now.duration_since(last_update) {
493
4
                    Ok(elapsed) => elapsed > ceiling,
494
0
                    Err(_) => false,
495
                }
496
            }
497
498
            // Registered here and gone quiet: ours, and it looks dead.
499
            WorkerLiveness::Stale => {
500
4
                let worker_should_update_before = awaited_action
501
4
                    .last_worker_updated_timestamp()
502
4
                    .checked_add(self.no_event_action_timeout)
503
4
                    .unwrap_or(now);
504
505
4
                worker_should_update_before < now
506
            }
507
        }
508
11
    }
509
510
580
    async fn apply_filter_predicate(
511
580
        &self,
512
580
        awaited_action: &AwaitedAction,
513
580
        subscriber: &T::Subscriber,
514
580
        filter: &OperationFilter,
515
580
    ) -> bool {
516
        // Note: The caller must filter `client_operation_id`.
517
518
580
        let mut maybe_reloaded_awaited_action: Option<AwaitedAction> = None;
519
580
        let now = (self.now_fn)().now();
520
521
        // Check if client has timed out
522
580
        if awaited_action.last_client_keepalive_timestamp() + self.client_action_timeout < now {
523
            // This may change if the version is out of date.
524
4
            let mut timed_out = true;
525
4
            if !awaited_action.state().stage.is_finished() {
526
4
                let mut state = awaited_action.state().as_ref().clone();
527
4
                warn!(operation_id = ?awaited_action.operation_id(), timeout_secs = self.client_action_timeout.as_secs_f32(), "Operation timed out having no more clients listening");
528
4
                state.stage = ActionStage::Completed(ActionResult {
529
4
                    error: Some(make_err!(
530
4
                        Code::DeadlineExceeded,
531
4
                        "Operation timed out {} seconds of having no more clients listening",
532
4
                        self.client_action_timeout.as_secs_f32(),
533
4
                    )),
534
4
                    ..ActionResult::default()
535
4
                });
536
4
                state.last_transition_timestamp = now;
537
4
                let state = Arc::new(state);
538
                // We may be competing with an client timestamp update, so try
539
                // this a few times.
540
4
                for attempt in 1..=MAX_UPDATE_RETRIES {
541
4
                    let mut new_awaited_action = match &maybe_reloaded_awaited_action {
542
4
                        None => awaited_action.clone(),
543
0
                        Some(reloaded_awaited_action) => reloaded_awaited_action.clone(),
544
                    };
545
4
                    new_awaited_action.worker_set_state(state.clone(), (self.now_fn)().now());
546
4
                    let 
err0
= match self
547
4
                        .action_db
548
4
                        .update_awaited_action(new_awaited_action)
549
4
                        .await
550
                    {
551
4
                        Ok(()) => break,
552
0
                        Err(err) => err,
553
                    };
554
                    // Reload from the database if the action was outdated.
555
0
                    let maybe_awaited_action =
556
0
                        if attempt == MAX_UPDATE_RETRIES || err.code != Code::Aborted {
557
0
                            None
558
                        } else {
559
0
                            subscriber.borrow().await.ok()
560
                        };
561
0
                    if let Some(reloaded_awaited_action) = maybe_awaited_action {
562
0
                        maybe_reloaded_awaited_action = Some(reloaded_awaited_action);
563
0
                    } else {
564
0
                        warn!(
565
                            "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}",
566
                        );
567
0
                        break;
568
                    }
569
                    // Re-check the predicate after reload.
570
0
                    if maybe_reloaded_awaited_action
571
0
                        .as_ref()
572
0
                        .is_some_and(|awaited_action| {
573
0
                            awaited_action.last_client_keepalive_timestamp()
574
0
                                + self.client_action_timeout
575
0
                                >= (self.now_fn)().now()
576
0
                        })
577
                    {
578
0
                        timed_out = false;
579
0
                        break;
580
0
                    } else if maybe_reloaded_awaited_action
581
0
                        .as_ref()
582
0
                        .is_some_and(|awaited_action| awaited_action.state().stage.is_finished())
583
                    {
584
0
                        break;
585
0
                    }
586
                }
587
0
            }
588
4
            if timed_out {
589
4
                return false;
590
0
            }
591
576
        }
592
        // If the action was reloaded, then use that for the rest of the checks
593
        // instead of the input parameter.
594
576
        let awaited_action = maybe_reloaded_awaited_action
595
576
            .as_ref()
596
576
            .unwrap_or(awaited_action);
597
598
576
        if let Some(
operation_id0
) = &filter.operation_id
599
0
            && operation_id != awaited_action.operation_id()
600
        {
601
0
            return false;
602
576
        }
603
604
576
        if filter.worker_id.is_some() && 
filter.worker_id.as_ref()0
!= awaited_action.worker_id() {
605
0
            return false;
606
576
        }
607
608
        {
609
576
            if let Some(
filter_unique_key0
) = &filter.unique_key {
610
0
                match &awaited_action.action_info().unique_qualifier {
611
0
                    ActionUniqueQualifier::Cacheable(unique_key) => {
612
0
                        if filter_unique_key != unique_key {
613
0
                            return false;
614
0
                        }
615
                    }
616
                    ActionUniqueQualifier::Uncacheable(_) => {
617
0
                        return false;
618
                    }
619
                }
620
576
            }
621
576
            if let Some(
action_digest0
) = filter.action_digest
622
0
                && action_digest != awaited_action.action_info().digest()
623
            {
624
0
                return false;
625
576
            }
626
        }
627
628
        {
629
576
            let last_worker_update_timestamp = awaited_action.last_worker_updated_timestamp();
630
576
            if let Some(
worker_update_before0
) = filter.worker_update_before
631
0
                && worker_update_before < last_worker_update_timestamp
632
            {
633
0
                return false;
634
576
            }
635
576
            if let Some(
completed_before0
) = filter.completed_before
636
0
                && awaited_action.state().stage.is_finished()
637
0
                && completed_before < last_worker_update_timestamp
638
            {
639
0
                return false;
640
576
            }
641
576
            if filter.stages != OperationStageFlags::Any {
642
572
                let stage_flag = match awaited_action.state().stage {
643
0
                    ActionStage::Unknown => OperationStageFlags::Any,
644
0
                    ActionStage::CacheCheck => OperationStageFlags::CacheCheck,
645
572
                    ActionStage::Queued => OperationStageFlags::Queued,
646
0
                    ActionStage::Executing => OperationStageFlags::Executing,
647
                    ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => {
648
0
                        OperationStageFlags::Completed
649
                    }
650
                };
651
572
                if !filter.stages.intersects(stage_flag) {
652
0
                    return false;
653
572
                }
654
4
            }
655
        }
656
657
576
        true
658
580
    }
659
660
    /// Let the scheduler know that an operation has timed out from
661
    /// the client side (ie: worker has not updated in a while).
662
1
    async fn timeout_operation_id(&self, operation_id: &OperationId) -> Result<(), Error> {
663
        // Ensure that only one timeout operation is running at a time.
664
        // Failing to do this could result in the same operation being
665
        // timed out multiple times at the same time.
666
        // Note: We could implement this on a per-operation_id basis, but it is quite
667
        // complex to manage the locks.
668
1
        let _lock = self.timeout_operation_mux.lock().await;
669
670
1
        let awaited_action_subscriber = self
671
1
            .action_db
672
1
            .get_by_operation_id(operation_id)
673
1
            .await
674
1
            .err_tip(|| "In SimpleSchedulerStateManager::timeout_operation_id")
?0
675
1
            .err_tip(|| 
{0
676
0
                format!("Operation id {operation_id} does not exist in SimpleSchedulerStateManager::timeout_operation_id")
677
0
            })?;
678
679
1
        let awaited_action = awaited_action_subscriber
680
1
            .borrow()
681
1
            .await
682
1
            .err_tip(|| "In SimpleSchedulerStateManager::timeout_operation_id")
?0
;
683
684
        // Re-check under the lock against freshly loaded state, and delegate
685
        // rather than re-deriving the rule: the two copies had drifted.
686
1
        if !self.should_timeout_operation(&awaited_action).await {
687
0
            trace!(
688
                %operation_id,
689
0
                worker_id = ?awaited_action.worker_id(),
690
                "Operation no longer needs timing out, skipping"
691
            );
692
0
            return Ok(());
693
1
        }
694
695
1
        warn!(
696
            %operation_id,
697
1
            worker_id = ?awaited_action.worker_id(),
698
            "Timing out operation"
699
        );
700
701
1
        self.assign_operation(
702
1
            operation_id,
703
1
            Err(make_err!(
704
1
                Code::DeadlineExceeded,
705
1
                "Operation timed out after {} seconds",
706
1
                self.no_event_action_timeout.as_secs_f32(),
707
1
            )),
708
1
        )
709
1
        .await
710
1
    }
711
712
74
    async fn inner_update_operation(
713
74
        &self,
714
74
        operation_id: &OperationId,
715
74
        maybe_worker_id: Option<&WorkerId>,
716
74
        update: UpdateOperationType,
717
74
    ) -> Result<(), Error> {
718
74
        let update_type_str = match &update {
719
0
            UpdateOperationType::KeepAlive => "KeepAlive",
720
56
            UpdateOperationType::UpdateWithActionStage(stage) => match stage {
721
0
                ActionStage::Queued => "Stage:Queued",
722
49
                ActionStage::Executing => "Stage:Executing",
723
7
                ActionStage::Completed(_) => "Stage:Completed",
724
0
                ActionStage::CompletedFromCache(_) => "Stage:CompletedFromCache",
725
0
                ActionStage::CacheCheck => "Stage:CacheCheck",
726
0
                ActionStage::Unknown => "Stage:Unknown",
727
            },
728
14
            UpdateOperationType::UpdateWithError(_) => "Error",
729
3
            UpdateOperationType::UpdateWithDisconnect => "Disconnect",
730
1
            UpdateOperationType::ExecutionComplete => "ExecutionComplete",
731
        };
732
733
74
        debug!(
734
            %operation_id,
735
            ?maybe_worker_id,
736
            update_type = %update_type_str,
737
            "inner_update_operation START"
738
        );
739
740
74
        let mut last_err = None;
741
74
        let mut retry_count = 0;
742
74
        for _ in 0..MAX_UPDATE_RETRIES {
743
78
            retry_count += 1;
744
78
            if retry_count > 1 {
745
4
                let base_delay = BASE_RETRY_DELAY_MS * (1 << (retry_count - 2).min(4));
746
4
                let jitter = std::time::SystemTime::now()
747
4
                    .duration_since(std::time::UNIX_EPOCH)
748
4
                    .map_or(0, |d| {
749
4
                        u64::try_from(d.as_nanos()).expect("u64 error") % MAX_RETRY_JITTER_MS
750
4
                    });
751
4
                let delay = Duration::from_millis(base_delay + jitter);
752
753
4
                warn!(
754
                    %operation_id,
755
                    ?maybe_worker_id,
756
                    retry_count,
757
4
                    delay_ms = delay.as_millis(),
758
                    update_type = %update_type_str,
759
                    "Retrying operation update due to version conflict (with backoff)"
760
                );
761
762
4
                tokio::time::sleep(delay).await;
763
74
            }
764
78
            let maybe_awaited_action_subscriber = self
765
78
                .action_db
766
78
                .get_by_operation_id(operation_id)
767
78
                .await
768
78
                .err_tip(|| "In SimpleSchedulerStateManager::update_operation")
?0
;
769
78
            let Some(
awaited_action_subscriber76
) = maybe_awaited_action_subscriber else {
770
                // No action found. It is ok if the action was not found. It
771
                // probably means that the action was dropped, but worker was
772
                // still processing it.
773
2
                warn!(
774
                    %operation_id,
775
                    "Unable to update action due to it being missing, probably dropped"
776
                );
777
2
                return Ok(());
778
            };
779
780
76
            let mut awaited_action = awaited_action_subscriber
781
76
                .borrow()
782
76
                .await
783
76
                .err_tip(|| "In SimpleSchedulerStateManager::update_operation")
?0
;
784
785
            // Make sure the worker id matches the awaited action worker id.
786
            // This might happen if the worker sending the update is not the
787
            // worker that was assigned.
788
76
            if awaited_action.worker_id().is_some()
789
28
                && maybe_worker_id.is_some()
790
26
                && maybe_worker_id != awaited_action.worker_id()
791
            {
792
                // If another worker is already assigned to the action, another
793
                // worker probably picked up the action. We should not update the
794
                // action in this case and abort this operation.
795
0
                let err = make_err!(
796
0
                    Code::Aborted,
797
                    "Worker ids do not match - {:?} != {:?} for {:?}",
798
                    maybe_worker_id,
799
0
                    awaited_action.worker_id(),
800
                    awaited_action,
801
                );
802
0
                info!(
803
                    "Worker ids do not match - {:?} != {:?} for {:?}. This is probably due to another worker picking up the action.",
804
                    maybe_worker_id,
805
0
                    awaited_action.worker_id(),
806
                    awaited_action,
807
                );
808
0
                return Err(err);
809
76
            }
810
811
            // Make sure we don't update an action that is already completed.
812
76
            if awaited_action.state().stage.is_finished() {
813
3
                match &update {
814
                    UpdateOperationType::UpdateWithDisconnect | UpdateOperationType::KeepAlive => {
815
                        // No need to error a keep-alive when it's completed, it's just
816
                        // unnecessary log noise.
817
1
                        return Ok(());
818
                    }
819
                    _ => {
820
2
                        return Err(make_err!(
821
2
                            Code::Internal,
822
2
                            "Action {operation_id} is already completed with state {:?} - maybe_worker_id: {:?}",
823
2
                            awaited_action.state().stage,
824
2
                            maybe_worker_id,
825
2
                        ));
826
                    }
827
                }
828
73
            }
829
830
73
            let mut is_retry = false;
831
73
            let 
stage69
= match &update {
832
                UpdateOperationType::KeepAlive => {
833
0
                    awaited_action.worker_keep_alive((self.now_fn)().now());
834
0
                    match self
835
0
                        .action_db
836
0
                        .update_awaited_action(awaited_action)
837
0
                        .await
838
0
                        .err_tip(|| "Failed to send KeepAlive in SimpleSchedulerStateManager::update_operation") {
839
                        // Try again if there was a version mismatch.
840
0
                        Err(err) if err.code == Code::Aborted => {
841
0
                            last_err = Some(err);
842
0
                            continue;
843
                        }
844
0
                        result => return result,
845
                    }
846
                }
847
58
                UpdateOperationType::UpdateWithActionStage(stage) => {
848
58
                    if stage == &ActionStage::Executing
849
52
                        && awaited_action.state().stage == ActionStage::Executing
850
                    {
851
4
                        warn!(state = ?awaited_action.state(), "Action already assigned");
852
4
                        return Err(make_err!(Code::Aborted, "Action already assigned"));
853
54
                    }
854
54
                    stage.clone()
855
                }
856
13
                UpdateOperationType::UpdateWithError(err) => {
857
                    // Don't count a backpressure failure as an attempt for an action.
858
13
                    let due_to_backpressure = err.code == Code::ResourceExhausted;
859
13
                    if !due_to_backpressure {
860
13
                        awaited_action.attempts += 1;
861
13
                    
}0
862
863
13
                    if awaited_action.attempts > self.max_job_retries {
864
2
                        ActionStage::Completed(ActionResult {
865
2
                            execution_metadata: ExecutionMetadata {
866
2
                                worker: maybe_worker_id.map_or_else(String::default, ToString::to_string),
867
2
                                ..ExecutionMetadata::default()
868
2
                            },
869
2
                            error: Some(err.clone().merge(make_err!(
870
2
                                Code::Internal,
871
2
                                "Job cancelled because it attempted to execute too many times {} > {} times {}",
872
2
                                awaited_action.attempts,
873
2
                                self.max_job_retries,
874
2
                                format!("for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"),
875
2
                            ))),
876
2
                            ..ActionResult::default()
877
2
                        })
878
                    } else {
879
11
                        is_retry = true;
880
11
                        ActionStage::Queued
881
                    }
882
                }
883
                UpdateOperationType::UpdateWithDisconnect => {
884
                    // A worker disconnect (e.g. OOMKill, pod eviction, network
885
                    // drop) used to requeue without counting as an attempt,
886
                    // which let an action that always crashes its worker loop
887
                    // forever until the Bazel client's --test_timeout fired.
888
                    // Count disconnects as attempts so max_job_retries caps the
889
                    // loop and the client sees a backend-attributable error.
890
2
                    awaited_action.attempts += 1;
891
892
2
                    if awaited_action.attempts > self.max_job_retries {
893
1
                        ActionStage::Completed(ActionResult {
894
1
                            execution_metadata: ExecutionMetadata {
895
1
                                worker: maybe_worker_id
896
1
                                    .map_or_else(String::default, ToString::to_string),
897
1
                                ..ExecutionMetadata::default()
898
1
                            },
899
1
                            error: Some(make_err!(
900
1
                                Code::Internal,
901
1
                                "Worker disconnected repeatedly while executing this action ({} > {} attempts); the runner likely OOMKilled or the pod was evicted. {}",
902
1
                                awaited_action.attempts,
903
1
                                self.max_job_retries,
904
1
                                format!(
905
1
                                    "for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"
906
1
                                ),
907
1
                            )),
908
1
                            ..ActionResult::default()
909
1
                        })
910
                    } else {
911
1
                        is_retry = true;
912
1
                        ActionStage::Queued
913
                    }
914
                }
915
                // We shouldn't get here, but we just ignore it if we do.
916
                UpdateOperationType::ExecutionComplete => {
917
0
                    warn!("inner_update_operation got an ExecutionComplete, that's unexpected.");
918
0
                    return Ok(());
919
                }
920
            };
921
69
            let now = (self.now_fn)().now();
922
69
            if 
matches!57
(stage, ActionStage::Queued) {
923
12
                // If the action is queued, we need to unset the worker id regardless of
924
12
                // which worker sent the update.
925
12
                awaited_action.set_worker_id(None, now);
926
57
            } else {
927
57
                awaited_action.set_worker_id(maybe_worker_id.cloned(), now);
928
57
            }
929
69
            awaited_action.worker_set_state(
930
69
                Arc::new(ActionState {
931
69
                    stage,
932
69
                    // Client id is not known here, it is the responsibility of
933
69
                    // the the subscriber impl to replace this with the
934
69
                    // correct client id.
935
69
                    client_operation_id: operation_id.clone(),
936
69
                    action_digest: awaited_action.action_info().digest(),
937
69
                    last_transition_timestamp: now,
938
69
                }),
939
69
                now,
940
            );
941
942
69
            let update_action_result = self
943
69
                .action_db
944
69
                .update_awaited_action(awaited_action.clone())
945
69
                .await
946
69
                .err_tip(|| "In SimpleSchedulerStateManager::update_operation");
947
69
            if let Err(
err5
) = update_action_result {
948
                // We use Aborted to signal that the action was not
949
                // updated due to the data being set was not the latest
950
                // but can be retried.
951
5
                if err.code == Code::Aborted {
952
4
                    debug!(
953
                        %operation_id,
954
                        retry_count,
955
                        update_type = %update_type_str,
956
                        "Version conflict (Aborted), will retry"
957
                    );
958
4
                    last_err = Some(err);
959
4
                    continue;
960
1
                }
961
1
                warn!(
962
                    %operation_id,
963
                    update_type = %update_type_str,
964
                    ?err,
965
                    "inner_update_operation FAILED (non-retryable)"
966
                );
967
1
                return Err(err);
968
64
            }
969
970
            // Record execution metrics after successful state update
971
64
            let action_state = awaited_action.state();
972
64
            let instance_name = awaited_action
973
64
                .action_info()
974
64
                .unique_qualifier
975
64
                .instance_name()
976
64
                .as_str();
977
64
            let worker_id = awaited_action
978
64
                .worker_id()
979
64
                .map(std::string::ToString::to_string);
980
64
            let priority = Some(awaited_action.action_info().priority);
981
982
            // Build base attributes for metrics
983
64
            let mut attrs = nativelink_util::metrics::make_execution_attributes(
984
64
                instance_name,
985
64
                worker_id.as_deref(),
986
64
                priority,
987
            );
988
989
            // Add stage attribute
990
64
            let execution_stage: ExecutionStage = (&action_state.stage).into();
991
64
            attrs.push(KeyValue::new(EXECUTION_STAGE, execution_stage));
992
993
            // Record stage transition
994
64
            EXECUTION_METRICS.execution_stage_transitions.add(1, &attrs);
995
996
            // For completed actions, record the completion count with result
997
64
            match &action_state.stage {
998
9
                ActionStage::Completed(action_result) => {
999
9
                    let result = if action_result.exit_code == 0 {
1000
5
                        ExecutionResult::Success
1001
                    } else {
1002
4
                        ExecutionResult::Failure
1003
                    };
1004
9
                    attrs.push(KeyValue::new(EXECUTION_RESULT, result));
1005
9
                    EXECUTION_METRICS.execution_completed_count.add(1, &attrs);
1006
9
                    nativelink_util::metrics::record_completed_execution_metrics(
1007
9
                        action_result,
1008
9
                        instance_name,
1009
9
                        worker_id.as_deref(),
1010
9
                        priority,
1011
                    );
1012
                }
1013
0
                ActionStage::CompletedFromCache(_) => {
1014
0
                    attrs.push(KeyValue::new(EXECUTION_RESULT, ExecutionResult::CacheHit));
1015
0
                    EXECUTION_METRICS.execution_completed_count.add(1, &attrs);
1016
0
                }
1017
55
                _ => {}
1018
            }
1019
1020
            // A failed attempt that re-queued the action counts as a retry.
1021
64
            if is_retry {
1022
12
                let retry_attrs = nativelink_util::metrics::make_execution_attributes(
1023
12
                    instance_name,
1024
12
                    worker_id.as_deref(),
1025
12
                    priority,
1026
12
                );
1027
12
                EXECUTION_METRICS.execution_retry_count.add(1, &retry_attrs);
1028
52
            }
1029
1030
64
            debug!(
1031
                %operation_id,
1032
                retry_count,
1033
                update_type = %update_type_str,
1034
                "inner_update_operation SUCCESS"
1035
            );
1036
64
            return Ok(());
1037
        }
1038
1039
0
        warn!(
1040
            %operation_id,
1041
            update_type = %update_type_str,
1042
            retry_count = MAX_UPDATE_RETRIES,
1043
            "inner_update_operation EXHAUSTED all retries"
1044
        );
1045
0
        Err(last_err.unwrap_or_else(|| {
1046
0
            make_err!(
1047
0
                Code::Internal,
1048
                "Failed to update action after {} retries with no error set",
1049
                MAX_UPDATE_RETRIES,
1050
            )
1051
0
        }))
1052
74
    }
1053
1054
43
    async fn inner_add_operation(
1055
43
        &self,
1056
43
        new_client_operation_id: OperationId,
1057
43
        action_info: Arc<ActionInfo>,
1058
43
    ) -> Result<T::Subscriber, Error> {
1059
43
        self.action_db
1060
43
            .add_action(
1061
43
                new_client_operation_id,
1062
43
                action_info,
1063
43
                self.no_event_action_timeout,
1064
43
            )
1065
43
            .await
1066
43
            .err_tip(|| "In SimpleSchedulerStateManager::add_operation")
1067
43
    }
1068
1069
663
    async fn inner_filter_operations<'a, F>(
1070
663
        &'a self,
1071
663
        filter: OperationFilter,
1072
663
        to_action_state_result: F,
1073
663
    ) -> Result<ActionStateResultStream<'a>, Error>
1074
663
    where
1075
663
        F: Fn(T::Subscriber) -> Box<dyn ActionStateResult> + Send + Sync + 'a,
1076
663
    {
1077
659
        const fn sorted_awaited_action_state_for_flags(
1078
659
            stage: OperationStageFlags,
1079
659
        ) -> Option<SortedAwaitedActionState> {
1080
659
            match stage {
1081
0
                OperationStageFlags::CacheCheck => Some(SortedAwaitedActionState::CacheCheck),
1082
653
                OperationStageFlags::Queued => Some(SortedAwaitedActionState::Queued),
1083
0
                OperationStageFlags::Executing => Some(SortedAwaitedActionState::Executing),
1084
0
                OperationStageFlags::Completed => Some(SortedAwaitedActionState::Completed),
1085
6
                _ => None,
1086
            }
1087
659
        }
1088
1089
663
        if let Some(
operation_id0
) = &filter.operation_id {
1090
0
            let maybe_subscriber = self
1091
0
                .action_db
1092
0
                .get_by_operation_id(operation_id)
1093
0
                .await
1094
0
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?;
1095
0
            let Some(subscriber) = maybe_subscriber else {
1096
0
                return Ok(Box::pin(stream::empty()));
1097
            };
1098
0
            let awaited_action = subscriber
1099
0
                .borrow()
1100
0
                .await
1101
0
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?;
1102
0
            if !self
1103
0
                .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1104
0
                .await
1105
            {
1106
0
                return Ok(Box::pin(stream::empty()));
1107
0
            }
1108
0
            return Ok(Box::pin(stream::once(async move {
1109
0
                to_action_state_result(subscriber)
1110
0
            })));
1111
663
        }
1112
663
        if let Some(
client_operation_id4
) = &filter.client_operation_id {
1113
4
            let maybe_subscriber = self
1114
4
                .action_db
1115
4
                .get_awaited_action_by_id(client_operation_id)
1116
4
                .await
1117
4
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1118
4
            let Some(subscriber) = maybe_subscriber else {
1119
0
                return Ok(Box::pin(stream::empty()));
1120
            };
1121
4
            let awaited_action = subscriber
1122
4
                .borrow()
1123
4
                .await
1124
4
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1125
4
            if !self
1126
4
                .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1127
4
                .await
1128
            {
1129
0
                return Ok(Box::pin(stream::empty()));
1130
4
            }
1131
4
            return Ok(Box::pin(stream::once(async move {
1132
4
                to_action_state_result(subscriber)
1133
4
            })));
1134
659
        }
1135
1136
653
        let Some(sorted_awaited_action_state) =
1137
659
            sorted_awaited_action_state_for_flags(filter.stages)
1138
        else {
1139
6
            let mut all_items: Vec<_> = self
1140
6
                .action_db
1141
6
                .get_all_awaited_actions()
1142
6
                .await
1143
6
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
1144
6
                .and_then(|awaited_action_subscriber| async move 
{4
1145
4
                    let awaited_action = awaited_action_subscriber
1146
4
                        .borrow()
1147
4
                        .await
1148
4
                        .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1149
4
                    Ok((awaited_action_subscriber, awaited_action))
1150
8
                })
1151
6
                .try_filter_map(|(subscriber, awaited_action)| 
{4
1152
4
                    let filter = filter.clone();
1153
4
                    async move {
1154
4
                        Ok(self
1155
4
                            .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1156
4
                            .await
1157
4
                            .then_some((subscriber, awaited_action.sort_key())))
1158
4
                    }
1159
4
                })
1160
6
                .try_collect()
1161
6
                .await
1162
6
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1163
1164
            #[allow(clippy::unnecessary_sort_by)]
1165
0
            match filter.order_by_priority_direction {
1166
0
                Some(OrderDirection::Asc) => all_items.sort_unstable_by(|(_, a), (_, b)| a.cmp(b)),
1167
0
                Some(OrderDirection::Desc) => all_items.sort_unstable_by(|(_, a), (_, b)| b.cmp(a)),
1168
6
                None => {}
1169
            }
1170
6
            return Ok(Box::pin(stream::iter(
1171
6
                all_items
1172
6
                    .into_iter()
1173
6
                    .map(move |(subscriber, _)| 
to_action_state_result0
(
subscriber0
)),
1174
            )));
1175
        };
1176
1177
653
        let desc = 
matches!503
(
1178
150
            filter.order_by_priority_direction,
1179
            Some(OrderDirection::Desc)
1180
        );
1181
653
        let stream = self
1182
653
            .action_db
1183
653
            .get_range_of_actions(
1184
653
                sorted_awaited_action_state,
1185
653
                Bound::Unbounded,
1186
653
                Bound::Unbounded,
1187
653
                desc,
1188
653
            )
1189
653
            .await
1190
653
            .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
1191
653
            .and_then(|awaited_action_subscriber| async move 
{572
1192
572
                let awaited_action = awaited_action_subscriber
1193
572
                    .borrow()
1194
572
                    .await
1195
572
                    .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1196
572
                Ok((awaited_action_subscriber, awaited_action))
1197
1.14k
            })
1198
653
            .try_filter_map(move |(subscriber, awaited_action)| 
{572
1199
572
                let filter = filter.clone();
1200
572
                async move {
1201
572
                    Ok(self
1202
572
                        .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1203
572
                        .await
1204
572
                        .then_some(subscriber))
1205
572
                }
1206
572
            })
1207
653
            .map(move |result| -> Box<dyn ActionStateResult> 
{572
1208
572
                result.map_or_else(
1209
0
                    |e| -> Box<dyn ActionStateResult> { Box::new(ErrorActionStateResult(e)) },
1210
572
                    |v| -> Box<dyn ActionStateResult> { to_action_state_result(v) },
1211
                )
1212
572
            });
1213
653
        Ok(Box::pin(stream))
1214
663
    }
1215
}
1216
1217
#[async_trait]
1218
impl<T, I, NowFn> ClientStateManager for SimpleSchedulerStateManager<T, I, NowFn>
1219
where
1220
    T: AwaitedActionDb,
1221
    I: InstantWrapper,
1222
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
1223
{
1224
    async fn add_action(
1225
        &self,
1226
        client_operation_id: OperationId,
1227
        action_info: Arc<ActionInfo>,
1228
43
    ) -> Result<Box<dyn ActionStateResult>, Error> {
1229
        let sub = self
1230
            .inner_add_operation(client_operation_id, action_info.clone())
1231
            .await?;
1232
1233
        Ok(Box::new(ClientActionStateResult::new(
1234
            sub,
1235
            self.weak_self.clone(),
1236
            self.no_event_action_timeout,
1237
            self.now_fn.clone(),
1238
        )))
1239
43
    }
1240
1241
    async fn filter_operations<'a>(
1242
        &'a self,
1243
        filter: OperationFilter,
1244
508
    ) -> Result<ActionStateResultStream<'a>, Error> {
1245
504
        self.inner_filter_operations(filter, move |rx| {
1246
504
            Box::new(ClientActionStateResult::new(
1247
504
                rx,
1248
504
                self.weak_self.clone(),
1249
504
                self.no_event_action_timeout,
1250
504
                self.now_fn.clone(),
1251
504
            ))
1252
504
        })
1253
        .await
1254
508
    }
1255
}
1256
1257
#[async_trait]
1258
impl<T, I, NowFn> WorkerStateManager for SimpleSchedulerStateManager<T, I, NowFn>
1259
where
1260
    T: AwaitedActionDb,
1261
    I: InstantWrapper,
1262
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
1263
{
1264
    async fn update_operation(
1265
        &self,
1266
        operation_id: &OperationId,
1267
        worker_id: &WorkerId,
1268
        update: UpdateOperationType,
1269
23
    ) -> Result<(), Error> {
1270
        self.inner_update_operation(operation_id, Some(worker_id), update)
1271
            .await
1272
23
    }
1273
1274
    async fn is_executing_on_worker(
1275
        &self,
1276
        operation_id: &OperationId,
1277
        worker_id: &WorkerId,
1278
17
    ) -> Result<bool, Error> {
1279
        let Some(subscriber) = self
1280
            .action_db
1281
            .get_by_operation_id(operation_id)
1282
            .await
1283
            .err_tip(|| "In SimpleSchedulerStateManager::is_executing_on_worker")?
1284
        else {
1285
            return Ok(false);
1286
        };
1287
        let awaited_action = match subscriber.borrow().await {
1288
            Ok(awaited_action) => awaited_action,
1289
            // Store-backed dbs hand out a subscriber for any id and only
1290
            // discover the operation is gone on read.
1291
            Err(err) if err.code == Code::NotFound => return Ok(false),
1292
            Err(err) => {
1293
                return Err(err)
1294
                    .err_tip(|| "In SimpleSchedulerStateManager::is_executing_on_worker");
1295
            }
1296
        };
1297
        Ok(
1298
            matches!(awaited_action.state().stage, ActionStage::Executing)
1299
                && awaited_action.worker_id() == Some(worker_id),
1300
        )
1301
17
    }
1302
}
1303
1304
#[async_trait]
1305
impl<T, I, NowFn> MatchingEngineStateManager for SimpleSchedulerStateManager<T, I, NowFn>
1306
where
1307
    T: AwaitedActionDb,
1308
    I: InstantWrapper,
1309
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
1310
{
1311
    async fn filter_operations<'a>(
1312
        &'a self,
1313
        filter: OperationFilter,
1314
155
    ) -> Result<ActionStateResultStream<'a>, Error> {
1315
72
        self.inner_filter_operations(filter, |rx| {
1316
72
            Box::new(MatchingEngineActionStateResult::new(
1317
72
                rx,
1318
72
                self.weak_self.clone(),
1319
72
                self.no_event_action_timeout,
1320
72
                self.now_fn.clone(),
1321
72
            ))
1322
72
        })
1323
        .await
1324
155
    }
1325
1326
    async fn assign_operation(
1327
        &self,
1328
        operation_id: &OperationId,
1329
        worker_id_or_reason_for_unassign: Result<&WorkerId, Error>,
1330
51
    ) -> Result<(), Error> {
1331
        let (maybe_worker_id, update) = match worker_id_or_reason_for_unassign {
1332
            Ok(worker_id) => (
1333
                Some(worker_id),
1334
                UpdateOperationType::UpdateWithActionStage(ActionStage::Executing),
1335
            ),
1336
            Err(err) => (None, UpdateOperationType::UpdateWithError(err)),
1337
        };
1338
        self.inner_update_operation(operation_id, maybe_worker_id, update)
1339
            .await
1340
51
    }
1341
}