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