Coverage Report

Created: 2026-09-18 20:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-service/src/capabilities_server.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 std::collections::HashMap;
16
use std::sync::Arc;
17
18
use nativelink_config::cas_server::{
19
    CapabilitiesConfig, CasStoreConfig, InstanceName, WithInstanceName,
20
};
21
use nativelink_error::{Error, ResultExt};
22
use nativelink_proto::build::bazel::remote::execution::v2::capabilities_server::{
23
    Capabilities, CapabilitiesServer as Server,
24
};
25
use nativelink_proto::build::bazel::remote::execution::v2::digest_function::Value as DigestFunction;
26
use nativelink_proto::build::bazel::remote::execution::v2::priority_capabilities::PriorityRange;
27
use nativelink_proto::build::bazel::remote::execution::v2::symlink_absolute_path_strategy::Value as SymlinkAbsolutePathStrategy;
28
use nativelink_proto::build::bazel::remote::execution::v2::{
29
    ActionCacheUpdateCapabilities, CacheCapabilities, ExecutionCapabilities, FastCdc2020Params,
30
    GetCapabilitiesRequest, PriorityCapabilities, ServerCapabilities, compressor,
31
};
32
use nativelink_proto::build::bazel::semver::SemVer;
33
use nativelink_scheduler::known_platform_property_provider::KnownPlatformPropertyProvider;
34
use nativelink_util::digest_hasher::default_digest_hasher_func;
35
use tonic::{Request, Response, Status};
36
use tracing::{Level, instrument};
37
38
use crate::wire_compression::RemoteCacheCompressionInstances;
39
40
const MAX_BATCH_TOTAL_SIZE: i64 = 64 * 1024;
41
42
#[derive(Debug, Default)]
43
pub struct CapabilitiesServer {
44
    supported_node_properties_for_instance: HashMap<InstanceName, Vec<String>>,
45
    // Kept separate from `supported_node_properties_for_instance`: that map is sent
46
    // verbatim to clients as `ExecutionCapabilities.supported_node_properties` (REAPI
47
    // file metadata like mtime/unix mode), while compression is advertised through
48
    // `CacheCapabilities.supported_compressors`. Merging them would leak a fake node
49
    // property onto the wire.
50
    remote_cache_compression_instances: RemoteCacheCompressionInstances,
51
    chunking_params_for_instance: HashMap<InstanceName, FastCdc2020Params>,
52
}
53
54
impl CapabilitiesServer {
55
3
    pub async fn new(
56
3
        configs: &[WithInstanceName<CapabilitiesConfig>],
57
3
        scheduler_map: &HashMap<String, Arc<dyn KnownPlatformPropertyProvider>>,
58
3
        remote_cache_compression_instances: &RemoteCacheCompressionInstances,
59
3
        cas_configs: &[WithInstanceName<CasStoreConfig>],
60
3
    ) -> Result<Self, Error> {
61
3
        let mut chunking_params_for_instance = HashMap::new();
62
3
        for 
cas_config0
in cas_configs {
63
0
            if let Some(chunking_config) = &cas_config.experimental_chunking {
64
0
                let avg_chunk_size_bytes = chunking_config
65
0
                    .validated_avg_chunk_size_bytes()
66
0
                    .err_tip(|| {
67
0
                        format!(
68
                            "In 'experimental_chunking' of instance '{}'",
69
                            cas_config.instance_name
70
                        )
71
0
                    })?;
72
0
                chunking_params_for_instance.insert(
73
0
                    cas_config.instance_name.clone(),
74
0
                    FastCdc2020Params {
75
0
                        avg_chunk_size_bytes,
76
0
                        seed: 0,
77
0
                    },
78
                );
79
0
            }
80
        }
81
82
3
        let mut supported_node_properties_for_instance = HashMap::new();
83
3
        for config in configs {
84
3
            if let Some(
remote_execution_cfg1
) = &config.remote_execution {
85
1
                let scheduler =
86
1
                    scheduler_map
87
1
                        .get(&remote_execution_cfg.scheduler)
88
1
                        .err_tip(|| 
{0
89
0
                            format!(
90
                                "Scheduler needs config for '{}' because it exists in capabilities",
91
                                remote_execution_cfg.scheduler
92
                            )
93
0
                        })?;
94
1
                let properties = scheduler
95
1
                    .get_known_properties(&config.instance_name)
96
1
                    .await
97
1
                    .err_tip(|| 
{0
98
0
                        format!(
99
                            "Failed to get platform properties for {}",
100
                            config.instance_name
101
                        )
102
0
                    })?;
103
1
                supported_node_properties_for_instance
104
1
                    .insert(config.instance_name.clone(), properties);
105
2
            }
106
        }
107
3
        Ok(Self {
108
3
            supported_node_properties_for_instance,
109
3
            remote_cache_compression_instances: remote_cache_compression_instances.clone(),
110
3
            chunking_params_for_instance,
111
3
        })
112
3
    }
113
114
0
    pub fn into_service(self) -> Server<Self> {
115
0
        Server::new(self)
116
0
    }
117
}
118
119
#[tonic::async_trait]
120
impl Capabilities for CapabilitiesServer {
121
    #[instrument(
122
        err,
123
        ret(level = Level::INFO),
124
        level = Level::ERROR,
125
        skip_all,
126
        fields(request = ?grpc_request.get_ref())
127
    )]
128
    async fn get_capabilities(
129
        &self,
130
        grpc_request: Request<GetCapabilitiesRequest>,
131
    ) -> Result<Response<ServerCapabilities>, Status> {
132
        let request = grpc_request.into_inner();
133
134
        let instance_name = request.instance_name;
135
        let maybe_supported_node_properties = self
136
            .supported_node_properties_for_instance
137
            .get(&instance_name);
138
        let execution_capabilities =
139
            maybe_supported_node_properties.map(|props_for_instance| ExecutionCapabilities {
140
1
                digest_function: default_digest_hasher_func().proto_digest_func().into(),
141
                exec_enabled: true, // TODO(palfrey) Make this configurable.
142
1
                execution_priority_capabilities: Some(PriorityCapabilities {
143
1
                    priorities: vec![PriorityRange {
144
1
                        min_priority: 0,
145
1
                        max_priority: i32::MAX,
146
1
                    }],
147
1
                }),
148
1
                supported_node_properties: props_for_instance.clone(),
149
1
                digest_functions: vec![
150
1
                    DigestFunction::Sha256.into(),
151
1
                    DigestFunction::Blake3.into(),
152
                ],
153
1
            });
154
155
        let supported_compressors = if self
156
            .remote_cache_compression_instances
157
            .enabled_for(&instance_name)
158
        {
159
            vec![compressor::Value::Zstd.into()]
160
        } else {
161
            Vec::new()
162
        };
163
164
        let chunking_params = self.chunking_params_for_instance.get(&instance_name);
165
        let resp = ServerCapabilities {
166
            cache_capabilities: Some(CacheCapabilities {
167
                digest_functions: vec![
168
                    DigestFunction::Sha256.into(),
169
                    DigestFunction::Blake3.into(),
170
                ],
171
                action_cache_update_capabilities: Some(ActionCacheUpdateCapabilities {
172
                    update_enabled: true,
173
                }),
174
                cache_priority_capabilities: None,
175
                max_batch_total_size_bytes: MAX_BATCH_TOTAL_SIZE,
176
                symlink_absolute_path_strategy: SymlinkAbsolutePathStrategy::Disallowed.into(),
177
                supported_compressors: supported_compressors.clone(),
178
                supported_batch_update_compressors: supported_compressors,
179
                max_cas_blob_size_bytes: 0,
180
                split_blob_support: chunking_params.is_some(),
181
                splice_blob_support: chunking_params.is_some(),
182
                fast_cdc_2020_params: chunking_params.copied(),
183
                rep_max_cdc_params: None,
184
            }),
185
            execution_capabilities,
186
            deprecated_api_version: None,
187
            low_api_version: Some(SemVer {
188
                major: 2,
189
                minor: 0,
190
                patch: 0,
191
                prerelease: String::new(),
192
            }),
193
            high_api_version: Some(SemVer {
194
                major: 2,
195
                minor: 3,
196
                patch: 0,
197
                prerelease: String::new(),
198
            }),
199
        };
200
        Ok(Response::new(resp))
201
    }
202
}