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/awaited_action_db/awaited_action.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 std::sync::Arc;
16
use std::time::{SystemTime, UNIX_EPOCH};
17
18
use nativelink_error::{Error, ResultExt, make_input_err};
19
use nativelink_metric::{
20
    MetricFieldData, MetricKind, MetricPublishKnownKindData, MetricsComponent,
21
};
22
use nativelink_util::action_messages::{
23
    ActionInfo, ActionStage, ActionState, OperationId, WorkerId,
24
};
25
use nativelink_util::origin_event::{
26
    BAZEL_METADATA_KEY, OriginMetadata, request_metadata_from_baggage,
27
};
28
use opentelemetry::baggage::BaggageExt;
29
use opentelemetry::context::Context;
30
use opentelemetry_semantic_conventions::attribute::ENDUSER_ID;
31
use serde::{Deserialize, Serialize};
32
use static_assertions::{assert_eq_size, const_assert, const_assert_eq};
33
34
/// The version of the awaited action.
35
/// This number will always increment by one each time
36
/// the action is updated.
37
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
38
struct AwaitedActionVersion(i64);
39
40
impl MetricsComponent for AwaitedActionVersion {
41
0
    fn publish(
42
0
        &self,
43
0
        _kind: MetricKind,
44
0
        _field_metadata: MetricFieldData,
45
0
    ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> {
46
0
        Ok(MetricPublishKnownKindData::Counter(u64::from_ne_bytes(
47
0
            self.0.to_ne_bytes(),
48
0
        )))
49
0
    }
50
}
51
52
/// An action that is being awaited on and last known state.
53
#[derive(Debug, Clone, MetricsComponent, Serialize, Deserialize)]
54
pub struct AwaitedAction {
55
    /// The current version of the action.
56
    #[metric(help = "The version of the AwaitedAction")]
57
    version: AwaitedActionVersion,
58
59
    /// The action that is being awaited on.
60
    #[metric(help = "The action info of the AwaitedAction")]
61
    action_info: Arc<ActionInfo>,
62
63
    /// The operation id of the action.
64
    // If you need the client operation id, it may be set in
65
    // ActionState::operation_id.
66
    #[metric(help = "The operation id of the AwaitedAction")]
67
    operation_id: OperationId,
68
69
    /// The sort key as older schedulers read it. Never used for ordering
70
    /// here; see `PersistedSortKey`.
71
    #[metric(help = "The sort key of the AwaitedAction")]
72
    sort_key: PersistedSortKey,
73
74
    /// The time the action was last updated.
75
    #[metric(help = "The last time the worker updated the AwaitedAction")]
76
    last_worker_updated_timestamp: SystemTime,
77
78
    /// The last time the client sent a keepalive message.
79
    #[metric(help = "The last time the client sent a keepalive message")]
80
    last_client_keepalive_timestamp: SystemTime,
81
82
    /// Worker that is currently running this action, None if unassigned.
83
    #[metric(help = "The worker id of the AwaitedAction")]
84
    worker_id: Option<WorkerId>,
85
86
    /// The current state of the action.
87
    #[metric(help = "The state of the AwaitedAction")]
88
    state: Arc<ActionState>,
89
90
    /// The origin metadata of the action.
91
    maybe_origin_metadata: Option<OriginMetadata>,
92
93
    /// Number of attempts the job has been tried.
94
    #[metric(help = "The number of attempts the AwaitedAction has been tried")]
95
    pub attempts: usize,
96
97
    /// Requeues after the worker running the action was lost. Not an
98
    /// attempt: the action did nothing wrong. Absent in records written
99
    /// before the field existed.
100
    #[serde(default)]
101
    #[metric(help = "How many times the AwaitedAction lost its worker")]
102
    pub worker_losses: usize,
103
104
    /// Requeues with a larger memory reservation after a memory kill. Not
105
    /// an attempt either. Absent in older records.
106
    #[serde(default)]
107
    #[metric(help = "How many times the AwaitedAction was escalated")]
108
    pub escalations: usize,
109
110
    /// Requeues after the worker reported it could not reach its store
111
    /// (`Unavailable`): a CAS pod being replaced, not the action's doing,
112
    /// so not an attempt. Bounded like a lost worker. Absent in older
113
    /// records.
114
    #[serde(default)]
115
    #[metric(help = "How many times the AwaitedAction's worker could not reach its store")]
116
    pub store_losses: usize,
117
118
    /// Requeues after the command ran past its timeout on a worker. Not an
119
    /// attempt: a worker gone slow times out everything it is given, and
120
    /// the retry is the second opinion from another worker. Absent in
121
    /// older records, and left out while zero so a record without one
122
    /// reads as before.
123
    #[serde(default, skip_serializing_if = "is_zero")]
124
    #[metric(help = "How many times the AwaitedAction timed out on a worker and was queued again")]
125
    pub timeouts: usize,
126
127
    /// Workers this action failed or timed out on, most recent last; a
128
    /// retry prefers any other worker while one can take it. Absent in
129
    /// older records, and left out while empty.
130
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
131
    avoid_worker_ids: Vec<WorkerId>,
132
133
    /// The nodes of those workers, as they advertised them, so every
134
    /// scheduler replica can keep a retry off the node and not only the
135
    /// replica that knows the worker. Absent in older records, and left out
136
    /// while empty.
137
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
138
    avoid_node_names: Vec<String>,
139
}
140
141
/// Most workers an action remembers failing on.
142
const MAX_AVOIDED_WORKERS: usize = 8;
143
144
// serde's `skip_serializing_if` takes the field by reference.
145
#[allow(clippy::trivially_copy_pass_by_ref)]
146
15.8k
const fn is_zero(n: &usize) -> bool {
147
15.8k
    *n == 0
148
15.8k
}
149
150
impl AwaitedAction {
151
16.6k
    pub fn new(operation_id: OperationId, action_info: Arc<ActionInfo>, now: SystemTime) -> Self {
152
16.6k
        let sort_key = PersistedSortKey::from_action_info(&action_info);
153
16.6k
        let action_state = Arc::new(ActionState {
154
16.6k
            stage: ActionStage::Queued,
155
16.6k
            // Note: We don't use the real client_operation_id here because
156
16.6k
            // the only place AwaitedAction::new should ever be called is
157
16.6k
            // when the action is first created and this struct will be stored
158
16.6k
            // in the database, so we don't want to accidentally leak the
159
16.6k
            // client_operation_id to all clients.
160
16.6k
            client_operation_id: operation_id.clone(),
161
16.6k
            action_digest: action_info.unique_qualifier.digest(),
162
16.6k
            last_transition_timestamp: now,
163
16.6k
        });
164
165
16.6k
        let ctx = Context::current();
166
16.6k
        let baggage = ctx.baggage();
167
168
16.6k
        let maybe_origin_metadata = if baggage.is_empty() {
169
16.6k
            None
170
        } else {
171
5
            let bazel_metadata = baggage
172
5
                .get(BAZEL_METADATA_KEY)
173
5
                .and_then(|value| 
request_metadata_from_baggage1
(
value.as_str()1
).
ok1
());
174
            Some(OriginMetadata {
175
5
                identity: baggage
176
5
                    .get(ENDUSER_ID)
177
5
                    .map(|v| v.as_str().to_string())
178
5
                    .unwrap_or_default(),
179
5
                bazel_metadata,
180
            })
181
        };
182
183
16.6k
        Self {
184
16.6k
            version: AwaitedActionVersion(0),
185
16.6k
            action_info,
186
16.6k
            operation_id,
187
16.6k
            sort_key,
188
16.6k
            attempts: 0,
189
16.6k
            worker_losses: 0,
190
16.6k
            escalations: 0,
191
16.6k
            store_losses: 0,
192
16.6k
            timeouts: 0,
193
16.6k
            avoid_worker_ids: Vec::new(),
194
16.6k
            avoid_node_names: Vec::new(),
195
16.6k
            last_worker_updated_timestamp: now,
196
16.6k
            last_client_keepalive_timestamp: now,
197
16.6k
            maybe_origin_metadata,
198
16.6k
            worker_id: None,
199
16.6k
            state: action_state,
200
16.6k
        }
201
16.6k
    }
202
203
56.0k
    pub(crate) const fn version(&self) -> i64 {
204
56.0k
        self.version.0
205
56.0k
    }
206
207
549k
    pub(crate) const fn set_version(&mut self, version: i64) {
208
549k
        self.version = AwaitedActionVersion(version);
209
549k
    }
210
211
21.5k
    pub(crate) const fn increment_version(&mut self) {
212
21.5k
        self.version = AwaitedActionVersion(self.version.0 + 1);
213
21.5k
    }
214
215
6.84M
    pub const fn action_info(&self) -> &Arc<ActionInfo> {
216
6.84M
        &self.action_info
217
6.84M
    }
218
219
558k
    pub const fn operation_id(&self) -> &OperationId {
220
558k
        &self.operation_id
221
558k
    }
222
223
    /// The key this action is ordered by. Computed from the action info
224
    /// rather than read from the record, so a record written by any version
225
    /// of the scheduler orders the same way.
226
41.0k
    pub(crate) fn sort_key(&self) -> AwaitedActionSortKey {
227
41.0k
        AwaitedActionSortKey::new_with_unique_key(
228
41.0k
            self.action_info.priority,
229
41.0k
            &self.action_info.insert_timestamp,
230
        )
231
41.0k
    }
232
233
13.7M
    pub const fn state(&self) -> &Arc<ActionState> {
234
13.7M
        &self.state
235
13.7M
    }
236
237
7.90k
    pub fn is_complete(&self) -> bool {
238
7.90k
        match &self.state.stage {
239
            ActionStage::Unknown
240
            | ActionStage::CacheCheck
241
            | ActionStage::Queued
242
6.35k
            | ActionStage::Executing => false,
243
1.55k
            ActionStage::Completed(_) | ActionStage::CompletedFromCache(_) => true,
244
        }
245
7.90k
    }
246
247
6.73M
    pub(crate) const fn maybe_origin_metadata(&self) -> Option<&OriginMetadata> {
248
6.73M
        self.maybe_origin_metadata.as_ref()
249
6.73M
    }
250
251
66.6k
    pub(crate) const fn worker_id(&self) -> Option<&WorkerId> {
252
66.6k
        self.worker_id.as_ref()
253
66.6k
    }
254
255
6.67M
    pub(crate) const fn last_worker_updated_timestamp(&self) -> SystemTime {
256
6.67M
        self.last_worker_updated_timestamp
257
6.67M
    }
258
259
    /// Workers this action failed or timed out on, most recent last.
260
6.66M
    pub fn avoid_worker_ids(&self) -> &[WorkerId] {
261
6.66M
        &self.avoid_worker_ids
262
6.66M
    }
263
264
    /// Remembers that this action failed or timed out on `worker_id`, so a
265
    /// retry prefers another worker. Bounded; the oldest is forgotten.
266
270
    pub(crate) fn avoid_worker(&mut self, worker_id: WorkerId) {
267
270
        self.avoid_worker_ids.retain(|id| 
id2
!=
&worker_id2
);
268
270
        self.avoid_worker_ids.push(worker_id);
269
270
        if self.avoid_worker_ids.len() > MAX_AVOIDED_WORKERS {
270
0
            self.avoid_worker_ids.remove(0);
271
270
        }
272
270
    }
273
274
    /// The nodes this action failed or timed out on, most recent last.
275
6.66M
    pub fn avoid_node_names(&self) -> &[String] {
276
6.66M
        &self.avoid_node_names
277
6.66M
    }
278
279
    /// Remembers the node of a worker this action failed on. Bounded like
280
    /// the workers.
281
2
    pub(crate) fn avoid_node(&mut self, node: &str) {
282
2
        self.avoid_node_names.retain(|n| 
n0
!=
node0
);
283
2
        self.avoid_node_names.push(node.to_owned());
284
2
        if self.avoid_node_names.len() > MAX_AVOIDED_WORKERS {
285
0
            self.avoid_node_names.remove(0);
286
2
        }
287
2
    }
288
289
36.2k
    pub(crate) const fn worker_keep_alive(&mut self, now: SystemTime) {
290
36.2k
        self.last_worker_updated_timestamp = now;
291
36.2k
    }
292
293
7.21M
    pub(crate) const fn last_client_keepalive_timestamp(&self) -> SystemTime {
294
7.21M
        self.last_client_keepalive_timestamp
295
7.21M
    }
296
297
53.7k
    pub(crate) const fn update_client_keep_alive(&mut self, now: SystemTime) {
298
53.7k
        self.last_client_keepalive_timestamp = now;
299
53.7k
    }
300
301
    /// Replaces the action's info, for an escalated reservation. The sort
302
    /// key and identity stay: the action is the same one, asking for more.
303
23
    pub(crate) fn set_action_info(&mut self, action_info: Arc<ActionInfo>) {
304
23
        self.action_info = action_info;
305
23
    }
306
307
106k
    pub(crate) fn set_client_operation_id(&mut self, client_operation_id: OperationId) {
308
106k
        Arc::make_mut(&mut self.state).client_operation_id = client_operation_id;
309
106k
    }
310
311
    /// Sets the worker id that is currently processing this action.
312
21.9k
    pub fn set_worker_id(&mut self, new_maybe_worker_id: Option<WorkerId>, now: SystemTime) {
313
21.9k
        if self.worker_id != new_maybe_worker_id {
314
11.8k
            self.worker_id = new_maybe_worker_id;
315
11.8k
            self.worker_keep_alive(now);
316
11.8k
        
}10.1k
317
21.9k
    }
318
319
    /// Sets the current state of the action and updates the last worker updated timestamp.
320
24.4k
    pub fn worker_set_state(&mut self, mut state: Arc<ActionState>, now: SystemTime) {
321
24.4k
        core::mem::swap(&mut self.state, &mut state);
322
24.4k
        self.worker_keep_alive(now);
323
24.4k
    }
324
}
325
326
impl TryFrom<&[u8]> for AwaitedAction {
327
    type Error = Error;
328
2
    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
329
2
        serde_json::from_slice(value)
330
2
            .map_err(|e| 
make_input_err!0
("{}",
e0
.
to_string0
()))
331
2
            .err_tip(|| "In AwaitedAction::TryFrom::&[u8]")
332
2
    }
333
}
334
335
/// The key used to sort the awaited actions.
336
///
337
/// The rules for sorting are as follows:
338
/// 1. priority of the action (higher runs sooner)
339
/// 2. insert order of the action (older runs sooner)
340
///
341
/// The key packs both into one `u128`: priority in bits 64 to 95 and the
342
/// insert time in nanoseconds since the epoch, inverted, in the low 64 bits,
343
/// so that reading the set in descending order gives the rules above. The
344
/// high 32 bits are zero. Before this the low half held whole seconds, so a
345
/// burst inserted within one second had no order at all and each backend
346
/// picked its own; nanoseconds order every arrival a clock can tell apart,
347
/// and the operation id breaks the rest.
348
///
349
/// This key is not what the record carries; that is `PersistedSortKey`,
350
/// which older schedulers can read. Every record, whichever version wrote
351
/// it, orders by this key computed from its action info. What does differ
352
/// during a rolling upgrade is the Redis index: an older scheduler writes
353
/// the persisted key as 16 hex digits and this one writes this key as 32,
354
/// with leading zeros, and the index sorts the strings lexicographically.
355
/// Under the descending read the 16-digit entries come first, so the
356
/// actions queued before the upgrade run first, which is their order anyway.
357
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
358
#[repr(transparent)]
359
pub struct AwaitedActionSortKey(u128);
360
361
/// The sort key in the layout every released scheduler reads: the priority
362
/// above 32 bits of inverted whole seconds, as a `u64`. It is what the
363
/// record carries under `sort_key`, so a record written here still loads on
364
/// an older scheduler during a rolling upgrade or after a rollback; a
365
/// `u128` there fails to parse as `u64` and takes queue reads and operation
366
/// lookups with it. Ordering never reads it: see `AwaitedAction::sort_key`.
367
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
368
#[repr(transparent)]
369
pub struct PersistedSortKey(u64);
370
371
impl MetricsComponent for PersistedSortKey {
372
0
    fn publish(
373
0
        &self,
374
0
        _kind: MetricKind,
375
0
        _field_metadata: MetricFieldData,
376
0
    ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> {
377
0
        Ok(MetricPublishKnownKindData::Counter(self.0))
378
0
    }
379
}
380
381
impl PersistedSortKey {
382
16.6k
    const fn new(priority: i32, insert_seconds: u32) -> Self {
383
16.6k
        let priority_u32 = i32::MIN.unsigned_abs().wrapping_add_signed(priority);
384
16.6k
        let timestamp = insert_seconds ^ u32::MAX;
385
16.6k
        Self(((priority_u32 as u64) << 32) | timestamp as u64)
386
16.6k
    }
387
388
16.6k
    pub fn from_action_info(action_info: &ActionInfo) -> Self {
389
16.6k
        let seconds = u32::try_from(
390
16.6k
            action_info
391
16.6k
                .insert_timestamp
392
16.6k
                .duration_since(UNIX_EPOCH)
393
16.6k
                .unwrap()
394
16.6k
                .as_secs(),
395
        )
396
16.6k
        .unwrap_or(u32::MAX);
397
16.6k
        Self::new(action_info.priority, seconds)
398
16.6k
    }
399
400
    /// The key as an older scheduler writes it into the Redis index.
401
    #[must_use]
402
5
    pub fn index_field(self) -> String {
403
5
        format!("{:016x}", self.0)
404
5
    }
405
406
    #[must_use]
407
0
    pub const fn as_u64(self) -> u64 {
408
0
        self.0
409
0
    }
410
}
411
412
// The released layout, bit for bit.
413
const_assert_eq!(
414
    PersistedSortKey::new(0x1234_5678, 0x9abc_def0).0,
415
    0x9234_5678_6543_210f
416
);
417
418
impl MetricsComponent for AwaitedActionSortKey {
419
0
    fn publish(
420
0
        &self,
421
0
        _kind: MetricKind,
422
0
        _field_metadata: MetricFieldData,
423
0
    ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> {
424
        // The priority and the top of the timestamp; the low bits are noise
425
        // for a metric.
426
        #[expect(
427
            clippy::cast_possible_truncation,
428
            reason = "the high half is what is wanted"
429
        )]
430
0
        Ok(MetricPublishKnownKindData::Counter((self.0 >> 32) as u64))
431
0
    }
432
}
433
434
impl AwaitedActionSortKey {
435
41.0k
    const fn new(priority: i32, insert_nanos: u64) -> Self {
436
        // Shift the signed i32 range [i32::MIN, i32::MAX] to the unsigned u32 range
437
        // [0, u32::MAX] to preserve ordering when we convert to bytes for sorting.
438
41.0k
        let priority_u32 = i32::MIN.unsigned_abs().wrapping_add_signed(priority);
439
440
        // Invert the timestamp so the larger the timestamp the lower the
441
        // number: descending order reads the oldest first.
442
41.0k
        let timestamp = insert_nanos ^ u64::MAX;
443
444
41.0k
        Self(((priority_u32 as u128) << 64) | timestamp as u128)
445
41.0k
    }
446
447
41.0k
    fn new_with_unique_key(priority: i32, insert_timestamp: &SystemTime) -> Self {
448
41.0k
        let nanos = u64::try_from(
449
41.0k
            insert_timestamp
450
41.0k
                .duration_since(UNIX_EPOCH)
451
41.0k
                .unwrap()
452
41.0k
                .as_nanos(),
453
        )
454
41.0k
        .unwrap_or(u64::MAX);
455
41.0k
        Self::new(priority, nanos)
456
41.0k
    }
457
458
0
    pub const fn as_u128(self) -> u128 {
459
0
        self.0
460
0
    }
461
462
    /// The key as this scheduler writes it into the Redis index.
463
    #[must_use]
464
7.91k
    pub fn index_field(self) -> String {
465
7.91k
        format!("{:032x}", self.0)
466
7.91k
    }
467
}
468
469
// Ensure the size of the sort key is the same as a `u128`.
470
assert_eq_size!(AwaitedActionSortKey, u128);
471
472
const_assert_eq!(
473
    AwaitedActionSortKey::new(0x1234_5678, 0x9abc_def0_1234_5678).0,
474
    // Note: Result has 0x12345678 + 0x80000000 = 0x92345678 because we need
475
    // to shift the `i32::MIN` value to be represented by zero.
476
    // Note: `6543210f_edcb_a987` are the inverted bits of the timestamp.
477
    AwaitedActionSortKey(0x9234_5678_6543_210f_edcb_a987).0
478
);
479
// Ensure the priority is used as the sort key first.
480
const_assert!(
481
    AwaitedActionSortKey::new(i32::MAX, 0).0 > AwaitedActionSortKey::new(i32::MAX - 1, 0).0
482
);
483
const_assert!(AwaitedActionSortKey::new(i32::MAX - 1, 0).0 > AwaitedActionSortKey::new(1, 0).0);
484
const_assert!(AwaitedActionSortKey::new(1, 0).0 > AwaitedActionSortKey::new(0, 0).0);
485
const_assert!(AwaitedActionSortKey::new(0, 0).0 > AwaitedActionSortKey::new(-1, 0).0);
486
const_assert!(AwaitedActionSortKey::new(-1, 0).0 > AwaitedActionSortKey::new(i32::MIN + 1, 0).0);
487
const_assert!(
488
    AwaitedActionSortKey::new(i32::MIN + 1, 0).0 > AwaitedActionSortKey::new(i32::MIN, 0).0
489
);
490
491
// Ensure the insert timestamp is used as the sort key second, and that one
492
// nanosecond is enough to tell two arrivals apart.
493
const_assert!(AwaitedActionSortKey::new(0, u64::MIN).0 > AwaitedActionSortKey::new(0, u64::MAX).0);
494
const_assert!(AwaitedActionSortKey::new(0, 1_000).0 > AwaitedActionSortKey::new(0, 1_001).0);