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/grpc_scheduler.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::future::Future;
16
use core::time::Duration;
17
use std::collections::HashMap;
18
use std::sync::Arc;
19
20
use async_trait::async_trait;
21
use futures::stream::unfold;
22
use futures::{StreamExt, TryFutureExt};
23
use nativelink_config::schedulers::GrpcSpec;
24
use nativelink_error::{Code, Error, ResultExt, error_if, make_err};
25
use nativelink_metric::{MetricsComponent, RootMetricsComponent};
26
use nativelink_proto::build::bazel::remote::execution::v2::capabilities_client::CapabilitiesClient;
27
use nativelink_proto::build::bazel::remote::execution::v2::execution_client::ExecutionClient;
28
use nativelink_proto::build::bazel::remote::execution::v2::{
29
    ExecuteRequest, ExecutionPolicy, GetCapabilitiesRequest, WaitExecutionRequest,
30
};
31
use nativelink_proto::google::longrunning::Operation;
32
use nativelink_util::action_messages::{
33
    ActionInfo, ActionState, ActionUniqueQualifier, DEFAULT_EXECUTION_PRIORITY, OperationId,
34
};
35
use nativelink_util::connection_manager::ConnectionManager;
36
use nativelink_util::operation_state_manager::{
37
    ActionStateResult, ActionStateResultStream, ClientStateManager, OperationFilter,
38
};
39
use nativelink_util::origin_event::OriginMetadata;
40
use nativelink_util::retry::{Retrier, RetryResult};
41
use nativelink_util::telemetry::{forward_client_headers, inject_context};
42
use nativelink_util::{background_spawn, tls_utils};
43
use parking_lot::Mutex;
44
use tokio::select;
45
use tokio::sync::watch;
46
use tokio::time::sleep;
47
use tonic::{Request, Streaming};
48
use tracing::{error, info, warn};
49
50
use crate::known_platform_property_provider::KnownPlatformPropertyProvider;
51
52
struct GrpcActionStateResult {
53
    client_operation_id: OperationId,
54
    rx: watch::Receiver<Arc<ActionState>>,
55
}
56
57
#[async_trait]
58
impl ActionStateResult for GrpcActionStateResult {
59
1
    async fn as_state(&self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
60
        let mut action_state = self.rx.borrow().clone();
61
        Arc::make_mut(&mut action_state).client_operation_id = self.client_operation_id.clone();
62
        // TODO(palfrey) We currently don't support OriginMetadata in this implementation, but
63
        // we should.
64
        Ok((action_state, None))
65
1
    }
66
67
2
    async fn changed(&mut self) -> Result<(Arc<ActionState>, Option<OriginMetadata>), Error> {
68
0
        self.rx.changed().await.map_err(|e| {
69
0
            Error::from_std_err(Code::Internal, &e)
70
0
                .append("Channel closed in GrpcActionStateResult::changed")
71
0
        })?;
72
        let mut action_state = self.rx.borrow().clone();
73
        Arc::make_mut(&mut action_state).client_operation_id = self.client_operation_id.clone();
74
        // TODO(palfrey) We currently don't support OriginMetadata in this implementation, but
75
        // we should.
76
        Ok((action_state, None))
77
2
    }
78
79
0
    async fn as_action_info(&self) -> Result<(Arc<ActionInfo>, Option<OriginMetadata>), Error> {
80
        // TODO(palfrey) We should probably remove as_action_info()
81
        // or implement it properly.
82
        return Err(make_err!(
83
            Code::Unimplemented,
84
            "as_action_info not implemented for GrpcActionStateResult::as_action_info"
85
        ));
86
0
    }
87
}
88
89
#[derive(Debug, MetricsComponent)]
90
pub struct GrpcScheduler {
91
    #[metric(group = "property_managers")]
92
    supported_props: Mutex<HashMap<String, Vec<String>>>,
93
    retrier: Retrier,
94
    connection_manager: ConnectionManager,
95
    forward_headers: Vec<String>,
96
}
97
98
impl GrpcScheduler {
99
1
    pub fn new(spec: &GrpcSpec) -> Result<Self, Error> {
100
1
        Self::new_with_jitter(spec, spec.retry.make_jitter_fn())
101
1
    }
102
103
1
    pub fn new_with_jitter(
104
1
        spec: &GrpcSpec,
105
1
        jitter_fn: Arc<dyn Fn(Duration) -> Duration + Send + Sync>,
106
1
    ) -> Result<Self, Error> {
107
1
        let endpoint = tls_utils::endpoint(&spec.endpoint)
?0
;
108
        Ok(Self {
109
1
            supported_props: Mutex::new(HashMap::new()),
110
1
            retrier: Retrier::new(
111
1
                Arc::new(|duration| 
Box::pin0
(
sleep0
(
duration0
))),
112
1
                jitter_fn.clone(),
113
1
                spec.retry.clone(),
114
            )
115
1
            .with_client("grpc_scheduler"),
116
1
            connection_manager: ConnectionManager::new(
117
1
                core::iter::once(endpoint),
118
1
                spec.connections_per_endpoint,
119
1
                spec.max_concurrent_requests,
120
1
                spec.retry.clone(),
121
1
                jitter_fn,
122
            ),
123
1
            forward_headers: spec
124
1
                .forward_headers
125
1
                .iter()
126
1
                .map(|name| name.to_lowercase())
127
1
                .collect(),
128
        })
129
1
    }
130
131
    /// Wraps a message for the upstream with what this server was handed: the
132
    /// client's nominated headers, the trace and the baggage. The upstream
133
    /// reads the identity and the build metadata from the baggage as this
134
    /// server did; a bare request would leave its origin events anonymous and
135
    /// its buck2 isolation off.
136
3
    fn enrich_request<T>(&self, message: T) -> Request<T> {
137
3
        let mut request = Request::new(message);
138
3
        forward_client_headers(&mut request, &self.forward_headers);
139
3
        inject_context(&mut request);
140
3
        request
141
3
    }
142
143
3
    async fn perform_request<F, Fut, R, I>(&self, input: I, mut request: F) -> Result<R, Error>
144
3
    where
145
3
        F: FnMut(I) -> Fut + Send + Copy,
146
3
        Fut: Future<Output = Result<R, Error>> + Send,
147
3
        R: Send,
148
3
        I: Send + Clone,
149
3
    {
150
3
        self.retrier
151
3
            .retry(unfold(input, move |input| async move {
152
3
                let input_clone = input.clone();
153
                Some((
154
3
                    request(input_clone)
155
3
                        .await
156
3
                        .map_or_else(RetryResult::Retry, RetryResult::Ok),
157
3
                    input,
158
                ))
159
6
            }))
160
3
            .await
161
3
    }
162
163
2
    async fn stream_state(
164
2
        mut result_stream: Streaming<Operation>,
165
2
    ) -> Result<Box<dyn ActionStateResult>, Error> {
166
2
        if let Some(initial_response) = result_stream
167
2
            .message()
168
2
            .await
169
2
            .err_tip(|| "Receiving response from upstream scheduler")
?0
170
        {
171
2
            let client_operation_id = OperationId::from(initial_response.name.as_str());
172
            // Our operation_id is not needed here is just a place holder to recycle existing object.
173
            // The only thing that actually matters is the operation_id.
174
2
            let operation_id = OperationId::default();
175
2
            let action_state =
176
2
                ActionState::try_from_operation(initial_response, operation_id.clone())
177
2
                    .err_tip(|| "In GrpcScheduler::stream_state")
?0
;
178
2
            let (tx, mut rx) = watch::channel(Arc::new(action_state));
179
2
            rx.mark_changed();
180
2
            background_spawn!("grpc_scheduler_stream_state", async move 
{1
181
                loop {
182
1
                    select!(
183
1
                        () = tx.closed() => {
184
0
                            info!(
185
                                "Client disconnected in GrpcScheduler"
186
                            );
187
0
                            return;
188
                        }
189
1
                        
response0
= result_stream.message() => {
190
                            // When the upstream closes the channel, close the
191
                            // downstream too.
192
0
                            let Ok(Some(response)) = response else {
193
0
                                return;
194
                            };
195
0
                            let maybe_action_state = ActionState::try_from_operation(response, operation_id.clone());
196
0
                            match maybe_action_state {
197
0
                                Ok(response) => {
198
0
                                    if let Err(err) = tx.send(Arc::new(response)) {
199
0
                                        info!(
200
                                            ?err,
201
                                            "Client error in GrpcScheduler"
202
                                        );
203
0
                                        return;
204
0
                                    }
205
                                }
206
0
                                Err(err) => {
207
0
                                    error!(
208
                                        ?err,
209
                                        "Error converting response to ActionState in GrpcScheduler"
210
                                    );
211
                                },
212
                            }
213
                        }
214
                    );
215
                }
216
0
            });
217
218
2
            return Ok(Box::new(GrpcActionStateResult {
219
2
                client_operation_id,
220
2
                rx,
221
2
            }));
222
0
        }
223
0
        Err(make_err!(
224
0
            Code::Internal,
225
0
            "Upstream scheduler didn't accept action."
226
0
        ))
227
2
    }
228
229
1
    async fn inner_get_known_properties(&self, instance_name: &str) -> Result<Vec<String>, Error> {
230
1
        if let Some(
supported_props0
) = self.supported_props.lock().get(instance_name) {
231
0
            return Ok(supported_props.clone());
232
1
        }
233
234
1
        self.perform_request(instance_name, |instance_name| async move {
235
            // Not in the cache, lookup the capabilities with the upstream.
236
1
            let channel = self
237
1
                .connection_manager
238
1
                .connection("get_known_properties".into())
239
1
                .await
240
1
                .err_tip(|| "in get_platform_property_manager()")
?0
;
241
1
            let capabilities_result = CapabilitiesClient::new(channel)
242
1
                .get_capabilities(self.enrich_request(GetCapabilitiesRequest {
243
1
                    instance_name: instance_name.to_string(),
244
1
                }))
245
1
                .await
246
1
                .err_tip(|| "Retrieving upstream GrpcScheduler capabilities");
247
1
            let capabilities = capabilities_result
?0
.into_inner();
248
1
            let supported_props = capabilities
249
1
                .execution_capabilities
250
1
                .err_tip(|| "Unable to get execution properties in GrpcScheduler")
?0
251
                .supported_node_properties
252
1
                .into_iter()
253
1
                .collect::<Vec<String>>();
254
255
1
            self.supported_props
256
1
                .lock()
257
1
                .insert(instance_name.to_string(), supported_props.clone());
258
1
            Ok(supported_props)
259
2
        })
260
1
        .await
261
1
    }
262
263
1
    async fn inner_add_action(
264
1
        &self,
265
1
        _client_operation_id: OperationId,
266
1
        action_info: Arc<ActionInfo>,
267
1
    ) -> Result<Box<dyn ActionStateResult>, Error> {
268
1
        let execution_policy = if action_info.priority == DEFAULT_EXECUTION_PRIORITY {
269
1
            None
270
        } else {
271
0
            Some(ExecutionPolicy {
272
0
                priority: action_info.priority,
273
0
            })
274
        };
275
1
        let skip_cache_lookup = match action_info.unique_qualifier {
276
1
            ActionUniqueQualifier::Cacheable(_) => false,
277
0
            ActionUniqueQualifier::Uncacheable(_) => true,
278
        };
279
1
        let request = ExecuteRequest {
280
1
            instance_name: action_info.instance_name().clone(),
281
1
            skip_cache_lookup,
282
1
            action_digest: Some(action_info.digest().into()),
283
1
            execution_policy,
284
1
            // TODO: Get me from the original request, not very important as we ignore it.
285
1
            results_cache_policy: None,
286
1
            digest_function: action_info
287
1
                .unique_qualifier
288
1
                .digest_function()
289
1
                .proto_digest_func()
290
1
                .into(),
291
1
            // Hints only (REAPI v2.9), and not carried in `ActionInfo`, so a
292
1
            // forwarded request does not ask the upstream to inline outputs.
293
1
            inline_stdout: false,
294
1
            inline_stderr: false,
295
1
            inline_output_files: Vec::new(),
296
1
        };
297
1
        let result_stream = self
298
1
            .perform_request(request, |request| async move {
299
1
                let channel = self
300
1
                    .connection_manager
301
1
                    .connection(format!("add_action: {:?}", request.action_digest))
302
1
                    .await
303
1
                    .err_tip(|| "in add_action()")
?0
;
304
1
                ExecutionClient::new(channel)
305
1
                    .execute(self.enrich_request(request))
306
1
                    .await
307
1
                    .err_tip(|| "Sending action to upstream scheduler")
308
2
            })
309
1
            .await
?0
310
1
            .into_inner();
311
1
        Self::stream_state(result_stream).await
312
1
    }
313
314
1
    async fn inner_filter_operations(
315
1
        &self,
316
1
        filter: OperationFilter,
317
1
    ) -> Result<ActionStateResultStream<'_>, Error> {
318
0
        error_if!(
319
1
            filter
320
1
                != OperationFilter {
321
1
                    client_operation_id: filter.client_operation_id.clone(),
322
1
                    ..Default::default()
323
1
                },
324
            "Unsupported filter in GrpcScheduler::filter_operations. Only client_operation_id is supported - {filter:?}"
325
        );
326
1
        let client_operation_id = filter.client_operation_id.ok_or_else(|| 
{0
327
0
            make_err!(Code::InvalidArgument, "`client_operation_id` is the only supported filter in GrpcScheduler::filter_operations")
328
0
        })?;
329
1
        let request = WaitExecutionRequest {
330
1
            name: client_operation_id.to_string(),
331
1
        };
332
1
        let result_stream = self
333
1
            .perform_request(request, |request| async move {
334
1
                let channel = self
335
1
                    .connection_manager
336
1
                    .connection(format!("filter_operations: {}", request.name))
337
1
                    .await
338
1
                    .err_tip(|| "in find_by_client_operation_id()")
?0
;
339
1
                ExecutionClient::new(channel)
340
1
                    .wait_execution(self.enrich_request(request))
341
1
                    .await
342
1
                    .err_tip(|| "While getting wait_execution stream")
343
2
            })
344
1
            .and_then(|result_stream| Self::stream_state(result_stream.into_inner()))
345
1
            .await;
346
1
        match result_stream {
347
1
            Ok(result_stream) => Ok(unfold(
348
1
                Some(result_stream),
349
2
                |maybe_result_stream| async move 
{1
maybe_result_stream1
.
map1
(|v| (
v1
,
None1
)) },
350
            )
351
1
            .boxed()),
352
0
            Err(err) => {
353
0
                warn!(?err, "Error looking up action with upstream scheduler");
354
0
                Ok(futures::stream::empty().boxed())
355
            }
356
        }
357
1
    }
358
}
359
360
#[async_trait]
361
impl ClientStateManager for GrpcScheduler {
362
    async fn add_action(
363
        &self,
364
        client_operation_id: OperationId,
365
        action_info: Arc<ActionInfo>,
366
1
    ) -> Result<Box<dyn ActionStateResult>, Error> {
367
        self.inner_add_action(client_operation_id, action_info)
368
            .await
369
1
    }
370
371
    async fn filter_operations<'a>(
372
        &'a self,
373
        filter: OperationFilter,
374
1
    ) -> Result<ActionStateResultStream<'a>, Error> {
375
        self.inner_filter_operations(filter).await
376
1
    }
377
}
378
379
#[async_trait]
380
impl KnownPlatformPropertyProvider for GrpcScheduler {
381
1
    async fn get_known_properties(&self, instance_name: &str) -> Result<Vec<String>, Error> {
382
        self.inner_get_known_properties(instance_name).await
383
1
    }
384
}
385
386
impl RootMetricsComponent for GrpcScheduler {}