Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-scheduler/src/memory_awaited_action_db.rs
Line
Count
Source
1
// Copyright 2024 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::ops::{Bound, RangeBounds};
16
use core::time::Duration;
17
use std::collections::hash_map::Entry;
18
use std::collections::{BTreeMap, BTreeSet, HashMap};
19
use std::sync::Arc;
20
21
use async_lock::Mutex;
22
use futures::{FutureExt, Stream, stream};
23
use nativelink_config::stores::EvictionPolicy;
24
use nativelink_error::{Code, Error, ResultExt, error_if, make_err};
25
use nativelink_metric::MetricsComponent;
26
use nativelink_util::action_messages::{
27
    ActionInfo, ActionStage, ActionUniqueKey, ActionUniqueQualifier, OperationId,
28
};
29
use nativelink_util::evicting_map::{EvictingMap, LenEntry};
30
use nativelink_util::instant_wrapper::InstantWrapper;
31
use nativelink_util::metrics::{
32
    EXECUTION_METRICS, ExecutionResult, ExecutionStage, make_execution_attributes,
33
};
34
use nativelink_util::spawn;
35
use nativelink_util::task::JoinHandleDropGuard;
36
use tokio::sync::{Notify, mpsc, watch};
37
use tracing::{debug, error};
38
39
use crate::awaited_action_db::{
40
    AwaitedAction, AwaitedActionDb, AwaitedActionSubscriber, CLIENT_KEEPALIVE_DURATION,
41
    SortedAwaitedAction, SortedAwaitedActionState,
42
};
43
44
/// Number of events to process per cycle.
45
const MAX_ACTION_EVENTS_RX_PER_CYCLE: usize = 1024;
46
47
/// Represents a client that is currently listening to an action.
48
/// When the client is dropped, it will send the `AwaitedAction` to the
49
/// `event_tx` if there are other cleanups needed.
50
#[derive(Debug)]
51
struct ClientAwaitedAction {
52
    /// The `OperationId` that the client is listening to.
53
    operation_id: OperationId,
54
55
    /// The sender to notify of this struct being dropped.
56
    event_tx: mpsc::UnboundedSender<ActionEvent>,
57
}
58
59
impl ClientAwaitedAction {
60
11.5k
    pub(crate) const fn new(
61
11.5k
        operation_id: OperationId,
62
11.5k
        event_tx: mpsc::UnboundedSender<ActionEvent>,
63
11.5k
    ) -> Self {
64
11.5k
        Self {
65
11.5k
            operation_id,
66
11.5k
            event_tx,
67
11.5k
        }
68
11.5k
    }
69
70
3
    pub(crate) const fn operation_id(&self) -> &OperationId {
71
3
        &self.operation_id
72
3
    }
73
}
74
75
impl Drop for ClientAwaitedAction {
76
11.5k
    fn drop(&mut self) {
77
        // If we failed to send it means noone is listening.
78
11.5k
        drop(self.event_tx.send(ActionEvent::ClientDroppedOperation(
79
11.5k
            self.operation_id.clone(),
80
11.5k
        )));
81
11.5k
    }
82
}
83
84
/// Trait to be able to use the `EvictingMap` with `ClientAwaitedAction`.
85
/// Note: We only use `EvictingMap` for a time based eviction, which is
86
/// why the implementation has fixed default values in it.
87
impl LenEntry for ClientAwaitedAction {
88
    #[inline]
89
23.1k
    fn len(&self) -> u64 {
90
23.1k
        0
91
23.1k
    }
92
93
    #[inline]
94
0
    fn is_empty(&self) -> bool {
95
0
        true
96
0
    }
97
}
98
99
/// Actions the `AwaitedActionsDb` needs to process.
100
#[derive(Debug)]
101
pub(crate) enum ActionEvent {
102
    /// A client has sent a keep alive message.
103
    ClientKeepAlive(OperationId),
104
    /// A client has dropped and pointed to `OperationId`.
105
    ClientDroppedOperation(OperationId),
106
}
107
108
/// Information required to track an individual client
109
/// keep alive config and state.
110
#[derive(Debug)]
111
struct ClientInfo<I: InstantWrapper, NowFn: Fn() -> I> {
112
    /// The client operation id.
113
    client_operation_id: OperationId,
114
    /// The last time a keep alive was sent.
115
    last_keep_alive: I,
116
    /// The function to get the current time.
117
    now_fn: NowFn,
118
    /// The sender to notify of this struct had an event.
119
    event_tx: mpsc::UnboundedSender<ActionEvent>,
120
}
121
122
/// Subscriber that clients can be used to monitor when `AwaitedActions` change.
123
#[derive(Debug)]
124
pub struct MemoryAwaitedActionSubscriber<I: InstantWrapper, NowFn: Fn() -> I> {
125
    /// The receiver to listen for changes.
126
    awaited_action_rx: watch::Receiver<AwaitedAction>,
127
    /// If a client id is known this is the info needed to keep the client
128
    /// action alive.
129
    client_info: Option<ClientInfo<I, NowFn>>,
130
}
131
132
impl<I: InstantWrapper, NowFn: Fn() -> I> MemoryAwaitedActionSubscriber<I, NowFn> {
133
6.22M
    fn new(mut awaited_action_rx: watch::Receiver<AwaitedAction>) -> Self {
134
6.22M
        awaited_action_rx.mark_changed();
135
6.22M
        Self {
136
6.22M
            awaited_action_rx,
137
6.22M
            client_info: None,
138
6.22M
        }
139
6.22M
    }
140
141
11.5k
    fn new_with_client(
142
11.5k
        mut awaited_action_rx: watch::Receiver<AwaitedAction>,
143
11.5k
        client_operation_id: OperationId,
144
11.5k
        event_tx: mpsc::UnboundedSender<ActionEvent>,
145
11.5k
        now_fn: NowFn,
146
11.5k
    ) -> Self
147
11.5k
    where
148
11.5k
        NowFn: Fn() -> I,
149
    {
150
11.5k
        awaited_action_rx.mark_changed();
151
11.5k
        Self {
152
11.5k
            awaited_action_rx,
153
11.5k
            client_info: Some(ClientInfo {
154
11.5k
                client_operation_id,
155
11.5k
                last_keep_alive: I::from_secs(0),
156
11.5k
                now_fn,
157
11.5k
                event_tx,
158
11.5k
            }),
159
11.5k
        }
160
11.5k
    }
161
}
162
163
impl<I, NowFn> AwaitedActionSubscriber for MemoryAwaitedActionSubscriber<I, NowFn>
164
where
165
    I: InstantWrapper,
166
    NowFn: Fn() -> I + Send + Sync + 'static,
167
{
168
33.8k
    async fn changed(&mut self) -> Result<AwaitedAction, Error> {
169
30.0k
        let client_operation_id = {
170
33.8k
            let changed_fut = self.awaited_action_rx.changed().map(|r| 
{30.0k
171
30.0k
                r.map_err(|e| 
{0
172
0
                    Error::from_std_err(Code::Internal, &e)
173
0
                        .append("Failed to wait for awaited action to change")
174
0
                })
175
30.0k
            });
176
33.8k
            let Some(client_info) = self.client_info.as_mut() else {
177
0
                changed_fut.await?;
178
0
                return Ok(self.awaited_action_rx.borrow().clone());
179
            };
180
33.8k
            tokio::pin!(changed_fut);
181
            loop {
182
34.7k
                if client_info.last_keep_alive.elapsed() > CLIENT_KEEPALIVE_DURATION {
183
12.1k
                    client_info.last_keep_alive = (client_info.now_fn)();
184
12.1k
                    // Failing to send just means our receiver dropped.
185
12.1k
                    drop(client_info.event_tx.send(ActionEvent::ClientKeepAlive(
186
12.1k
                        client_info.client_operation_id.clone(),
187
12.1k
                    )));
188
22.5k
                }
189
34.7k
                let sleep_fut = (client_info.now_fn)().sleep(CLIENT_KEEPALIVE_DURATION);
190
34.7k
                tokio::select! {
191
34.7k
                    
result30.0k
= &mut changed_fut => {
192
30.0k
                        result
?0
;
193
30.0k
                        break;
194
                    }
195
34.7k
                    () = sleep_fut => {
196
866
                        // If we haven't received any updates for a while, we should
197
866
                        // let the database know that we are still listening to prevent
198
866
                        // the action from being dropped.
199
866
                    }
200
                }
201
            }
202
30.0k
            client_info.client_operation_id.clone()
203
        };
204
        // At this stage we know that this event is a client request, so we need
205
        // to populate the client_operation_id.
206
30.0k
        let mut awaited_action = self.awaited_action_rx.borrow().clone();
207
30.0k
        awaited_action.set_client_operation_id(client_operation_id);
208
30.0k
        Ok(awaited_action)
209
30.0k
    }
210
211
18.6M
    fn borrow(&self) -> impl Future<Output = Result<AwaitedAction, Error>> + Send {
212
18.6M
        let mut awaited_action = self.awaited_action_rx.borrow().clone();
213
18.6M
        if let Some(
client_info12.8k
) = self.client_info.as_ref() {
214
12.8k
            awaited_action.set_client_operation_id(client_info.client_operation_id.clone());
215
18.6M
        }
216
18.6M
        std::future::ready(Ok(awaited_action))
217
18.6M
    }
218
}
219
220
/// A struct that is used to keep the developer from trying to
221
/// return early from a function.
222
struct NoEarlyReturn;
223
224
#[derive(Debug, Default, MetricsComponent)]
225
struct SortedAwaitedActions {
226
    #[metric(group = "unknown")]
227
    unknown: BTreeSet<SortedAwaitedAction>,
228
    #[metric(group = "cache_check")]
229
    cache_check: BTreeSet<SortedAwaitedAction>,
230
    #[metric(group = "queued")]
231
    queued: BTreeSet<SortedAwaitedAction>,
232
    #[metric(group = "executing")]
233
    executing: BTreeSet<SortedAwaitedAction>,
234
    #[metric(group = "completed")]
235
    completed: BTreeSet<SortedAwaitedAction>,
236
}
237
238
impl SortedAwaitedActions {
239
21.5k
    const fn btree_for_state(&mut self, state: &ActionStage) -> &mut BTreeSet<SortedAwaitedAction> {
240
21.5k
        match state {
241
0
            ActionStage::Unknown => &mut self.unknown,
242
0
            ActionStage::CacheCheck => &mut self.cache_check,
243
11.8k
            ActionStage::Queued => &mut self.queued,
244
9.70k
            ActionStage::Executing => &mut self.executing,
245
1
            ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => &mut self.completed,
246
        }
247
21.5k
    }
248
249
33.1k
    fn insert_sort_map_for_stage(
250
33.1k
        &mut self,
251
33.1k
        stage: &ActionStage,
252
33.1k
        sorted_awaited_action: &SortedAwaitedAction,
253
33.1k
    ) -> Result<(), Error> {
254
33.1k
        let newly_inserted = match stage {
255
0
            ActionStage::Unknown => self.unknown.insert(sorted_awaited_action.clone()),
256
0
            ActionStage::CacheCheck => self.cache_check.insert(sorted_awaited_action.clone()),
257
12.1k
            ActionStage::Queued => self.queued.insert(sorted_awaited_action.clone()),
258
9.86k
            ActionStage::Executing => self.executing.insert(sorted_awaited_action.clone()),
259
11.1k
            ActionStage::Completed(_) => self.completed.insert(sorted_awaited_action.clone()),
260
            ActionStage::CompletedFromCache(_) => {
261
0
                self.completed.insert(sorted_awaited_action.clone())
262
            }
263
        };
264
33.1k
        if !newly_inserted {
265
0
            return Err(make_err!(
266
0
                Code::Internal,
267
0
                "Tried to insert an action that was already in the sorted map. This should never happen. {:?} - {:?}",
268
0
                stage,
269
0
                sorted_awaited_action
270
0
            ));
271
33.1k
        }
272
33.1k
        Ok(())
273
33.1k
    }
274
275
21.5k
    fn process_state_changes(
276
21.5k
        &mut self,
277
21.5k
        old_awaited_action: &AwaitedAction,
278
21.5k
        new_awaited_action: &AwaitedAction,
279
21.5k
    ) -> Result<(), Error> {
280
21.5k
        let btree = self.btree_for_state(&old_awaited_action.state().stage);
281
21.5k
        let maybe_sorted_awaited_action = btree.take(&SortedAwaitedAction {
282
21.5k
            sort_key: old_awaited_action.sort_key(),
283
21.5k
            operation_id: new_awaited_action.operation_id().clone(),
284
21.5k
        });
285
286
21.5k
        let Some(sorted_awaited_action) = maybe_sorted_awaited_action else {
287
0
            return Err(make_err!(
288
0
                Code::Internal,
289
0
                "sorted_action_info_hash_keys and action_info_hash_key_to_awaited_action are out of sync - {} - {:?}",
290
0
                new_awaited_action.operation_id(),
291
0
                new_awaited_action,
292
0
            ));
293
        };
294
295
21.5k
        self.insert_sort_map_for_stage(&new_awaited_action.state().stage, &sorted_awaited_action)
296
21.5k
            .err_tip(|| "In AwaitedActionDb::update_awaited_action")
?0
;
297
21.5k
        Ok(())
298
21.5k
    }
299
}
300
301
/// The database for storing the state of all actions.
302
#[derive(Debug, MetricsComponent)]
303
pub struct AwaitedActionDbImpl<I: InstantWrapper, NowFn: Fn() -> I> {
304
    /// A lookup table to lookup the state of an action by its client operation id.
305
    #[metric(group = "client_operation_ids")]
306
    client_operation_to_awaited_action:
307
        EvictingMap<OperationId, OperationId, Arc<ClientAwaitedAction>, I>,
308
309
    /// A lookup table to lookup the state of an action by its worker operation id.
310
    #[metric(group = "operation_ids")]
311
    operation_id_to_awaited_action: BTreeMap<OperationId, watch::Sender<AwaitedAction>>,
312
313
    /// A lookup table to lookup the state of an action by its unique qualifier.
314
    #[metric(group = "action_info_hash_key_to_awaited_action")]
315
    action_info_hash_key_to_awaited_action: HashMap<ActionUniqueKey, OperationId>,
316
317
    /// A sorted set of [`AwaitedAction`]s. A wrapper is used to perform sorting
318
    /// based on the [`AwaitedActionSortKey`] of the [`AwaitedAction`].
319
    ///
320
    /// See [`AwaitedActionSortKey`] for more information on the ordering.
321
    #[metric(group = "sorted_action_infos")]
322
    sorted_action_info_hash_keys: SortedAwaitedActions,
323
324
    /// The number of connected clients for each operation id.
325
    #[metric(group = "connected_clients_for_operation_id")]
326
    connected_clients_for_operation_id: HashMap<OperationId, usize>,
327
328
    /// Where to send notifications about important events related to actions.
329
    action_event_tx: mpsc::UnboundedSender<ActionEvent>,
330
331
    /// The function to get the current time.
332
    now_fn: NowFn,
333
334
    /// Mints the id of each new operation.
335
    operation_id_fn: OperationIdFn,
336
}
337
338
/// Mints operation ids; a random UUID unless a test needs them repeatable.
339
#[derive(Clone)]
340
pub struct OperationIdFn(Arc<dyn Fn() -> OperationId + Send + Sync>);
341
342
impl OperationIdFn {
343
306
    pub fn new(f: impl Fn() -> OperationId + Send + Sync + 'static) -> Self {
344
306
        Self(Arc::new(f))
345
306
    }
346
}
347
348
impl Default for OperationIdFn {
349
189
    fn default() -> Self {
350
189
        Self::new(OperationId::default)
351
189
    }
352
}
353
354
impl core::fmt::Debug for OperationIdFn {
355
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
356
0
        f.debug_struct("OperationIdFn").finish_non_exhaustive()
357
0
    }
358
}
359
360
impl<I: InstantWrapper, NowFn: Fn() -> I + Clone + Send + Sync> AwaitedActionDbImpl<I, NowFn> {
361
204
    async fn get_awaited_action_by_id(
362
204
        &self,
363
204
        client_operation_id: &OperationId,
364
204
    ) -> Result<Option<MemoryAwaitedActionSubscriber<I, NowFn>>, Error> {
365
204
        let maybe_client_awaited_action = self
366
204
            .client_operation_to_awaited_action
367
204
            .get(client_operation_id)
368
204
            .await;
369
204
        let Some(
client_awaited_action3
) = maybe_client_awaited_action else {
370
201
            return Ok(None);
371
        };
372
373
3
        self.operation_id_to_awaited_action
374
3
            .get(client_awaited_action.operation_id())
375
3
            .map(|tx| {
376
3
                Some(MemoryAwaitedActionSubscriber::new_with_client(
377
3
                    tx.subscribe(),
378
3
                    client_operation_id.clone(),
379
3
                    self.action_event_tx.clone(),
380
3
                    self.now_fn.clone(),
381
3
                ))
382
3
            })
383
3
            .ok_or_else(|| 
{0
384
0
                make_err!(
385
0
                    Code::Internal,
386
                    "Failed to get client operation id {client_operation_id:?}"
387
                )
388
0
            })
389
204
    }
390
391
    /// Processes action events that need to be handled by the database.
392
11.2k
    async fn handle_action_events(
393
11.2k
        &mut self,
394
11.2k
        action_events: impl IntoIterator<Item = ActionEvent>,
395
11.2k
    ) -> NoEarlyReturn {
396
12.1k
        for action in 
action_events11.2k
{
397
12.1k
            debug!(?action, "Handling action");
398
12.1k
            match action {
399
3
                ActionEvent::ClientDroppedOperation(operation_id) => {
400
                    // Cleanup operation_id_to_awaited_action.
401
3
                    let Some(tx) = self.operation_id_to_awaited_action.remove(&operation_id) else {
402
0
                        error!(
403
                            %operation_id,
404
                            "operation_id_to_awaited_action does not have operation_id"
405
                        );
406
0
                        continue;
407
                    };
408
409
3
                    let connected_clients = match self
410
3
                        .connected_clients_for_operation_id
411
3
                        .entry(operation_id.clone())
412
                    {
413
3
                        Entry::Occupied(entry) => {
414
3
                            let value = *entry.get();
415
3
                            entry.remove();
416
3
                            value - 1
417
                        }
418
                        Entry::Vacant(_) => {
419
0
                            error!(
420
                                %operation_id,
421
                                "connected_clients_for_operation_id does not have operation_id"
422
                            );
423
0
                            0
424
                        }
425
                    };
426
427
                    // Note: It is rare to have more than one client listening
428
                    // to the same action, so we assume that we are the last
429
                    // client and insert it back into the map if we detect that
430
                    // there are still clients listening (ie: the happy path
431
                    // is operation.connected_clients == 0).
432
3
                    if connected_clients != 0 {
433
1
                        self.operation_id_to_awaited_action
434
1
                            .insert(operation_id.clone(), tx);
435
1
                        self.connected_clients_for_operation_id
436
1
                            .insert(operation_id, connected_clients);
437
1
                        continue;
438
2
                    }
439
2
                    debug!(%operation_id, "Clearing operation from state manager");
440
2
                    let awaited_action = tx.borrow().clone();
441
                    // A removed operation has no later stage transition to
442
                    // decrement its active count. This also covers clients
443
                    // that disappear while an action is still executing.
444
2
                    let stage_attrs = vec![opentelemetry::KeyValue::new(
445
                        nativelink_util::metrics::EXECUTION_STAGE,
446
2
                        ExecutionStage::from(&awaited_action.state().stage),
447
                    )];
448
2
                    EXECUTION_METRICS
449
2
                        .execution_active_count
450
2
                        .add(-1, &stage_attrs);
451
                    // Cleanup action_info_hash_key_to_awaited_action if it was marked cached.
452
2
                    match &awaited_action.action_info().unique_qualifier {
453
2
                        ActionUniqueQualifier::Cacheable(action_key) => {
454
                            // Once this operation finished, a newer operation for the
455
                            // same action key may have claimed the entry; removing it
456
                            // unconditionally here would orphan that operation's
457
                            // deduplication entry.
458
2
                            let owned_by_this_operation = self
459
2
                                .action_info_hash_key_to_awaited_action
460
2
                                .get(action_key)
461
2
                                .is_some_and(|id| id == &operation_id);
462
2
                            if owned_by_this_operation {
463
1
                                self.action_info_hash_key_to_awaited_action
464
1
                                    .remove(action_key);
465
1
                            } else if !awaited_action.state().stage.is_finished() {
466
0
                                error!(
467
                                    %operation_id,
468
                                    ?awaited_action,
469
                                    ?action_key,
470
                                    "action_info_hash_key_to_awaited_action and operation_id_to_awaited_action are out of sync",
471
                                );
472
1
                            }
473
                        }
474
0
                        ActionUniqueQualifier::Uncacheable(_action_key) => {
475
0
                            // This Operation should not be in the hash_key map.
476
0
                        }
477
                    }
478
479
                    // Cleanup sorted_awaited_action.
480
2
                    let sort_key = awaited_action.sort_key();
481
2
                    let sort_btree_for_state = self
482
2
                        .sorted_action_info_hash_keys
483
2
                        .btree_for_state(&awaited_action.state().stage);
484
485
2
                    let maybe_sorted_awaited_action =
486
2
                        sort_btree_for_state.take(&SortedAwaitedAction {
487
2
                            sort_key,
488
2
                            operation_id: operation_id.clone(),
489
2
                        });
490
2
                    if maybe_sorted_awaited_action.is_none() {
491
0
                        error!(
492
                            %operation_id,
493
                            ?sort_key,
494
                            "Expected maybe_sorted_awaited_action to have {sort_key:?}",
495
                        );
496
2
                    }
497
                }
498
12.1k
                ActionEvent::ClientKeepAlive(client_id) => {
499
12.1k
                    if let Some(client_awaited_action) = self
500
12.1k
                        .client_operation_to_awaited_action
501
12.1k
                        .get(&client_id)
502
12.1k
                        .await
503
                    {
504
12.1k
                        if let Some(awaited_action_sender) = self
505
12.1k
                            .operation_id_to_awaited_action
506
12.1k
                            .get(&client_awaited_action.operation_id)
507
                        {
508
12.1k
                            awaited_action_sender.send_if_modified(|awaited_action| {
509
12.1k
                                awaited_action.update_client_keep_alive((self.now_fn)().now());
510
12.1k
                                false
511
12.1k
                            });
512
0
                        }
513
                    } else {
514
0
                        error!(
515
                            ?client_id,
516
                            "client_operation_to_awaited_action does not have client_id",
517
                        );
518
                    }
519
                }
520
            }
521
        }
522
11.2k
        NoEarlyReturn
523
11.2k
    }
524
525
8
    fn get_awaited_actions_range(
526
8
        &self,
527
8
        start: Bound<&OperationId>,
528
8
        end: Bound<&OperationId>,
529
8
    ) -> impl Iterator<Item = (&'_ OperationId, MemoryAwaitedActionSubscriber<I, NowFn>)>
530
8
    + use<'_, I, NowFn> {
531
8
        self.operation_id_to_awaited_action
532
8
            .range((start, end))
533
12
            .
map8
(|(operation_id, tx)| {
534
12
                (
535
12
                    operation_id,
536
12
                    MemoryAwaitedActionSubscriber::<I, NowFn>::new(tx.subscribe()),
537
12
                )
538
12
            })
539
8
    }
540
541
6.22M
    fn get_by_operation_id(
542
6.22M
        &self,
543
6.22M
        operation_id: &OperationId,
544
6.22M
    ) -> Option<MemoryAwaitedActionSubscriber<I, NowFn>> {
545
6.22M
        self.operation_id_to_awaited_action
546
6.22M
            .get(operation_id)
547
6.22M
            .map(|tx| 
MemoryAwaitedActionSubscriber::<I, NowFn>::new6.22M
(
tx6.22M
.
subscribe6.22M
()))
548
6.22M
    }
549
550
31.4k
    fn get_range_of_actions(
551
31.4k
        &self,
552
31.4k
        state: SortedAwaitedActionState,
553
31.4k
        range: impl RangeBounds<SortedAwaitedAction>,
554
31.4k
    ) -> impl DoubleEndedIterator<
555
31.4k
        Item = Result<
556
31.4k
            (
557
31.4k
                &SortedAwaitedAction,
558
31.4k
                MemoryAwaitedActionSubscriber<I, NowFn>,
559
31.4k
            ),
560
31.4k
            Error,
561
31.4k
        >,
562
31.4k
    > {
563
31.4k
        let btree = match state {
564
0
            SortedAwaitedActionState::CacheCheck => &self.sorted_action_info_hash_keys.cache_check,
565
31.3k
            SortedAwaitedActionState::Queued => &self.sorted_action_info_hash_keys.queued,
566
124
            SortedAwaitedActionState::Executing => &self.sorted_action_info_hash_keys.executing,
567
0
            SortedAwaitedActionState::Completed => &self.sorted_action_info_hash_keys.completed,
568
        };
569
6.19M
        
btree31.4k
.
range31.4k
(
range31.4k
).
map31.4k
(|sorted_awaited_action| {
570
6.19M
            let operation_id = &sorted_awaited_action.operation_id;
571
6.19M
            self.get_by_operation_id(operation_id)
572
6.19M
                .ok_or_else(|| 
{0
573
0
                    make_err!(
574
0
                        Code::Internal,
575
                        "Failed to get operation id {}",
576
                        operation_id
577
                    )
578
0
                })
579
6.19M
                .map(|subscriber| (sorted_awaited_action, subscriber))
580
6.19M
        })
581
31.4k
    }
582
583
21.5k
    fn process_state_changes_for_hash_key_map(
584
21.5k
        action_info_hash_key_to_awaited_action: &mut HashMap<ActionUniqueKey, OperationId>,
585
21.5k
        new_awaited_action: &AwaitedAction,
586
21.5k
    ) {
587
        // Only process changes if the stage is not finished.
588
21.5k
        if !new_awaited_action.state().stage.is_finished() {
589
10.4k
            return;
590
11.1k
        }
591
11.1k
        match &new_awaited_action.action_info().unique_qualifier {
592
10.8k
            ActionUniqueQualifier::Cacheable(action_key) => {
593
10.8k
                match action_info_hash_key_to_awaited_action.get(action_key) {
594
10.8k
                    Some(owning_operation_id)
595
10.8k
                        if owning_operation_id == new_awaited_action.operation_id() => {}
596
0
                    Some(owning_operation_id) => {
597
                        // The entry belongs to a newer operation for the same
598
                        // action key; leave it in place.
599
0
                        error!(
600
                            ?owning_operation_id,
601
                            ?new_awaited_action,
602
                            ?action_key,
603
                            "action_info_hash_key_to_awaited_action and operation_id_to_awaited_action are out of sync",
604
                        );
605
0
                        return;
606
                    }
607
                    None => {
608
0
                        error!(
609
                            ?new_awaited_action,
610
                            ?action_key,
611
                            "action_info_hash_key_to_awaited_action out of sync, it should have had the unique_key",
612
                        );
613
0
                        return;
614
                    }
615
                }
616
10.8k
                action_info_hash_key_to_awaited_action.remove(action_key);
617
            }
618
277
            ActionUniqueQualifier::Uncacheable(_action_key) => {
619
277
                // If we are not cacheable, the action should not be in the
620
277
                // hash_key map, so we don't need to process anything in
621
277
                // action_info_hash_key_to_awaited_action.
622
277
            }
623
        }
624
21.5k
    }
625
626
21.5k
    fn update_awaited_action(
627
21.5k
        &mut self,
628
21.5k
        mut new_awaited_action: AwaitedAction,
629
21.5k
    ) -> Result<(), Error> {
630
21.5k
        let tx = self
631
21.5k
            .operation_id_to_awaited_action
632
21.5k
            .get(new_awaited_action.operation_id())
633
21.5k
            .ok_or_else(|| 
{0
634
0
                make_err!(
635
0
                    Code::Internal,
636
                    "OperationId does not exist in map in AwaitedActionDb::update_awaited_action"
637
                )
638
0
            })?;
639
        {
640
            // Note: It's important to drop old_awaited_action before we call
641
            // send_replace or we will have a deadlock.
642
21.5k
            let old_awaited_action = tx.borrow();
643
644
            // Do not process changes if the action version is not in sync with
645
            // what the sender based the update on.
646
21.5k
            if old_awaited_action.version() != new_awaited_action.version() {
647
0
                return Err(make_err!(
648
0
                    // From: https://grpc.github.io/grpc/core/md_doc_statuscodes.html
649
0
                    // Use ABORTED if the client should retry at a higher level
650
0
                    // (e.g., when a client-specified test-and-set fails,
651
0
                    // indicating the client should restart a read-modify-write
652
0
                    // sequence)
653
0
                    Code::Aborted,
654
0
                    "{} Expected {} but got {} for operation_id {:?} - {:?}",
655
0
                    "Tried to update an awaited action with an incorrect version.",
656
0
                    old_awaited_action.version(),
657
0
                    new_awaited_action.version(),
658
0
                    old_awaited_action,
659
0
                    new_awaited_action,
660
0
                ));
661
21.5k
            }
662
21.5k
            new_awaited_action.increment_version();
663
664
0
            error_if!(
665
21.5k
                old_awaited_action.action_info().unique_qualifier
666
21.5k
                    != new_awaited_action.action_info().unique_qualifier,
667
                "Unique key changed for operation_id {:?} - {:?} - {:?}",
668
0
                new_awaited_action.operation_id(),
669
0
                old_awaited_action.action_info(),
670
0
                new_awaited_action.action_info(),
671
            );
672
21.5k
            let is_same_stage = old_awaited_action
673
21.5k
                .state()
674
21.5k
                .stage
675
21.5k
                .is_same_stage(&new_awaited_action.state().stage);
676
677
21.5k
            if !is_same_stage {
678
                // Record metrics for stage transitions
679
21.5k
                let metrics = &*EXECUTION_METRICS;
680
21.5k
                let old_stage = &old_awaited_action.state().stage;
681
21.5k
                let new_stage = &new_awaited_action.state().stage;
682
683
                // Track stage transitions
684
21.5k
                let base_attrs = make_execution_attributes(
685
21.5k
                    "unknown",
686
21.5k
                    None,
687
21.5k
                    Some(old_awaited_action.action_info().priority),
688
                );
689
21.5k
                metrics.execution_stage_transitions.add(1, &base_attrs);
690
691
                // Update active count for old stage
692
21.5k
                let old_stage_attrs = vec![opentelemetry::KeyValue::new(
693
                    nativelink_util::metrics::EXECUTION_STAGE,
694
21.5k
                    ExecutionStage::from(old_stage),
695
                )];
696
21.5k
                metrics.execution_active_count.add(-1, &old_stage_attrs);
697
698
                // Update active count for new stage
699
21.5k
                let new_stage_attrs = vec![opentelemetry::KeyValue::new(
700
                    nativelink_util::metrics::EXECUTION_STAGE,
701
21.5k
                    ExecutionStage::from(new_stage),
702
                )];
703
21.5k
                metrics.execution_active_count.add(1, &new_stage_attrs);
704
705
                // Record completion metrics with action digest for failure tracking
706
21.5k
                let action_digest = old_awaited_action.action_info().digest().to_string();
707
21.5k
                if let ActionStage::Completed(
action_result11.1k
) = new_stage {
708
11.1k
                    let result_attrs = vec![
709
11.1k
                        opentelemetry::KeyValue::new(
710
                            nativelink_util::metrics::EXECUTION_RESULT,
711
11.1k
                            if action_result.exit_code == 0 {
712
9.03k
                                ExecutionResult::Success
713
                            } else {
714
2.07k
                                ExecutionResult::Failure
715
                            },
716
                        ),
717
11.1k
                        opentelemetry::KeyValue::new(
718
                            nativelink_util::metrics::EXECUTION_ACTION_DIGEST,
719
11.1k
                            action_digest,
720
                        ),
721
                    ];
722
11.1k
                    metrics.execution_completed_count.add(1, &result_attrs);
723
10.4k
                } else if let ActionStage::CompletedFromCache(_) = new_stage {
724
0
                    let result_attrs = vec![
725
0
                        opentelemetry::KeyValue::new(
726
0
                            nativelink_util::metrics::EXECUTION_RESULT,
727
0
                            ExecutionResult::CacheHit,
728
0
                        ),
729
0
                        opentelemetry::KeyValue::new(
730
0
                            nativelink_util::metrics::EXECUTION_ACTION_DIGEST,
731
0
                            action_digest,
732
0
                        ),
733
0
                    ];
734
0
                    metrics.execution_completed_count.add(1, &result_attrs);
735
10.4k
                }
736
737
21.5k
                self.sorted_action_info_hash_keys
738
21.5k
                    .process_state_changes(&old_awaited_action, &new_awaited_action)
?0
;
739
21.5k
                Self::process_state_changes_for_hash_key_map(
740
21.5k
                    &mut self.action_info_hash_key_to_awaited_action,
741
21.5k
                    &new_awaited_action,
742
                );
743
0
            }
744
        }
745
746
        // Notify all listeners of the new state and ignore if no one is listening.
747
        // Note: Do not use `.send()` as it will not update the state if all listeners
748
        // are dropped.
749
21.5k
        drop(tx.send_replace(new_awaited_action));
750
751
21.5k
        Ok(())
752
21.5k
    }
753
754
    /// Creates a new [`ClientAwaitedAction`] and a [`watch::Receiver`] to
755
    /// listen for changes. We don't do this in-line because it is important
756
    /// to ALWAYS construct a [`ClientAwaitedAction`] before inserting it into
757
    /// the map. Failing to do so may result in memory leaks. This is because
758
    /// [`ClientAwaitedAction`] implements a drop function that will trigger
759
    /// cleanup of the other maps on drop.
760
11.5k
    fn make_client_awaited_action(
761
11.5k
        &mut self,
762
11.5k
        operation_id: &OperationId,
763
11.5k
        awaited_action: AwaitedAction,
764
11.5k
    ) -> (Arc<ClientAwaitedAction>, watch::Receiver<AwaitedAction>) {
765
11.5k
        let (tx, rx) = watch::channel(awaited_action);
766
11.5k
        let client_awaited_action = Arc::new(ClientAwaitedAction::new(
767
11.5k
            operation_id.clone(),
768
11.5k
            self.action_event_tx.clone(),
769
        ));
770
11.5k
        self.operation_id_to_awaited_action
771
11.5k
            .insert(operation_id.clone(), tx);
772
11.5k
        self.connected_clients_for_operation_id
773
11.5k
            .insert(operation_id.clone(), 1);
774
11.5k
        (client_awaited_action, rx)
775
11.5k
    }
776
777
11.5k
    async fn add_action(
778
11.5k
        &mut self,
779
11.5k
        client_operation_id: OperationId,
780
11.5k
        action_info: Arc<ActionInfo>,
781
11.5k
    ) -> Result<MemoryAwaitedActionSubscriber<I, NowFn>, Error> {
782
        // Check to see if the action is already known and subscribe if it is.
783
11.5k
        let subscription_result = self
784
11.5k
            .try_subscribe(
785
11.5k
                &client_operation_id,
786
11.5k
                &action_info.unique_qualifier,
787
11.5k
                action_info.priority,
788
11.5k
            )
789
11.5k
            .await
790
11.5k
            .err_tip(|| "In AwaitedActionDb::subscribe_or_add_action");
791
11.5k
        match subscription_result {
792
0
            Err(err) => return Err(err),
793
8
            Ok(Some(subscription)) => return Ok(subscription),
794
11.5k
            Ok(None) => { /* Add item to queue. */ }
795
        }
796
797
11.5k
        let maybe_unique_key = match &action_info.unique_qualifier {
798
11.2k
            ActionUniqueQualifier::Cacheable(unique_key) => Some(unique_key.clone()),
799
309
            ActionUniqueQualifier::Uncacheable(_unique_key) => None,
800
        };
801
11.5k
        let operation_id = (self.operation_id_fn.0)();
802
11.5k
        let awaited_action = AwaitedAction::new(
803
11.5k
            operation_id.clone(),
804
11.5k
            action_info.clone(),
805
11.5k
            (self.now_fn)().now(),
806
        );
807
11.5k
        debug_assert!(
808
0
            ActionStage::Queued == awaited_action.state().stage,
809
            "Expected action to be queued"
810
        );
811
11.5k
        let sort_key = awaited_action.sort_key();
812
813
11.5k
        let (client_awaited_action, rx) =
814
11.5k
            self.make_client_awaited_action(&operation_id.clone(), awaited_action);
815
816
11.5k
        debug!(
817
            %client_operation_id,
818
            %operation_id,
819
            ?client_awaited_action,
820
            "Adding action"
821
        );
822
823
11.5k
        self.client_operation_to_awaited_action
824
11.5k
            .insert(client_operation_id.clone(), client_awaited_action)
825
11.5k
            .await;
826
827
        // Note: We only put items in the map that are cacheable.
828
11.5k
        if let Some(
unique_key11.2k
) = maybe_unique_key {
829
11.2k
            let old_value = self
830
11.2k
                .action_info_hash_key_to_awaited_action
831
11.2k
                .insert(unique_key, operation_id.clone());
832
11.2k
            if let Some(
old_value0
) = old_value {
833
0
                error!(
834
                    %operation_id,
835
                    ?old_value,
836
                    "action_info_hash_key_to_awaited_action already has unique_key"
837
                );
838
11.2k
            }
839
309
        }
840
841
        // Record metric for new action entering the queue
842
11.5k
        let metrics = &*EXECUTION_METRICS;
843
11.5k
        let _base_attrs = make_execution_attributes("unknown", None, Some(action_info.priority));
844
11.5k
        let queued_attrs = vec![opentelemetry::KeyValue::new(
845
            nativelink_util::metrics::EXECUTION_STAGE,
846
11.5k
            ExecutionStage::Queued,
847
        )];
848
11.5k
        metrics.execution_active_count.add(1, &queued_attrs);
849
850
11.5k
        self.sorted_action_info_hash_keys
851
11.5k
            .insert_sort_map_for_stage(
852
11.5k
                &ActionStage::Queued,
853
11.5k
                &SortedAwaitedAction {
854
11.5k
                    sort_key,
855
11.5k
                    operation_id,
856
11.5k
                },
857
            )
858
11.5k
            .err_tip(|| "In AwaitedActionDb::subscribe_or_add_action")
?0
;
859
860
11.5k
        Ok(MemoryAwaitedActionSubscriber::new_with_client(
861
11.5k
            rx,
862
11.5k
            client_operation_id,
863
11.5k
            self.action_event_tx.clone(),
864
11.5k
            self.now_fn.clone(),
865
11.5k
        ))
866
11.5k
    }
867
868
11.5k
    async fn try_subscribe(
869
11.5k
        &mut self,
870
11.5k
        client_operation_id: &OperationId,
871
11.5k
        unique_qualifier: &ActionUniqueQualifier,
872
11.5k
        // TODO(palfrey) To simplify the scheduler 2024 refactor, we
873
11.5k
        // removed the ability to upgrade priorities of actions.
874
11.5k
        // we should add priority upgrades back in.
875
11.5k
        _priority: i32,
876
11.5k
    ) -> Result<Option<MemoryAwaitedActionSubscriber<I, NowFn>>, Error> {
877
11.5k
        let 
unique_key11.2k
= match unique_qualifier {
878
11.2k
            ActionUniqueQualifier::Cacheable(unique_key) => unique_key,
879
309
            ActionUniqueQualifier::Uncacheable(_unique_key) => return Ok(None),
880
        };
881
882
11.2k
        let Some(
operation_id8
) = self.action_info_hash_key_to_awaited_action.get(unique_key) else {
883
11.2k
            return Ok(None); // Not currently running.
884
        };
885
886
8
        let Some(tx) = self.operation_id_to_awaited_action.get(operation_id) else {
887
0
            return Err(make_err!(
888
0
                Code::Internal,
889
0
                "operation_id_to_awaited_action and action_info_hash_key_to_awaited_action are out of sync for {unique_key:?} - {operation_id}"
890
0
            ));
891
        };
892
893
0
        error_if!(
894
8
            tx.borrow().state().stage.is_finished(),
895
            "Tried to subscribe to a completed action but it already finished. This should never happen. {:?}",
896
0
            tx.borrow()
897
        );
898
899
8
        let maybe_connected_clients = self
900
8
            .connected_clients_for_operation_id
901
8
            .get_mut(operation_id);
902
8
        let Some(connected_clients) = maybe_connected_clients else {
903
0
            return Err(make_err!(
904
0
                Code::Internal,
905
0
                "connected_clients_for_operation_id and operation_id_to_awaited_action are out of sync for {unique_key:?} - {operation_id}"
906
0
            ));
907
        };
908
8
        *connected_clients += 1;
909
910
        // Immediately mark the keep alive, we don't need to wake anyone
911
        // so we always fake that it was not actually changed.
912
        // Failing update the client could lead to the client connecting
913
        // then not updating the keep alive in time, resulting in the
914
        // operation timing out due to async behavior.
915
8
        tx.send_if_modified(|awaited_action| {
916
8
            awaited_action.update_client_keep_alive((self.now_fn)().now());
917
8
            false
918
8
        });
919
8
        let subscription = tx.subscribe();
920
921
8
        self.client_operation_to_awaited_action
922
8
            .insert(
923
8
                client_operation_id.clone(),
924
8
                Arc::new(ClientAwaitedAction::new(
925
8
                    operation_id.clone(),
926
8
                    self.action_event_tx.clone(),
927
8
                )),
928
8
            )
929
8
            .await;
930
931
8
        Ok(Some(MemoryAwaitedActionSubscriber::new_with_client(
932
8
            subscription,
933
8
            client_operation_id.clone(),
934
8
            self.action_event_tx.clone(),
935
8
            self.now_fn.clone(),
936
8
        )))
937
11.5k
    }
938
}
939
940
#[derive(Debug, MetricsComponent)]
941
pub struct MemoryAwaitedActionDb<I: InstantWrapper, NowFn: Fn() -> I> {
942
    #[metric]
943
    inner: Arc<Mutex<AwaitedActionDbImpl<I, NowFn>>>,
944
    tasks_change_notify: Arc<Notify>,
945
    _handle_awaited_action_events: JoinHandleDropGuard<()>,
946
}
947
948
impl<I: InstantWrapper, NowFn: Fn() -> I + Clone + Send + Sync + 'static>
949
    MemoryAwaitedActionDb<I, NowFn>
950
{
951
189
    pub fn new(
952
189
        eviction_config: &EvictionPolicy,
953
189
        tasks_change_notify: Arc<Notify>,
954
189
        now_fn: NowFn,
955
189
    ) -> Self {
956
189
        Self::new_with_operation_id_fn(
957
189
            eviction_config,
958
189
            tasks_change_notify,
959
189
            now_fn,
960
189
            OperationIdFn::default(),
961
        )
962
189
    }
963
964
    /// Same as `new()`, with the source of new operation ids.
965
306
    pub fn new_with_operation_id_fn(
966
306
        eviction_config: &EvictionPolicy,
967
306
        tasks_change_notify: Arc<Notify>,
968
306
        now_fn: NowFn,
969
306
        operation_id_fn: OperationIdFn,
970
306
    ) -> Self {
971
306
        let (action_event_tx, mut action_event_rx) = mpsc::unbounded_channel();
972
306
        let inner = Arc::new(Mutex::new(AwaitedActionDbImpl {
973
306
            client_operation_to_awaited_action: EvictingMap::new(eviction_config, (now_fn)()),
974
306
            operation_id_to_awaited_action: BTreeMap::new(),
975
306
            action_info_hash_key_to_awaited_action: HashMap::new(),
976
306
            sorted_action_info_hash_keys: SortedAwaitedActions::default(),
977
306
            connected_clients_for_operation_id: HashMap::new(),
978
306
            action_event_tx,
979
306
            now_fn,
980
306
            operation_id_fn,
981
306
        }));
982
306
        let weak_inner = Arc::downgrade(&inner);
983
        Self {
984
306
            inner,
985
306
            tasks_change_notify,
986
306
            _handle_awaited_action_events: spawn!("handle_awaited_action_events", async move 
{260
987
260
                let mut dropped_operation_ids = Vec::with_capacity(MAX_ACTION_EVENTS_RX_PER_CYCLE);
988
                loop {
989
11.4k
                    dropped_operation_ids.clear();
990
11.4k
                    action_event_rx
991
11.4k
                        .recv_many(&mut dropped_operation_ids, MAX_ACTION_EVENTS_RX_PER_CYCLE)
992
11.4k
                        .await;
993
11.2k
                    let Some(inner) = weak_inner.upgrade() else {
994
0
                        return; // Nothing to cleanup, our struct is dropped.
995
                    };
996
11.2k
                    let mut inner = inner.lock().await;
997
11.2k
                    inner
998
11.2k
                        .handle_action_events(dropped_operation_ids.drain(..))
999
11.2k
                        .await;
1000
                }
1001
0
            }),
1002
        }
1003
306
    }
1004
}
1005
1006
impl<I: InstantWrapper, NowFn: Fn() -> I + Clone + Send + Sync + 'static> AwaitedActionDb
1007
    for MemoryAwaitedActionDb<I, NowFn>
1008
{
1009
    type Subscriber = MemoryAwaitedActionSubscriber<I, NowFn>;
1010
1011
204
    async fn get_awaited_action_by_id(
1012
204
        &self,
1013
204
        client_operation_id: &OperationId,
1014
204
    ) -> Result<Option<Self::Subscriber>, Error> {
1015
204
        self.inner
1016
204
            .lock()
1017
204
            .await
1018
204
            .get_awaited_action_by_id(client_operation_id)
1019
204
            .await
1020
204
    }
1021
1022
    /// The listing is read whole, under one hold of the lock taken here,
1023
    /// before the stream exists, and the stream then only hands out what
1024
    /// was read. The lock must not be taken inside the stream: a reader
1025
    /// that prefetches rows (the filter pass reads 64 at a time) would be
1026
    /// left with a lock wait parked inside the stream while it hands rows
1027
    /// on, and if the reader then writes, on the same task, it waits for
1028
    /// the same lock; the release goes to the parked wait, which nothing
1029
    /// polls, and the scheduler stops for good.
1030
8
    async fn get_all_awaited_actions(
1031
8
        &self,
1032
8
    ) -> Result<impl Stream<Item = Result<Self::Subscriber, Error>> + Send, Error> {
1033
8
        let listed: Vec<Result<Self::Subscriber, Error>> = {
1034
8
            let inner = self.inner.lock().await;
1035
8
            inner
1036
8
                .get_awaited_actions_range(Bound::Unbounded, Bound::Unbounded)
1037
12
                .
map8
(|(_, item)| Ok(item))
1038
8
                .collect()
1039
        };
1040
8
        Ok(stream::iter(listed))
1041
8
    }
1042
1043
25.6k
    async fn get_by_operation_id(
1044
25.6k
        &self,
1045
25.6k
        operation_id: &OperationId,
1046
25.6k
    ) -> Result<Option<Self::Subscriber>, Error> {
1047
25.6k
        Ok(self.inner.lock().await.get_by_operation_id(operation_id))
1048
25.6k
    }
1049
1050
    /// Read whole under one hold of the lock, like `get_all_awaited_actions`,
1051
    /// and for the same reason.
1052
31.4k
    async fn get_range_of_actions(
1053
31.4k
        &self,
1054
31.4k
        state: SortedAwaitedActionState,
1055
31.4k
        start: Bound<SortedAwaitedAction>,
1056
31.4k
        end: Bound<SortedAwaitedAction>,
1057
31.4k
        desc: bool,
1058
31.4k
    ) -> Result<impl Stream<Item = Result<Self::Subscriber, Error>> + Send, Error> {
1059
31.4k
        let listed: Vec<Result<Self::Subscriber, Error>> = {
1060
31.4k
            let inner = self.inner.lock().await;
1061
31.4k
            let iterator = inner
1062
31.4k
                .get_range_of_actions(state, (start.as_ref(), end.as_ref()))
1063
6.19M
                .
map31.4k
(|res| {
1064
6.19M
                    res.map(|(_, item)| item)
1065
6.19M
                        .err_tip(|| "In AwaitedActionDb::get_range_of_actions")
1066
6.19M
                });
1067
            // TODO(palfrey) This should probably use the `.left()/right()` pattern,
1068
            // but that doesn't exist in the std or any libraries we use.
1069
31.4k
            if desc {
1070
30.9k
                iterator.rev().collect()
1071
            } else {
1072
517
                iterator.collect()
1073
            }
1074
        };
1075
31.4k
        Ok(stream::iter(listed))
1076
31.4k
    }
1077
1078
21.5k
    async fn update_awaited_action(&self, new_awaited_action: AwaitedAction) -> Result<(), Error> {
1079
21.5k
        self.inner
1080
21.5k
            .lock()
1081
21.5k
            .await
1082
21.5k
            .update_awaited_action(new_awaited_action)
?0
;
1083
21.5k
        self.tasks_change_notify.notify_one();
1084
21.5k
        Ok(())
1085
21.5k
    }
1086
1087
11.5k
    async fn add_action(
1088
11.5k
        &self,
1089
11.5k
        client_operation_id: OperationId,
1090
11.5k
        action_info: Arc<ActionInfo>,
1091
11.5k
        _no_event_action_timeout: Duration,
1092
11.5k
    ) -> Result<Self::Subscriber, Error> {
1093
11.5k
        let subscriber = self
1094
11.5k
            .inner
1095
11.5k
            .lock()
1096
11.5k
            .await
1097
11.5k
            .add_action(client_operation_id, action_info)
1098
11.5k
            .await
?0
;
1099
11.5k
        self.tasks_change_notify.notify_one();
1100
11.5k
        Ok(subscriber)
1101
11.5k
    }
1102
}