Coverage Report

Created: 2026-09-18 20:40

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