Coverage Report

Created: 2026-10-02 11:24

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-scheduler/src/store_awaited_action_db.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::mem::Discriminant;
16
use core::ops::Bound;
17
use core::sync::atomic::{AtomicU64, Ordering};
18
use core::time::Duration;
19
use std::borrow::Cow;
20
use std::sync::{Arc, Weak};
21
use std::time::{SystemTime, UNIX_EPOCH};
22
23
use bytes::Bytes;
24
use futures::{Stream, TryStreamExt};
25
use nativelink_error::{Code, Error, ResultExt, make_err};
26
use nativelink_metric::MetricsComponent;
27
use nativelink_util::action_messages::{
28
    ActionInfo, ActionStage, ActionUniqueQualifier, OperationId,
29
};
30
use nativelink_util::instant_wrapper::InstantWrapper;
31
use nativelink_util::metrics::{EXECUTION_METRICS, EXECUTION_STAGE, ExecutionStage};
32
use nativelink_util::platform_properties::PlatformProperties;
33
use nativelink_util::spawn;
34
use nativelink_util::store_trait::{
35
    FalseValue, SchedulerCurrentVersionProvider, SchedulerIndexProvider, SchedulerStore,
36
    SchedulerStoreDataProvider, SchedulerStoreDecodeTo, SchedulerStoreKeyProvider,
37
    SchedulerSubscription, SchedulerSubscriptionManager, StoreKey, TrueValue,
38
};
39
use nativelink_util::task::JoinHandleDropGuard;
40
use opentelemetry::KeyValue;
41
use serde::{Deserialize, Serialize};
42
use tokio::sync::{Notify, OnceCell};
43
use tracing::{error, warn};
44
45
use crate::awaited_action_db::{
46
    AwaitedAction, AwaitedActionDb, AwaitedActionSubscriber, CLIENT_KEEPALIVE_DURATION,
47
    SortedAwaitedAction, SortedAwaitedActionState,
48
};
49
use crate::worker_registry::{ORPHANED_ACTION_TIMEOUT, SharedWorkerRegistry, WorkerLiveness};
50
51
type ClientOperationId = OperationId;
52
53
/// Maximum number of retries to update client keep alive.
54
const MAX_RETRIES_FOR_CLIENT_KEEPALIVE: u32 = 8;
55
56
/// Use separate non-versioned Redis key for client keepalives.
57
const USE_SEPARATE_CLIENT_KEEPALIVE_KEY: bool = true;
58
59
/// How often the actions in each stage are recounted for
60
/// `execution.active.count`.
61
const ACTIVE_COUNT_REFRESH_INTERVAL: Duration = Duration::from_secs(15);
62
63
/// Every stage the store indexes, with the label it is reported under.
64
const COUNTED_STATES: [(SortedAwaitedActionState, ExecutionStage); 4] = [
65
    (
66
        SortedAwaitedActionState::CacheCheck,
67
        ExecutionStage::CacheCheck,
68
    ),
69
    (SortedAwaitedActionState::Queued, ExecutionStage::Queued),
70
    (
71
        SortedAwaitedActionState::Executing,
72
        ExecutionStage::Executing,
73
    ),
74
    (
75
        SortedAwaitedActionState::Completed,
76
        ExecutionStage::Completed,
77
    ),
78
];
79
80
enum OperationSubscriberState<Sub> {
81
    Unsubscribed,
82
    Subscribed(Sub),
83
}
84
85
pub struct OperationSubscriber<S: SchedulerStore, I: InstantWrapper, NowFn: Fn() -> I> {
86
    maybe_client_operation_id: Option<ClientOperationId>,
87
    subscription_key: OperationIdToAwaitedAction<'static>,
88
    weak_store: Weak<S>,
89
    state: OperationSubscriberState<
90
        <S::SubscriptionManager as SchedulerSubscriptionManager>::Subscription,
91
    >,
92
    last_known_keepalive_ts: AtomicU64,
93
    now_fn: NowFn,
94
    /// The record as a listing read it, when this subscriber came from one.
95
    /// A listing already loads every record it returns, so `borrow` serves
96
    /// this instead of reading the record again; only the client keepalive,
97
    /// kept under its own key, is read, once. The matching pass borrows each
98
    /// queued action two or three times per pass, so without this a pass
99
    /// cost several store round trips per queued action.
100
    listed: Option<AwaitedAction>,
101
    /// `listed` with the client keepalive merged in, filled on first borrow.
102
    listed_with_keepalive: OnceCell<AwaitedAction>,
103
    // If the SchedulerSubscriptionManager is not reliable, then this is populated
104
    // when the state is set to subscribed.  When set it causes the state to be polled
105
    // as well as listening for the publishing.
106
    maybe_last_stage: Option<Discriminant<ActionStage>>,
107
    retain_completed_for: Duration,
108
    /// How long a written client keepalive stays meaningful. Past it, an
109
    /// absent key and a stale one say the same thing, so the key may as
110
    /// well be gone.
111
    client_keepalive_ttl: Duration,
112
}
113
114
impl<S: SchedulerStore, I: InstantWrapper, NowFn: Fn() -> I + core::fmt::Debug> core::fmt::Debug
115
    for OperationSubscriber<S, I, NowFn>
116
where
117
    OperationSubscriberState<
118
        <S::SubscriptionManager as SchedulerSubscriptionManager>::Subscription,
119
    >: core::fmt::Debug,
120
{
121
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
122
0
        f.debug_struct("OperationSubscriber")
123
0
            .field("maybe_client_operation_id", &self.maybe_client_operation_id)
124
0
            .field("subscription_key", &self.subscription_key)
125
0
            .field("weak_store", &self.weak_store)
126
0
            .field("state", &self.state)
127
0
            .field("last_known_keepalive_ts", &self.last_known_keepalive_ts)
128
0
            .field("now_fn", &self.now_fn)
129
0
            .finish()
130
0
    }
131
}
132
impl<S, I, NowFn> OperationSubscriber<S, I, NowFn>
133
where
134
    S: SchedulerStore,
135
    I: InstantWrapper,
136
    NowFn: Fn() -> I,
137
{
138
77
    const fn new(
139
77
        maybe_client_operation_id: Option<ClientOperationId>,
140
77
        subscription_key: OperationIdToAwaitedAction<'static>,
141
77
        weak_store: Weak<S>,
142
77
        now_fn: NowFn,
143
77
        retain_completed_for: Duration,
144
77
        client_keepalive_ttl: Duration,
145
77
    ) -> Self {
146
77
        Self {
147
77
            maybe_client_operation_id,
148
77
            subscription_key,
149
77
            weak_store,
150
77
            last_known_keepalive_ts: AtomicU64::new(0),
151
77
            state: OperationSubscriberState::Unsubscribed,
152
77
            now_fn,
153
77
            listed: None,
154
77
            listed_with_keepalive: OnceCell::const_new(),
155
77
            maybe_last_stage: None,
156
77
            retain_completed_for,
157
77
            client_keepalive_ttl,
158
77
        }
159
77
    }
160
161
    /// Serves `borrow` from the record a listing already read.
162
34
    fn listed(mut self, awaited_action: AwaitedAction) -> Self {
163
34
        self.listed = Some(awaited_action);
164
34
        self
165
34
    }
166
167
47
    async fn inner_get_awaited_action(
168
47
        store: &S,
169
47
        key: OperationIdToAwaitedAction<'_>,
170
47
        maybe_client_operation_id: Option<ClientOperationId>,
171
47
        last_known_keepalive_ts: &AtomicU64,
172
47
    ) -> Result<AwaitedAction, Error> {
173
47
        let mut awaited_action = store
174
47
            .get_and_decode(key.borrow())
175
47
            .await
176
47
            .err_tip(|| 
format!0
("In OperationSubscriber::get_awaited_action {key:?}"))
?0
177
47
            .ok_or_else(|| 
{0
178
0
                make_err!(
179
0
                    Code::NotFound,
180
                    "Could not find AwaitedAction for the given operation id {key:?}",
181
                )
182
0
            })?;
183
47
        if let Some(
client_operation_id21
) = maybe_client_operation_id {
184
21
            awaited_action.set_client_operation_id(client_operation_id);
185
26
        }
186
47
        Self::merge_client_keepalive(
187
47
            store,
188
47
            key.0.as_ref(),
189
47
            &mut awaited_action,
190
47
            last_known_keepalive_ts,
191
47
        )
192
47
        .await;
193
47
        Ok(awaited_action)
194
47
    }
195
196
    /// Folds the client keepalive kept under its own key into the action's
197
    /// timestamp and remembers it.
198
81
    async fn merge_client_keepalive(
199
81
        store: &S,
200
81
        operation_id: &OperationId,
201
81
        awaited_action: &mut AwaitedAction,
202
81
        last_known_keepalive_ts: &AtomicU64,
203
81
    ) {
204
        // Helper to convert SystemTime to unix timestamp
205
81
        let to_unix_ts =
206
81
            |t: SystemTime| -> u64 { t.duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs()) };
207
208
        // Check the separate keepalive key for the most recent timestamp.
209
81
        let keepalive_ts = if USE_SEPARATE_CLIENT_KEEPALIVE_KEY {
210
81
            match store.get_and_decode(ClientKeepaliveKey(operation_id)).await {
211
0
                Ok(Some(ts)) => {
212
0
                    let awaited_ts = to_unix_ts(awaited_action.last_client_keepalive_timestamp());
213
0
                    if ts > awaited_ts {
214
0
                        let timestamp = UNIX_EPOCH + Duration::from_secs(ts);
215
0
                        awaited_action.update_client_keep_alive(timestamp);
216
0
                        ts
217
                    } else {
218
0
                        awaited_ts
219
                    }
220
                }
221
81
                Ok(None) | Err(_) => to_unix_ts(awaited_action.last_client_keepalive_timestamp()),
222
            }
223
        } else {
224
0
            to_unix_ts(awaited_action.last_client_keepalive_timestamp())
225
        };
226
227
81
        last_known_keepalive_ts.store(keepalive_ts, Ordering::Release);
228
81
    }
229
230
    #[expect(clippy::future_not_send)] // TODO(jhpratt) remove this
231
126
    async fn get_awaited_action(&self) -> Result<AwaitedAction, Error> {
232
126
        let store = self
233
126
            .weak_store
234
126
            .upgrade()
235
126
            .err_tip(|| "Store gone in OperationSubscriber::get_awaited_action")
?0
;
236
126
        if let Some(
listed81
) = &self.listed {
237
81
            let merged = self
238
81
                .listed_with_keepalive
239
81
                .get_or_init(|| async 
{34
240
34
                    let mut awaited_action = listed.clone();
241
34
                    Self::merge_client_keepalive(
242
34
                        store.as_ref(),
243
34
                        self.subscription_key.0.as_ref(),
244
34
                        &mut awaited_action,
245
34
                        &self.last_known_keepalive_ts,
246
34
                    )
247
34
                    .await;
248
34
                    awaited_action
249
68
                })
250
81
                .await;
251
81
            return Ok(merged.clone());
252
45
        }
253
45
        Self::inner_get_awaited_action(
254
45
            store.as_ref(),
255
45
            self.subscription_key.borrow(),
256
45
            self.maybe_client_operation_id.clone(),
257
45
            &self.last_known_keepalive_ts,
258
45
        )
259
45
        .await
260
126
    }
261
}
262
263
impl<S, I, NowFn> AwaitedActionSubscriber for OperationSubscriber<S, I, NowFn>
264
where
265
    S: SchedulerStore,
266
    I: InstantWrapper,
267
    NowFn: Fn() -> I + Send + Sync + 'static,
268
{
269
2
    async fn changed(&mut self) -> Result<AwaitedAction, Error> {
270
2
        let store = self
271
2
            .weak_store
272
2
            .upgrade()
273
2
            .err_tip(|| "Store gone in OperationSubscriber::get_awaited_action")
?0
;
274
2
        let 
subscription1
= match &mut self.state {
275
1
            OperationSubscriberState::Subscribed(subscription) => subscription,
276
            OperationSubscriberState::Unsubscribed => {
277
1
                let subscription = store
278
1
                    .subscription_manager()
279
1
                    .await
280
1
                    .err_tip(|| "In OperationSubscriber::changed::subscription_manager")
?0
281
1
                    .subscribe(self.subscription_key.borrow())
282
1
                    .err_tip(|| "In OperationSubscriber::changed::subscribe")
?0
;
283
1
                self.state = OperationSubscriberState::Subscribed(subscription);
284
                // When we've just subscribed, there may have been changes before now.
285
1
                let action = Self::inner_get_awaited_action(
286
1
                    store.as_ref(),
287
1
                    self.subscription_key.borrow(),
288
1
                    self.maybe_client_operation_id.clone(),
289
1
                    &self.last_known_keepalive_ts,
290
1
                )
291
1
                .await
292
1
                .err_tip(|| "In OperationSubscriber::changed")
?0
;
293
1
                if !<S as SchedulerStore>::SubscriptionManager::is_reliable() {
294
1
                    self.maybe_last_stage = Some(core::mem::discriminant(&action.state().stage));
295
1
                
}0
296
                // Existing changes are only interesting if the state is past queued.
297
1
                if !matches!(action.state().stage, ActionStage::Queued) {
298
1
                    return Ok(action);
299
0
                }
300
0
                let OperationSubscriberState::Subscribed(subscription) = &mut self.state else {
301
0
                    unreachable!("Subscription should be in Subscribed state");
302
                };
303
0
                subscription
304
            }
305
        };
306
307
1
        let changed_fut = subscription.changed();
308
1
        tokio::pin!(changed_fut);
309
        loop {
310
            // This is set if the maybe_last_state doesn't match the state in the store.
311
1
            let mut maybe_changed_action = None;
312
313
1
            let last_known_keepalive_ts = self.last_known_keepalive_ts.load(Ordering::Acquire);
314
            // Only a subscriber that stands for a client may say a client is
315
            // still there. The matching engine subscribes to every queued
316
            // action it considers, and `get_range_of_actions` builds those
317
            // subscribers with no client operation id precisely because there
318
            // is no client behind them. Letting them write the keepalive
319
            // makes the scheduler hold open the actions it is supposed to be
320
            // retiring: the keepalive is refreshed, the client timeout in
321
            // `apply_filter_predicate` never comes due, and the action is
322
            // offered to the matcher again, which refreshes it again.
323
            //
324
            // The effect is that every queued action carries a keepalive
325
            // only seconds old however long its client has been gone, and
326
            // none of them are ever retired.
327
1
            if self.maybe_client_operation_id.is_some()
328
1
                && I::from_secs(last_known_keepalive_ts).elapsed() > CLIENT_KEEPALIVE_DURATION
329
            {
330
0
                let now = (self.now_fn)().now();
331
0
                let now_ts = now.duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs());
332
333
0
                if USE_SEPARATE_CLIENT_KEEPALIVE_KEY {
334
0
                    let operation_id = self.subscription_key.0.as_ref();
335
0
                    let update_result = store
336
0
                        .update_data(
337
0
                            UpdateClientKeepalive {
338
0
                                operation_id,
339
0
                                timestamp: now_ts,
340
0
                            },
341
0
                            // Written with no expiry these outlive every
342
0
                            // action that produced them and accumulate
343
0
                            // without bound. A keepalive older than the
344
0
                            // client timeout cannot change any decision,
345
0
                            // because the timeout has already come due and
346
0
                            // the stored timestamp is consulted either way.
347
0
                            Some(self.client_keepalive_ttl),
348
0
                        )
349
0
                        .await;
350
351
0
                    if let Err(e) = update_result {
352
0
                        warn!(
353
                            ?self.subscription_key,
354
                            ?e,
355
                            "Failed to update client keepalive (non-versioned)"
356
                        );
357
0
                    }
358
359
                    // Update local timestamp
360
0
                    self.last_known_keepalive_ts
361
0
                        .store(now_ts, Ordering::Release);
362
363
                    // Check if state changed (for unreliable subscription managers)
364
0
                    if self.maybe_last_stage.is_some() {
365
0
                        let awaited_action = Self::inner_get_awaited_action(
366
0
                            store.as_ref(),
367
0
                            self.subscription_key.borrow(),
368
0
                            self.maybe_client_operation_id.clone(),
369
0
                            &self.last_known_keepalive_ts,
370
0
                        )
371
0
                        .await
372
0
                        .err_tip(|| "In OperationSubscriber::changed")?;
373
374
0
                        if self.maybe_last_stage.as_ref().is_some_and(|last_stage| {
375
0
                            *last_stage != core::mem::discriminant(&awaited_action.state().stage)
376
0
                        }) {
377
0
                            maybe_changed_action = Some(awaited_action);
378
0
                        }
379
0
                    }
380
                } else {
381
0
                    for attempt in 1..=MAX_RETRIES_FOR_CLIENT_KEEPALIVE {
382
0
                        if attempt > 1 {
383
0
                            (self.now_fn)().sleep(Duration::from_millis(100)).await;
384
0
                            warn!(
385
                                ?self.subscription_key,
386
                                attempt,
387
                                "Client keepalive retry due to version conflict"
388
                            );
389
0
                        }
390
0
                        let mut awaited_action = Self::inner_get_awaited_action(
391
0
                            store.as_ref(),
392
0
                            self.subscription_key.borrow(),
393
0
                            self.maybe_client_operation_id.clone(),
394
0
                            &self.last_known_keepalive_ts,
395
0
                        )
396
0
                        .await
397
0
                        .err_tip(|| "In OperationSubscriber::changed")?;
398
0
                        awaited_action.update_client_keep_alive(now);
399
0
                        maybe_changed_action = self
400
0
                            .maybe_last_stage
401
0
                            .as_ref()
402
0
                            .is_some_and(|last_stage| {
403
0
                                *last_stage
404
0
                                    != core::mem::discriminant(&awaited_action.state().stage)
405
0
                            })
406
0
                            .then(|| awaited_action.clone());
407
0
                        let expiry = if awaited_action.is_complete() {
408
0
                            Some(self.retain_completed_for)
409
                        } else {
410
0
                            None
411
                        };
412
0
                        match inner_update_awaited_action(store.as_ref(), awaited_action, expiry)
413
0
                            .await
414
                        {
415
0
                            Ok(()) => break,
416
0
                            err if attempt == MAX_RETRIES_FOR_CLIENT_KEEPALIVE => {
417
0
                                err.err_tip_with_code(|_| {
418
0
                                    (Code::Aborted, "Could not update client keep alive")
419
0
                                })?;
420
                            }
421
0
                            _ => (),
422
                        }
423
                    }
424
                }
425
1
            }
426
427
            // If the polling shows that it's changed state then publish now.
428
1
            if let Some(
changed_action0
) = maybe_changed_action {
429
0
                self.maybe_last_stage =
430
0
                    Some(core::mem::discriminant(&changed_action.state().stage));
431
0
                return Ok(changed_action);
432
1
            }
433
            // Determine the sleep time based on the last client keep alive.
434
1
            let sleep_time = CLIENT_KEEPALIVE_DURATION
435
1
                .checked_sub(
436
1
                    I::from_secs(self.last_known_keepalive_ts.load(Ordering::Acquire)).elapsed(),
437
                )
438
1
                .unwrap_or(Duration::from_millis(100));
439
1
            tokio::select! {
440
1
                result = &mut changed_fut => {
441
1
                    result
?0
;
442
1
                    break;
443
                }
444
1
                () = (self.now_fn)().sleep(sleep_time) => {
445
0
                    // If we haven't received any updates for a while, we should
446
0
                    // let the database know that we are still listening to prevent
447
0
                    // the action from being dropped.  Also poll for updates if the
448
0
                    // subscription manager is unreliable.
449
0
                }
450
            }
451
        }
452
453
1
        let awaited_action = Self::inner_get_awaited_action(
454
1
            store.as_ref(),
455
1
            self.subscription_key.borrow(),
456
1
            self.maybe_client_operation_id.clone(),
457
1
            &self.last_known_keepalive_ts,
458
1
        )
459
1
        .await
460
1
        .err_tip(|| "In OperationSubscriber::changed")
?0
;
461
1
        if self.maybe_last_stage.is_some() {
462
1
            self.maybe_last_stage = Some(core::mem::discriminant(&awaited_action.state().stage));
463
1
        
}0
464
1
        Ok(awaited_action)
465
2
    }
466
467
126
    async fn borrow(&self) -> Result<AwaitedAction, Error> {
468
126
        self.get_awaited_action()
469
126
            .await
470
126
            .err_tip(|| "In OperationSubscriber::borrow")
471
126
    }
472
}
473
474
93
fn awaited_action_decode(version: i64, data: &Bytes) -> Result<AwaitedAction, Error> {
475
93
    let mut awaited_action: AwaitedAction = serde_json::from_slice(data).map_err(|e| 
{0
476
0
        Error::from_std_err(Code::InvalidArgument, &e).append("In AwaitedAction::decode")
477
0
    })?;
478
93
    awaited_action.set_version(version);
479
93
    Ok(awaited_action)
480
93
}
481
482
const OPERATION_ID_TO_AWAITED_ACTION_KEY_PREFIX: &str = "aa_";
483
const CLIENT_ID_TO_OPERATION_ID_KEY_PREFIX: &str = "cid_";
484
/// TTL bounding the cid_* mapping's lifetime so it cannot outlive its
485
/// aa_* key and accumulate as a permanent orphan (24h safely exceeds
486
/// any real action lifetime).
487
const CLIENT_ID_MAPPING_TTL: Duration = Duration::from_hours(24);
488
/// Phase 2: Separate key prefix for client keepalives (non-versioned).
489
const CLIENT_KEEPALIVE_KEY_PREFIX: &str = "ck_";
490
/// One record per scheduler holding what its connected workers can run
491
/// (non-versioned, expires on its own).
492
const FLEET_CAPABILITIES_KEY_PREFIX: &str = "fc_";
493
/// Index over every fleet capabilities record. The value is the same for
494
/// all of them; the index exists so peers can be listed.
495
const FLEET_CAPABILITIES_INDEX_NAME: &str = "fleet";
496
497
#[derive(Debug)]
498
struct OperationIdToAwaitedAction<'a>(Cow<'a, OperationId>);
499
impl OperationIdToAwaitedAction<'_> {
500
95
    fn borrow(&self) -> OperationIdToAwaitedAction<'_> {
501
95
        OperationIdToAwaitedAction(Cow::Borrowed(self.0.as_ref()))
502
95
    }
503
}
504
impl SchedulerStoreKeyProvider for OperationIdToAwaitedAction<'_> {
505
    type Versioned = TrueValue;
506
101
    fn get_key(&self) -> StoreKey<'static> {
507
101
        StoreKey::Str(Cow::Owned(format!(
508
101
            "{OPERATION_ID_TO_AWAITED_ACTION_KEY_PREFIX}{}",
509
101
            self.0
510
101
        )))
511
101
    }
512
}
513
impl SchedulerStoreDecodeTo for OperationIdToAwaitedAction<'_> {
514
    type DecodeOutput = AwaitedAction;
515
48
    fn decode(version: i64, data: Bytes) -> Result<Self::DecodeOutput, Error> {
516
48
        awaited_action_decode(version, &data)
517
48
    }
518
}
519
520
struct ClientIdToOperationId<'a>(&'a OperationId);
521
impl SchedulerStoreKeyProvider for ClientIdToOperationId<'_> {
522
    type Versioned = FalseValue;
523
18
    fn get_key(&self) -> StoreKey<'static> {
524
18
        StoreKey::Str(Cow::Owned(format!(
525
18
            "{CLIENT_ID_TO_OPERATION_ID_KEY_PREFIX}{}",
526
18
            self.0
527
18
        )))
528
18
    }
529
}
530
impl SchedulerStoreDecodeTo for ClientIdToOperationId<'_> {
531
    type DecodeOutput = OperationId;
532
2
    fn decode(_version: i64, data: Bytes) -> Result<Self::DecodeOutput, Error> {
533
2
        serde_json::from_slice(&data).map_err(|e| 
{0
534
0
            Error::from_std_err(Code::InvalidArgument, &e).append(format!(
535
                "In ClientIdToOperationId::decode (data: {data:02x?})",
536
            ))
537
0
        })
538
2
    }
539
}
540
541
struct ClientKeepaliveKey<'a>(&'a OperationId);
542
impl SchedulerStoreKeyProvider for ClientKeepaliveKey<'_> {
543
    type Versioned = FalseValue;
544
81
    fn get_key(&self) -> StoreKey<'static> {
545
81
        StoreKey::Str(Cow::Owned(format!(
546
81
            "{CLIENT_KEEPALIVE_KEY_PREFIX}{}",
547
81
            self.0
548
81
        )))
549
81
    }
550
}
551
impl SchedulerStoreDecodeTo for ClientKeepaliveKey<'_> {
552
    type DecodeOutput = u64;
553
0
    fn decode(_version: i64, data: Bytes) -> Result<Self::DecodeOutput, Error> {
554
0
        let s = core::str::from_utf8(&data).map_err(|e| {
555
0
            Error::from_std_err(Code::InvalidArgument, &e)
556
0
                .append("In ClientKeepaliveKey::decode utf8")
557
0
        })?;
558
0
        s.parse::<u64>().map_err(|e| {
559
0
            Error::from_std_err(Code::InvalidArgument, &e)
560
0
                .append("In ClientKeepaliveKey::decode parse")
561
0
        })
562
0
    }
563
}
564
565
struct UpdateClientKeepalive<'a> {
566
    operation_id: &'a OperationId,
567
    timestamp: u64,
568
}
569
impl SchedulerStoreKeyProvider for UpdateClientKeepalive<'_> {
570
    type Versioned = FalseValue;
571
0
    fn get_key(&self) -> StoreKey<'static> {
572
0
        ClientKeepaliveKey(self.operation_id).get_key()
573
0
    }
574
}
575
impl SchedulerStoreDataProvider for UpdateClientKeepalive<'_> {
576
0
    fn try_into_bytes(self) -> Result<Bytes, Error> {
577
0
        Ok(Bytes::from(self.timestamp.to_string()))
578
0
    }
579
}
580
581
/// What one scheduler's workers can run, as published to its peers.
582
#[derive(Serialize, Deserialize)]
583
struct FleetCapabilities {
584
    scheduler_id: String,
585
    workers: Vec<PlatformProperties>,
586
}
587
588
struct UpdateFleetCapabilities(FleetCapabilities);
589
impl SchedulerStoreKeyProvider for UpdateFleetCapabilities {
590
    type Versioned = FalseValue;
591
58
    fn get_key(&self) -> StoreKey<'static> {
592
58
        StoreKey::Str(Cow::Owned(format!(
593
58
            "{FLEET_CAPABILITIES_KEY_PREFIX}{}",
594
58
            self.0.scheduler_id
595
58
        )))
596
58
    }
597
}
598
impl SchedulerStoreDataProvider for UpdateFleetCapabilities {
599
58
    fn try_into_bytes(self) -> Result<Bytes, Error> {
600
58
        serde_json::to_string(&self.0)
601
58
            .map(Bytes::from)
602
58
            .map_err(|e| 
{0
603
0
                Error::from_std_err(Code::InvalidArgument, &e)
604
0
                    .append("Could not convert FleetCapabilities to json")
605
0
            })
606
58
    }
607
58
    fn get_indexes(&self) -> Result<Vec<(&'static str, Bytes)>, Error> {
608
58
        Ok(vec![(
609
58
            FLEET_CAPABILITIES_INDEX_NAME,
610
58
            Bytes::from_static(b"1"),
611
58
        )])
612
58
    }
613
}
614
615
struct SearchFleetCapabilities;
616
impl SchedulerIndexProvider for SearchFleetCapabilities {
617
    const KEY_PREFIX: &'static str = FLEET_CAPABILITIES_KEY_PREFIX;
618
    const INDEX_NAME: &'static str = FLEET_CAPABILITIES_INDEX_NAME;
619
    type Versioned = FalseValue;
620
58
    fn index_value(&self) -> Cow<'_, str> {
621
58
        Cow::Borrowed("1")
622
58
    }
623
}
624
impl SchedulerStoreDecodeTo for SearchFleetCapabilities {
625
    type DecodeOutput = FleetCapabilities;
626
100
    fn decode(_version: i64, data: Bytes) -> Result<Self::DecodeOutput, Error> {
627
100
        serde_json::from_slice(&data).map_err(|e| 
{0
628
0
            Error::from_std_err(Code::Internal, &e).append("In SearchFleetCapabilities::decode")
629
0
        })
630
100
    }
631
}
632
633
// TODO(palfrey) We only need operation_id here, it would be nice if we had a way
634
// to tell the decoder we only care about specific fields.
635
struct SearchUniqueQualifierToAwaitedAction<'a>(&'a ActionUniqueQualifier);
636
impl SchedulerIndexProvider for SearchUniqueQualifierToAwaitedAction<'_> {
637
    const KEY_PREFIX: &'static str = OPERATION_ID_TO_AWAITED_ACTION_KEY_PREFIX;
638
    const INDEX_NAME: &'static str = "unique_qualifier";
639
    type Versioned = TrueValue;
640
27
    fn index_value(&self) -> Cow<'_, str> {
641
27
        Cow::Owned(format!("{}", self.0))
642
27
    }
643
}
644
impl SchedulerStoreDecodeTo for SearchUniqueQualifierToAwaitedAction<'_> {
645
    type DecodeOutput = AwaitedAction;
646
11
    fn decode(version: i64, data: Bytes) -> Result<Self::DecodeOutput, Error> {
647
11
        awaited_action_decode(version, &data)
648
11
    }
649
}
650
651
struct SearchStateToAwaitedAction(&'static str);
652
impl SchedulerIndexProvider for SearchStateToAwaitedAction {
653
    const KEY_PREFIX: &'static str = OPERATION_ID_TO_AWAITED_ACTION_KEY_PREFIX;
654
    const INDEX_NAME: &'static str = "state";
655
    const MAYBE_SORT_KEY: Option<&'static str> = Some("sort_key");
656
    // The matcher walks the queue highest priority first, oldest first,
657
    // which is descending over the packed sort key (see
658
    // `AwaitedActionSortKey`). The memory backend does the same with
659
    // `.rev()` over its `BTreeSet`.
660
    const SORT_DESCENDING: bool = true;
661
    type Versioned = TrueValue;
662
41
    fn index_value(&self) -> Cow<'_, str> {
663
41
        Cow::Borrowed(self.0)
664
41
    }
665
}
666
impl SchedulerStoreDecodeTo for SearchStateToAwaitedAction {
667
    type DecodeOutput = AwaitedAction;
668
34
    fn decode(version: i64, data: Bytes) -> Result<Self::DecodeOutput, Error> {
669
34
        awaited_action_decode(version, &data)
670
34
    }
671
}
672
673
/// Counts the actions in a state. Uses the same index as
674
/// [`SearchStateToAwaitedAction`], but only ever as a `count_by_index_prefix`
675
/// argument, so the store returns a total and never reads the actions.
676
struct CountActionsInState(&'static str);
677
impl SchedulerIndexProvider for CountActionsInState {
678
    const KEY_PREFIX: &'static str = OPERATION_ID_TO_AWAITED_ACTION_KEY_PREFIX;
679
    const INDEX_NAME: &'static str = "state";
680
    const MAYBE_SORT_KEY: Option<&'static str> = Some("sort_key");
681
    type Versioned = TrueValue;
682
0
    fn index_value(&self) -> Cow<'_, str> {
683
0
        Cow::Borrowed(self.0)
684
0
    }
685
}
686
687
92
const fn get_state_prefix(state: SortedAwaitedActionState) -> &'static str {
688
92
    match state {
689
3
        SortedAwaitedActionState::CacheCheck => "cache_check",
690
67
        SortedAwaitedActionState::Queued => "queued",
691
17
        SortedAwaitedActionState::Executing => "executing",
692
5
        SortedAwaitedActionState::Completed => "completed",
693
    }
694
92
}
695
696
/// Reports how many actions the store holds in each stage as
697
/// `execution.active.count`.
698
///
699
/// The store is shared, so every scheduler replica reports the same totals:
700
/// aggregate across replicas with `max`, not `sum`. Recording the change since
701
/// the last pass, rather than adding and subtracting per transition, keeps the
702
/// count right across restarts and for transitions another replica made.
703
3
async fn report_active_counts<S: SchedulerStore>(
704
3
    store: &S,
705
3
    reported: &mut [Option<i64>; COUNTED_STATES.len()],
706
3
) {
707
12
    for ((state, stage), last) in 
COUNTED_STATES3
.
iter3
().
zip3
(
reported3
.
iter_mut3
()) {
708
12
        let count = store
709
12
            .count_by_index_prefix(CountActionsInState(get_state_prefix(*state)))
710
12
            .await
711
12
            .map(|count| i64::try_from(count).unwrap_or(i64::MAX));
712
12
        match count {
713
            // The first pass records even a zero, so every stage has a series
714
            // and an empty queue reads as 0 rather than no data.
715
12
            Ok(
count4
) if *last != Some(count
)4
=> {
716
4
                EXECUTION_METRICS.execution_active_count.add(
717
4
                    count - last.unwrap_or(0),
718
4
                    &[KeyValue::new(EXECUTION_STAGE, *stage)],
719
4
                );
720
4
                *last = Some(count);
721
4
            }
722
8
            Ok(_) => {}
723
0
            Err(err) => warn!(
724
                ?err,
725
                ?stage,
726
                "Failed to count actions for execution.active.count"
727
            ),
728
        }
729
    }
730
3
}
731
732
#[derive(Debug)]
733
pub struct UpdateOperationIdToAwaitedAction(AwaitedAction);
734
impl SchedulerCurrentVersionProvider for UpdateOperationIdToAwaitedAction {
735
38
    fn current_version(&self) -> i64 {
736
38
        self.0.version()
737
38
    }
738
}
739
impl SchedulerStoreKeyProvider for UpdateOperationIdToAwaitedAction {
740
    type Versioned = TrueValue;
741
38
    fn get_key(&self) -> StoreKey<'static> {
742
38
        OperationIdToAwaitedAction(Cow::Borrowed(self.0.operation_id())).get_key()
743
38
    }
744
}
745
impl SchedulerStoreDataProvider for UpdateOperationIdToAwaitedAction {
746
39
    fn try_into_bytes(self) -> Result<Bytes, Error> {
747
39
        serde_json::to_string(&self.0)
748
39
            .map(Bytes::from)
749
39
            .map_err(|e| 
{0
750
0
                Error::from_std_err(Code::InvalidArgument, &e)
751
0
                    .append("Could not convert AwaitedAction to json")
752
0
            })
753
39
    }
754
38
    fn get_indexes(&self) -> Result<Vec<(&'static str, Bytes)>, Error> {
755
38
        let unique_qualifier = &self.0.action_info().unique_qualifier;
756
38
        let maybe_unique_qualifier = match &unique_qualifier {
757
38
            ActionUniqueQualifier::Cacheable(_) => Some(unique_qualifier),
758
0
            ActionUniqueQualifier::Uncacheable(_) => None,
759
        };
760
38
        let mut output = Vec::with_capacity(2 + maybe_unique_qualifier.map_or(0, |_| 1));
761
38
        if maybe_unique_qualifier.is_some() {
762
38
            output.push((
763
38
                "unique_qualifier",
764
38
                Bytes::from(unique_qualifier.to_string()),
765
38
            ));
766
38
        
}0
767
        {
768
38
            let state = SortedAwaitedActionState::try_from(&self.0.state().stage)
769
38
                .err_tip(|| "In UpdateOperationIdToAwaitedAction::get_index")
?0
;
770
38
            output.push(("state", Bytes::from(get_state_prefix(state))));
771
38
            let sorted_awaited_action = SortedAwaitedAction::from(&self.0);
772
38
            output.push((
773
38
                "sort_key",
774
38
                // We encode to fixed-width hex to ensure that the sort key is
775
38
                // lexicographically sorted.
776
38
                Bytes::from(sorted_awaited_action.sort_key.index_field()),
777
38
            ));
778
        }
779
38
        Ok(output)
780
38
    }
781
}
782
783
struct UpdateClientIdToOperationId {
784
    client_operation_id: ClientOperationId,
785
    operation_id: OperationId,
786
}
787
impl SchedulerStoreKeyProvider for UpdateClientIdToOperationId {
788
    type Versioned = FalseValue;
789
16
    fn get_key(&self) -> StoreKey<'static> {
790
16
        ClientIdToOperationId(&self.client_operation_id).get_key()
791
16
    }
792
}
793
impl SchedulerStoreDataProvider for UpdateClientIdToOperationId {
794
16
    fn try_into_bytes(self) -> Result<Bytes, Error> {
795
16
        serde_json::to_string(&self.operation_id)
796
16
            .map(Bytes::from)
797
16
            .map_err(|e| 
{0
798
0
                Error::from_std_err(Code::InvalidArgument, &e)
799
0
                    .append("Could not convert OperationId to json")
800
0
            })
801
16
    }
802
}
803
804
23
pub async fn inner_update_awaited_action(
805
23
    store: &impl SchedulerStore,
806
23
    mut new_awaited_action: AwaitedAction,
807
23
    expiry: Option<Duration>,
808
23
) -> Result<(), Error> {
809
23
    let operation_id = new_awaited_action.operation_id().clone();
810
23
    if new_awaited_action.state().client_operation_id != operation_id {
811
0
        new_awaited_action.set_client_operation_id(operation_id.clone());
812
23
    }
813
814
23
    let _is_finished = new_awaited_action.state().stage.is_finished();
815
816
23
    let maybe_version = store
817
23
        .update_data(UpdateOperationIdToAwaitedAction(new_awaited_action), expiry)
818
23
        .await
819
23
        .err_tip(|| "In RedisAwaitedActionDb::update_awaited_action")
?0
;
820
821
23
    if maybe_version.is_none() {
822
5
        warn!(
823
            %operation_id,
824
            "Could not update AwaitedAction because the version did not match"
825
        );
826
5
        return Err(make_err!(
827
5
            Code::Aborted,
828
5
            "Could not update AwaitedAction because the version did not match for {operation_id}",
829
5
        ));
830
18
    }
831
832
18
    Ok(())
833
23
}
834
835
#[derive(Debug, MetricsComponent)]
836
pub struct StoreAwaitedActionDb<S, F, I, NowFn>
837
where
838
    S: SchedulerStore,
839
    F: Fn() -> OperationId,
840
    I: InstantWrapper,
841
    NowFn: Fn() -> I,
842
{
843
    store: Arc<S>,
844
    now_fn: NowFn,
845
    operation_id_creator: F,
846
    _pull_task_change_subscriber_spawn: JoinHandleDropGuard<()>,
847
    _active_count_spawn: Option<JoinHandleDropGuard<()>>,
848
    retain_completed_for: Duration,
849
    client_keepalive_ttl: Duration,
850
    worker_registry: Option<SharedWorkerRegistry>,
851
}
852
853
impl<S, F, I, NowFn> StoreAwaitedActionDb<S, F, I, NowFn>
854
where
855
    S: SchedulerStore,
856
    F: Fn() -> OperationId,
857
    I: InstantWrapper,
858
    NowFn: Fn() -> I + Send + Sync + Clone + 'static,
859
{
860
24
    pub async fn new(
861
24
        store: Arc<S>,
862
24
        task_change_publisher: Arc<Notify>,
863
24
        now_fn: NowFn,
864
24
        operation_id_creator: F,
865
24
        retain_completed_for_s: u32,
866
24
        client_action_timeout_s: u64,
867
24
        enable_active_action_count_metric: bool,
868
24
    ) -> Result<Self, Error> {
869
24
        let mut subscription = store
870
24
            .subscription_manager()
871
24
            .await
872
24
            .err_tip(|| "In RedisAwaitedActionDb::new")
?0
873
24
            .subscribe(OperationIdToAwaitedAction(Cow::Owned(OperationId::String(
874
24
                String::new(),
875
24
            ))))
876
24
            .err_tip(|| "In RedisAwaitedActionDb::new")
?0
;
877
24
        let pull_task_change_subscriber = spawn!(
878
            "redis_awaited_action_db_pull_task_change_subscriber",
879
17
            async move {
880
                loop {
881
61
                    let 
changed_res44
= subscription
882
61
                        .changed()
883
61
                        .await
884
44
                        .err_tip(|| "In RedisAwaitedActionDb::new");
885
44
                    if let Err(
err0
) = changed_res {
886
0
                        error!(
887
                            "Error waiting for pull task change subscriber in RedisAwaitedActionDb::new  - {err:?}"
888
                        );
889
                        // Sleep for a second to avoid a busy loop, then trigger the notify
890
                        // so if a reconnect happens we let local resources know that things
891
                        // might have changed.
892
0
                        tokio::time::sleep(Duration::from_secs(1)).await;
893
44
                    }
894
44
                    task_change_publisher.as_ref().notify_one();
895
                }
896
            }
897
        );
898
        // Off by default: counting queries the same store that serves action
899
        // scheduling, once per interval per replica, so it is opt-in rather
900
        // than a cost every deployment pays for a metric it may not read.
901
24
        let active_count_spawn = enable_active_action_count_metric.then(|| 
{1
902
1
            let weak_store = Arc::downgrade(&store);
903
1
            spawn!("store_awaited_action_db_active_count", async move {
904
1
                let mut reported = [None; COUNTED_STATES.len()];
905
                loop {
906
                    // Wait first, so constructing the db never queries the store.
907
4
                    tokio::time::sleep(ACTIVE_COUNT_REFRESH_INTERVAL).await;
908
3
                    let Some(store) = weak_store.upgrade() else {
909
0
                        return;
910
                    };
911
3
                    report_active_counts(store.as_ref(), &mut reported).await;
912
                }
913
0
            })
914
1
        });
915
24
        Ok(Self {
916
24
            store,
917
24
            now_fn,
918
24
            operation_id_creator,
919
24
            _pull_task_change_subscriber_spawn: pull_task_change_subscriber,
920
24
            _active_count_spawn: active_count_spawn,
921
24
            retain_completed_for: Duration::from_secs(retain_completed_for_s.into()),
922
24
            client_keepalive_ttl: Duration::from_secs(client_action_timeout_s),
923
24
            worker_registry: None,
924
24
        })
925
24
    }
926
927
    /// Whether an executing action looks abandoned and should be recreated
928
    /// rather than joined.
929
    ///
930
    /// `last_worker_updated_timestamp` lives in the shared store and
931
    /// heartbeats never refresh it, so on its own it goes stale on any action
932
    /// outliving `worker_timeout_s` and a healthy worker looks abandoned.
933
    /// Consult the registry first and only fall back to the timestamp for
934
    /// workers this instance owns.
935
    #[expect(clippy::future_not_send)] // TODO(jhpratt) remove this
936
11
    async fn executing_action_is_abandoned(
937
11
        &self,
938
11
        awaited_action: &AwaitedAction,
939
11
        no_event_action_timeout: Duration,
940
11
        now: SystemTime,
941
11
    ) -> bool {
942
11
        if awaited_action.state().stage != ActionStage::Executing {
943
3
            return false;
944
8
        }
945
946
8
        let liveness = match (&self.worker_registry, awaited_action.worker_id()) {
947
7
            (Some(worker_registry), Some(worker_id)) => {
948
7
                worker_registry
949
7
                    .check_liveness(worker_id, no_event_action_timeout, now)
950
7
                    .await
951
            }
952
            // No registry, or not assigned yet: timestamp only, as before.
953
1
            _ => WorkerLiveness::Stale,
954
        };
955
956
8
        let 
ceiling4
= match liveness {
957
            // Ours and heartbeating. Never recreate: that forks a second
958
            // execution of work already running.
959
4
            WorkerLiveness::Alive => return false,
960
            // Ours and gone quiet.
961
2
            WorkerLiveness::Stale => no_event_action_timeout,
962
            // A peer's worker, or an orphan nobody owns.
963
2
            WorkerLiveness::Unknown => ORPHANED_ACTION_TIMEOUT,
964
        };
965
966
4
        awaited_action
967
4
            .last_worker_updated_timestamp()
968
4
            .checked_add(ceiling)
969
4
            .is_some_and(|deadline| deadline < now)
970
11
    }
971
972
    // `pub` so integration tests in `tests/` can drive this directly;
973
    // matches the precedent of `inner_update_awaited_action` below.
974
    #[expect(clippy::future_not_send)] // TODO(jhpratt) remove this
975
24
    pub async fn try_subscribe(
976
24
        &self,
977
24
        client_operation_id: &ClientOperationId,
978
24
        unique_qualifier: &ActionUniqueQualifier,
979
24
        no_event_action_timeout: Duration,
980
24
        // TODO(palfrey) To simplify the scheduler 2024 refactor, we
981
24
        // removed the ability to upgrade priorities of actions.
982
24
        // we should add priority upgrades back in.
983
24
        _priority: i32,
984
24
    ) -> Result<Option<AwaitedAction>, Error> {
985
        // Retry once on miss: closes the RediSearch index-visibility
986
        // window where two concurrent `add_action` calls can both see
987
        // empty and create duplicate scheduler operations.
988
        const SUBSCRIBE_RACE_RETRY_DELAY: Duration = Duration::from_millis(20);
989
24
        match unique_qualifier {
990
23
            ActionUniqueQualifier::Cacheable(_) => {}
991
1
            ActionUniqueQualifier::Uncacheable(_) => return Ok(None),
992
        }
993
23
        let mut maybe_awaited_action: Option<AwaitedAction> = None;
994
36
        for attempt in 
0..2_u3223
{
995
36
            if attempt > 0 {
996
13
                tokio::time::sleep(SUBSCRIBE_RACE_RETRY_DELAY).await;
997
23
            }
998
36
            let stream = self
999
36
                .store
1000
36
                .search_by_index_prefix(SearchUniqueQualifierToAwaitedAction(unique_qualifier))
1001
36
                .await
1002
36
                .err_tip(|| "In RedisAwaitedActionDb::try_subscribe")
?0
;
1003
36
            tokio::pin!(stream);
1004
            // This index search is by prefix. In particular, an unscoped key
1005
            // is a prefix of its scoped variants and must not join those jobs.
1006
36
            while let Some(
candidate11
) = stream
1007
36
                .try_next()
1008
36
                .await
1009
36
                .err_tip(|| "In RedisAwaitedActionDb::try_subscribe")
?0
1010
            {
1011
11
                if &candidate.action_info().unique_qualifier == unique_qualifier {
1012
11
                    maybe_awaited_action = Some(candidate);
1013
11
                    break;
1014
0
                }
1015
            }
1016
36
            if maybe_awaited_action.is_some() {
1017
11
                break;
1018
25
            }
1019
        }
1020
23
        match maybe_awaited_action {
1021
11
            Some(awaited_action) => {
1022
                // TODO(palfrey) We don't support joining completed jobs because we
1023
                // need to also check that all the data is still in the cache.
1024
                // If the existing job failed then we need to set back to queued or we get
1025
                // a version mismatch.  Equally we need to check the timeout as the job
1026
                // may be abandoned in the store.
1027
11
                let abandoned = self
1028
11
                    .executing_action_is_abandoned(
1029
11
                        &awaited_action,
1030
11
                        no_event_action_timeout,
1031
11
                        (self.now_fn)().now(),
1032
11
                    )
1033
11
                    .await;
1034
11
                let awaited_action = if awaited_action.state().stage.is_finished() || abandoned {
1035
3
                    tracing::debug!(
1036
                        "Recreating action {:?} for operation {client_operation_id}",
1037
3
                        awaited_action.action_info().digest()
1038
                    );
1039
                    // The version is reset because we have a new operation ID.
1040
3
                    AwaitedAction::new(
1041
3
                        (self.operation_id_creator)(),
1042
3
                        awaited_action.action_info().clone(),
1043
3
                        (self.now_fn)().now(),
1044
                    )
1045
                } else {
1046
8
                    tracing::debug!(
1047
                        "Subscribing to existing action {:?} for operation {client_operation_id}",
1048
8
                        awaited_action.action_info().digest()
1049
                    );
1050
8
                    awaited_action
1051
                };
1052
11
                Ok(Some(awaited_action))
1053
            }
1054
12
            None => Ok(None),
1055
        }
1056
24
    }
1057
1058
    #[expect(clippy::future_not_send)] // TODO(jhpratt) remove this
1059
2
    async fn inner_get_awaited_action_by_id(
1060
2
        &self,
1061
2
        client_operation_id: &ClientOperationId,
1062
2
    ) -> Result<Option<OperationSubscriber<S, I, NowFn>>, Error> {
1063
2
        let maybe_operation_id = self
1064
2
            .store
1065
2
            .get_and_decode(ClientIdToOperationId(client_operation_id))
1066
2
            .await
1067
2
            .err_tip(|| "In RedisAwaitedActionDb::get_awaited_action_by_id")
?0
;
1068
2
        let Some(operation_id) = maybe_operation_id else {
1069
0
            return Ok(None);
1070
        };
1071
1072
        // Validate that the internal operation actually exists.
1073
        // If it doesn't, this is an orphaned client operation mapping that should be cleaned up.
1074
        // This can happen when an operation is deleted (completed/timed out) but the
1075
        // client_id -> operation_id mapping persists in the store.
1076
2
        let maybe_awaited_action = match self
1077
2
            .store
1078
2
            .get_and_decode(OperationIdToAwaitedAction(Cow::Borrowed(&operation_id)))
1079
2
            .await
1080
        {
1081
2
            Ok(maybe_action) => maybe_action,
1082
0
            Err(err) if err.code == Code::NotFound => {
1083
0
                tracing::warn!(
1084
                    "Orphaned client operation mapping detected: client_id={} maps to operation_id={}, \
1085
                    but the operation does not exist in the store (NotFound). This typically happens when \
1086
                    an operation completes or times out but the client mapping persists.",
1087
                    client_operation_id,
1088
                    operation_id
1089
                );
1090
0
                None
1091
            }
1092
0
            Err(err) => {
1093
                // Some other error occurred
1094
0
                return Err(err).err_tip(
1095
                    || "In RedisAwaitedActionDb::get_awaited_action_by_id::validate_operation",
1096
                );
1097
            }
1098
        };
1099
1100
2
        if maybe_awaited_action.is_none() {
1101
1
            tracing::warn!(
1102
                "Found orphaned client operation mapping: client_id={} -> operation_id={}, \
1103
                but operation no longer exists. Returning None to prevent client from polling \
1104
                a non-existent operation.",
1105
                client_operation_id,
1106
                operation_id
1107
            );
1108
1
            return Ok(None);
1109
1
        }
1110
1111
1
        Ok(Some(OperationSubscriber::new(
1112
1
            Some(client_operation_id.clone()),
1113
1
            OperationIdToAwaitedAction(Cow::Owned(operation_id)),
1114
1
            Arc::downgrade(&self.store),
1115
1
            self.now_fn.clone(),
1116
1
            self.retain_completed_for,
1117
1
            self.client_keepalive_ttl,
1118
1
        )))
1119
2
    }
1120
}
1121
1122
impl<S, F, I, NowFn> AwaitedActionDb for StoreAwaitedActionDb<S, F, I, NowFn>
1123
where
1124
    S: SchedulerStore,
1125
    F: Fn() -> OperationId + Send + Sync + Unpin + 'static,
1126
    I: InstantWrapper,
1127
    NowFn: Fn() -> I + Send + Sync + Unpin + Clone + 'static,
1128
{
1129
    type Subscriber = OperationSubscriber<S, I, NowFn>;
1130
1131
2
    async fn get_awaited_action_by_id(
1132
2
        &self,
1133
2
        client_operation_id: &ClientOperationId,
1134
2
    ) -> Result<Option<Self::Subscriber>, Error> {
1135
2
        self.inner_get_awaited_action_by_id(client_operation_id)
1136
2
            .await
1137
2
    }
1138
1139
26
    fn get_by_operation_id(
1140
26
        &self,
1141
26
        operation_id: &OperationId,
1142
26
    ) -> impl Future<Output = Result<Option<Self::Subscriber>, Error>> {
1143
26
        std::future::ready(Ok(Some(OperationSubscriber::new(
1144
26
            None,
1145
26
            OperationIdToAwaitedAction(Cow::Owned(operation_id.clone())),
1146
26
            Arc::downgrade(&self.store),
1147
26
            self.now_fn.clone(),
1148
26
            self.retain_completed_for,
1149
26
            self.client_keepalive_ttl,
1150
26
        ))))
1151
26
    }
1152
1153
22
    async fn update_awaited_action(&self, new_awaited_action: AwaitedAction) -> Result<(), Error> {
1154
22
        let expiry = if new_awaited_action.is_complete() {
1155
2
            Some(self.retain_completed_for)
1156
        } else {
1157
20
            None
1158
        };
1159
22
        inner_update_awaited_action(self.store.as_ref(), new_awaited_action, expiry).await
1160
22
    }
1161
1162
16
    async fn add_action(
1163
16
        &self,
1164
16
        client_operation_id: ClientOperationId,
1165
16
        action_info: Arc<ActionInfo>,
1166
16
        no_event_action_timeout: Duration,
1167
16
    ) -> Result<Self::Subscriber, Error> {
1168
        loop {
1169
            // Check to see if the action is already known and subscribe if it is.
1170
16
            let mut awaited_action = self
1171
16
                .try_subscribe(
1172
16
                    &client_operation_id,
1173
16
                    &action_info.unique_qualifier,
1174
16
                    no_event_action_timeout,
1175
16
                    action_info.priority,
1176
16
                )
1177
16
                .await
1178
16
                .err_tip(|| "In RedisAwaitedActionDb::add_action")
?0
1179
16
                .unwrap_or_else(|| 
{11
1180
11
                    tracing::debug!(
1181
                        "Creating new action {:?} for operation {client_operation_id}",
1182
11
                        action_info.digest()
1183
                    );
1184
11
                    AwaitedAction::new(
1185
11
                        (self.operation_id_creator)(),
1186
11
                        action_info.clone(),
1187
11
                        (self.now_fn)().now(),
1188
                    )
1189
11
                });
1190
1191
16
            debug_assert!(
1192
0
                !awaited_action.state().stage.is_finished(),
1193
                "Expected a queued or executing action; completed actions must be recreated"
1194
            );
1195
1196
16
            let operation_id = awaited_action.operation_id().clone();
1197
16
            if awaited_action.state().client_operation_id != operation_id {
1198
0
                // Just in case the client_operation_id was set to something else
1199
0
                // we put it back to the underlying operation_id.
1200
0
                awaited_action.set_client_operation_id(operation_id.clone());
1201
16
            }
1202
16
            awaited_action.update_client_keep_alive((self.now_fn)().now());
1203
1204
16
            let version = awaited_action.version();
1205
16
            let expiry = if awaited_action.is_complete() {
1206
0
                Some(self.retain_completed_for)
1207
            } else {
1208
16
                None
1209
            };
1210
16
            if self
1211
16
                .store
1212
16
                .update_data(UpdateOperationIdToAwaitedAction(awaited_action), expiry)
1213
16
                .await
1214
16
                .err_tip(|| "In RedisAwaitedActionDb::add_action")
?0
1215
16
                .is_none()
1216
            {
1217
                // The version was out of date, try again.
1218
0
                tracing::info!(
1219
                    "Version out of date for {:?} {operation_id} {version}, retrying.",
1220
0
                    action_info.digest()
1221
                );
1222
0
                continue;
1223
16
            }
1224
1225
            // Bound the cid_* mapping's lifetime (see CLIENT_ID_MAPPING_TTL).
1226
16
            self.store
1227
16
                .update_data(
1228
16
                    UpdateClientIdToOperationId {
1229
16
                        client_operation_id: client_operation_id.clone(),
1230
16
                        operation_id: operation_id.clone(),
1231
16
                    },
1232
16
                    Some(CLIENT_ID_MAPPING_TTL),
1233
16
                )
1234
16
                .await
1235
16
                .err_tip(|| "In RedisAwaitedActionDb::add_action while adding client mapping")
?0
;
1236
1237
16
            return Ok(OperationSubscriber::new(
1238
16
                Some(client_operation_id),
1239
16
                OperationIdToAwaitedAction(Cow::Owned(operation_id)),
1240
16
                Arc::downgrade(&self.store),
1241
16
                self.now_fn.clone(),
1242
16
                self.retain_completed_for,
1243
16
                self.client_keepalive_ttl,
1244
16
            ));
1245
        }
1246
16
    }
1247
1248
8
    fn set_worker_registry(&mut self, worker_registry: SharedWorkerRegistry) {
1249
8
        self.worker_registry = Some(worker_registry);
1250
8
    }
1251
1252
58
    async fn exchange_fleet_capabilities(
1253
58
        &self,
1254
58
        scheduler_id: &str,
1255
58
        local: Vec<PlatformProperties>,
1256
58
        ttl: Duration,
1257
58
    ) -> Result<Vec<PlatformProperties>, Error> {
1258
58
        self.store
1259
58
            .update_data(
1260
58
                UpdateFleetCapabilities(FleetCapabilities {
1261
58
                    scheduler_id: scheduler_id.to_string(),
1262
58
                    workers: local,
1263
58
                }),
1264
58
                Some(ttl),
1265
58
            )
1266
58
            .await
1267
58
            .err_tip(|| "In StoreAwaitedActionDb::exchange_fleet_capabilities publish")
?0
;
1268
1269
58
        let stream = self
1270
58
            .store
1271
58
            .search_by_index_prefix(SearchFleetCapabilities)
1272
58
            .await
1273
58
            .err_tip(|| "In StoreAwaitedActionDb::exchange_fleet_capabilities search")
?0
;
1274
        // Records that have expired on peers that stopped publishing are
1275
        // gone from the search already, so everything found here is live.
1276
58
        stream
1277
100
            .
try_filter58
(|record| futures::future::ready(record.scheduler_id != scheduler_id))
1278
58
            .try_fold(Vec::new(), |mut peers, record| async move 
{42
1279
42
                peers.extend(record.workers);
1280
42
                Ok(peers)
1281
84
            })
1282
58
            .await
1283
58
            .err_tip(|| "In StoreAwaitedActionDb::exchange_fleet_capabilities collect")
1284
58
    }
1285
1286
4
    fn shares_state(&self) -> bool {
1287
4
        true
1288
4
    }
1289
1290
42
    async fn get_range_of_actions(
1291
42
        &self,
1292
42
        state: SortedAwaitedActionState,
1293
42
        start: Bound<SortedAwaitedAction>,
1294
42
        end: Bound<SortedAwaitedAction>,
1295
42
        desc: bool,
1296
42
    ) -> Result<impl Stream<Item = Result<Self::Subscriber, Error>> + Send, Error> {
1297
42
        if !
matches!0
(start, Bound::Unbounded) {
1298
0
            return Err(make_err!(
1299
0
                Code::Unimplemented,
1300
0
                "Start bound is not supported in RedisAwaitedActionDb::get_range_of_actions",
1301
0
            ));
1302
42
        }
1303
42
        if !
matches!0
(end, Bound::Unbounded) {
1304
0
            return Err(make_err!(
1305
0
                Code::Unimplemented,
1306
0
                "End bound is not supported in RedisAwaitedActionDb::get_range_of_actions",
1307
0
            ));
1308
42
        }
1309
        // The index is read in one direction, fixed by
1310
        // `SearchStateToAwaitedAction::SORT_DESCENDING`, and every caller
1311
        // asks for that direction. Ascending has no caller, so it is not
1312
        // implemented rather than silently served backwards.
1313
42
        if !desc {
1314
0
            return Err(make_err!(
1315
0
                Code::Unimplemented,
1316
0
                "Ascending order is not supported in RedisAwaitedActionDb::get_range_of_actions",
1317
0
            ));
1318
42
        }
1319
42
        Ok(self
1320
42
            .store
1321
42
            .search_by_index_prefix(SearchStateToAwaitedAction(get_state_prefix(state)))
1322
42
            .await
1323
42
            .err_tip(|| "In RedisAwaitedActionDb::get_range_of_actions")
?0
1324
42
            .map_ok(move |awaited_action| 
{34
1325
34
                OperationSubscriber::new(
1326
34
                    None,
1327
34
                    OperationIdToAwaitedAction(Cow::Owned(awaited_action.operation_id().clone())),
1328
34
                    Arc::downgrade(&self.store),
1329
34
                    self.now_fn.clone(),
1330
34
                    self.retain_completed_for,
1331
34
                    self.client_keepalive_ttl,
1332
                )
1333
34
                .listed(awaited_action)
1334
34
            }))
1335
42
    }
1336
1337
0
    async fn get_all_awaited_actions(
1338
0
        &self,
1339
0
    ) -> Result<impl Stream<Item = Result<Self::Subscriber, Error>>, Error> {
1340
0
        Ok(self
1341
0
            .store
1342
0
            .search_by_index_prefix(SearchStateToAwaitedAction(""))
1343
0
            .await
1344
0
            .err_tip(|| "In RedisAwaitedActionDb::get_range_of_actions")?
1345
0
            .map_ok(move |awaited_action| {
1346
0
                OperationSubscriber::new(
1347
0
                    None,
1348
0
                    OperationIdToAwaitedAction(Cow::Owned(awaited_action.operation_id().clone())),
1349
0
                    Arc::downgrade(&self.store),
1350
0
                    self.now_fn.clone(),
1351
0
                    self.retain_completed_for,
1352
0
                    self.client_keepalive_ttl,
1353
                )
1354
0
                .listed(awaited_action)
1355
0
            }))
1356
0
    }
1357
}