Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-service/src/execution_server.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::convert::Into;
16
use core::pin::Pin;
17
use core::time::Duration;
18
use std::collections::HashMap;
19
use std::fmt;
20
use std::sync::Arc;
21
use std::time::{SystemTime, UNIX_EPOCH};
22
23
use bytes::Bytes;
24
use futures::stream::unfold;
25
use futures::{Stream, StreamExt};
26
use nativelink_config::cas_server::{ExecutionConfig, InstanceName, WithInstanceName};
27
use nativelink_error::{Error, ErrorContext, ResultExt, make_input_err};
28
use nativelink_proto::build::bazel::remote::execution::v2::execution_server::{
29
    Execution, ExecutionServer as Server,
30
};
31
use nativelink_proto::build::bazel::remote::execution::v2::{
32
    Action, Command, ExecuteRequest, WaitExecutionRequest,
33
};
34
use nativelink_proto::google::longrunning::operations_server::{Operations, OperationsServer};
35
use nativelink_proto::google::longrunning::{
36
    CancelOperationRequest, DeleteOperationRequest, GetOperationRequest, ListOperationsRequest,
37
    ListOperationsResponse, Operation, WaitOperationRequest,
38
};
39
use nativelink_proto::google::rpc::{
40
    PreconditionFailure, Status as GrpcStatusProto, precondition_failure,
41
};
42
use nativelink_scheduler::known_platform_property_provider::KnownPlatformPropertyProvider;
43
use nativelink_store::ac_utils::get_and_decode_digest;
44
use nativelink_store::existence_cache_store::ExistenceCacheStore;
45
use nativelink_store::store_manager::StoreManager;
46
use nativelink_util::action_messages::{
47
    ActionInfo, ActionStage, ActionState, ActionUniqueKey, ActionUniqueQualifier,
48
    DEFAULT_EXECUTION_PRIORITY, OperationId, TypeUrl,
49
};
50
use nativelink_util::common::{self, DigestInfo};
51
use nativelink_util::digest_hasher::{DigestHasher, DigestHasherFunc, make_ctx_for_hash_func};
52
use nativelink_util::metrics::{record_execute_stream_drop, record_execute_stream_heartbeat};
53
use nativelink_util::operation_state_manager::{ActionStateResult, OperationFilter};
54
use nativelink_util::origin_event::{BAZEL_METADATA_KEY, request_metadata_from_baggage};
55
use nativelink_util::spawn;
56
use nativelink_util::store_trait::{Store, StoreLike};
57
use nativelink_util::task::JoinHandleDropGuard;
58
use opentelemetry::Context;
59
use opentelemetry::baggage::BaggageExt;
60
use opentelemetry::context::FutureExt;
61
use opentelemetry_semantic_conventions::attribute::ENDUSER_ID;
62
use prost::Message as _;
63
use tonic::{Code, Request, Response, Status};
64
use tracing::{Instrument, Level, debug, error, error_span, info, instrument, warn};
65
66
/// Result of a synchronous `Execute` decision before the async
67
/// scheduling stream begins. Stream is the happy path; Reject is a
68
/// client-facing gRPC `Status` returned without going through NL's
69
/// internal Error/instrumentation pipeline.
70
/// When an action fails because one of its blobs is missing, stop this
71
/// instance's existence cache vouching for that blob before the client
72
/// hears about it. Bazel answers the failure by asking `FindMissingBlobs`
73
/// again and uploading what it is told is missing; if the cache still
74
/// says present (the blob left the object store, which sends no removal
75
/// event, after the cache learned it), nothing is uploaded and the retry
76
/// fails the same way until the cache entry expires.
77
40.9k
async fn forget_missing_input(cas_store: &Store, stage: &ActionStage) {
78
40.9k
    let ActionStage::Completed(
result9.97k
) = stage else {
79
31.0k
        return;
80
    };
81
1
    let Some(ErrorContext::MissingDigest { hash, size }) =
82
9.97k
        result.error.as_ref().map(|err| &err.context)
83
    else {
84
9.96k
        return;
85
    };
86
1
    let Ok(digest) = DigestInfo::try_new(hash, *size) else {
87
0
        return;
88
    };
89
1
    if let Some(cache) = cas_store.downcast_ref::<ExistenceCacheStore<SystemTime>>(None) {
90
1
        cache.remove_from_cache(&digest).await;
91
1
        debug!(%digest, "Action input missing; dropped it from the existence cache");
92
0
    }
93
40.9k
}
94
95
enum ExecuteOutcome<S> {
96
    Stream(S),
97
    Reject(Status),
98
}
99
100
/// Converts `Action.timeout` to a [`Duration`]. An absent timeout means "as
101
/// long as the server allows", which the worker maps to its maximum.
102
///
103
/// A `google.protobuf.Duration` with a negative field, or `nanos` outside
104
/// `0..1_000_000_000`, is not a usable timeout. Casting it straight to
105
/// unsigned integers turned a negative timeout into a near-infinite one, and
106
/// `seconds: -1, nanos: -1` panicked in `Duration::new` when the nanosecond
107
/// carry overflowed. Such a timeout is now rejected with `INVALID_ARGUMENT`.
108
16.2k
fn action_timeout(timeout: Option<prost_types::Duration>) -> Result<Duration, Error> {
109
16.2k
    let Some(
timeout4
) = timeout else {
110
16.2k
        return Ok(Duration::MAX);
111
    };
112
4
    let (Ok(
seconds1
), Ok(
nanos1
)) = (u64::try_from(timeout.seconds), u32::try_from(timeout.nanos))
113
    else {
114
3
        return Err(make_input_err!(
115
3
            "Action.timeout must not be negative, got {}s {}ns",
116
3
            timeout.seconds,
117
3
            timeout.nanos
118
3
        ));
119
    };
120
1
    if nanos >= 1_000_000_000 {
121
1
        return Err(make_input_err!(
122
1
            "Action.timeout nanos must be less than 1000000000, got {nanos}"
123
1
        ));
124
0
    }
125
0
    Ok(Duration::new(seconds, nanos))
126
16.2k
}
127
128
/// Build a tonic [`Status`] of code `FAILED_PRECONDITION` whose details
129
/// carry a `google.rpc.PreconditionFailure` listing the missing CAS
130
/// blobs.
131
///
132
/// The pre-check that calls this is intentionally shallow: it only
133
/// validates the top-level Action proto, `command_digest`, and
134
/// `input_root_digest`. Nested Directory protos and file contents
135
/// under the input root are not walked here — the worker path fetches
136
/// those lazily and reports them via the same mechanism (see
137
/// `action_messages::to_execute_response`, which dispatches on
138
/// `Error::context`).
139
///
140
/// Race note: the pre-check uses `has_many` then the action is
141
/// scheduled; a blob present at check time may be evicted before the
142
/// worker fetches it. That case is intentionally not addressed here —
143
/// the worker path covers it with the same `FAILED_PRECONDITION`
144
/// surfacing, so Bazel retries either way.
145
4
fn missing_blobs_failed_precondition(
146
4
    missing: &[(DigestInfo, &'static str)],
147
4
    summary: &str,
148
4
) -> Status {
149
4
    let pf = PreconditionFailure {
150
4
        violations: missing
151
4
            .iter()
152
4
            .map(|(d, ctx)| precondition_failure::Violation {
153
5
                r#type: common::VIOLATION_TYPE_MISSING.to_string(),
154
                // Per REv2, the subject for a missing-blob violation is
155
                // `blobs/<hash>/<size>` so the client knows exactly
156
                // which digest to re-upload.
157
5
                subject: format!("blobs/{}/{}", d.packed_hash(), d.size_bytes()),
158
5
                description: (*ctx).to_string(),
159
5
            })
160
4
            .collect(),
161
    };
162
163
    // Wrap PreconditionFailure into a google.protobuf.Any.
164
4
    let mut pf_buf: Vec<u8> = Vec::with_capacity(pf.encoded_len());
165
4
    pf.encode(&mut pf_buf)
166
4
        .expect("encoding prost message into Vec<u8> cannot fail");
167
4
    let any = prost_types::Any {
168
4
        type_url: PreconditionFailure::TYPE_URL.to_string(),
169
4
        value: pf_buf,
170
4
    };
171
172
4
    let status_proto = GrpcStatusProto {
173
4
        code: Code::FailedPrecondition as i32,
174
4
        message: summary.to_string(),
175
4
        details: vec![any],
176
4
    };
177
4
    let mut status_buf: Vec<u8> = Vec::with_capacity(status_proto.encoded_len());
178
4
    status_proto
179
4
        .encode(&mut status_buf)
180
4
        .expect("encoding prost message into Vec<u8> cannot fail");
181
182
4
    Status::with_details(
183
4
        Code::FailedPrecondition,
184
4
        summary.to_string(),
185
4
        Bytes::from(status_buf),
186
    )
187
4
}
188
189
type InstanceInfoName = String;
190
191
struct NativelinkOperationId {
192
    instance_name: InstanceInfoName,
193
    client_operation_id: OperationId,
194
}
195
196
impl NativelinkOperationId {
197
16.6k
    const fn new(instance_name: InstanceInfoName, client_operation_id: OperationId) -> Self {
198
16.6k
        Self {
199
16.6k
            instance_name,
200
16.6k
            client_operation_id,
201
16.6k
        }
202
16.6k
    }
203
}
204
205
/// Splits an operation name into the instance that serves it and the id the
206
/// scheduler knows, trying the configured instance names longest first. A
207
/// client operation id may itself carry a `/`: a grpc scheduler hands back the
208
/// upstream's `<instance>/<uuid>` as the id, so the name a client holds reads
209
/// `main/main/<uuid>`, and splitting at the last `/` would look for an
210
/// instance called `main/main`. The instance is the longest configured name
211
/// the operation name starts with, and the id is whatever follows it.
212
424
fn resolve_operation_name<'a>(
213
424
    instances: impl IntoIterator<Item = &'a str>,
214
424
    name: &str,
215
424
) -> Option<(&'a str, OperationId)> {
216
424
    let mut instances: Vec<&str> = instances.into_iter().collect();
217
424
    instances.sort_by_key(|instance| core::cmp::Reverse(
instance42
.
len42
()));
218
434
    
instances.into_iter()424
.
find_map424
(|instance| {
219
434
        name.strip_prefix(instance)
220
434
            .and_then(|rest| 
rest426
.
strip_prefix426
('/'))
221
434
            .filter(|id| !
id422
.
is_empty422
())
222
434
            .map(|id| (
instance421
,
OperationId::from421
(
id421
)))
223
434
    })
224
424
}
225
226
impl fmt::Display for NativelinkOperationId {
227
32.8k
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228
32.8k
        write!(f, "{}/{}", self.instance_name, self.client_operation_id)
229
32.8k
    }
230
}
231
232
#[derive(Clone)]
233
struct InstanceInfo {
234
    scheduler: Arc<dyn KnownPlatformPropertyProvider>,
235
    cas_store: Store,
236
    buck2_invocation_isolation: bool,
237
    /// Re-send the current operation state on an otherwise idle `Execute`
238
    /// or `WaitExecution` stream at this interval. `None` sends no
239
    /// heartbeats.
240
    execute_stream_heartbeat: Option<Duration>,
241
    /// The time an action is queued at.
242
    now_fn: NowFn,
243
}
244
245
/// The clock queued actions are stamped from.
246
pub type NowFn = Arc<dyn Fn() -> SystemTime + Send + Sync>;
247
248
impl fmt::Debug for InstanceInfo {
249
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250
0
        f.debug_struct("InstanceInfo")
251
0
            .field("cas_store", &self.cas_store)
252
0
            .finish_non_exhaustive()
253
0
    }
254
}
255
256
impl InstanceInfo {
257
16.2k
    fn execution_scope(&self) -> Option<String> {
258
16.2k
        if !self.buck2_invocation_isolation {
259
16.1k
            return None;
260
7
        }
261
7
        let context = Context::current();
262
7
        let baggage = context.baggage();
263
6
        let metadata =
264
7
            
request_metadata_from_baggage6
(baggage.get(BAZEL_METADATA_KEY)
?1
.
as_str6
()).
ok6
()
?0
;
265
6
        if metadata.tool_invocation_id.is_empty()
266
5
            || !metadata
267
5
                .tool_details
268
5
                .as_ref()
269
5
                .is_some_and(|tool| tool.tool_name.eq_ignore_ascii_case("buck2"))
270
        {
271
2
            return None;
272
4
        }
273
4
        let identity = baggage
274
4
            .get(ENDUSER_ID)
275
4
            .map(opentelemetry::StringValue::as_str)
276
4
            .unwrap_or_default();
277
4
        let mut hash = DigestHasherFunc::Sha256.hasher();
278
        // Length-prefix the identity so the two fields cannot alias. Use a
279
        // fixed width so all scheduler architectures compute the same scope.
280
4
        hash.update(&(identity.len() as u64).to_be_bytes());
281
4
        hash.update(identity.as_bytes());
282
4
        hash.update(metadata.tool_invocation_id.as_bytes());
283
4
        Some(hash.finalize_digest().packed_hash().to_string())
284
16.2k
    }
285
286
16.2k
    async fn build_action_info(
287
16.2k
        &self,
288
16.2k
        instance_name: String,
289
16.2k
        action_digest: DigestInfo,
290
16.2k
        action: Action,
291
16.2k
        priority: i32,
292
16.2k
        skip_cache_lookup: bool,
293
16.2k
        digest_function: DigestHasherFunc,
294
16.2k
    ) -> Result<ActionInfo, Error> {
295
16.2k
        let command_digest = DigestInfo::try_from(
296
16.2k
            action
297
16.2k
                .command_digest
298
16.2k
                .clone()
299
16.2k
                .ok_or_else(|| 
make_input_err!0
("Expected command_digest to exist"))
?0
,
300
        )
301
16.2k
        .err_tip(|| "Could not decode command digest")
?0
;
302
303
16.2k
        let input_root_digest = DigestInfo::try_from(
304
16.2k
            action
305
16.2k
                .clone()
306
16.2k
                .input_root_digest
307
16.2k
                .ok_or_else(|| 
make_input_err!0
("Expected input_digest_root"))
?0
,
308
0
        )?;
309
16.2k
        let 
timeout16.2k
= action_timeout(action.timeout)
?4
;
310
311
16.2k
        let mut platform_properties = HashMap::new();
312
16.2k
        if let Some(
platform2
) = action.platform {
313
2
            for property in platform.properties {
314
2
                platform_properties.insert(property.name, property.value);
315
2
            }
316
16.2k
        }
317
318
        // Goma puts the properties in the Command.
319
16.2k
        if platform_properties.is_empty() {
320
16.2k
            let command =
321
16.2k
                get_and_decode_digest::<Command>(&self.cas_store, command_digest.into()).await
?0
;
322
16.2k
            if let Some(
platform0
) = command.platform {
323
0
                for property in platform.properties {
324
0
                    platform_properties.insert(property.name, property.value);
325
0
                }
326
16.2k
            }
327
2
        }
328
329
16.2k
        let action_key = ActionUniqueKey {
330
16.2k
            execution_scope: self.execution_scope(),
331
16.2k
            instance_name,
332
16.2k
            digest_function,
333
16.2k
            digest: action_digest,
334
16.2k
        };
335
        // REv2 `Action.do_not_cache`: "the Action's result cannot be cached,
336
        // and in-flight requests for the same Action may not be merged".
337
        // `Uncacheable` skips the cache lookup and the in-flight merge.
338
16.2k
        let unique_qualifier = if skip_cache_lookup || 
action.do_not_cache15.8k
{
339
339
            ActionUniqueQualifier::Uncacheable(action_key)
340
        } else {
341
15.8k
            ActionUniqueQualifier::Cacheable(action_key)
342
        };
343
344
16.2k
        Ok(ActionInfo {
345
16.2k
            command_digest,
346
16.2k
            input_root_digest,
347
16.2k
            timeout,
348
16.2k
            platform_properties,
349
16.2k
            priority,
350
16.2k
            load_timestamp: UNIX_EPOCH,
351
16.2k
            insert_timestamp: (self.now_fn)(),
352
16.2k
            unique_qualifier,
353
16.2k
        })
354
16.2k
    }
355
}
356
357
#[derive(Debug, Clone)]
358
pub struct ExecutionServer {
359
    instance_infos: HashMap<InstanceName, InstanceInfo>,
360
}
361
362
type ExecuteStream = Pin<Box<dyn Stream<Item = Result<Operation, Status>> + Send>>;
363
364
/// Watches an `Execute`/`WaitExecution` response stream so a stream torn
365
/// down before its operation finished is recorded — usually a client that
366
/// walked away, but a server shutdown counts too. tonic drops the stream on
367
/// disconnect without running any handler code, so the `Drop` impl is the
368
/// only place the server can see it; the operation's fate after this is the
369
/// scheduler's client keepalive timeout, which the dropped subscriber (or
370
/// the aborted drive task) stops refreshing.
371
struct ExecuteStreamMonitor {
372
    inner: ExecuteStream,
373
    operation_id: String,
374
    finished: bool,
375
}
376
377
impl Stream for ExecuteStreamMonitor {
378
    type Item = Result<Operation, Status>;
379
380
626k
    fn poll_next(
381
626k
        mut self: Pin<&mut Self>,
382
626k
        cx: &mut core::task::Context<'_>,
383
626k
    ) -> core::task::Poll<Option<Self::Item>> {
384
626k
        let this = self.as_mut().get_mut();
385
626k
        let poll = this.inner.as_mut().poll_next(cx);
386
626k
        if let core::task::Poll::Ready(
item40.9k
) = &poll {
387
40.9k
            match item {
388
40.9k
                Some(Ok(operation)) if operation.don
e9.97k
=>
this.finished = true9.97k
,
389
7
                Some(Err(_)) | None => this.finished = true,
390
31.0k
                Some(Ok(_)) => {}
391
            }
392
585k
        }
393
626k
        poll
394
626k
    }
395
}
396
397
impl Drop for ExecuteStreamMonitor {
398
16.4k
    fn drop(&mut self) {
399
16.4k
        if !self.finished {
400
6.43k
            info!(
401
                operation_id = %self.operation_id,
402
                "Execute stream dropped before the operation finished"
403
            );
404
6.43k
            record_execute_stream_drop();
405
9.97k
        }
406
16.4k
    }
407
}
408
409
impl ExecutionServer {
410
215
    pub fn new(
411
215
        configs: &[WithInstanceName<ExecutionConfig>],
412
215
        scheduler_map: &HashMap<String, Arc<dyn KnownPlatformPropertyProvider>>,
413
215
        store_manager: &StoreManager,
414
215
    ) -> Result<Self, Error> {
415
215
        let mut instance_infos = HashMap::with_capacity(configs.len());
416
215
        for config in configs {
417
215
            let cas_store = store_manager.get_store(&config.cas_store).ok_or_else(|| 
{0
418
0
                make_input_err!("'cas_store': '{}' does not exist", config.cas_store)
419
0
            })?;
420
215
            let scheduler = scheduler_map
421
215
                .get(&config.scheduler)
422
215
                .err_tip(|| 
{0
423
0
                    format!(
424
                        "Scheduler needs config for '{}' because it exists in execution",
425
0
                        config.scheduler
426
                    )
427
0
                })?
428
215
                .clone();
429
430
215
            instance_infos.insert(
431
215
                config.instance_name.clone(),
432
                InstanceInfo {
433
215
                    scheduler,
434
215
                    cas_store,
435
215
                    buck2_invocation_isolation: config.experimental_buck2_invocation_isolation,
436
215
                    execute_stream_heartbeat: (config.execute_stream_heartbeat_s > 0)
437
215
                        .then(|| 
Duration::from_secs1
(
config.execute_stream_heartbeat_s as u641
)),
438
215
                    now_fn: Arc::new(SystemTime::now),
439
                },
440
            );
441
        }
442
215
        Ok(Self { instance_infos })
443
215
    }
444
445
    /// Stamps queued actions from `now_fn` instead of the wall clock, so
446
    /// they share the scheduler's clock in a test that injects one.
447
    #[must_use]
448
188
    pub fn with_now_fn(mut self, now_fn: impl Fn() -> SystemTime + Send + Sync + 'static) -> Self {
449
188
        let now_fn: NowFn = Arc::new(now_fn);
450
188
        for info in self.instance_infos.values_mut() {
451
188
            info.now_fn = now_fn.clone();
452
188
        }
453
188
        self
454
188
    }
455
456
1
    pub fn into_service(self) -> Server<Self> {
457
1
        Server::new(self)
458
1
    }
459
460
0
    pub fn into_operations_service(self) -> OperationsServer<Self> {
461
0
        OperationsServer::new(self)
462
0
    }
463
464
    /// Converts one action state into the client's `Operation`, with the
465
    /// side effects every sent update carries. The bool says whether the
466
    /// operation is finished.
467
40.9k
    async fn operation_from_update(
468
40.9k
        action_update: Arc<ActionState>,
469
40.9k
        client_operation_id: &OperationId,
470
40.9k
        cas_store: &Store,
471
40.9k
    ) -> (Operation, bool) {
472
40.9k
        debug!(?action_update, "Execute Resp Stream");
473
40.9k
        forget_missing_input(cas_store, &action_update.stage).await;
474
40.9k
        let finished = action_update.stage.is_finished();
475
40.9k
        (
476
40.9k
            action_update.as_operation(client_operation_id.clone()),
477
40.9k
            finished,
478
40.9k
        )
479
40.9k
    }
480
481
    /// Waits for the next update and converts it for the client. The bool
482
    /// says whether the stream is over (a finished stage or an error).
483
47.3k
    async fn next_operation(
484
47.3k
        action_listener: &mut Box<dyn ActionStateResult>,
485
47.3k
        client_operation_id: &OperationId,
486
47.3k
        cas_store: &Store,
487
47.3k
    ) -> (Result<Operation, Status>, bool) {
488
47.3k
        match action_listener.changed().await {
489
40.9k
            Ok((action_update, _maybe_origin_metadata)) => {
490
40.9k
                let (operation, finished) =
491
40.9k
                    Self::operation_from_update(action_update, client_operation_id, cas_store)
492
40.9k
                        .await;
493
40.9k
                (Ok(operation), finished)
494
            }
495
6
            Err(err) => {
496
6
                error!(?err, "Error in action_listener stream");
497
6
                (Err(err.into()), true)
498
            }
499
        }
500
40.9k
    }
501
502
16.4k
    fn to_execute_stream(
503
16.4k
        nl_client_operation_id: &NativelinkOperationId,
504
16.4k
        action_listener: Box<dyn ActionStateResult>,
505
16.4k
        cas_store: Store,
506
16.4k
        heartbeat_interval: Option<Duration>,
507
16.4k
    ) -> impl Stream<Item = Result<Operation, Status>> + Send + use<> {
508
        struct HeartbeatState {
509
            rx: tokio::sync::mpsc::Receiver<Result<Operation, Status>>,
510
            /// The last update sent, re-sent as the heartbeat.
511
            last_operation: Option<Operation>,
512
            /// Aborts the driving task when the stream is dropped.
513
            _drive_guard: JoinHandleDropGuard<()>,
514
        }
515
16.4k
        let client_operation_id = OperationId::from(nl_client_operation_id.to_string());
516
16.4k
        let inner: ExecuteStream = if let Some(
interval1
) = heartbeat_interval {
517
            // `changed()` must be awaited without interruption: subscriber
518
            // implementations keep their no-progress watchdog (the
519
            // `no_event_action_timeout` select in the state manager) inside
520
            // one `changed()` call, so cancelling it on a timer would reset
521
            // that watchdog forever. A task drives the listener and feeds a
522
            // channel, whose `recv()` is cancel-safe, and the heartbeat
523
            // timer races the channel instead.
524
1
            let (tx, rx) = tokio::sync::mpsc::channel::<Result<Operation, Status>>(1);
525
1
            let task_client_operation_id = client_operation_id.clone();
526
1
            let mut action_listener = action_listener;
527
            // The guard aborts the task when the stream is dropped, so a
528
            // client disconnect still drops the subscriber and lets its
529
            // keepalive lapse, exactly as without the heartbeat.
530
1
            let drive_guard = spawn!("execute_stream_updates", async move {
531
                // Send the current state first: it seeds the heartbeat, so
532
                // the stream is never silent while waiting for the first
533
                // transition. Subscribers typically re-yield the current
534
                // state as their first update too; duplicates are permitted.
535
1
                match action_listener.as_state().await {
536
1
                    Ok((state, _maybe_origin_metadata)) => {
537
1
                        let (operation, finished) = Self::operation_from_update(
538
1
                            state,
539
1
                            &task_client_operation_id,
540
1
                            &cas_store,
541
                        )
542
1
                        .await;
543
1
                        if tx.send(Ok(operation)).await.is_err() || finished {
544
0
                            return;
545
1
                        }
546
                    }
547
0
                    Err(err) => {
548
0
                        drop(tx.send(Err(err.into())).await);
549
0
                        return;
550
                    }
551
                }
552
                loop {
553
2
                    let (
item1
,
finished1
) = Self::next_operation(
554
2
                        &mut action_listener,
555
2
                        &task_client_operation_id,
556
2
                        &cas_store,
557
                    )
558
2
                    .await;
559
1
                    if tx.send(item).await.is_err() || finished {
560
0
                        return;
561
1
                    }
562
                }
563
0
            });
564
1
            Box::pin(unfold(
565
1
                Some(HeartbeatState {
566
1
                    rx,
567
1
                    last_operation: None,
568
1
                    _drive_guard: drive_guard,
569
1
                }),
570
4
                move |maybe_state| async move {
571
4
                    let mut state = maybe_state
?0
;
572
4
                    let 
received2
= if state.last_operation.is_some() {
573
3
                        match tokio::time::timeout(interval, state.rx.recv()).await {
574
1
                            Ok(received) => received,
575
                            // No update within the interval: re-send the last
576
                            // state. `recv()` is cancel-safe, so no update is
577
                            // lost, and the listener keeps running in its task.
578
2
                            Err(_elapsed) => {
579
2
                                let heartbeat =
580
2
                                    state.last_operation.clone().expect("checked is_some above");
581
2
                                record_execute_stream_heartbeat();
582
2
                                return Some((Ok(heartbeat), Some(state)));
583
                            }
584
                        }
585
                    } else {
586
1
                        state.rx.recv().await
587
                    };
588
2
                    match received {
589
2
                        Some(Ok(operation)) => {
590
2
                            let finished = operation.done;
591
2
                            if !finished {
592
2
                                state.last_operation = Some(operation.clone());
593
2
                            
}0
594
2
                            Some((Ok(operation), (!finished).then_some(state)))
595
                        }
596
0
                        Some(Err(err)) => Some((Err(err), None)),
597
                        // The producer only returns after a terminal item or
598
                        // when this side is gone, so an empty channel here
599
                        // means the drive task died (a panic). Surface an
600
                        // error rather than a clean, successful-looking end.
601
0
                        None => Some((
602
0
                            Err(Status::internal(
603
0
                                "Execute stream update task ended before the operation finished",
604
0
                            )),
605
0
                            None,
606
0
                        )),
607
                    }
608
8
                },
609
            ))
610
        } else {
611
16.4k
            Box::pin(unfold(
612
16.4k
                Some(action_listener),
613
47.3k
                move |maybe_action_listener| {
614
47.3k
                    let client_operation_id = client_operation_id.clone();
615
47.3k
                    let cas_store = cas_store.clone();
616
47.3k
                    async move {
617
47.3k
                        let 
mut action_listener47.3k
= maybe_action_listener
?1
;
618
47.3k
                        let (
item40.9k
,
finished40.9k
) = Self::next_operation(
619
47.3k
                            &mut action_listener,
620
47.3k
                            &client_operation_id,
621
47.3k
                            &cas_store,
622
                        )
623
47.3k
                        .await;
624
40.9k
                        Some((item, (!finished).then_some(action_listener)))
625
40.9k
                    }
626
47.3k
                },
627
            ))
628
        };
629
16.4k
        ExecuteStreamMonitor {
630
16.4k
            inner,
631
16.4k
            operation_id: nl_client_operation_id.to_string(),
632
16.4k
            finished: false,
633
16.4k
        }
634
16.4k
    }
635
636
16.2k
    async fn inner_execute(
637
16.2k
        &self,
638
16.2k
        request: ExecuteRequest,
639
16.2k
    ) -> Result<ExecuteOutcome<impl Stream<Item = Result<Operation, Status>> + Send + use<>>, Error>
640
16.2k
    {
641
16.2k
        let instance_name = request.instance_name;
642
643
16.2k
        let 
instance_info16.2k
= self.instance_infos.get(&instance_name).ok_or_else(||
{1
644
1
            make_input_err!("'instance_name' not configured for '{instance_name}'")
645
1
        })?;
646
647
16.2k
        let digest = DigestInfo::try_from(
648
16.2k
            request
649
16.2k
                .action_digest
650
16.2k
                .ok_or_else(|| 
make_input_err!0
("Expected action_digest to exist"))
?0
,
651
        )
652
16.2k
        .err_tip(|| "Failed to unwrap action cache")
?0
;
653
654
16.2k
        let priority = request
655
16.2k
            .execution_policy
656
16.2k
            .map_or(DEFAULT_EXECUTION_PRIORITY, |p| p.priority);
657
658
16.2k
        let 
action16.2k
= match get_and_decode_digest::<Action>(&instance_info.cas_store, digest.into())
659
16.2k
            .await
660
        {
661
16.2k
            Ok(a) => a,
662
1
            Err(e) if e.code == Code::NotFound => {
663
1
                warn!(
664
                    %digest,
665
                    %e,
666
                    "Execute: Action proto missing from CAS; returning FAILED_PRECONDITION with PreconditionFailure detail so Bazel can re-upload"
667
                );
668
1
                let summary = format!(
669
                    "Action {digest} is missing from CAS; client should re-upload it and retry"
670
                );
671
1
                return Ok(ExecuteOutcome::Reject(missing_blobs_failed_precondition(
672
1
                    &[(digest, "Action")],
673
1
                    &summary,
674
1
                )));
675
            }
676
0
            Err(e) => return Err(e).err_tip(|| "Decoding Action proto in Execute")?,
677
        };
678
679
16.2k
        let action_command_digest = action
680
16.2k
            .command_digest
681
16.2k
            .as_ref()
682
16.2k
            .map(|d| DigestInfo::try_from(d.clone()))
683
16.2k
            .transpose()
684
16.2k
            .err_tip(|| "Failed to parse command_digest from Action")
?0
;
685
16.2k
        let action_input_root_digest = action
686
16.2k
            .input_root_digest
687
16.2k
            .as_ref()
688
16.2k
            .map(|d| DigestInfo::try_from(d.clone()))
689
16.2k
            .transpose()
690
16.2k
            .err_tip(|| "Failed to parse input_root_digest from Action")
?0
;
691
16.2k
        let mut blobs_to_check: Vec<DigestInfo> = Vec::with_capacity(2);
692
16.2k
        if let Some(d) = action_command_digest {
693
16.2k
            blobs_to_check.push(d);
694
16.2k
        
}0
695
16.2k
        if let Some(d) = action_input_root_digest {
696
16.2k
            blobs_to_check.push(d);
697
16.2k
        
}0
698
16.2k
        if !blobs_to_check.is_empty() {
699
32.4k
            let 
store_keys16.2k
:
Vec<_>16.2k
=
blobs_to_check.iter()16.2k
.
map16.2k
(|d| (*d).into()).
collect16.2k
();
700
16.2k
            let sizes = instance_info
701
16.2k
                .cas_store
702
16.2k
                .has_many(&store_keys)
703
16.2k
                .await
704
16.2k
                .err_tip(|| "Validating Action input blobs in CAS")
?0
;
705
16.2k
            let mut missing: Vec<(DigestInfo, &'static str)> = Vec::new();
706
32.4k
            for ((digest, present), label) in blobs_to_check
707
16.2k
                .iter()
708
16.2k
                .zip(sizes.iter())
709
16.2k
                .zip(["Action.command_digest", "Action.input_root_digest"].iter())
710
            {
711
32.4k
                if present.is_none() {
712
4
                    missing.push((*digest, label));
713
32.4k
                }
714
            }
715
16.2k
            if !missing.is_empty() {
716
3
                warn!(
717
                    ?missing,
718
                    %digest,
719
                    "Execute pre-check found missing CAS blobs; returning FAILED_PRECONDITION with PreconditionFailure detail so Bazel can re-upload"
720
                );
721
3
                let summary = format!(
722
                    "{} CAS blob(s) referenced by action {} are missing; client should re-upload them and retry",
723
3
                    missing.len(),
724
                    digest,
725
                );
726
3
                return Ok(ExecuteOutcome::Reject(missing_blobs_failed_precondition(
727
3
                    &missing, &summary,
728
3
                )));
729
16.2k
            }
730
0
        }
731
732
16.2k
        let 
action_info16.2k
= instance_info
733
16.2k
            .build_action_info(
734
16.2k
                instance_name.clone(),
735
16.2k
                digest,
736
16.2k
                action,
737
16.2k
                priority,
738
16.2k
                request.skip_cache_lookup,
739
16.2k
                request
740
16.2k
                    .digest_function
741
16.2k
                    .try_into()
742
16.2k
                    .err_tip(|| "Could not convert digest function in inner_execute()")
?0
,
743
            )
744
16.2k
            .await
?4
;
745
746
16.2k
        let action_listener = instance_info
747
16.2k
            .scheduler
748
16.2k
            .add_action(OperationId::default(), Arc::new(action_info))
749
16.2k
            .await
750
16.2k
            .err_tip(|| "Failed to schedule task")
?0
;
751
752
16.2k
        Ok(ExecuteOutcome::Stream(Self::to_execute_stream(
753
16.2k
            &NativelinkOperationId::new(
754
16.2k
                instance_name,
755
16.2k
                action_listener
756
16.2k
                    .as_state()
757
16.2k
                    .await
758
16.2k
                    .err_tip(|| "In ExecutionServer::inner_execute")
?0
759
                    .0
760
                    .client_operation_id
761
16.2k
                    .clone(),
762
            ),
763
16.2k
            action_listener,
764
16.2k
            instance_info.cas_store.clone(),
765
16.2k
            instance_info.execute_stream_heartbeat,
766
        )))
767
16.2k
    }
768
769
417
    async fn inner_wait_execution(
770
417
        &self,
771
417
        request: WaitExecutionRequest,
772
417
    ) -> Result<impl Stream<Item = Result<Operation, Status>> + Send + use<>, Status> {
773
417
        let Some((instance_name, client_operation_id)) = resolve_operation_name(
774
417
            self.instance_infos.keys().map(String::as_str),
775
417
            &request.name,
776
417
        ) else {
777
0
            return Err(Status::not_found(format!(
778
0
                "No scheduler serves the instance of operation {}",
779
0
                request.name,
780
0
            )));
781
        };
782
417
        let nl_operation_id =
783
417
            NativelinkOperationId::new(instance_name.to_string(), client_operation_id);
784
417
        let instance_info = self
785
417
            .instance_infos
786
417
            .get(instance_name)
787
417
            .ok_or_else(|| 
Status::internal0
("Instance resolved from its own map is missing"))
?0
;
788
417
        let Some(
rx210
) = instance_info
789
417
            .scheduler
790
417
            .filter_operations(OperationFilter {
791
417
                client_operation_id: Some(nl_operation_id.client_operation_id.clone()),
792
417
                ..Default::default()
793
417
            })
794
417
            .await
795
417
            .err_tip(|| "Error running find_existing_action in ExecutionServer::wait_execution")
?0
796
417
            .next()
797
417
            .await
798
        else {
799
207
            return Err(Status::not_found("Failed to find existing task"));
800
        };
801
210
        Ok(Self::to_execute_stream(
802
210
            &nl_operation_id,
803
210
            rx,
804
210
            instance_info.cas_store.clone(),
805
210
            instance_info.execute_stream_heartbeat,
806
210
        ))
807
417
    }
808
}
809
810
#[tonic::async_trait]
811
impl Execution for ExecutionServer {
812
    type ExecuteStream = ExecuteStream;
813
    type WaitExecutionStream = ExecuteStream;
814
815
    #[instrument(
816
        err,
817
        level = Level::ERROR,
818
        skip_all,
819
        fields(request = ?grpc_request.get_ref())
820
    )]
821
    async fn execute(
822
        &self,
823
        grpc_request: Request<ExecuteRequest>,
824
    ) -> Result<Response<ExecuteStream>, Status> {
825
        let request = grpc_request.into_inner();
826
827
        let digest_function = request.digest_function;
828
        let result = self
829
            .inner_execute(request)
830
            .instrument(error_span!("execution_server_execute"))
831
            .with_context(
832
                make_ctx_for_hash_func(digest_function)
833
                    .err_tip(|| "In ExecutionServer::execute")?,
834
            )
835
            .await
836
            .err_tip(|| "Failed on execute() command")?;
837
838
        match result {
839
            ExecuteOutcome::Stream(stream) => Ok(Response::new(Box::pin(stream))),
840
            ExecuteOutcome::Reject(status) => Err(status),
841
        }
842
    }
843
844
    #[instrument(
845
        err,
846
        level = Level::ERROR,
847
        skip_all,
848
        fields(request = ?grpc_request.get_ref())
849
    )]
850
    async fn wait_execution(
851
        &self,
852
        grpc_request: Request<WaitExecutionRequest>,
853
    ) -> Result<Response<ExecuteStream>, Status> {
854
        let request = grpc_request.into_inner();
855
856
        let stream_result = self
857
            .inner_wait_execution(request)
858
            .await
859
            .err_tip(|| "Failed on wait_execution() command")
860
            .map_err(Into::into);
861
        let stream = match stream_result {
862
            Ok(stream) => stream,
863
            Err(e) => return Err(e),
864
        };
865
        debug!(return = "Ok(<stream>)");
866
        Ok(Response::new(Box::pin(stream)))
867
    }
868
}
869
870
#[tonic::async_trait]
871
impl Operations for ExecutionServer {
872
    async fn list_operations(
873
        &self,
874
        _request: Request<ListOperationsRequest>,
875
1
    ) -> Result<Response<ListOperationsResponse>, Status> {
876
        Err(Status::unimplemented("list_operations not implemented"))
877
1
    }
878
879
    async fn delete_operation(
880
        &self,
881
        _request: Request<DeleteOperationRequest>,
882
1
    ) -> Result<Response<()>, Status> {
883
        Err(Status::unimplemented("delete_operation not implemented"))
884
1
    }
885
886
    async fn cancel_operation(
887
        &self,
888
        _request: Request<CancelOperationRequest>,
889
1
    ) -> Result<Response<()>, Status> {
890
        Err(Status::unimplemented("cancel_operation not implemented"))
891
1
    }
892
893
    async fn get_operation(
894
        &self,
895
        request: Request<GetOperationRequest>,
896
2
    ) -> Result<Response<Operation>, Status> {
897
        let inner_request = request.into_inner();
898
899
        let mut stream = Box::pin(
900
            self.inner_wait_execution(WaitExecutionRequest {
901
                name: inner_request.name,
902
            })
903
            .await?,
904
        );
905
906
        let operation = stream
907
            .next()
908
            .await
909
0
            .ok_or_else(|| Status::not_found("Operation not found"))??;
910
911
        Ok(Response::new(operation))
912
2
    }
913
914
    async fn wait_operation(
915
        &self,
916
        request: Request<WaitOperationRequest>,
917
2
    ) -> Result<Response<Operation>, Status> {
918
        let inner_request = request.into_inner();
919
1
        let timeout_opt = inner_request.timeout.map(|d| {
920
1
            let secs = u64::try_from(d.seconds).unwrap_or(0);
921
1
            let nanos = u32::try_from(d.nanos).unwrap_or(0);
922
1
            Duration::new(secs, nanos)
923
1
        });
924
925
        let mut stream = Box::pin(
926
            self.inner_wait_execution(WaitExecutionRequest {
927
                name: inner_request.name,
928
            })
929
            .await?,
930
        );
931
932
        let mut last_operation = stream
933
            .next()
934
            .await
935
0
            .ok_or_else(|| Status::not_found("Operation not found"))??;
936
937
        if last_operation.done {
938
            return Ok(Response::new(last_operation));
939
        }
940
941
1
        let end_time = timeout_opt.map(|t| tokio::time::Instant::now() + t);
942
943
        loop {
944
            let next_fut = stream.next();
945
            let next_res = if let Some(end) = end_time {
946
                match tokio::time::timeout_at(end, next_fut).await {
947
                    Ok(res) => res,
948
                    Err(_) => break,
949
                }
950
            } else {
951
                next_fut.await
952
            };
953
954
            match next_res {
955
                Some(Ok(operation)) => {
956
                    let is_done = operation.done;
957
                    last_operation = operation;
958
                    if is_done {
959
                        break;
960
                    }
961
                }
962
                Some(Err(e)) => return Err(e),
963
                None => break,
964
            }
965
        }
966
967
        Ok(Response::new(last_operation))
968
2
    }
969
}
970
971
#[cfg(test)]
972
#[test]
973
1
fn test_resolve_operation_name() {
974
1
    let instances = ["main", "", "a/b/c"];
975
1
    let examples = [
976
1
        ("main/bar", Some(("main", "bar"))),
977
1
        // The id a grpc scheduler hands back carries the upstream's instance.
978
1
        ("main/main/bar", Some(("main", "main/bar"))),
979
1
        ("/bar", Some(("", "bar"))),
980
1
        ("a/b/c/d", Some(("a/b/c", "d"))),
981
1
        // Not an instance this server has, and not the default one either.
982
1
        ("other/bar", None),
983
1
        // A prefix that is not a whole instance name.
984
1
        ("mainline/bar", None),
985
1
        ("main/", None),
986
1
    ];
987
988
7
    for (input, expected) in 
examples1
{
989
7
        let resolved = resolve_operation_name(instances, input)
990
7
            .map(|(instance, id)| (
instance4
,
id4
.
to_string4
()));
991
7
        assert_eq!(
992
7
            resolved.as_ref().map(|(i, id)| (
*i4
,
id4
.
as_str4
())),
993
            expected,
994
            "{input}"
995
        );
996
    }
997
1
}