Coverage Report

Created: 2026-07-21 15:28

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::SharedWorkerRegistry;
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
539
    const fn new(
92
539
        sub: U,
93
539
        simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>,
94
539
        no_event_action_timeout: Duration,
95
539
        now_fn: NowFn,
96
539
    ) -> Self {
97
539
        Self {
98
539
            inner: MatchingEngineActionStateResult::new(
99
539
                sub,
100
539
                simple_scheduler_state_manager,
101
539
                no_event_action_timeout,
102
539
                now_fn,
103
539
            ),
104
539
        }
105
539
    }
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
6
    async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
117
        self.inner.as_state().await
118
6
    }
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
599
    const fn new(
149
599
        awaited_action_sub: U,
150
599
        simple_scheduler_state_manager: Weak<SimpleSchedulerStateManager<T, I, NowFn>>,
151
599
        no_event_action_timeout: Duration,
152
599
        now_fn: NowFn,
153
599
    ) -> Self {
154
599
        Self {
155
599
            awaited_action_sub,
156
599
            simple_scheduler_state_manager,
157
599
            no_event_action_timeout,
158
599
            now_fn,
159
599
        }
160
599
    }
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
47
    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
47
    }
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
60
    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
60
    }
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
31
    pub fn new(
328
31
        max_job_retries: usize,
329
31
        no_event_action_timeout: Duration,
330
31
        client_action_timeout: Duration,
331
31
        max_executing_timeout: Duration,
332
31
        action_db: T,
333
31
        now_fn: NowFn,
334
31
        worker_registry: Option<SharedWorkerRegistry>,
335
31
    ) -> Arc<Self> {
336
31
        Arc::new_cyclic(|weak_self| Self {
337
31
            action_db,
338
31
            max_job_retries,
339
31
            no_event_action_timeout,
340
31
            client_action_timeout,
341
31
            max_executing_timeout,
342
31
            timeout_operation_mux: Mutex::new(()),
343
31
            weak_self: weak_self.clone(),
344
31
            now_fn,
345
31
            worker_registry,
346
31
        })
347
31
    }
348
349
3
    pub async fn should_timeout_operation(&self, awaited_action: &AwaitedAction) -> bool {
350
3
        if !
matches!0
(awaited_action.state().stage, ActionStage::Executing) {
351
0
            return false;
352
3
        }
353
354
3
        let now = (self.now_fn)().now();
355
356
        // Honor the per-action `Action.timeout` from the RBE protocol as a
357
        // backend wall-clock deadline. Without this, the only enforcement is
358
        // the Bazel client's --test_timeout, which surfaces as TIMEOUT/NO
359
        // STATUS instead of a backend signal pointing at the worker.
360
3
        let action_timeout = awaited_action.action_info().timeout;
361
3
        if action_timeout > Duration::ZERO {
362
3
            let executing_started_at = awaited_action.state().last_transition_timestamp;
363
3
            if let Ok(elapsed) = now.duration_since(executing_started_at)
364
3
                && elapsed > action_timeout
365
            {
366
1
                return true;
367
2
            }
368
0
        }
369
370
2
        let registry_alive = if let Some(
ref worker_registry1
) = self.worker_registry {
371
1
            if let Some(worker_id) = awaited_action.worker_id() {
372
1
                worker_registry
373
1
                    .is_worker_alive(worker_id, self.no_event_action_timeout, now)
374
1
                    .await
375
            } else {
376
0
                false
377
            }
378
        } else {
379
1
            false
380
        };
381
382
2
        if registry_alive {
383
0
            if self.max_executing_timeout > Duration::ZERO {
384
0
                let last_update = awaited_action.last_worker_updated_timestamp();
385
0
                if let Ok(elapsed) = now.duration_since(last_update) {
386
0
                    return elapsed > self.max_executing_timeout;
387
0
                }
388
0
            }
389
0
            return false;
390
2
        }
391
392
2
        let worker_should_update_before = awaited_action
393
2
            .last_worker_updated_timestamp()
394
2
            .checked_add(self.no_event_action_timeout)
395
2
            .unwrap_or(now);
396
397
2
        worker_should_update_before < now
398
3
    }
399
400
564
    async fn apply_filter_predicate(
401
564
        &self,
402
564
        awaited_action: &AwaitedAction,
403
564
        subscriber: &T::Subscriber,
404
564
        filter: &OperationFilter,
405
564
    ) -> bool {
406
        // Note: The caller must filter `client_operation_id`.
407
408
564
        let mut maybe_reloaded_awaited_action: Option<AwaitedAction> = None;
409
564
        let now = (self.now_fn)().now();
410
411
        // Check if client has timed out
412
564
        if awaited_action.last_client_keepalive_timestamp() + self.client_action_timeout < now {
413
            // This may change if the version is out of date.
414
0
            let mut timed_out = true;
415
0
            if !awaited_action.state().stage.is_finished() {
416
0
                let mut state = awaited_action.state().as_ref().clone();
417
0
                warn!(operation_id = ?awaited_action.operation_id(), timeout_secs = self.client_action_timeout.as_secs_f32(), "Operation timed out having no more clients listening");
418
0
                state.stage = ActionStage::Completed(ActionResult {
419
0
                    error: Some(make_err!(
420
0
                        Code::DeadlineExceeded,
421
0
                        "Operation timed out {} seconds of having no more clients listening",
422
0
                        self.client_action_timeout.as_secs_f32(),
423
0
                    )),
424
0
                    ..ActionResult::default()
425
0
                });
426
0
                state.last_transition_timestamp = now;
427
0
                let state = Arc::new(state);
428
                // We may be competing with an client timestamp update, so try
429
                // this a few times.
430
0
                for attempt in 1..=MAX_UPDATE_RETRIES {
431
0
                    let mut new_awaited_action = match &maybe_reloaded_awaited_action {
432
0
                        None => awaited_action.clone(),
433
0
                        Some(reloaded_awaited_action) => reloaded_awaited_action.clone(),
434
                    };
435
0
                    new_awaited_action.worker_set_state(state.clone(), (self.now_fn)().now());
436
0
                    let err = match self
437
0
                        .action_db
438
0
                        .update_awaited_action(new_awaited_action)
439
0
                        .await
440
                    {
441
0
                        Ok(()) => break,
442
0
                        Err(err) => err,
443
                    };
444
                    // Reload from the database if the action was outdated.
445
0
                    let maybe_awaited_action =
446
0
                        if attempt == MAX_UPDATE_RETRIES || err.code != Code::Aborted {
447
0
                            None
448
                        } else {
449
0
                            subscriber.borrow().await.ok()
450
                        };
451
0
                    if let Some(reloaded_awaited_action) = maybe_awaited_action {
452
0
                        maybe_reloaded_awaited_action = Some(reloaded_awaited_action);
453
0
                    } else {
454
0
                        warn!(
455
                            "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}",
456
                        );
457
0
                        break;
458
                    }
459
                    // Re-check the predicate after reload.
460
0
                    if maybe_reloaded_awaited_action
461
0
                        .as_ref()
462
0
                        .is_some_and(|awaited_action| {
463
0
                            awaited_action.last_client_keepalive_timestamp()
464
0
                                + self.client_action_timeout
465
0
                                >= (self.now_fn)().now()
466
0
                        })
467
                    {
468
0
                        timed_out = false;
469
0
                        break;
470
0
                    } else if maybe_reloaded_awaited_action
471
0
                        .as_ref()
472
0
                        .is_some_and(|awaited_action| awaited_action.state().stage.is_finished())
473
                    {
474
0
                        break;
475
0
                    }
476
                }
477
0
            }
478
0
            if timed_out {
479
0
                return false;
480
0
            }
481
564
        }
482
        // If the action was reloaded, then use that for the rest of the checks
483
        // instead of the input parameter.
484
564
        let awaited_action = maybe_reloaded_awaited_action
485
564
            .as_ref()
486
564
            .unwrap_or(awaited_action);
487
488
564
        if let Some(
operation_id0
) = &filter.operation_id
489
0
            && operation_id != awaited_action.operation_id()
490
        {
491
0
            return false;
492
564
        }
493
494
564
        if filter.worker_id.is_some() && 
filter.worker_id.as_ref()0
!= awaited_action.worker_id() {
495
0
            return false;
496
564
        }
497
498
        {
499
564
            if let Some(
filter_unique_key0
) = &filter.unique_key {
500
0
                match &awaited_action.action_info().unique_qualifier {
501
0
                    ActionUniqueQualifier::Cacheable(unique_key) => {
502
0
                        if filter_unique_key != unique_key {
503
0
                            return false;
504
0
                        }
505
                    }
506
                    ActionUniqueQualifier::Uncacheable(_) => {
507
0
                        return false;
508
                    }
509
                }
510
564
            }
511
564
            if let Some(
action_digest0
) = filter.action_digest
512
0
                && action_digest != awaited_action.action_info().digest()
513
            {
514
0
                return false;
515
564
            }
516
        }
517
518
        {
519
564
            let last_worker_update_timestamp = awaited_action.last_worker_updated_timestamp();
520
564
            if let Some(
worker_update_before0
) = filter.worker_update_before
521
0
                && worker_update_before < last_worker_update_timestamp
522
            {
523
0
                return false;
524
564
            }
525
564
            if let Some(
completed_before0
) = filter.completed_before
526
0
                && awaited_action.state().stage.is_finished()
527
0
                && completed_before < last_worker_update_timestamp
528
            {
529
0
                return false;
530
564
            }
531
564
            if filter.stages != OperationStageFlags::Any {
532
560
                let stage_flag = match awaited_action.state().stage {
533
0
                    ActionStage::Unknown => OperationStageFlags::Any,
534
0
                    ActionStage::CacheCheck => OperationStageFlags::CacheCheck,
535
560
                    ActionStage::Queued => OperationStageFlags::Queued,
536
0
                    ActionStage::Executing => OperationStageFlags::Executing,
537
                    ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => {
538
0
                        OperationStageFlags::Completed
539
                    }
540
                };
541
560
                if !filter.stages.intersects(stage_flag) {
542
0
                    return false;
543
560
                }
544
4
            }
545
        }
546
547
564
        true
548
564
    }
549
550
    /// Let the scheduler know that an operation has timed out from
551
    /// the client side (ie: worker has not updated in a while).
552
1
    async fn timeout_operation_id(&self, operation_id: &OperationId) -> Result<(), Error> {
553
        // Ensure that only one timeout operation is running at a time.
554
        // Failing to do this could result in the same operation being
555
        // timed out multiple times at the same time.
556
        // Note: We could implement this on a per-operation_id basis, but it is quite
557
        // complex to manage the locks.
558
1
        let _lock = self.timeout_operation_mux.lock().await;
559
560
1
        let awaited_action_subscriber = self
561
1
            .action_db
562
1
            .get_by_operation_id(operation_id)
563
1
            .await
564
1
            .err_tip(|| "In SimpleSchedulerStateManager::timeout_operation_id")
?0
565
1
            .err_tip(|| 
{0
566
0
                format!("Operation id {operation_id} does not exist in SimpleSchedulerStateManager::timeout_operation_id")
567
0
            })?;
568
569
1
        let awaited_action = awaited_action_subscriber
570
1
            .borrow()
571
1
            .await
572
1
            .err_tip(|| "In SimpleSchedulerStateManager::timeout_operation_id")
?0
;
573
574
        // If the action is not executing, we should not timeout the action.
575
1
        if !
matches!0
(awaited_action.state().stage, ActionStage::Executing) {
576
0
            return Ok(());
577
1
        }
578
579
1
        let now = (self.now_fn)().now();
580
581
        // Check worker liveness via registry if available.
582
1
        let registry_alive = if let Some(ref worker_registry) = self.worker_registry {
583
1
            if let Some(worker_id) = awaited_action.worker_id() {
584
1
                worker_registry
585
1
                    .is_worker_alive(worker_id, self.no_event_action_timeout, now)
586
1
                    .await
587
            } else {
588
0
                false
589
            }
590
        } else {
591
0
            false
592
        };
593
594
1
        let timestamp_alive = {
595
1
            let worker_should_update_before = awaited_action
596
1
                .last_worker_updated_timestamp()
597
1
                .checked_add(self.no_event_action_timeout)
598
1
                .unwrap_or(now);
599
1
            worker_should_update_before >= now
600
        };
601
602
1
        if registry_alive || timestamp_alive {
603
0
            trace!(
604
                %operation_id,
605
0
                worker_id = ?awaited_action.worker_id(),
606
                registry_alive,
607
                timestamp_alive,
608
                "Worker is alive, operation not timed out"
609
            );
610
0
            return Ok(());
611
1
        }
612
613
1
        warn!(
614
            %operation_id,
615
1
            worker_id = ?awaited_action.worker_id(),
616
            registry_alive,
617
            timestamp_alive,
618
            "Worker not alive via registry or timestamp, timing out operation"
619
        );
620
621
1
        self.assign_operation(
622
1
            operation_id,
623
1
            Err(make_err!(
624
1
                Code::DeadlineExceeded,
625
1
                "Operation timed out after {} seconds",
626
1
                self.no_event_action_timeout.as_secs_f32(),
627
1
            )),
628
1
        )
629
1
        .await
630
1
    }
631
632
61
    async fn inner_update_operation(
633
61
        &self,
634
61
        operation_id: &OperationId,
635
61
        maybe_worker_id: Option<&WorkerId>,
636
61
        update: UpdateOperationType,
637
61
    ) -> Result<(), Error> {
638
61
        let update_type_str = match &update {
639
0
            UpdateOperationType::KeepAlive => "KeepAlive",
640
46
            UpdateOperationType::UpdateWithActionStage(stage) => match stage {
641
0
                ActionStage::Queued => "Stage:Queued",
642
41
                ActionStage::Executing => "Stage:Executing",
643
5
                ActionStage::Completed(_) => "Stage:Completed",
644
0
                ActionStage::CompletedFromCache(_) => "Stage:CompletedFromCache",
645
0
                ActionStage::CacheCheck => "Stage:CacheCheck",
646
0
                ActionStage::Unknown => "Stage:Unknown",
647
            },
648
12
            UpdateOperationType::UpdateWithError(_) => "Error",
649
2
            UpdateOperationType::UpdateWithDisconnect => "Disconnect",
650
1
            UpdateOperationType::ExecutionComplete => "ExecutionComplete",
651
        };
652
653
61
        debug!(
654
            %operation_id,
655
            ?maybe_worker_id,
656
            update_type = %update_type_str,
657
            "inner_update_operation START"
658
        );
659
660
61
        let mut last_err = None;
661
61
        let mut retry_count = 0;
662
61
        for _ in 0..MAX_UPDATE_RETRIES {
663
65
            retry_count += 1;
664
65
            if retry_count > 1 {
665
4
                let base_delay = BASE_RETRY_DELAY_MS * (1 << (retry_count - 2).min(4));
666
4
                let jitter = std::time::SystemTime::now()
667
4
                    .duration_since(std::time::UNIX_EPOCH)
668
4
                    .map_or(0, |d| {
669
4
                        u64::try_from(d.as_nanos()).expect("u64 error") % MAX_RETRY_JITTER_MS
670
4
                    });
671
4
                let delay = Duration::from_millis(base_delay + jitter);
672
673
4
                warn!(
674
                    %operation_id,
675
                    ?maybe_worker_id,
676
                    retry_count,
677
4
                    delay_ms = delay.as_millis(),
678
                    update_type = %update_type_str,
679
                    "Retrying operation update due to version conflict (with backoff)"
680
                );
681
682
4
                tokio::time::sleep(delay).await;
683
61
            }
684
65
            let maybe_awaited_action_subscriber = self
685
65
                .action_db
686
65
                .get_by_operation_id(operation_id)
687
65
                .await
688
65
                .err_tip(|| "In SimpleSchedulerStateManager::update_operation")
?0
;
689
65
            let Some(
awaited_action_subscriber63
) = maybe_awaited_action_subscriber else {
690
                // No action found. It is ok if the action was not found. It
691
                // probably means that the action was dropped, but worker was
692
                // still processing it.
693
2
                warn!(
694
                    %operation_id,
695
                    "Unable to update action due to it being missing, probably dropped"
696
                );
697
2
                return Ok(());
698
            };
699
700
63
            let mut awaited_action = awaited_action_subscriber
701
63
                .borrow()
702
63
                .await
703
63
                .err_tip(|| "In SimpleSchedulerStateManager::update_operation")
?0
;
704
705
            // Make sure the worker id matches the awaited action worker id.
706
            // This might happen if the worker sending the update is not the
707
            // worker that was assigned.
708
63
            if awaited_action.worker_id().is_some()
709
23
                && maybe_worker_id.is_some()
710
22
                && maybe_worker_id != awaited_action.worker_id()
711
            {
712
                // If another worker is already assigned to the action, another
713
                // worker probably picked up the action. We should not update the
714
                // action in this case and abort this operation.
715
0
                let err = make_err!(
716
0
                    Code::Aborted,
717
                    "Worker ids do not match - {:?} != {:?} for {:?}",
718
                    maybe_worker_id,
719
0
                    awaited_action.worker_id(),
720
                    awaited_action,
721
                );
722
0
                info!(
723
                    "Worker ids do not match - {:?} != {:?} for {:?}. This is probably due to another worker picking up the action.",
724
                    maybe_worker_id,
725
0
                    awaited_action.worker_id(),
726
                    awaited_action,
727
                );
728
0
                return Err(err);
729
63
            }
730
731
            // Make sure we don't update an action that is already completed.
732
63
            if awaited_action.state().stage.is_finished() {
733
0
                match &update {
734
                    UpdateOperationType::UpdateWithDisconnect | UpdateOperationType::KeepAlive => {
735
                        // No need to error a keep-alive when it's completed, it's just
736
                        // unnecessary log noise.
737
0
                        return Ok(());
738
                    }
739
                    _ => {
740
0
                        return Err(make_err!(
741
0
                            Code::Internal,
742
0
                            "Action {operation_id} is already completed with state {:?} - maybe_worker_id: {:?}",
743
0
                            awaited_action.state().stage,
744
0
                            maybe_worker_id,
745
0
                        ));
746
                    }
747
                }
748
63
            }
749
750
63
            let 
stage59
= match &update {
751
                UpdateOperationType::KeepAlive => {
752
0
                    awaited_action.worker_keep_alive((self.now_fn)().now());
753
0
                    match self
754
0
                        .action_db
755
0
                        .update_awaited_action(awaited_action)
756
0
                        .await
757
0
                        .err_tip(|| "Failed to send KeepAlive in SimpleSchedulerStateManager::update_operation") {
758
                        // Try again if there was a version mismatch.
759
0
                        Err(err) if err.code == Code::Aborted => {
760
0
                            last_err = Some(err);
761
0
                            continue;
762
                        }
763
0
                        result => return result,
764
                    }
765
                }
766
49
                UpdateOperationType::UpdateWithActionStage(stage) => {
767
49
                    if stage == &ActionStage::Executing
768
44
                        && awaited_action.state().stage == ActionStage::Executing
769
                    {
770
4
                        warn!(state = ?awaited_action.state(), "Action already assigned");
771
4
                        return Err(make_err!(Code::Aborted, "Action already assigned"));
772
45
                    }
773
45
                    stage.clone()
774
                }
775
12
                UpdateOperationType::UpdateWithError(err) => {
776
                    // Don't count a backpressure failure as an attempt for an action.
777
12
                    let due_to_backpressure = err.code == Code::ResourceExhausted;
778
12
                    if !due_to_backpressure {
779
12
                        awaited_action.attempts += 1;
780
12
                    
}0
781
782
12
                    if awaited_action.attempts > self.max_job_retries {
783
2
                        ActionStage::Completed(ActionResult {
784
2
                            execution_metadata: ExecutionMetadata {
785
2
                                worker: maybe_worker_id.map_or_else(String::default, ToString::to_string),
786
2
                                ..ExecutionMetadata::default()
787
2
                            },
788
2
                            error: Some(err.clone().merge(make_err!(
789
2
                                Code::Internal,
790
2
                                "Job cancelled because it attempted to execute too many times {} > {} times {}",
791
2
                                awaited_action.attempts,
792
2
                                self.max_job_retries,
793
2
                                format!("for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"),
794
2
                            ))),
795
2
                            ..ActionResult::default()
796
2
                        })
797
                    } else {
798
10
                        ActionStage::Queued
799
                    }
800
                }
801
                UpdateOperationType::UpdateWithDisconnect => {
802
                    // A worker disconnect (e.g. OOMKill, pod eviction, network
803
                    // drop) used to requeue without counting as an attempt,
804
                    // which let an action that always crashes its worker loop
805
                    // forever until the Bazel client's --test_timeout fired.
806
                    // Count disconnects as attempts so max_job_retries caps the
807
                    // loop and the client sees a backend-attributable error.
808
2
                    awaited_action.attempts += 1;
809
810
2
                    if awaited_action.attempts > self.max_job_retries {
811
1
                        ActionStage::Completed(ActionResult {
812
1
                            execution_metadata: ExecutionMetadata {
813
1
                                worker: maybe_worker_id
814
1
                                    .map_or_else(String::default, ToString::to_string),
815
1
                                ..ExecutionMetadata::default()
816
1
                            },
817
1
                            error: Some(make_err!(
818
1
                                Code::Internal,
819
1
                                "Worker disconnected repeatedly while executing this action ({} > {} attempts); the runner likely OOMKilled or the pod was evicted. {}",
820
1
                                awaited_action.attempts,
821
1
                                self.max_job_retries,
822
1
                                format!(
823
1
                                    "for operation_id: {operation_id}, maybe_worker_id: {maybe_worker_id:?}"
824
1
                                ),
825
1
                            )),
826
1
                            ..ActionResult::default()
827
1
                        })
828
                    } else {
829
1
                        ActionStage::Queued
830
                    }
831
                }
832
                // We shouldn't get here, but we just ignore it if we do.
833
                UpdateOperationType::ExecutionComplete => {
834
0
                    warn!("inner_update_operation got an ExecutionComplete, that's unexpected.");
835
0
                    return Ok(());
836
                }
837
            };
838
59
            let now = (self.now_fn)().now();
839
59
            if 
matches!48
(stage, ActionStage::Queued) {
840
11
                // If the action is queued, we need to unset the worker id regardless of
841
11
                // which worker sent the update.
842
11
                awaited_action.set_worker_id(None, now);
843
48
            } else {
844
48
                awaited_action.set_worker_id(maybe_worker_id.cloned(), now);
845
48
            }
846
59
            awaited_action.worker_set_state(
847
59
                Arc::new(ActionState {
848
59
                    stage,
849
59
                    // Client id is not known here, it is the responsibility of
850
59
                    // the the subscriber impl to replace this with the
851
59
                    // correct client id.
852
59
                    client_operation_id: operation_id.clone(),
853
59
                    action_digest: awaited_action.action_info().digest(),
854
59
                    last_transition_timestamp: now,
855
59
                }),
856
59
                now,
857
            );
858
859
59
            let update_action_result = self
860
59
                .action_db
861
59
                .update_awaited_action(awaited_action.clone())
862
59
                .await
863
59
                .err_tip(|| "In SimpleSchedulerStateManager::update_operation");
864
59
            if let Err(
err5
) = update_action_result {
865
                // We use Aborted to signal that the action was not
866
                // updated due to the data being set was not the latest
867
                // but can be retried.
868
5
                if err.code == Code::Aborted {
869
4
                    debug!(
870
                        %operation_id,
871
                        retry_count,
872
                        update_type = %update_type_str,
873
                        "Version conflict (Aborted), will retry"
874
                    );
875
4
                    last_err = Some(err);
876
4
                    continue;
877
1
                }
878
1
                warn!(
879
                    %operation_id,
880
                    update_type = %update_type_str,
881
                    ?err,
882
                    "inner_update_operation FAILED (non-retryable)"
883
                );
884
1
                return Err(err);
885
54
            }
886
887
            // Record execution metrics after successful state update
888
54
            let action_state = awaited_action.state();
889
54
            let instance_name = awaited_action
890
54
                .action_info()
891
54
                .unique_qualifier
892
54
                .instance_name()
893
54
                .as_str();
894
54
            let worker_id = awaited_action
895
54
                .worker_id()
896
54
                .map(std::string::ToString::to_string);
897
54
            let priority = Some(awaited_action.action_info().priority);
898
899
            // Build base attributes for metrics
900
54
            let mut attrs = nativelink_util::metrics::make_execution_attributes(
901
54
                instance_name,
902
54
                worker_id.as_deref(),
903
54
                priority,
904
            );
905
906
            // Add stage attribute
907
54
            let execution_stage: ExecutionStage = (&action_state.stage).into();
908
54
            attrs.push(KeyValue::new(EXECUTION_STAGE, execution_stage));
909
910
            // Record stage transition
911
54
            EXECUTION_METRICS.execution_stage_transitions.add(1, &attrs);
912
913
            // For completed actions, record the completion count with result
914
54
            match &action_state.stage {
915
8
                ActionStage::Completed(action_result) => {
916
8
                    let result = if action_result.exit_code == 0 {
917
5
                        ExecutionResult::Success
918
                    } else {
919
3
                        ExecutionResult::Failure
920
                    };
921
8
                    attrs.push(KeyValue::new(EXECUTION_RESULT, result));
922
8
                    EXECUTION_METRICS.execution_completed_count.add(1, &attrs);
923
                }
924
0
                ActionStage::CompletedFromCache(_) => {
925
0
                    attrs.push(KeyValue::new(EXECUTION_RESULT, ExecutionResult::CacheHit));
926
0
                    EXECUTION_METRICS.execution_completed_count.add(1, &attrs);
927
0
                }
928
46
                _ => {}
929
            }
930
931
54
            debug!(
932
                %operation_id,
933
                retry_count,
934
                update_type = %update_type_str,
935
                "inner_update_operation SUCCESS"
936
            );
937
54
            return Ok(());
938
        }
939
940
0
        warn!(
941
            %operation_id,
942
            update_type = %update_type_str,
943
            retry_count = MAX_UPDATE_RETRIES,
944
            "inner_update_operation EXHAUSTED all retries"
945
        );
946
0
        Err(last_err.unwrap_or_else(|| {
947
0
            make_err!(
948
0
                Code::Internal,
949
                "Failed to update action after {} retries with no error set",
950
                MAX_UPDATE_RETRIES,
951
            )
952
0
        }))
953
61
    }
954
955
35
    async fn inner_add_operation(
956
35
        &self,
957
35
        new_client_operation_id: OperationId,
958
35
        action_info: Arc<ActionInfo>,
959
35
    ) -> Result<T::Subscriber, Error> {
960
35
        self.action_db
961
35
            .add_action(
962
35
                new_client_operation_id,
963
35
                action_info,
964
35
                self.no_event_action_timeout,
965
35
            )
966
35
            .await
967
35
            .err_tip(|| "In SimpleSchedulerStateManager::add_operation")
968
35
    }
969
970
638
    async fn inner_filter_operations<'a, F>(
971
638
        &'a self,
972
638
        filter: OperationFilter,
973
638
        to_action_state_result: F,
974
638
    ) -> Result<ActionStateResultStream<'a>, Error>
975
638
    where
976
638
        F: Fn(T::Subscriber) -> Box<dyn ActionStateResult> + Send + Sync + 'a,
977
638
    {
978
634
        const fn sorted_awaited_action_state_for_flags(
979
634
            stage: OperationStageFlags,
980
634
        ) -> Option<SortedAwaitedActionState> {
981
634
            match stage {
982
0
                OperationStageFlags::CacheCheck => Some(SortedAwaitedActionState::CacheCheck),
983
632
                OperationStageFlags::Queued => Some(SortedAwaitedActionState::Queued),
984
0
                OperationStageFlags::Executing => Some(SortedAwaitedActionState::Executing),
985
0
                OperationStageFlags::Completed => Some(SortedAwaitedActionState::Completed),
986
2
                _ => None,
987
            }
988
634
        }
989
990
638
        if let Some(
operation_id0
) = &filter.operation_id {
991
0
            let maybe_subscriber = self
992
0
                .action_db
993
0
                .get_by_operation_id(operation_id)
994
0
                .await
995
0
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?;
996
0
            let Some(subscriber) = maybe_subscriber else {
997
0
                return Ok(Box::pin(stream::empty()));
998
            };
999
0
            let awaited_action = subscriber
1000
0
                .borrow()
1001
0
                .await
1002
0
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?;
1003
0
            if !self
1004
0
                .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1005
0
                .await
1006
            {
1007
0
                return Ok(Box::pin(stream::empty()));
1008
0
            }
1009
0
            return Ok(Box::pin(stream::once(async move {
1010
0
                to_action_state_result(subscriber)
1011
0
            })));
1012
638
        }
1013
638
        if let Some(
client_operation_id4
) = &filter.client_operation_id {
1014
4
            let maybe_subscriber = self
1015
4
                .action_db
1016
4
                .get_awaited_action_by_id(client_operation_id)
1017
4
                .await
1018
4
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1019
4
            let Some(subscriber) = maybe_subscriber else {
1020
0
                return Ok(Box::pin(stream::empty()));
1021
            };
1022
4
            let awaited_action = subscriber
1023
4
                .borrow()
1024
4
                .await
1025
4
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1026
4
            if !self
1027
4
                .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1028
4
                .await
1029
            {
1030
0
                return Ok(Box::pin(stream::empty()));
1031
4
            }
1032
4
            return Ok(Box::pin(stream::once(async move {
1033
4
                to_action_state_result(subscriber)
1034
4
            })));
1035
634
        }
1036
1037
632
        let Some(sorted_awaited_action_state) =
1038
634
            sorted_awaited_action_state_for_flags(filter.stages)
1039
        else {
1040
2
            let mut all_items: Vec<_> = self
1041
2
                .action_db
1042
2
                .get_all_awaited_actions()
1043
2
                .await
1044
2
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
1045
2
                .and_then(|awaited_action_subscriber| async move 
{0
1046
0
                    let awaited_action = awaited_action_subscriber
1047
0
                        .borrow()
1048
0
                        .await
1049
0
                        .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")?;
1050
0
                    Ok((awaited_action_subscriber, awaited_action))
1051
0
                })
1052
2
                .try_filter_map(|(subscriber, awaited_action)| 
{0
1053
0
                    let filter = filter.clone();
1054
0
                    async move {
1055
0
                        Ok(self
1056
0
                            .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1057
0
                            .await
1058
0
                            .then_some((subscriber, awaited_action.sort_key())))
1059
0
                    }
1060
0
                })
1061
2
                .try_collect()
1062
2
                .await
1063
2
                .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1064
1065
            #[allow(clippy::unnecessary_sort_by)]
1066
0
            match filter.order_by_priority_direction {
1067
0
                Some(OrderDirection::Asc) => all_items.sort_unstable_by(|(_, a), (_, b)| a.cmp(b)),
1068
0
                Some(OrderDirection::Desc) => all_items.sort_unstable_by(|(_, a), (_, b)| b.cmp(a)),
1069
2
                None => {}
1070
            }
1071
2
            return Ok(Box::pin(stream::iter(
1072
2
                all_items
1073
2
                    .into_iter()
1074
2
                    .map(move |(subscriber, _)| 
to_action_state_result0
(
subscriber0
)),
1075
            )));
1076
        };
1077
1078
632
        let desc = 
matches!500
(
1079
132
            filter.order_by_priority_direction,
1080
            Some(OrderDirection::Desc)
1081
        );
1082
632
        let stream = self
1083
632
            .action_db
1084
632
            .get_range_of_actions(
1085
632
                sorted_awaited_action_state,
1086
632
                Bound::Unbounded,
1087
632
                Bound::Unbounded,
1088
632
                desc,
1089
632
            )
1090
632
            .await
1091
632
            .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
1092
632
            .and_then(|awaited_action_subscriber| async move 
{560
1093
560
                let awaited_action = awaited_action_subscriber
1094
560
                    .borrow()
1095
560
                    .await
1096
560
                    .err_tip(|| "In SimpleSchedulerStateManager::filter_operations")
?0
;
1097
560
                Ok((awaited_action_subscriber, awaited_action))
1098
1.12k
            })
1099
632
            .try_filter_map(move |(subscriber, awaited_action)| 
{560
1100
560
                let filter = filter.clone();
1101
560
                async move {
1102
560
                    Ok(self
1103
560
                        .apply_filter_predicate(&awaited_action, &subscriber, &filter)
1104
560
                        .await
1105
560
                        .then_some(subscriber))
1106
560
                }
1107
560
            })
1108
632
            .map(move |result| -> Box<dyn ActionStateResult> 
{560
1109
560
                result.map_or_else(
1110
0
                    |e| -> Box<dyn ActionStateResult> { Box::new(ErrorActionStateResult(e)) },
1111
560
                    |v| -> Box<dyn ActionStateResult> { to_action_state_result(v) },
1112
                )
1113
560
            });
1114
632
        Ok(Box::pin(stream))
1115
638
    }
1116
}
1117
1118
#[async_trait]
1119
impl<T, I, NowFn> ClientStateManager for SimpleSchedulerStateManager<T, I, NowFn>
1120
where
1121
    T: AwaitedActionDb,
1122
    I: InstantWrapper,
1123
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
1124
{
1125
    async fn add_action(
1126
        &self,
1127
        client_operation_id: OperationId,
1128
        action_info: Arc<ActionInfo>,
1129
35
    ) -> Result<Box<dyn ActionStateResult>, Error> {
1130
        let sub = self
1131
            .inner_add_operation(client_operation_id, action_info.clone())
1132
            .await?;
1133
1134
        Ok(Box::new(ClientActionStateResult::new(
1135
            sub,
1136
            self.weak_self.clone(),
1137
            self.no_event_action_timeout,
1138
            self.now_fn.clone(),
1139
        )))
1140
35
    }
1141
1142
    async fn filter_operations<'a>(
1143
        &'a self,
1144
        filter: OperationFilter,
1145
504
    ) -> Result<ActionStateResultStream<'a>, Error> {
1146
504
        self.inner_filter_operations(filter, move |rx| {
1147
504
            Box::new(ClientActionStateResult::new(
1148
504
                rx,
1149
504
                self.weak_self.clone(),
1150
504
                self.no_event_action_timeout,
1151
504
                self.now_fn.clone(),
1152
504
            ))
1153
504
        })
1154
        .await
1155
504
    }
1156
}
1157
1158
#[async_trait]
1159
impl<T, I, NowFn> WorkerStateManager for SimpleSchedulerStateManager<T, I, NowFn>
1160
where
1161
    T: AwaitedActionDb,
1162
    I: InstantWrapper,
1163
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
1164
{
1165
    async fn update_operation(
1166
        &self,
1167
        operation_id: &OperationId,
1168
        worker_id: &WorkerId,
1169
        update: UpdateOperationType,
1170
19
    ) -> Result<(), Error> {
1171
        self.inner_update_operation(operation_id, Some(worker_id), update)
1172
            .await
1173
19
    }
1174
}
1175
1176
#[async_trait]
1177
impl<T, I, NowFn> MatchingEngineStateManager for SimpleSchedulerStateManager<T, I, NowFn>
1178
where
1179
    T: AwaitedActionDb,
1180
    I: InstantWrapper,
1181
    NowFn: Fn() -> I + Clone + Send + Unpin + Sync + 'static,
1182
{
1183
    async fn filter_operations<'a>(
1184
        &'a self,
1185
        filter: OperationFilter,
1186
134
    ) -> Result<ActionStateResultStream<'a>, Error> {
1187
60
        self.inner_filter_operations(filter, |rx| {
1188
60
            Box::new(MatchingEngineActionStateResult::new(
1189
60
                rx,
1190
60
                self.weak_self.clone(),
1191
60
                self.no_event_action_timeout,
1192
60
                self.now_fn.clone(),
1193
60
            ))
1194
60
        })
1195
        .await
1196
134
    }
1197
1198
    async fn assign_operation(
1199
        &self,
1200
        operation_id: &OperationId,
1201
        worker_id_or_reason_for_unassign: Result<&WorkerId, Error>,
1202
42
    ) -> Result<(), Error> {
1203
        let (maybe_worker_id, update) = match worker_id_or_reason_for_unassign {
1204
            Ok(worker_id) => (
1205
                Some(worker_id),
1206
                UpdateOperationType::UpdateWithActionStage(ActionStage::Executing),
1207
            ),
1208
            Err(err) => (None, UpdateOperationType::UpdateWithError(err)),
1209
        };
1210
        self.inner_update_operation(operation_id, maybe_worker_id, update)
1211
            .await
1212
42
    }
1213
}