/build/source/nativelink-config/src/cas_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 std::collections::HashMap; |
16 | | |
17 | | use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err}; |
18 | | #[cfg(feature = "dev-schema")] |
19 | | use schemars::JsonSchema; |
20 | | use serde::{Deserialize, Serialize}; |
21 | | use tracing::warn; |
22 | | |
23 | | use crate::schedulers::SchedulerSpec; |
24 | | use crate::serde_utils::{ |
25 | | convert_boolean_with_shellexpand, convert_data_size_with_shellexpand, |
26 | | convert_duration_with_shellexpand, convert_numeric_with_shellexpand, |
27 | | convert_optional_numeric_with_shellexpand, convert_optional_string_with_shellexpand, |
28 | | convert_string_with_shellexpand, convert_vec_string_with_shellexpand, |
29 | | }; |
30 | | use crate::stores::{ |
31 | | ClientTlsConfig, ConfigDigestHashFunction, StoreRefName, StoreSpec, StoreType, |
32 | | }; |
33 | | |
34 | | /// Name of the scheduler. This type will be used when referencing a |
35 | | /// scheduler in the `CasConfig::schedulers`'s map key. |
36 | | pub type SchedulerRefName = String; |
37 | | |
38 | | /// Used when the config references `instance_name` in the protocol. |
39 | | pub type InstanceName = String; |
40 | | |
41 | | #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] |
42 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
43 | | pub struct WithInstanceName<T> { |
44 | | /// Used when the config references `instance_name` in the protocol. |
45 | | #[serde(default)] |
46 | | pub instance_name: InstanceName, |
47 | | #[serde(flatten)] |
48 | | pub config: T, |
49 | | } |
50 | | |
51 | | impl<T> core::ops::Deref for WithInstanceName<T> { |
52 | | type Target = T; |
53 | | |
54 | 1.35k | fn deref(&self) -> &Self::Target { |
55 | 1.35k | &self.config |
56 | 1.35k | } |
57 | | } |
58 | | |
59 | | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] |
60 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
61 | | pub struct NamedConfig<Spec> { |
62 | | pub name: String, |
63 | | #[serde(flatten)] |
64 | | pub spec: Spec, |
65 | | } |
66 | | |
67 | | #[derive(Deserialize, Serialize, Debug, Default, Clone, Copy)] |
68 | | #[serde(rename_all = "snake_case")] |
69 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
70 | | pub enum HttpCompressionAlgorithm { |
71 | | /// No compression. |
72 | | #[default] |
73 | | None, |
74 | | |
75 | | /// Zlib compression. |
76 | | Gzip, |
77 | | } |
78 | | |
79 | | /// Note: Compressing data in the cloud rarely has a benefit, since most |
80 | | /// cloud providers have very high bandwidth backplanes. However, for |
81 | | /// clients not inside the data center, it might be a good idea to |
82 | | /// compress data to and from the cloud. This will however come at a high |
83 | | /// CPU and performance cost. If you are making remote execution share the |
84 | | /// same CAS/AC servers as client's remote cache, you can create multiple |
85 | | /// services with different compression settings that are served on |
86 | | /// different ports. Then configure the non-cloud clients to use one port |
87 | | /// and cloud-clients to use another. |
88 | | #[derive(Deserialize, Serialize, Debug, Default)] |
89 | | #[serde(deny_unknown_fields)] |
90 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
91 | | pub struct HttpCompressionConfig { |
92 | | /// The compression algorithm that the server will use when sending |
93 | | /// responses to clients. Enabling this will likely save a lot of |
94 | | /// data transfer, but will consume a lot of CPU and add a lot of |
95 | | /// latency. |
96 | | /// see: <https://github.com/tracemachina/nativelink/issues/109> |
97 | | /// |
98 | | /// Default: `HttpCompressionAlgorithm::None` |
99 | | pub send_compression_algorithm: Option<HttpCompressionAlgorithm>, |
100 | | |
101 | | /// The compression algorithm that the server will accept from clients. |
102 | | /// The server will broadcast the supported compression algorithms to |
103 | | /// clients and the client will choose which compression algorithm to |
104 | | /// use. Enabling this will likely save a lot of data transfer, but |
105 | | /// will consume a lot of CPU and add a lot of latency. |
106 | | /// see: <https://github.com/tracemachina/nativelink/issues/109> |
107 | | /// |
108 | | /// Default: {no supported compression} |
109 | | pub accepted_compression_algorithms: Vec<HttpCompressionAlgorithm>, |
110 | | } |
111 | | |
112 | | #[derive(Deserialize, Serialize, Debug)] |
113 | | #[serde(deny_unknown_fields)] |
114 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
115 | | pub struct AcStoreConfig { |
116 | | /// The store name referenced in the `stores` map in the main config. |
117 | | /// This store name referenced here may be reused multiple times. |
118 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
119 | | pub ac_store: StoreRefName, |
120 | | |
121 | | /// Whether the Action Cache store may be written to, this if set to false |
122 | | /// it is only possible to read from the Action Cache. |
123 | | #[serde(default)] |
124 | | pub read_only: bool, |
125 | | } |
126 | | |
127 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
128 | | #[serde(deny_unknown_fields)] |
129 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
130 | | pub struct CasStoreConfig { |
131 | | /// The store name referenced in the `stores` map in the main config. |
132 | | /// This store name referenced here may be reused multiple times. |
133 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
134 | | pub cas_store: StoreRefName, |
135 | | |
136 | | /// Optional and experimental: enables the REAPI `SplitBlob`/`SpliceBlob` |
137 | | /// RPCs used by content-defined chunking clients (e.g. Bazel's |
138 | | /// `--experimental_remote_cache_chunking`). When set, the capabilities |
139 | | /// service advertises blob split/splice support and `FastCDC` 2020 |
140 | | /// parameters for this instance. When `cas_store` is a grpc store the |
141 | | /// RPCs are forwarded to the backend (which must support chunking with |
142 | | /// matching parameters); otherwise they are served locally. |
143 | | /// |
144 | | /// See `nativelink-config/examples/chunking_cas.json5` for a complete |
145 | | /// configuration example. |
146 | | /// |
147 | | /// Default: not set — chunking RPCs are rejected, nothing is advertised, |
148 | | /// and behavior is identical to when this option did not exist. |
149 | | #[serde(default)] |
150 | | pub experimental_chunking: Option<CasChunkingConfig>, |
151 | | } |
152 | | |
153 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
154 | | #[serde(deny_unknown_fields)] |
155 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
156 | | pub struct CasChunkingConfig { |
157 | | /// The store name referenced in the `stores` map in the main config used |
158 | | /// to persist blob-to-chunks layouts. Keys are the digests of the |
159 | | /// original blobs and values are serialized chunk layouts (which do not |
160 | | /// hash to those digests), so this store MUST NOT perform content digest |
161 | | /// verification and MUST NOT be the same store as `cas_store` — writing |
162 | | /// layouts into the CAS would overwrite blob content. Using the same |
163 | | /// store name as `cas_store` is rejected at startup. |
164 | | /// |
165 | | /// Required unless `cas_store` is a grpc store: for proxied instances |
166 | | /// the `SplitBlob`/`SpliceBlob` RPCs are forwarded to the backend, which |
167 | | /// owns the chunk layouts, and setting an `index_store` is rejected at |
168 | | /// startup. |
169 | | #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")] |
170 | | pub index_store: Option<StoreRefName>, |
171 | | |
172 | | /// The average chunk size in bytes advertised to clients through the |
173 | | /// `FastCDC` 2020 capability parameters and used for server-side |
174 | | /// chunking in `SplitBlob`. Clients derive the minimum and maximum |
175 | | /// chunk sizes from this value (avg / 4 and avg * 4). The value must |
176 | | /// be between 1 KiB and 1 MiB. |
177 | | /// |
178 | | /// Default: 524288 (512 KiB) |
179 | | #[serde(default)] |
180 | | pub avg_chunk_size_bytes: u64, |
181 | | |
182 | | /// Maximum number of chunks accepted in a `SpliceBlob` request or |
183 | | /// produced by on-demand chunking in `SplitBlob`. Blobs that would |
184 | | /// produce more chunks are served without chunking (`SplitBlob` returns |
185 | | /// `NOT_FOUND` and clients fall back to a regular download). This bounds |
186 | | /// the size of stored chunk layouts and of `SplitBlobResponse` messages |
187 | | /// (roughly 80-140 bytes per chunk). At the default average chunk size |
188 | | /// the default cap supports blobs up to ~25 GiB; note that values above |
189 | | /// ~50000 may produce responses that exceed default gRPC message size |
190 | | /// limits on clients. |
191 | | /// |
192 | | /// Default: 50000 |
193 | | #[serde(default)] |
194 | | pub max_chunk_count: u64, |
195 | | } |
196 | | |
197 | | impl CasChunkingConfig { |
198 | | /// Default for `avg_chunk_size_bytes`, the value recommended by the |
199 | | /// REAPI spec for `FastCdc2020Params`. |
200 | | pub const DEFAULT_AVG_CHUNK_SIZE_BYTES: u64 = 512 * 1024; |
201 | | /// Bounds for `avg_chunk_size_bytes` mandated by the REAPI spec for |
202 | | /// `FastCdc2020Params`. |
203 | | pub const MIN_AVG_CHUNK_SIZE_BYTES: u64 = 1024; |
204 | | pub const MAX_AVG_CHUNK_SIZE_BYTES: u64 = 1024 * 1024; |
205 | | /// Default for `max_chunk_count`. |
206 | | pub const DEFAULT_MAX_CHUNK_COUNT: u64 = 50_000; |
207 | | |
208 | | /// Returns `avg_chunk_size_bytes` with the default applied. |
209 | | #[must_use] |
210 | 13 | pub const fn resolved_avg_chunk_size_bytes(&self) -> u64 { |
211 | 13 | if self.avg_chunk_size_bytes == 0 { |
212 | 11 | Self::DEFAULT_AVG_CHUNK_SIZE_BYTES |
213 | | } else { |
214 | 2 | self.avg_chunk_size_bytes |
215 | | } |
216 | 13 | } |
217 | | |
218 | | /// Returns `max_chunk_count` with the default applied. |
219 | | #[must_use] |
220 | 0 | pub const fn resolved_max_chunk_count(&self) -> u64 { |
221 | 0 | if self.max_chunk_count == 0 { |
222 | 0 | Self::DEFAULT_MAX_CHUNK_COUNT |
223 | | } else { |
224 | 0 | self.max_chunk_count |
225 | | } |
226 | 0 | } |
227 | | |
228 | | /// Returns `avg_chunk_size_bytes` with the default applied, or an error |
229 | | /// when the configured value is outside the REAPI-mandated bounds. |
230 | 13 | pub fn validated_avg_chunk_size_bytes(&self) -> Result<u64, Error> { |
231 | 13 | let avg_chunk_size_bytes = self.resolved_avg_chunk_size_bytes(); |
232 | 13 | if !(Self::MIN_AVG_CHUNK_SIZE_BYTES..=Self::MAX_AVG_CHUNK_SIZE_BYTES) |
233 | 13 | .contains(&avg_chunk_size_bytes) |
234 | | { |
235 | 0 | return Err(make_err!( |
236 | 0 | Code::InvalidArgument, |
237 | 0 | "'experimental_chunking.avg_chunk_size_bytes' is {avg_chunk_size_bytes}, must be between {} and {}", |
238 | 0 | Self::MIN_AVG_CHUNK_SIZE_BYTES, |
239 | 0 | Self::MAX_AVG_CHUNK_SIZE_BYTES |
240 | 0 | )); |
241 | 13 | } |
242 | 13 | Ok(avg_chunk_size_bytes) |
243 | 13 | } |
244 | | } |
245 | | |
246 | | #[derive(Deserialize, Serialize, Debug, Default)] |
247 | | #[serde(deny_unknown_fields)] |
248 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
249 | | pub struct CapabilitiesRemoteExecutionConfig { |
250 | | /// Scheduler used to configure the capabilities of remote execution. |
251 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
252 | | pub scheduler: SchedulerRefName, |
253 | | } |
254 | | |
255 | | #[derive(Deserialize, Serialize, Debug, Default)] |
256 | | #[serde(deny_unknown_fields)] |
257 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
258 | | pub struct CapabilitiesConfig { |
259 | | /// Configuration for remote execution capabilities. |
260 | | /// If not set the capabilities service will inform the client that remote |
261 | | /// execution is not supported. |
262 | | pub remote_execution: Option<CapabilitiesRemoteExecutionConfig>, |
263 | | |
264 | | /// Whether this instance supports Bazel remote cache compression. |
265 | | /// When enabled, the capabilities service advertises zstd wire compression |
266 | | /// and the ByteStream/CAS services accept REAPI compressed-blobs/zstd data. |
267 | | /// |
268 | | /// Bazel clients enable this with `--remote_cache_compression`. |
269 | | #[serde( |
270 | | default, |
271 | | skip_serializing_if = "is_default", |
272 | | deserialize_with = "convert_boolean_with_shellexpand" |
273 | | )] |
274 | | pub remote_cache_compression: bool, |
275 | | } |
276 | | |
277 | | #[derive(Deserialize, Serialize, Debug)] |
278 | | #[serde(deny_unknown_fields)] |
279 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
280 | | pub struct ExecutionConfig { |
281 | | /// Keep identical in-flight Buck2 actions from different invocations on |
282 | | /// separate workers. Completed action-cache results remain shared. Pair |
283 | | /// this with single-use workers when capturing execution-container files. |
284 | | /// Other build tools and requests without Buck2 invocation metadata retain |
285 | | /// the default deduplication behavior. All schedulers serving the instance |
286 | | /// must use a runtime supporting this execution scope before enabling it. |
287 | | #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")] |
288 | | pub experimental_buck2_invocation_isolation: bool, |
289 | | |
290 | | /// Seconds between re-sends of the operation's current state on an |
291 | | /// `Execute` or `WaitExecution` stream that has produced no stage |
292 | | /// change. The Remote Execution API permits duplicate updates, and the |
293 | | /// periodic message gives the client and every intermediary (an L7 load |
294 | | /// balancer, for example) proof the stream is alive. Without it the |
295 | | /// server is silent for as long as an action sits queued or executing, |
296 | | /// so a client holding a connection whose server side is gone waits |
297 | | /// until its own timeout rather than failing when the path died. |
298 | | /// |
299 | | /// Default: 0 (disabled, no heartbeats are sent) |
300 | | #[serde( |
301 | | default, |
302 | | deserialize_with = "convert_duration_with_shellexpand", |
303 | | skip_serializing_if = "is_default", |
304 | | alias = "execute_stream_heartbeat" |
305 | | )] |
306 | | pub execute_stream_heartbeat_s: usize, |
307 | | |
308 | | /// The store name referenced in the `stores` map in the main config. |
309 | | /// This store name referenced here may be reused multiple times. |
310 | | /// This value must be a CAS store reference. |
311 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
312 | | pub cas_store: StoreRefName, |
313 | | |
314 | | /// The scheduler name referenced in the `schedulers` map in the main config. |
315 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
316 | | pub scheduler: SchedulerRefName, |
317 | | } |
318 | | |
319 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
320 | | #[serde(deny_unknown_fields)] |
321 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
322 | | pub struct FetchConfig { |
323 | | /// The store name referenced in the `stores` map in the main config. |
324 | | /// This store name referenced here may be reused multiple times. |
325 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
326 | | pub fetch_store: StoreRefName, |
327 | | |
328 | | /// Optional HTTP and HTTPS downloader. Omitted means lookup-only, with no origin access. |
329 | | #[serde(default, skip_serializing_if = "Option::is_none")] |
330 | | pub downloader: Option<FetchDownloaderConfig>, |
331 | | } |
332 | | |
333 | 3 | const fn default_allow_unknown_fetch_qualifiers() -> bool { |
334 | 3 | true |
335 | 3 | } |
336 | | |
337 | | /// Opt-in origin retrieval for the Remote Asset Fetch service. |
338 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
339 | | #[serde(deny_unknown_fields)] |
340 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
341 | | pub struct FetchDownloaderConfig { |
342 | | /// CAS used by this instance's CAS and `ByteStream` services. |
343 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
344 | | pub cas_store: StoreRefName, |
345 | | |
346 | | /// Explicit HTTP forward proxy URL, optionally including credentials. |
347 | | /// Prefer `http_proxy_secret_file` for credentials. Setting both is invalid. |
348 | | /// The default HTTP client connects directly and ignores proxy environment variables. |
349 | | /// An explicit proxy adds a concurrent alternative; the first successful transfer wins. |
350 | | #[serde(default, skip_serializing_if = "Option::is_none")] |
351 | | pub http_proxy: Option<String>, |
352 | | |
353 | | /// File containing the proxy URL. Whitespace is trimmed; read once at startup. |
354 | | #[serde(default, skip_serializing_if = "Option::is_none")] |
355 | | pub http_proxy_secret_file: Option<String>, |
356 | | |
357 | | /// Hosts bypassing the explicit proxy, using `reqwest` matching rules. |
358 | | /// Only applies to the configured proxy; an empty list bypasses no hosts. |
359 | | /// Proxy environment variables are not consulted. |
360 | | #[serde(default)] |
361 | | pub no_proxy: Vec<String>, |
362 | | |
363 | | /// Reject downloads without a supported checksum. Default: false. |
364 | | #[serde(default)] |
365 | | pub require_checksum: bool, |
366 | | |
367 | | /// Accept unrecognized qualifiers as opaque cache-key metadata without applying |
368 | | /// their semantics. Set false to reject them. Default: true. |
369 | | /// `bazel.canonical_id` is recognized as cache identity metadata. |
370 | | #[serde(default = "default_allow_unknown_fetch_qualifiers")] |
371 | | pub allow_unknown_qualifiers: bool, |
372 | | |
373 | | /// Maximum downloaded object size, checked even without Content-Length. |
374 | | /// Zero means no size limit (the default). |
375 | | #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")] |
376 | | pub max_size_bytes: u64, |
377 | | |
378 | | /// Maximum simultaneous download requests per instance, including CAS publication. |
379 | | /// Zero selects the default: 4. Each request can race two attempts when a proxy |
380 | | /// is configured. A positive `max_size_bytes` also bounds each attempt's spool. |
381 | | #[serde(default)] |
382 | | pub max_concurrent_downloads: usize, |
383 | | |
384 | | /// Total origin retrieval timeout across all candidate URIs, including CAS writes. |
385 | | /// Request timeouts are clamped to this limit. Zero selects the default: 300 seconds. |
386 | | #[serde(default, deserialize_with = "convert_duration_with_shellexpand")] |
387 | | pub timeout_s: u64, |
388 | | } |
389 | | |
390 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
391 | | #[serde(deny_unknown_fields)] |
392 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
393 | | pub struct PushConfig { |
394 | | /// The store name referenced in the `stores` map in the main config. |
395 | | /// This store name referenced here may be reused multiple times. |
396 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
397 | | pub push_store: StoreRefName, |
398 | | |
399 | | /// Whether the Action Cache store may be written to, this if set to false |
400 | | /// it is only possible to read from the Action Cache. |
401 | | #[serde(default)] |
402 | | pub read_only: bool, |
403 | | } |
404 | | |
405 | | // From https://github.com/serde-rs/serde/issues/818#issuecomment-287438544 |
406 | 98 | fn is_default<T: Default + PartialEq>(t: &T) -> bool { |
407 | 98 | *t == Default::default() |
408 | 98 | } |
409 | | |
410 | | #[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] |
411 | | #[serde(deny_unknown_fields)] |
412 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
413 | | pub struct ByteStreamConfig { |
414 | | /// Name of the store in the "stores" configuration. |
415 | | pub cas_store: StoreRefName, |
416 | | |
417 | | /// Max number of bytes to send on each grpc stream chunk. |
418 | | /// According to <https://github.com/grpc/grpc.github.io/issues/371> |
419 | | /// 16KiB - 64KiB is optimal. |
420 | | /// |
421 | | /// |
422 | | /// Default: 64KiB |
423 | | #[serde( |
424 | | default, |
425 | | deserialize_with = "convert_data_size_with_shellexpand", |
426 | | skip_serializing_if = "is_default" |
427 | | )] |
428 | | pub max_bytes_per_stream: usize, |
429 | | |
430 | | /// In the event a client disconnects while uploading a blob, we will hold |
431 | | /// the internal stream open for this many seconds before closing it. |
432 | | /// This allows clients that disconnect to reconnect and continue uploading |
433 | | /// the same blob. |
434 | | /// |
435 | | /// Default: 10 seconds |
436 | | #[serde( |
437 | | default, |
438 | | deserialize_with = "convert_duration_with_shellexpand", |
439 | | skip_serializing_if = "is_default", |
440 | | alias = "persist_stream_on_disconnect_timeout" |
441 | | )] |
442 | | pub persist_stream_on_disconnect_timeout_s: usize, |
443 | | |
444 | | /// Recover a resumed upload (a write whose first offset is nonzero) that |
445 | | /// reaches an instance holding no state for its UUID, which happens when |
446 | | /// several instances sit behind a load balancer without session affinity. |
447 | | /// When the blob already exists in the store, the write completes |
448 | | /// immediately; otherwise the client receives UNAVAILABLE, re-queries the |
449 | | /// write status (an instance holding the upload answers with its offset, |
450 | | /// any other answers 0) and resumes or restarts from there. With this |
451 | | /// off, such a resume fails the upload with a non-retryable |
452 | | /// "out of order data" error. |
453 | | /// |
454 | | /// Default: true |
455 | | #[serde(default = "default_true")] |
456 | | pub recover_unknown_uuid_resumes: bool, |
457 | | } |
458 | | |
459 | | // Not derived: the derive would default `recover_unknown_uuid_resumes` to |
460 | | // false, diverging from the deserialization default above. |
461 | | impl Default for ByteStreamConfig { |
462 | 50 | fn default() -> Self { |
463 | 50 | Self { |
464 | 50 | cas_store: StoreRefName::default(), |
465 | 50 | max_bytes_per_stream: 0, |
466 | 50 | persist_stream_on_disconnect_timeout_s: 0, |
467 | 50 | recover_unknown_uuid_resumes: true, |
468 | 50 | } |
469 | 50 | } |
470 | | } |
471 | | |
472 | | // Older bytestream config. All fields are as per the newer docs, but this requires |
473 | | // the hashed `cas_stores` v.s. the WithInstanceName approach. This should _not_ be updated |
474 | | // with newer fields, and eventually dropped |
475 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
476 | | #[serde(deny_unknown_fields)] |
477 | | pub struct OldByteStreamConfig { |
478 | | pub cas_stores: HashMap<InstanceName, StoreRefName>, |
479 | | #[serde( |
480 | | default, |
481 | | deserialize_with = "convert_data_size_with_shellexpand", |
482 | | skip_serializing_if = "is_default" |
483 | | )] |
484 | | pub max_bytes_per_stream: usize, |
485 | | #[serde( |
486 | | default, |
487 | | deserialize_with = "convert_data_size_with_shellexpand", |
488 | | skip_serializing_if = "is_default" |
489 | | )] |
490 | | pub max_decoding_message_size: usize, |
491 | | #[serde( |
492 | | default, |
493 | | deserialize_with = "convert_duration_with_shellexpand", |
494 | | skip_serializing_if = "is_default", |
495 | | alias = "persist_stream_on_disconnect_timeout" |
496 | | )] |
497 | | pub persist_stream_on_disconnect_timeout_s: usize, |
498 | | } |
499 | | |
500 | | #[derive(Deserialize, Serialize, Debug)] |
501 | | #[serde(deny_unknown_fields)] |
502 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
503 | | pub struct WorkerApiConfig { |
504 | | /// The scheduler name referenced in the `schedulers` map in the main config. |
505 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
506 | | pub scheduler: SchedulerRefName, |
507 | | |
508 | | /// Disable the periodic sweep that tells workers to kill operations |
509 | | /// the scheduler no longer has executing on them (for example because |
510 | | /// every client disconnected before the action finished). With the |
511 | | /// sweep disabled such orphaned actions run to completion and still |
512 | | /// warm the action cache, which can be preferable for deployments |
513 | | /// with long or expensive actions whose clients merely retry. |
514 | | /// |
515 | | /// Default: false (the sweep runs) |
516 | | #[serde(default)] |
517 | | pub disable_kill_revoked_operations: bool, |
518 | | |
519 | | /// How often, in seconds, the scheduler checks for operations that |
520 | | /// are still running on a worker but are no longer executing |
521 | | /// according to the state manager, and tells the worker to kill |
522 | | /// them. Each pass costs one state-manager lookup per running |
523 | | /// operation, so store-backed deployments with many concurrent |
524 | | /// actions may want a longer interval. |
525 | | /// |
526 | | /// Default: 5 (0 uses the default) |
527 | | #[serde(default)] |
528 | | pub kill_revoked_operations_interval_s: u64, |
529 | | } |
530 | | |
531 | | #[derive(Deserialize, Serialize, Debug, Default)] |
532 | | #[serde(deny_unknown_fields)] |
533 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
534 | | pub struct AdminConfig { |
535 | | /// Path to register the admin API. If path is "/admin", and your |
536 | | /// domain is "example.com", you can reach the endpoint with: |
537 | | /// <http://example.com/admin>. |
538 | | /// |
539 | | /// Default: "/admin" |
540 | | #[serde(default)] |
541 | | pub path: String, |
542 | | } |
543 | | |
544 | | #[derive(Deserialize, Serialize, Debug, Default)] |
545 | | #[serde(deny_unknown_fields)] |
546 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
547 | | pub struct HealthConfig { |
548 | | /// Path to register the health status check. If path is "/status", and your |
549 | | /// domain is "example.com", you can reach the endpoint with: |
550 | | /// <http://example.com/status>. |
551 | | /// |
552 | | /// Default: "/status" |
553 | | #[serde(default)] |
554 | | pub path: String, |
555 | | |
556 | | /// Timeout on health checks. Default: 5s. |
557 | | #[serde(default)] |
558 | | pub timeout_seconds: u64, |
559 | | |
560 | | /// Path of the readiness check, the stricter sibling of `path`: it |
561 | | /// answers 503 while any component is still initializing, where `path` |
562 | | /// answers 200. A worker's registration with its scheduler is such a |
563 | | /// component, so a Kubernetes readiness probe on this path turns Ready |
564 | | /// only once the worker can take work. Must differ from `path`; the |
565 | | /// same path for both is refused at startup. |
566 | | /// |
567 | | /// Default: "/ready" |
568 | | #[serde(default)] |
569 | | pub readiness_path: String, |
570 | | } |
571 | | |
572 | | #[derive(Deserialize, Serialize, Debug)] |
573 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
574 | | pub struct BepConfig { |
575 | | /// The store to publish build events to. |
576 | | /// The store name referenced in the `stores` map in the main config. |
577 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
578 | | pub store: StoreRefName, |
579 | | } |
580 | | |
581 | | #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
582 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
583 | | pub struct IdentityHeaderSpec { |
584 | | /// The name of the header to look for the identity in. |
585 | | /// Default: "x-identity" |
586 | | #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")] |
587 | | pub header_name: Option<String>, |
588 | | |
589 | | /// If the header is required to be set or fail the request. |
590 | | #[serde(default)] |
591 | | pub required: bool, |
592 | | } |
593 | | |
594 | | #[derive(Deserialize, Serialize, Clone, Debug)] |
595 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
596 | | pub struct OriginEventsPublisherSpec { |
597 | | /// The store to publish nativelink events to. |
598 | | /// The store name referenced in the `stores` map in the main config. |
599 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
600 | | pub store: StoreRefName, |
601 | | } |
602 | | |
603 | | #[derive(Deserialize, Serialize, Clone, Debug)] |
604 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
605 | | pub struct OriginEventsSpec { |
606 | | /// The publisher configuration for origin events. |
607 | | pub publisher: OriginEventsPublisherSpec, |
608 | | |
609 | | /// The maximum number of events to queue before applying back pressure. |
610 | | /// IMPORTANT: Backpressure causes all clients to slow down significantly. |
611 | | /// Zero is default. |
612 | | /// |
613 | | /// Default: 65536 (zero defaults to this) |
614 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
615 | | pub max_event_queue_size: usize, |
616 | | } |
617 | | |
618 | | #[derive(Deserialize, Serialize, Debug)] |
619 | | #[serde(deny_unknown_fields)] |
620 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
621 | | pub struct ServicesConfig { |
622 | | /// The Content Addressable Storage (CAS) backend config. |
623 | | /// The key is the `instance_name` used in the protocol and the |
624 | | /// value is the underlying CAS store config. |
625 | | #[serde( |
626 | | default, |
627 | | deserialize_with = "super::backcompat::opt_vec_with_instance_name" |
628 | | )] |
629 | | pub cas: Option<Vec<WithInstanceName<CasStoreConfig>>>, |
630 | | |
631 | | /// The Action Cache (AC) backend config. |
632 | | /// The key is the `instance_name` used in the protocol and the |
633 | | /// value is the underlying AC store config. |
634 | | #[serde( |
635 | | default, |
636 | | deserialize_with = "super::backcompat::opt_vec_with_instance_name" |
637 | | )] |
638 | | pub ac: Option<Vec<WithInstanceName<AcStoreConfig>>>, |
639 | | |
640 | | /// Capabilities service is required in order to use most of the |
641 | | /// bazel protocol. This service is used to provide the supported |
642 | | /// features and versions of this bazel gRPC service. |
643 | | #[serde( |
644 | | default, |
645 | | deserialize_with = "super::backcompat::opt_vec_with_instance_name" |
646 | | )] |
647 | | pub capabilities: Option<Vec<WithInstanceName<CapabilitiesConfig>>>, |
648 | | |
649 | | /// The remote execution service configuration. |
650 | | /// NOTE: This service is under development and is currently just a |
651 | | /// place holder. |
652 | | #[serde( |
653 | | default, |
654 | | deserialize_with = "super::backcompat::opt_vec_with_instance_name" |
655 | | )] |
656 | | pub execution: Option<Vec<WithInstanceName<ExecutionConfig>>>, |
657 | | |
658 | | /// This is the service used to stream data to and from the CAS. |
659 | | /// Bazel's protocol strongly encourages users to use this streaming |
660 | | /// interface to interact with the CAS when the data is large. |
661 | | #[serde(default, deserialize_with = "super::backcompat::opt_bytestream")] |
662 | | pub bytestream: Option<Vec<WithInstanceName<ByteStreamConfig>>>, |
663 | | |
664 | | /// These two are collectively the Remote Asset protocol, but it's |
665 | | /// defined as two separate services |
666 | | #[serde( |
667 | | default, |
668 | | deserialize_with = "super::backcompat::opt_vec_with_instance_name" |
669 | | )] |
670 | | pub fetch: Option<Vec<WithInstanceName<FetchConfig>>>, |
671 | | |
672 | | #[serde( |
673 | | default, |
674 | | deserialize_with = "super::backcompat::opt_vec_with_instance_name" |
675 | | )] |
676 | | pub push: Option<Vec<WithInstanceName<PushConfig>>>, |
677 | | |
678 | | /// This is the service used for workers to connect and communicate |
679 | | /// through. |
680 | | /// NOTE: This service should be served on a different, non-public port. |
681 | | /// In other words, `worker_api` configuration should not have any other |
682 | | /// services that are served on the same port. Doing so is a security |
683 | | /// risk, as workers have a different permission set than a client |
684 | | /// that makes the remote execution/cache requests. |
685 | | pub worker_api: Option<WorkerApiConfig>, |
686 | | |
687 | | /// Experimental - Build Event Protocol (BEP) configuration. This is |
688 | | /// the service that will consume build events from the client and |
689 | | /// publish them to a store for processing by an external service. |
690 | | pub experimental_bep: Option<BepConfig>, |
691 | | |
692 | | /// This is the service for any administrative tasks. |
693 | | /// It provides a REST API endpoint for administrative purposes. |
694 | | pub admin: Option<AdminConfig>, |
695 | | |
696 | | /// This is the service for health status check. |
697 | | pub health: Option<HealthConfig>, |
698 | | } |
699 | | |
700 | | #[derive(Deserialize, Serialize, Debug)] |
701 | | #[serde(deny_unknown_fields)] |
702 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
703 | | pub struct TlsConfig { |
704 | | /// Path to the certificate file. |
705 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
706 | | pub cert_file: String, |
707 | | |
708 | | /// Path to the private key file. |
709 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
710 | | pub key_file: String, |
711 | | |
712 | | /// Path to the certificate authority for mTLS, if client authentication is |
713 | | /// required for this endpoint. |
714 | | #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")] |
715 | | pub client_ca_file: Option<String>, |
716 | | |
717 | | /// Path to the certificate revocation list for mTLS, if client |
718 | | /// authentication is required for this endpoint. |
719 | | #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")] |
720 | | pub client_crl_file: Option<String>, |
721 | | } |
722 | | |
723 | | /// Advanced Http configurations. These are generally should not be set. |
724 | | /// For documentation on what each of these do, see the hyper documentation: |
725 | | /// See: <https://docs.rs/hyper/latest/hyper/server/conn/struct.Http.html> |
726 | | /// |
727 | | /// Note: All of these default to hyper's default values unless otherwise |
728 | | /// specified. |
729 | | #[derive(Deserialize, Serialize, Debug, Default, Clone, Copy)] |
730 | | #[serde(deny_unknown_fields)] |
731 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
732 | | pub struct HttpServerConfig { |
733 | | /// Interval to send keep-alive pings via HTTP2. |
734 | | /// Note: This is in seconds. |
735 | | #[serde( |
736 | | default, |
737 | | deserialize_with = "convert_optional_numeric_with_shellexpand" |
738 | | )] |
739 | | pub http2_keep_alive_interval: Option<u32>, |
740 | | |
741 | | #[serde( |
742 | | default, |
743 | | deserialize_with = "convert_optional_numeric_with_shellexpand" |
744 | | )] |
745 | | pub experimental_http2_max_pending_accept_reset_streams: Option<u32>, |
746 | | |
747 | | #[serde( |
748 | | default, |
749 | | deserialize_with = "convert_optional_numeric_with_shellexpand" |
750 | | )] |
751 | | pub experimental_http2_initial_stream_window_size: Option<u32>, |
752 | | |
753 | | #[serde( |
754 | | default, |
755 | | deserialize_with = "convert_optional_numeric_with_shellexpand" |
756 | | )] |
757 | | pub experimental_http2_initial_connection_window_size: Option<u32>, |
758 | | |
759 | | #[serde(default)] |
760 | | pub experimental_http2_adaptive_window: Option<bool>, |
761 | | |
762 | | #[serde( |
763 | | default, |
764 | | deserialize_with = "convert_optional_numeric_with_shellexpand" |
765 | | )] |
766 | | pub experimental_http2_max_frame_size: Option<u32>, |
767 | | |
768 | | #[serde( |
769 | | default, |
770 | | deserialize_with = "convert_optional_numeric_with_shellexpand" |
771 | | )] |
772 | | pub experimental_http2_max_concurrent_streams: Option<u32>, |
773 | | |
774 | | #[serde( |
775 | | default, |
776 | | deserialize_with = "convert_optional_numeric_with_shellexpand", |
777 | | alias = "experimental_http2_keep_alive_timeout" |
778 | | )] |
779 | | pub experimental_http2_keep_alive_timeout_s: Option<u32>, |
780 | | |
781 | | #[serde( |
782 | | default, |
783 | | deserialize_with = "convert_optional_numeric_with_shellexpand" |
784 | | )] |
785 | | pub experimental_http2_max_send_buf_size: Option<u32>, |
786 | | |
787 | | #[serde(default)] |
788 | | pub experimental_http2_enable_connect_protocol: Option<bool>, |
789 | | |
790 | | #[serde( |
791 | | default, |
792 | | deserialize_with = "convert_optional_numeric_with_shellexpand" |
793 | | )] |
794 | | pub experimental_http2_max_header_list_size: Option<u32>, |
795 | | } |
796 | | |
797 | | #[derive(Deserialize, Serialize, Debug)] |
798 | | #[serde(rename_all = "snake_case")] |
799 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
800 | | pub enum ListenerConfig { |
801 | | /// Listener for HTTP/HTTPS/HTTP2 sockets. |
802 | | Http(HttpListener), |
803 | | } |
804 | | |
805 | | #[derive(Deserialize, Serialize, Debug, Default)] |
806 | | #[serde(deny_unknown_fields)] |
807 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
808 | | pub struct HttpListener { |
809 | | /// Address to listen on. Example: `127.0.0.1:8080` or `:8080` to listen |
810 | | /// to all IPs. |
811 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
812 | | pub socket_address: String, |
813 | | |
814 | | /// Allow binding `socket_address` before it is assigned locally. |
815 | | /// |
816 | | /// Default: false |
817 | | #[serde(default)] |
818 | | pub freebind: bool, |
819 | | |
820 | | /// Disable batching of small TCP writes on accepted connections, including |
821 | | /// HTTP/2 control frames. |
822 | | /// Applies to both plaintext and TLS connections. Set to true to opt into |
823 | | /// sending small writes promptly. When false or omitted, preserves default |
824 | | /// TCP buffering behavior. |
825 | | /// Older binaries without this option reject the field even when set to |
826 | | /// false. Config templates must omit it until all consuming binaries support |
827 | | /// it. Remove the field entirely before rolling back to an older binary. |
828 | | /// |
829 | | /// Default: false |
830 | | #[serde(default)] |
831 | | pub tcp_nodelay: bool, |
832 | | |
833 | | /// Data transport compression configuration to use for this service. |
834 | | #[serde(default)] |
835 | | pub compression: HttpCompressionConfig, |
836 | | |
837 | | /// Advanced Http server configuration. |
838 | | #[serde(default)] |
839 | | pub advanced_http: HttpServerConfig, |
840 | | |
841 | | /// Maximum number of bytes to decode on each grpc stream chunk. |
842 | | /// Default: 4 MiB |
843 | | #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")] |
844 | | pub max_decoding_message_size: usize, |
845 | | |
846 | | /// Tls Configuration for this server. |
847 | | /// If not set, the server will not use TLS. |
848 | | /// |
849 | | /// Default: None |
850 | | #[serde(default)] |
851 | | pub tls: Option<TlsConfig>, |
852 | | } |
853 | | |
854 | | #[derive(Deserialize, Serialize, Debug)] |
855 | | #[serde(deny_unknown_fields)] |
856 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
857 | | pub struct ServerConfig { |
858 | | /// Name of the server. This is used to help identify the service |
859 | | /// for telemetry and logs. |
860 | | /// |
861 | | /// Default: {index of server in config} |
862 | | #[serde(default, deserialize_with = "convert_string_with_shellexpand")] |
863 | | pub name: String, |
864 | | |
865 | | /// Configuration |
866 | | pub listener: ListenerConfig, |
867 | | |
868 | | /// Services to attach to server. |
869 | | pub services: Option<ServicesConfig>, |
870 | | |
871 | | /// The config related to identifying the client. |
872 | | /// Default: {see `IdentityHeaderSpec`} |
873 | | #[serde(default)] |
874 | | pub experimental_identity_header: IdentityHeaderSpec, |
875 | | } |
876 | | |
877 | | /// The scale a CPU property is advertised on. |
878 | | #[derive(Copy, Clone, Deserialize, Serialize, Debug, Default, PartialEq, Eq)] |
879 | | #[serde(rename_all = "snake_case")] |
880 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
881 | | pub enum CpuUnit { |
882 | | /// Whole cores, rounded down: what `cpu_count: { query_cmd: "nproc" }` |
883 | | /// advertises and what an action asking for `cpu_count=1` means. |
884 | | #[default] |
885 | | Cores, |
886 | | /// Thousandths of a core, for a fleet whose actions and workers already |
887 | | /// speak that scale. |
888 | | Millicores, |
889 | | } |
890 | | |
891 | | /// Advertise CPU and memory from what the worker can actually see. The |
892 | | /// worker reads `cpu.max` and `memory.max` from its own cgroup v2 directory |
893 | | /// (found through `/proc/self/cgroup`, so a privileged container that sees |
894 | | /// the host's tree still finds its own) up to the nearest limited ancestor, |
895 | | /// the pod's limits on Kubernetes; with no limit at any level the configured |
896 | | /// properties stand. It takes off what it needs for itself, divides |
897 | | /// the memory by the enforcement headroom, and sets the two properties to |
898 | | /// the result at registration. What the scheduler packs against is then |
899 | | /// derived from the one number that is enforced, instead of typed in twice. |
900 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
901 | | #[serde(deny_unknown_fields)] |
902 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
903 | | pub struct CapacityConfig { |
904 | | /// Property that carries CPU, on the scale `cpu_unit` names. |
905 | | /// Default: `cpu_count` |
906 | | #[serde( |
907 | | default = "default_capacity_cpu_property_name", |
908 | | deserialize_with = "convert_string_with_shellexpand" |
909 | | )] |
910 | | pub cpu_property_name: String, |
911 | | |
912 | | /// Property that carries memory, in KiB. |
913 | | /// Default: `memory_kb` |
914 | | #[serde( |
915 | | default = "default_capacity_memory_property_name", |
916 | | deserialize_with = "convert_string_with_shellexpand" |
917 | | )] |
918 | | pub memory_property_name: String, |
919 | | |
920 | | /// The scale the CPU property is advertised on. Whole cores by default, |
921 | | /// the scale every other example in this configuration uses; a fleet |
922 | | /// that advertises and requests thousandths of a core sets `millicores`. |
923 | | /// Getting this wrong is a thousandfold error in how many actions the |
924 | | /// scheduler packs onto the worker. |
925 | | /// Default: cores |
926 | | #[serde(default)] |
927 | | pub cpu_unit: CpuUnit, |
928 | | |
929 | | /// CPU the worker keeps for itself, in thousandths of a core whatever |
930 | | /// `cpu_unit` says. |
931 | | /// Default: 1000 |
932 | | #[serde( |
933 | | default = "default_capacity_overhead_cpu_millicores", |
934 | | deserialize_with = "convert_numeric_with_shellexpand" |
935 | | )] |
936 | | pub overhead_cpu_millicores: u64, |
937 | | |
938 | | /// Memory the worker keeps for itself (the process, its directory cache, |
939 | | /// output buffers), in KiB. |
940 | | /// Default: 2097152 (2 GiB) |
941 | | #[serde( |
942 | | default = "default_capacity_overhead_memory_kb", |
943 | | deserialize_with = "convert_numeric_with_shellexpand" |
944 | | )] |
945 | | pub overhead_memory_kb: u64, |
946 | | |
947 | | /// Percent the advertised memory is reduced by, so that actions at their |
948 | | /// enforcement ceiling (reservation plus headroom) still fit the limit. |
949 | | /// Unset, it follows `resource_enforcement.memory_headroom_percent` |
950 | | /// while memory enforcement is on, and is 0 otherwise, so the two |
951 | | /// cannot drift apart by being typed twice. |
952 | | /// Default: unset |
953 | | #[serde( |
954 | | default, |
955 | | deserialize_with = "convert_optional_numeric_with_shellexpand" |
956 | | )] |
957 | | pub memory_headroom_percent: Option<u64>, |
958 | | } |
959 | | |
960 | 0 | fn default_capacity_cpu_property_name() -> String { |
961 | 0 | "cpu_count".to_string() |
962 | 0 | } |
963 | | |
964 | 0 | fn default_capacity_memory_property_name() -> String { |
965 | 0 | "memory_kb".to_string() |
966 | 0 | } |
967 | | |
968 | 0 | const fn default_capacity_overhead_cpu_millicores() -> u64 { |
969 | 0 | 1000 |
970 | 0 | } |
971 | | |
972 | 0 | const fn default_capacity_overhead_memory_kb() -> u64 { |
973 | 0 | 2_097_152 |
974 | 0 | } |
975 | | |
976 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
977 | | #[serde(rename_all = "snake_case")] |
978 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
979 | | pub enum WorkerProperty { |
980 | | /// List of static values. |
981 | | /// Note: Generally there should only ever be 1 value, but if the platform |
982 | | /// property key is `PropertyType::Priority` it may have more than one value. |
983 | | #[serde(deserialize_with = "convert_vec_string_with_shellexpand")] |
984 | | Values(Vec<String>), |
985 | | |
986 | | /// A dynamic configuration. The string will be executed as a command |
987 | | /// (not shell) and will be split by "\n" (new line character). |
988 | | QueryCmd(String), |
989 | | } |
990 | | |
991 | | /// Generic config for an endpoint and associated configs. |
992 | | #[derive(Deserialize, Serialize, Debug, Default)] |
993 | | #[serde(deny_unknown_fields)] |
994 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
995 | | pub struct EndpointConfig { |
996 | | /// URI of the endpoint. |
997 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
998 | | pub uri: String, |
999 | | |
1000 | | /// Timeout in seconds for connecting to the endpoint and for a request |
1001 | | /// to be answered. The worker's messages to the scheduler (keepalives, |
1002 | | /// acknowledgements, results) ride on the one `ConnectWorker` stream |
1003 | | /// opened at registration, which this does not bound; the `GoingAway` |
1004 | | /// sent at shutdown is bounded by it, so a stalled connection cannot |
1005 | | /// hold a pod past its grace period. A keepalive goes out every half of |
1006 | | /// it. |
1007 | | /// Default: 5 seconds |
1008 | | pub timeout: Option<f32>, |
1009 | | |
1010 | | /// TCP keepalive interval (seconds), probing the socket under the |
1011 | | /// scheduler connection so a dead peer is noticed at the OS level. |
1012 | | /// If not set or 0, defaults to 30 seconds. |
1013 | | #[serde(default, deserialize_with = "convert_duration_with_shellexpand")] |
1014 | | pub tcp_keepalive_s: u64, |
1015 | | |
1016 | | /// HTTP/2 keepalive interval (seconds): PING frames on the scheduler |
1017 | | /// connection, so one that has gone dead underneath fails instead of |
1018 | | /// hanging every message on it. |
1019 | | /// If not set or 0, defaults to 30 seconds. |
1020 | | #[serde(default, deserialize_with = "convert_duration_with_shellexpand")] |
1021 | | pub http2_keepalive_interval_s: u64, |
1022 | | |
1023 | | /// HTTP/2 keepalive timeout (seconds): a PING unanswered for this long |
1024 | | /// ends the connection, and the worker reconnects. |
1025 | | /// If not set or 0, defaults to 20 seconds. |
1026 | | #[serde(default, deserialize_with = "convert_duration_with_shellexpand")] |
1027 | | pub http2_keepalive_timeout_s: u64, |
1028 | | |
1029 | | /// The TLS configuration to use to connect to the endpoint. |
1030 | | pub tls_config: Option<ClientTlsConfig>, |
1031 | | } |
1032 | | |
1033 | | #[derive(Copy, Clone, Deserialize, Serialize, Debug, Default, PartialEq, Eq)] |
1034 | | #[serde(rename_all = "snake_case")] |
1035 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1036 | | pub enum MemoryEnforcement { |
1037 | | /// Measure only. |
1038 | | None, |
1039 | | /// Kill the action's process group once two consecutive samples exceed |
1040 | | /// the reservation plus headroom, or one sample exceeds twice it. |
1041 | | #[default] |
1042 | | Soft, |
1043 | | } |
1044 | | |
1045 | | #[derive(Clone, Deserialize, Serialize, Debug)] |
1046 | | #[serde(deny_unknown_fields)] |
1047 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1048 | | pub struct ResourceEnforcementConfig { |
1049 | | /// What to do with an action that exceeds its memory reservation. |
1050 | | /// Default: soft |
1051 | | #[serde(default)] |
1052 | | pub memory: MemoryEnforcement, |
1053 | | |
1054 | | /// Whether to refuse an action whose disk reservation exceeds the free |
1055 | | /// space under the work directory, less what the actions already |
1056 | | /// admitted reserved, before its inputs are fetched. The |
1057 | | /// refusal is `ResourceExhausted`, which the scheduler requeues without |
1058 | | /// counting an attempt and holds off this worker until its next |
1059 | | /// keepalive. |
1060 | | /// Default: none |
1061 | | #[serde(default)] |
1062 | | pub disk: DiskEnforcement, |
1063 | | |
1064 | | /// Platform property carrying the action's disk reservation in KiB. |
1065 | | /// Default: `disk_kb` |
1066 | | #[serde( |
1067 | | default = "default_disk_property_name", |
1068 | | deserialize_with = "convert_string_with_shellexpand" |
1069 | | )] |
1070 | | pub disk_property_name: String, |
1071 | | |
1072 | | /// Platform property carrying the action's memory reservation in KiB, |
1073 | | /// as the scheduler sends it. An action without the property is not |
1074 | | /// enforced. |
1075 | | /// Default: `memory_kb` |
1076 | | #[serde( |
1077 | | default = "default_memory_property_name", |
1078 | | deserialize_with = "convert_string_with_shellexpand" |
1079 | | )] |
1080 | | pub memory_property_name: String, |
1081 | | |
1082 | | /// Percent above the reservation an action may reach before it is |
1083 | | /// killed. |
1084 | | /// Default: 20 |
1085 | | #[serde( |
1086 | | default = "default_memory_headroom_percent", |
1087 | | deserialize_with = "convert_numeric_with_shellexpand" |
1088 | | )] |
1089 | | pub memory_headroom_percent: u64, |
1090 | | |
1091 | | /// Percent above its disk reservation an action's own files may reach |
1092 | | /// before it is killed, with `disk: soft`. |
1093 | | /// Default: 20 |
1094 | | #[serde( |
1095 | | default = "default_memory_headroom_percent", |
1096 | | deserialize_with = "convert_numeric_with_shellexpand" |
1097 | | )] |
1098 | | pub disk_headroom_percent: u64, |
1099 | | |
1100 | | /// Kill one running action once the worker's cgroup has spent at least |
1101 | | /// this share of the last ten seconds, as a whole number from 0 to 100, |
1102 | | /// with every task stalled on memory (`full avg10` in cgroup v2 |
1103 | | /// `memory.pressure`) for `memory_pressure_kill_after_s` without a |
1104 | | /// break. A worker whose actions fit under its limit only by thrashing |
1105 | | /// never reaches the OOM killer and stops answering the scheduler; this |
1106 | | /// ends the action instead, as a memory kill, so the scheduler requeues |
1107 | | /// it with a larger reservation. The kill lands on the action furthest |
1108 | | /// over its reservation, or the largest when none is over, and never |
1109 | | /// on one that reserves the whole worker. It does not check that the |
1110 | | /// actions caused the pressure: pressure from the worker itself (page |
1111 | | /// cache from the filesystem store, a large upload) still kills an |
1112 | | /// action. Needs `memory: soft`, which supplies the per-action samples |
1113 | | /// the choice is made from. Values over 100 are read as 100. 0 disables. |
1114 | | /// Default: 0 |
1115 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1116 | | pub memory_pressure_kill_percent: u64, |
1117 | | |
1118 | | /// How long `memory.pressure` must stay at or above |
1119 | | /// `memory_pressure_kill_percent` before an action is killed, in |
1120 | | /// seconds. Another kill waits this long again. 0 is read as 1. |
1121 | | /// Default: 30 |
1122 | | #[serde( |
1123 | | default = "default_memory_pressure_kill_after_s", |
1124 | | deserialize_with = "convert_duration_with_shellexpand" |
1125 | | )] |
1126 | | pub memory_pressure_kill_after_s: u64, |
1127 | | } |
1128 | | |
1129 | 0 | const fn default_memory_pressure_kill_after_s() -> u64 { |
1130 | 0 | 30 |
1131 | 0 | } |
1132 | | |
1133 | 0 | fn default_memory_property_name() -> String { |
1134 | 0 | "memory_kb".to_string() |
1135 | 0 | } |
1136 | | |
1137 | 18 | const fn default_true() -> bool { |
1138 | 18 | true |
1139 | 18 | } |
1140 | | |
1141 | 0 | fn default_disk_property_name() -> String { |
1142 | 0 | "disk_kb".to_string() |
1143 | 0 | } |
1144 | | |
1145 | | #[derive(Copy, Clone, Deserialize, Serialize, Debug, Default, PartialEq, Eq)] |
1146 | | #[serde(rename_all = "snake_case")] |
1147 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1148 | | pub enum DiskEnforcement { |
1149 | | /// Fetch inputs regardless of free space. |
1150 | | #[default] |
1151 | | None, |
1152 | | /// Refuse the action when the free space under the work directory is |
1153 | | /// below its reservation. |
1154 | | Guard, |
1155 | | /// `guard`, and kill an action whose files under its own directory |
1156 | | /// grow past its reservation plus `disk_headroom_percent`. Sampled |
1157 | | /// every ten seconds by walking the action directory, counting only |
1158 | | /// files the action itself wrote (inputs are hard links into the CAS |
1159 | | /// and are not counted). |
1160 | | Soft, |
1161 | | } |
1162 | | |
1163 | 0 | const fn default_memory_headroom_percent() -> u64 { |
1164 | 0 | 20 |
1165 | 0 | } |
1166 | | |
1167 | | #[derive(Copy, Clone, Deserialize, Serialize, Debug, Default)] |
1168 | | #[serde(rename_all = "snake_case")] |
1169 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1170 | | pub enum UploadCacheResultsStrategy { |
1171 | | /// Only upload action results with an exit code of 0. |
1172 | | #[default] |
1173 | | SuccessOnly, |
1174 | | |
1175 | | /// Don't upload any action results. |
1176 | | Never, |
1177 | | |
1178 | | /// Upload all action results that complete. |
1179 | | Everything, |
1180 | | |
1181 | | /// Only upload action results that fail. |
1182 | | FailuresOnly, |
1183 | | } |
1184 | | |
1185 | | #[derive(Clone, Deserialize, Serialize, Debug)] |
1186 | | #[serde(rename_all = "snake_case")] |
1187 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1188 | | pub enum EnvironmentSource { |
1189 | | /// The name of the platform property in the action to get the value from. |
1190 | | Property(String), |
1191 | | |
1192 | | /// The raw value to set. |
1193 | | Value(#[serde(deserialize_with = "convert_string_with_shellexpand")] String), |
1194 | | |
1195 | | /// Take the value from the local environment corresponding to the name key |
1196 | | FromEnvironment, |
1197 | | |
1198 | | /// The max amount of time in milliseconds the command is allowed to run |
1199 | | /// (requested by the client). |
1200 | | TimeoutMillis, |
1201 | | |
1202 | | /// A special file path will be provided that can be used to communicate |
1203 | | /// with the parent process about out-of-band information. This file |
1204 | | /// will be read after the command has finished executing. Based on the |
1205 | | /// contents of the file, the behavior of the result may be modified. |
1206 | | /// |
1207 | | /// The format of the file contents should be json with the following |
1208 | | /// schema: |
1209 | | /// { |
1210 | | /// // If set the command will be considered a failure. |
1211 | | /// // May be one of the following static strings: |
1212 | | /// // "timeout": Will Consider this task to be a timeout. |
1213 | | /// "failure": "timeout", |
1214 | | /// } |
1215 | | /// |
1216 | | /// All fields are optional, file does not need to be created and may be |
1217 | | /// empty. |
1218 | | SideChannelFile, |
1219 | | |
1220 | | /// A "root" directory for the action. This directory can be used to |
1221 | | /// store temporary files that are not needed after the action has |
1222 | | /// completed. This directory will be purged after the action has |
1223 | | /// completed. |
1224 | | /// |
1225 | | /// For example: |
1226 | | /// If an action writes temporary data to a path but nativelink should |
1227 | | /// clean up this path after the job has executed, you may create any |
1228 | | /// directory under the path provided in this variable. A common pattern |
1229 | | /// would be to use `entrypoint` to set a shell script that reads this |
1230 | | /// variable, `mkdir $ENV_VAR_NAME/tmp` and `export TMPDIR=$ENV_VAR_NAME/tmp`. |
1231 | | /// Another example might be to bind-mount the `/tmp` path in a container to |
1232 | | /// this path in `entrypoint`. |
1233 | | ActionDirectory, |
1234 | | } |
1235 | | |
1236 | | #[derive(Deserialize, Serialize, Debug, Default)] |
1237 | | #[serde(deny_unknown_fields)] |
1238 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1239 | | pub struct UploadActionResultConfig { |
1240 | | /// Underlying AC store that the worker will use to publish execution results |
1241 | | /// into. Objects placed in this store should be reachable from the |
1242 | | /// scheduler/client-cas after they have finished updating. |
1243 | | /// Default: {No uploading is done} |
1244 | | pub ac_store: Option<StoreRefName>, |
1245 | | |
1246 | | /// In which situations should the results be published to the `ac_store`, |
1247 | | /// if set to `SuccessOnly` then only results with an exit code of 0 will be |
1248 | | /// uploaded, if set to Everything all completed results will be uploaded. |
1249 | | /// |
1250 | | /// Default: `SuccessOnly` |
1251 | | #[serde(default)] |
1252 | | pub upload_ac_results_strategy: UploadCacheResultsStrategy, |
1253 | | |
1254 | | /// Store to upload historical results to. This should be a CAS store if set. |
1255 | | /// |
1256 | | /// Default: {CAS store of parent} |
1257 | | pub historical_results_store: Option<StoreRefName>, |
1258 | | |
1259 | | /// In which situations should the results be published to the historical CAS. |
1260 | | /// The historical CAS is where failures are published. These messages conform |
1261 | | /// to the CAS key-value lookup format and are always a `HistoricalExecuteResponse` |
1262 | | /// serialized message. |
1263 | | /// |
1264 | | /// Default: `FailuresOnly` |
1265 | | #[serde(default)] |
1266 | | pub upload_historical_results_strategy: Option<UploadCacheResultsStrategy>, |
1267 | | |
1268 | | /// Template to use for the `ExecuteResponse.message` property. This message |
1269 | | /// is attached to the response before it is sent to the client. The following |
1270 | | /// special variables are supported: |
1271 | | /// - `digest_function`: Digest function used to calculate the action digest. |
1272 | | /// - `action_digest_hash`: Action digest hash. |
1273 | | /// - `action_digest_size`: Action digest size. |
1274 | | /// - `historical_results_hash`: `HistoricalExecuteResponse` digest hash. |
1275 | | /// - `historical_results_size`: `HistoricalExecuteResponse` digest size. |
1276 | | /// |
1277 | | /// A common use case of this is to provide a link to the web page that |
1278 | | /// contains more useful information for the user. |
1279 | | /// |
1280 | | /// An example that is fully compatible with `bb_browser` is: |
1281 | | /// <https://example.com/my-instance-name-here/blobs/{digest_function}/action/{action_digest_hash}-{action_digest_size}/> |
1282 | | /// |
1283 | | /// Default: "" (no message) |
1284 | | #[serde(default, deserialize_with = "convert_string_with_shellexpand")] |
1285 | | pub success_message_template: String, |
1286 | | |
1287 | | /// Same as `success_message_template` but for failure case. |
1288 | | /// |
1289 | | /// An example that is fully compatible with `bb_browser` is: |
1290 | | /// <https://example.com/my-instance-name-here/blobs/{digest_function}/historical_execute_response/{historical_results_hash}-{historical_results_size}/> |
1291 | | /// |
1292 | | /// Default: "" (no message) |
1293 | | #[serde(default, deserialize_with = "convert_string_with_shellexpand")] |
1294 | | pub failure_message_template: String, |
1295 | | } |
1296 | | |
1297 | | /// The pool of persistent worker processes (Bazel `supports-workers`). |
1298 | | #[derive(Deserialize, Serialize, Debug, Clone, Copy)] |
1299 | | #[serde(deny_unknown_fields)] |
1300 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1301 | | pub struct PersistentWorkersConfig { |
1302 | | /// Run an action whose platform properties carry `supports-workers=1` |
1303 | | /// in a pooled worker process that outlives it and serves the next |
1304 | | /// action with the same executable, startup arguments and environment. |
1305 | | /// Off, every such action runs as a one-shot process like any other. |
1306 | | /// Default: true |
1307 | | #[serde(default = "default_true")] |
1308 | | pub enabled: bool, |
1309 | | |
1310 | | /// Most worker processes kept per key (executable, startup arguments, |
1311 | | /// environment, protocol). An action whose key is at the cap and has |
1312 | | /// no idle process waits `acquire_timeout_s` for one to come back, then |
1313 | | /// runs one-shot. 0 takes the default. |
1314 | | /// Default: 4 |
1315 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1316 | | pub max_workers_per_key: usize, |
1317 | | |
1318 | | /// Seconds an idle worker process is kept before the sweeper shuts it |
1319 | | /// down. 0 takes the default. |
1320 | | /// Default: 300 |
1321 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1322 | | pub idle_timeout_s: u64, |
1323 | | |
1324 | | /// Requests a worker process serves before it is retired and replaced, |
1325 | | /// which bounds what a long-lived process accumulates. 0 takes the |
1326 | | /// default. |
1327 | | /// Default: 200 |
1328 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1329 | | pub max_requests_per_worker: u64, |
1330 | | |
1331 | | /// Milliseconds a worker process being shut down gets to exit on its |
1332 | | /// own (its stdin is closed) before SIGKILL. 0 takes the default. |
1333 | | /// Default: 5000 |
1334 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1335 | | pub shutdown_grace_ms: u64, |
1336 | | |
1337 | | /// Seconds an action waits for a worker process to come back when its |
1338 | | /// key is at `max_workers_per_key` with none idle, before it runs |
1339 | | /// one-shot instead. 0 takes the default. |
1340 | | /// Default: 30 |
1341 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1342 | | pub acquire_timeout_s: u64, |
1343 | | } |
1344 | | |
1345 | | /// The same as an absent block: the pool on, every size at its default. |
1346 | | impl Default for PersistentWorkersConfig { |
1347 | 0 | fn default() -> Self { |
1348 | 0 | Self { |
1349 | 0 | enabled: true, |
1350 | 0 | max_workers_per_key: 0, |
1351 | 0 | idle_timeout_s: 0, |
1352 | 0 | max_requests_per_worker: 0, |
1353 | 0 | shutdown_grace_ms: 0, |
1354 | 0 | acquire_timeout_s: 0, |
1355 | 0 | } |
1356 | 0 | } |
1357 | | } |
1358 | | |
1359 | | /// Opt-in file capture for the actual container executing a Buck2 action. |
1360 | | /// Requires a fresh single-use container with no outer execution wrapper or |
1361 | | /// separate action mount namespace. For nested runtimes, run `NativeLink` and the |
1362 | | /// capture helper inside the execution container. Other build tools never start |
1363 | | /// capture. The helper uploads an archive before `NativeLink` cleans up the action. |
1364 | | /// |
1365 | | /// The operator supplies the helper executable and its authenticated ingest |
1366 | | /// service. `NativeLink` sends one JSON record on stdin and expects `ready\n` |
1367 | | /// on stdout within 30 seconds. Closing stdin requests final capture; the helper |
1368 | | /// must exit only after durable acknowledgement, or exit unsuccessfully if the |
1369 | | /// archive is partial. See `nativelink-worker/README.md` for the control protocol. |
1370 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
1371 | | #[serde(deny_unknown_fields)] |
1372 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1373 | | pub struct Buck2FileCaptureConfig { |
1374 | | /// Absolute path to the compatible `buck2-file-capture` executable. |
1375 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
1376 | | pub executable: String, |
1377 | | /// Authenticated ingest endpoint understood by the capture helper. |
1378 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
1379 | | pub gateway: String, |
1380 | | /// Private token file. The helper excludes its path and hard-link aliases. |
1381 | | /// The token is never included in the action environment or command line. |
1382 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
1383 | | pub token_file: String, |
1384 | | /// Private directory for the helper's bounded on-disk metadata index. |
1385 | | /// Mount a fresh empty volume here for each single-use container. |
1386 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
1387 | | pub state_directory: String, |
1388 | | /// Identity of the actual executing container, normally the pod UID. |
1389 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
1390 | | pub container_id: String, |
1391 | | /// Credential files/directories and platform configuration excluded from |
1392 | | /// browsing. Their inode aliases and private secret mounts are also denied. |
1393 | | #[serde(default, deserialize_with = "convert_vec_string_with_shellexpand")] |
1394 | | pub protected_paths: Vec<String>, |
1395 | | /// Internal CAS/cache paths excluded from browsing. Materialized action |
1396 | | /// inputs hard-linked from these caches remain visible in the action tree. |
1397 | | #[serde(default, deserialize_with = "convert_vec_string_with_shellexpand")] |
1398 | | pub internal_paths: Vec<String>, |
1399 | | /// Maximum final archive time before action cleanup proceeds. The helper |
1400 | | /// records a partial capture on timeout; build success/failure is unchanged. |
1401 | | /// Default: 120 seconds (zero selects this default). |
1402 | | #[serde(default, deserialize_with = "convert_duration_with_shellexpand")] |
1403 | | pub finalize_timeout_s: usize, |
1404 | | } |
1405 | | |
1406 | | #[expect( |
1407 | | clippy::struct_excessive_bools, |
1408 | | reason = "independent on/off switches of one worker; a nested struct per flag would not read better" |
1409 | | )] |
1410 | | #[derive(Deserialize, Serialize, Debug, Default)] |
1411 | | #[serde(deny_unknown_fields)] |
1412 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1413 | | pub struct LocalWorkerConfig { |
1414 | | /// Name of the worker. This is give a more friendly name to a worker for logging |
1415 | | /// and metric publishing. This is also the prefix of the worker id |
1416 | | /// (i.e. "{name}{uuidv6}"). |
1417 | | /// Default: {Index position in the workers list} |
1418 | | #[serde(default, deserialize_with = "convert_string_with_shellexpand")] |
1419 | | pub name: String, |
1420 | | |
1421 | | /// Endpoint which the worker will connect to the scheduler's `WorkerApiService`. |
1422 | | pub worker_api_endpoint: EndpointConfig, |
1423 | | |
1424 | | /// The maximum time an action is allowed to run. If a task requests for a timeout |
1425 | | /// longer than this time limit, the task will be rejected. Value in seconds. |
1426 | | /// |
1427 | | /// Default: 20 minutes |
1428 | | #[serde( |
1429 | | default, |
1430 | | deserialize_with = "convert_duration_with_shellexpand", |
1431 | | alias = "max_action_timeout" |
1432 | | )] |
1433 | | pub max_action_timeout_s: usize, |
1434 | | |
1435 | | /// Maximum time allowed for uploading action results to CAS after execution |
1436 | | /// completes. If upload takes longer than this, the action fails with |
1437 | | /// `DeadlineExceeded` and may be retried by the scheduler. Value in seconds. |
1438 | | /// |
1439 | | /// Default: 10 minutes |
1440 | | #[serde( |
1441 | | default, |
1442 | | deserialize_with = "convert_duration_with_shellexpand", |
1443 | | alias = "max_upload_timeout" |
1444 | | )] |
1445 | | pub max_upload_timeout_s: usize, |
1446 | | |
1447 | | /// Maximum time allowed for fetching an action's inputs into the worker's |
1448 | | /// store and its directory before the command runs. A fetch that hangs |
1449 | | /// (a store that never answers) otherwise holds the action's slot for |
1450 | | /// good while the worker looks healthy; past this the action fails with |
1451 | | /// `DeadlineExceeded` and may be retried by the scheduler, and a warning |
1452 | | /// names the action every minute before that. Value in seconds. |
1453 | | /// |
1454 | | /// A fetch that fails with a transient store error is fetched again |
1455 | | /// from this worker, with a pause doubling from 1 s to 30 s, for as |
1456 | | /// long as this leaves room for another attempt, or the action's own |
1457 | | /// timeout when that is shorter. The scheduler is not told of the |
1458 | | /// retries: it ends an executing action on its own at the action's |
1459 | | /// timeout plus `worker_timeout_s`, and at |
1460 | | /// `max_action_executing_timeout_s`, so keep this below those or the |
1461 | | /// action is requeued while the worker still retries. |
1462 | | /// |
1463 | | /// Default: 10 minutes |
1464 | | #[serde( |
1465 | | default, |
1466 | | deserialize_with = "convert_duration_with_shellexpand", |
1467 | | alias = "max_download_timeout" |
1468 | | )] |
1469 | | pub max_download_timeout_s: usize, |
1470 | | |
1471 | | /// How many input blobs this worker fetches into its store at once, |
1472 | | /// across every action it is staging. Each fetch is a read from the |
1473 | | /// CAS or the object store, so this bounds what one worker asks of |
1474 | | /// them at a time: several cold actions share this many fetches |
1475 | | /// instead of each opening its own. Blobs already in the store are |
1476 | | /// linked without one. 0 shares no pool; each action fetches on its |
1477 | | /// own. |
1478 | | /// |
1479 | | /// Without a pool, an action fetches up to 64 inputs at a time per |
1480 | | /// directory of its input tree. Set this when several cold actions on |
1481 | | /// one worker overload the CAS or the object store behind it; 64 is a |
1482 | | /// good start. A worker without it logs a warning suggesting it, at |
1483 | | /// most every 10 minutes, when an input fetch times out or fails with |
1484 | | /// a transient error other than `DataLoss`. |
1485 | | /// |
1486 | | /// Default: 0 (no shared pool) |
1487 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1488 | | pub max_concurrent_input_downloads: usize, |
1489 | | |
1490 | | /// Maximum time to wait for action directory cleanup before timing out. |
1491 | | /// Value in seconds. |
1492 | | /// |
1493 | | /// Default: 30 seconds |
1494 | | #[serde(default, deserialize_with = "convert_duration_with_shellexpand")] |
1495 | | pub max_cleanup_wait_s: usize, |
1496 | | |
1497 | | /// Maximum backoff duration for exponential backoff when waiting for cleanup. |
1498 | | /// Value in milliseconds. |
1499 | | /// |
1500 | | /// Default: 500 milliseconds |
1501 | | #[serde(default, deserialize_with = "convert_duration_with_shellexpand")] |
1502 | | pub max_cleanup_backoff_ms: usize, |
1503 | | |
1504 | | /// Maximum number of inflight tasks this worker can cope with. |
1505 | | /// |
1506 | | /// Default: 0 (infinite tasks) |
1507 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1508 | | pub max_inflight_tasks: u64, |
1509 | | |
1510 | | /// Accept one action, upload its results, and exit the worker process. |
1511 | | /// The worker advertises one execution slot and never accepts a second |
1512 | | /// action, including after a scheduler disconnect. A launcher must create |
1513 | | /// a fresh container and fresh writable volumes for its replacement. |
1514 | | /// Use one local worker per process, with no scheduler or services other |
1515 | | /// than health checks. Reusing the container or its writable volumes does |
1516 | | /// not provide isolation from the previous action. |
1517 | | /// |
1518 | | /// Default: false |
1519 | | #[serde(default)] |
1520 | | pub single_use: bool, |
1521 | | |
1522 | | /// Capture full execution-container files for Buck2 builds only. Disabled |
1523 | | /// when absent. Requires `single_use`; read the capture configuration's |
1524 | | /// isolation requirements before enabling it for a worker pool. |
1525 | | #[serde(default)] |
1526 | | pub experimental_buck2_file_capture: Option<Buck2FileCaptureConfig>, |
1527 | | |
1528 | | /// If timeout is handled in `entrypoint` or another wrapper script. |
1529 | | /// If set to true `NativeLink` will not honor the timeout the action requested |
1530 | | /// and instead will always force kill the action after `max_action_timeout` |
1531 | | /// has been reached. If this is set to false, the smaller value of the action's |
1532 | | /// timeout and `max_action_timeout` will be used to which `NativeLink` will kill |
1533 | | /// the action. |
1534 | | /// |
1535 | | /// The real timeout can be received via an environment variable set in: |
1536 | | /// `EnvironmentSource::TimeoutMillis`. |
1537 | | /// |
1538 | | /// Example on where this is useful: `entrypoint` launches the action inside |
1539 | | /// a docker container, but the docker container may need to be downloaded. Thus |
1540 | | /// the timer should not start until the docker container has started executing |
1541 | | /// the action. In this case, action will likely be wrapped in another program, |
1542 | | /// like `timeout` and propagate timeouts via `EnvironmentSource::SideChannelFile`. |
1543 | | /// |
1544 | | /// Default: false (`NativeLink` fully handles timeouts) |
1545 | | #[serde(default)] |
1546 | | pub timeout_handled_externally: bool, |
1547 | | |
1548 | | /// On shutdown, tell the scheduler at once that this worker is draining |
1549 | | /// so it stops dispatching here, then finish the running actions (up to |
1550 | | /// `max_action_timeout_s`) and exit. Off, the worker finishes first and |
1551 | | /// only then announces itself, while the scheduler keeps offering it |
1552 | | /// work it has to refuse. Needs a scheduler that understands the drain |
1553 | | /// flag (v1.7.3+); an older one removes the worker and requeues its |
1554 | | /// actions as soon as the message arrives. |
1555 | | /// Default: false |
1556 | | #[serde(default)] |
1557 | | pub drain_on_shutdown: bool, |
1558 | | |
1559 | | /// Kill an action that grows past its memory reservation before the |
1560 | | /// pod's cgroup limit does. On cgroup v2 the container cgroup carries |
1561 | | /// `memory.oom.group`, so the kernel's kill takes the worker and every |
1562 | | /// action on it; this one takes only the offender and fails it with |
1563 | | /// `FailedPrecondition`. The memory pressure kill reads a cgroup and is |
1564 | | /// Linux only; on Windows the disk axis is not sampled, and a kill ends |
1565 | | /// the action's own process rather than every process in it. |
1566 | | /// Actions run through a persistent worker are not covered, since that |
1567 | | /// process outlives the action and serves others. With either axis on, |
1568 | | /// the worker reads an action's platform properties from the |
1569 | | /// scheduler's `StartExecute` rather than the client's request, so a |
1570 | | /// reservation the scheduler placed the action by is the one enforced |
1571 | | /// and exported to the environment. |
1572 | | #[serde(default)] |
1573 | | pub resource_enforcement: Option<ResourceEnforcementConfig>, |
1574 | | |
1575 | | /// The command to execute on every execution request. This will be parsed as |
1576 | | /// a command + arguments (not shell). |
1577 | | /// Example: "run.sh" and a job with command: "sleep 5" will result in a |
1578 | | /// command like: "run.sh sleep 5". |
1579 | | /// Default: {Use the command from the job request}. |
1580 | | #[serde(default, deserialize_with = "convert_string_with_shellexpand")] |
1581 | | pub entrypoint: String, |
1582 | | |
1583 | | /// An optional script to run before every action is processed on the worker. |
1584 | | /// The value should be the full path to the script to execute and will pause |
1585 | | /// all actions on the worker if it returns an exit code other than 0. |
1586 | | /// If not set, then the worker will never pause and will continue to accept |
1587 | | /// jobs according to the scheduler configuration. |
1588 | | /// This is useful, for example, if the worker should not take any more |
1589 | | /// actions until there is enough resource available on the machine to |
1590 | | /// handle them. |
1591 | | pub experimental_precondition_script: Option<String>, |
1592 | | |
1593 | | /// Milliseconds the precondition script may run before it counts as |
1594 | | /// failed and the action is refused as backpressure. A script that |
1595 | | /// hangs used to hold the action forever. |
1596 | | /// Default: 30000 |
1597 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1598 | | pub precondition_timeout_ms: u64, |
1599 | | |
1600 | | /// Bytes of an action's stdout or stderr kept in memory. Past this the |
1601 | | /// output spills to a file under the action directory and is uploaded |
1602 | | /// from disk, so an action that prints gigabytes does not grow the |
1603 | | /// worker by that much. 0 keeps everything in memory. |
1604 | | /// Default: 0 |
1605 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1606 | | pub max_captured_output_bytes: u64, |
1607 | | |
1608 | | /// Seconds between sweeps of `work_directory` for action directories no |
1609 | | /// running action owns, left by a cleanup that failed. 0 disables the |
1610 | | /// sweep; the directory is still purged at startup. |
1611 | | /// Default: 0 |
1612 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1613 | | pub orphan_sweep_interval_s: u64, |
1614 | | |
1615 | | /// Milliseconds between the SIGTERM a timed-out or cancelled action |
1616 | | /// receives and the SIGKILL that follows if it is still running, so a |
1617 | | /// tool that handles SIGTERM can write its own cleanup. The memory |
1618 | | /// reservation kill skips the grace and sends SIGKILL at once. Linux |
1619 | | /// only; elsewhere the kill is immediate. 0 takes the default. |
1620 | | /// Default: 5000 |
1621 | | #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")] |
1622 | | pub kill_grace_ms: u64, |
1623 | | |
1624 | | /// Give every action its own `TMPDIR` under its action directory, |
1625 | | /// removed with it, so concurrent actions writing the same file name |
1626 | | /// under `$TMPDIR` no longer collide. A `TMPDIR` in the action's own |
1627 | | /// environment still wins. Code that hard-codes `/tmp` is unaffected. |
1628 | | /// Default: true |
1629 | | #[serde(default = "default_true")] |
1630 | | pub set_tmpdir: bool, |
1631 | | |
1632 | | /// Bazel persistent workers: how an action whose platform properties |
1633 | | /// carry `supports-workers=1` is run and how the pool of worker |
1634 | | /// processes is sized. Unset takes every default below. |
1635 | | #[serde(default)] |
1636 | | pub persistent_workers: Option<PersistentWorkersConfig>, |
1637 | | |
1638 | | /// Underlying CAS store that the worker will use to download CAS artifacts. |
1639 | | /// This store must be a `FastSlowStore`. The `fast` store must be a |
1640 | | /// `FileSystemStore` because it will use hardlinks when building out the files |
1641 | | /// instead of copying the files. The slow store must eventually resolve to the |
1642 | | /// same store the scheduler/client uses to send job requests. |
1643 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
1644 | | pub cas_fast_slow_store: StoreRefName, |
1645 | | |
1646 | | /// Configuration for uploading action results. |
1647 | | #[serde(default)] |
1648 | | pub upload_action_result: UploadActionResultConfig, |
1649 | | |
1650 | | /// The directory work jobs will be executed from. This directory will be fully |
1651 | | /// managed by the worker service and will be purged on startup. |
1652 | | /// This directory and the directory referenced in `local_filesystem_store_ref`'s |
1653 | | /// `stores::FilesystemStore::content_path` must be on the same filesystem. |
1654 | | /// Hardlinks will be used when placing files that are accessible to the jobs |
1655 | | /// that are sourced from `local_filesystem_store_ref`'s `content_path`. |
1656 | | #[serde(deserialize_with = "convert_string_with_shellexpand")] |
1657 | | pub work_directory: String, |
1658 | | |
1659 | | /// Properties of this worker. This configuration will be sent to the scheduler |
1660 | | /// and used to tell the scheduler to restrict what should be executed on this |
1661 | | /// worker. |
1662 | | pub platform_properties: HashMap<String, WorkerProperty>, |
1663 | | |
1664 | | /// Derive the CPU and memory properties from the worker's own cgroup |
1665 | | /// limits instead of the values above; see `CapacityConfig`. When the |
1666 | | /// cgroup cannot be read (not Linux, cgroup v1, no permission) the |
1667 | | /// values above stand and a warning says so; if they carry neither |
1668 | | /// property the worker refuses to start rather than register unable to |
1669 | | /// take any action that asks for CPU or memory. |
1670 | | #[serde(default)] |
1671 | | pub capacity: Option<CapacityConfig>, |
1672 | | |
1673 | | /// An optional mapping of environment names to set for the execution |
1674 | | /// as well as those specified in the action itself. If set, will set each |
1675 | | /// key as an environment variable before executing the job with the value |
1676 | | /// of the environment variable being the value of the property of the |
1677 | | /// action being executed of that name or the fixed value. |
1678 | | pub additional_environment: Option<HashMap<String, EnvironmentSource>>, |
1679 | | |
1680 | | /// Optional directory cache configuration for improving performance by caching |
1681 | | /// reconstructed input directories and using hardlinks instead of rebuilding |
1682 | | /// them from CAS for every action. |
1683 | | /// Default: None (directory cache disabled) |
1684 | | pub directory_cache: Option<DirectoryCacheConfig>, |
1685 | | |
1686 | | /// Optional and experimental: lease every digest in an active action's |
1687 | | /// input Merkle closure in the worker's locally eviction-managed CAS |
1688 | | /// tiers (the `cas_fast_slow_store`'s filesystem-backed stores) until the |
1689 | | /// action has finished cleanup. This prevents `Lost inputs no longer |
1690 | | /// available remotely` failures caused by local fast-tier eviction while |
1691 | | /// inputs are being materialized under cache pressure. |
1692 | | /// |
1693 | | /// Trade-off: while leases are held, a local filesystem tier may |
1694 | | /// temporarily exceed its configured `max_bytes` / `max_count` eviction |
1695 | | /// limits; normal eviction resumes once the action's leases are released. |
1696 | | /// Operators should leave headroom on the underlying disk when enabling |
1697 | | /// this. |
1698 | | /// |
1699 | | /// Default: false (eviction behavior is unchanged) |
1700 | | #[serde(default)] |
1701 | | pub experimental_active_input_leases: bool, |
1702 | | |
1703 | | /// Reuse immutable input directories through read-only bind mounts on Linux. |
1704 | | /// The worker prepares each selected subtree once per REAPI `Directory` |
1705 | | /// digest in the directory cache, then mounts it into each action instead of |
1706 | | /// recreating its filesystem entries. |
1707 | | /// |
1708 | | /// Paths are relative to the action input root, for example `["out/x/sdk"]`. |
1709 | | /// They must be nonempty, canonical, and non-overlapping. Absolute paths, |
1710 | | /// empty path components, and `.`/`..` components are rejected. |
1711 | | /// Requires `directory_cache`, `use_namespaces: true`, and |
1712 | | /// `use_mount_namespace: true`. |
1713 | | /// |
1714 | | /// Actions declaring overlapping outputs, working inside a selected subtree, |
1715 | | /// or using a persistent worker fall back to ordinary materialization. |
1716 | | /// Output paths reached through symlink aliases cannot be checked for |
1717 | | /// overlap. Use this only with trusted workloads that do not modify the |
1718 | | /// selected trees; read-only input mounts are not a security boundary. |
1719 | | /// |
1720 | | /// Select trees whose contents (and Directory digest) are reused across |
1721 | | /// many actions. Trees that change per action add cache construction work |
1722 | | /// without reuse. When mounts apply, other inputs use ordinary downloads, |
1723 | | /// bypassing both root and subtree directory caching for that action. |
1724 | | /// Failed mount preparation retries once without mounts; a failed mount |
1725 | | /// syscall still aborts the action before its command runs. |
1726 | | /// |
1727 | | /// An action can remount its inputs writable in its own user namespace |
1728 | | /// and corrupt the shared cache. Later actions can then produce incorrect |
1729 | | /// results from those inputs. Enable only for trusted commands. |
1730 | | /// |
1731 | | /// Cache entries remain pinned until action cleanup and may temporarily |
1732 | | /// exceed the cache's eviction budget. Pinned bytes have no separate limit, |
1733 | | /// so concurrent distinct trees can exhaust the disk. Leave headroom. |
1734 | | /// |
1735 | | /// Default: [] (disabled) |
1736 | | #[serde(default)] |
1737 | | pub experimental_readonly_input_mounts: Vec<String>, |
1738 | | |
1739 | | /// Whether to use namespaces to isolate the execution. This is only available |
1740 | | /// on Linux. It is highly recommended as it avoids a number of issues with |
1741 | | /// zombie processes and also provides additional hermeticity. If explicitly set |
1742 | | /// to true and it is not supported the worker will exit with an error. |
1743 | | /// |
1744 | | /// The action runs as PID 1 of its own PID namespace, and `/proc` is |
1745 | | /// remounted inside it, so the processes listed there are the action's |
1746 | | /// own, each under the PID the action knows it by: a process reading |
1747 | | /// `/proc/<pid>` for a child it forked finds that child, not whichever |
1748 | | /// host process has the number. If the worker's environment refuses that |
1749 | | /// mount (an unprivileged container that masks paths under `/proc` does) |
1750 | | /// the worker logs a warning at startup and actions see the host's |
1751 | | /// `/proc` instead. |
1752 | | /// |
1753 | | /// Note: this will fail for non-privileged Dockerised workers, as workers in |
1754 | | /// Docker don't have permissions to make a new user namespace. Privileged |
1755 | | /// containers can do this. |
1756 | | /// |
1757 | | /// Default: False. |
1758 | | pub use_namespaces: Option<bool>, |
1759 | | |
1760 | | /// Whether to use a mount namespace to isolate the worker root. This is only |
1761 | | /// available on Linux and when `use_namespaces` is true. It is highly recommended |
1762 | | /// provides additional hermeticity. If explicitly set to true and it is not |
1763 | | /// supported or `use_namespaces` is not set to true the worker will exit with an |
1764 | | /// error. |
1765 | | /// Default: False. |
1766 | | pub use_mount_namespace: Option<bool>, |
1767 | | |
1768 | | /// Whether to give each action a private `/tmp` inside its mount |
1769 | | /// namespace: the action's own `tmp` directory (the one `TMPDIR` points |
1770 | | /// at) is bound over `/tmp`, so tools that hardcode `/tmp` write there |
1771 | | /// too. Concurrent actions no longer collide on predictable paths under |
1772 | | /// `/tmp`, nothing leaks between actions through it, and what an action |
1773 | | /// writes there is removed with the action. |
1774 | | /// |
1775 | | /// This is only available on Linux and requires `use_namespaces` and |
1776 | | /// `use_mount_namespace` to be true. If explicitly set to true without |
1777 | | /// both the worker will exit with an error. Set it to false to keep the |
1778 | | /// host's `/tmp` visible to actions, for example when a tool they need |
1779 | | /// lives there. The bound `/tmp` carries the work volume's mount options |
1780 | | /// (`noexec`, `nodev`, `nosuid` if the volume has them), not the host |
1781 | | /// `/tmp`'s. |
1782 | | /// |
1783 | | /// The private `/tmp` lives on the worker's disk under the work |
1784 | | /// directory, not in memory, so it is bounded by the volume that holds |
1785 | | /// the work directory and seen by the disk guard, and a tool filling it |
1786 | | /// cannot take the worker's memory with it. |
1787 | | /// |
1788 | | /// If `/tmp` does not exist on the worker, for example in a minimal |
1789 | | /// container image, there is nothing for actions to collide on, so the |
1790 | | /// default is False there and a warning is logged at startup. |
1791 | | /// |
1792 | | /// Default: True when `use_mount_namespace` is true and `/tmp` exists, |
1793 | | /// otherwise False. |
1794 | | pub isolate_tmp: Option<bool>, |
1795 | | } |
1796 | | |
1797 | | #[derive(Deserialize, Serialize, Debug, Clone)] |
1798 | | #[serde(deny_unknown_fields)] |
1799 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1800 | | pub struct DirectoryCacheConfig { |
1801 | | /// Maximum number of cached directories. |
1802 | | /// Default: 1000 |
1803 | | #[serde(default = "default_directory_cache_max_entries")] |
1804 | | pub max_entries: usize, |
1805 | | |
1806 | | /// Maximum total size in bytes for all cached directories (0 = unlimited). |
1807 | | /// Default: 10737418240 (10 GB) |
1808 | | #[serde( |
1809 | | default = "default_directory_cache_max_size_bytes", |
1810 | | deserialize_with = "convert_data_size_with_shellexpand" |
1811 | | )] |
1812 | | pub max_size_bytes: u64, |
1813 | | |
1814 | | /// Base directory for cache storage. This directory will be managed by |
1815 | | /// the worker and should be on the same filesystem as `work_directory`. |
1816 | | /// Default: `{work_directory}/../directory_cache` |
1817 | | #[serde(default, deserialize_with = "convert_string_with_shellexpand")] |
1818 | | pub cache_root: String, |
1819 | | |
1820 | | /// Optional and experimental: additionally cache every subdirectory by |
1821 | | /// its own `Directory` digest, not just the root directory. REAPI Merkle |
1822 | | /// nodes are content-addressed, so a subtree that is byte-identical |
1823 | | /// between two different roots has the same digest and can be |
1824 | | /// materialized with a single hardlink pass instead of being rebuilt |
1825 | | /// from the CAS. This makes the common "one input file changed out of |
1826 | | /// thousands" case reuse every unchanged subtree. |
1827 | | /// |
1828 | | /// Note: subtree caching multiplies the entry COUNT in the cache (every |
1829 | | /// distinct subdirectory becomes its own entry, subject to the normal |
1830 | | /// eviction budgets), so operators enabling this should raise |
1831 | | /// `max_entries` accordingly (for example 10x). To avoid multiplying the |
1832 | | /// SIZE accounting too, an entry's recorded size covers only bytes not |
1833 | | /// owned by a descendant entry, so the cache's size total approximates |
1834 | | /// unique materialized bytes rather than counting each file once per |
1835 | | /// ancestor level. |
1836 | | /// |
1837 | | /// Trade-off: cold constructions pay roughly one extra hardlink pass per |
1838 | | /// tree level (measured ~10% cold overhead at depth 3, within run |
1839 | | /// variance) in exchange for the churn-path reuse above. |
1840 | | /// |
1841 | | /// Default: false (only root directories are cached; existing behavior) |
1842 | | #[serde(default)] |
1843 | | pub experimental_subtree_caching: bool, |
1844 | | |
1845 | | /// Maximum number of concurrent slow-store fetches across ALL directory |
1846 | | /// constructions of this cache. This bound protects backing stores from |
1847 | | /// RPC storms: per-level construction concurrency compounds |
1848 | | /// multiplicatively across tree levels and concurrent actions. |
1849 | | /// |
1850 | | /// Interaction with read coalescing: this semaphore fragments coalesced |
1851 | | /// batches to at most this many items and serializes fetch waves for |
1852 | | /// trees with many tiny files. Deployments using |
1853 | | /// `experimental_read_batching` on the underlying grpc store can raise |
1854 | | /// this substantially (for example 512), since batching collapses the |
1855 | | /// RPC count. Must be greater than 0. |
1856 | | /// |
1857 | | /// Default: 64 |
1858 | | #[serde(default = "default_directory_cache_max_concurrent_fetches")] |
1859 | | pub max_concurrent_fetches: usize, |
1860 | | /// On a directory-cache miss, prefetch every `Directory` proto of the |
1861 | | /// tree with one logical `GetTree` traversal instead of fetching protos |
1862 | | /// level by level (which costs one round trip per tree DEPTH). The |
1863 | | /// [REAPI request contract] permits servers to impose their own limit even |
1864 | | /// when no page size is specified, and the [REAPI response contract] |
1865 | | /// requires clients to continue with the returned page token. A paginated |
1866 | | /// traversal may therefore use multiple RPCs. Only takes effect when the |
1867 | | /// slow tier is a `grpc` store; |
1868 | | /// any prefetch failure falls back to the per-level path. Worthwhile when |
1869 | | /// worker-to-CAS latency is non-trivial and trees are deep; measured 5-34x |
1870 | | /// on the proto phase at 5-25ms RTT. Note the serving CAS pays the tree walk |
1871 | | /// against its own backend, so its directory protos should be served |
1872 | | /// from a fast tier. |
1873 | | /// |
1874 | | /// [REAPI request contract]: https://github.com/bazelbuild/remote-apis/blob/becdd8f9ff811df88a22d3eadd6341753d51d167/build/bazel/remote/execution/v2/remote_execution.proto#L1932-L1942 |
1875 | | /// [REAPI response contract]: https://github.com/bazelbuild/remote-apis/blob/becdd8f9ff811df88a22d3eadd6341753d51d167/build/bazel/remote/execution/v2/remote_execution.proto#L1961-L1965 |
1876 | | /// Default: false |
1877 | | #[serde(default)] |
1878 | | pub experimental_get_tree_prefetch: bool, |
1879 | | } |
1880 | | |
1881 | 0 | const fn default_directory_cache_max_entries() -> usize { |
1882 | 0 | 1000 |
1883 | 0 | } |
1884 | | |
1885 | 0 | const fn default_directory_cache_max_concurrent_fetches() -> usize { |
1886 | 0 | 64 |
1887 | 0 | } |
1888 | | |
1889 | 0 | const fn default_directory_cache_max_size_bytes() -> u64 { |
1890 | 0 | 10 * 1024 * 1024 * 1024 // 10 GB |
1891 | 0 | } |
1892 | | |
1893 | | #[derive(Deserialize, Serialize, Debug)] |
1894 | | #[serde(rename_all = "snake_case")] |
1895 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1896 | | pub enum WorkerConfig { |
1897 | | /// A worker type that executes jobs locally on this machine. |
1898 | | Local(LocalWorkerConfig), |
1899 | | } |
1900 | | |
1901 | | #[derive(Deserialize, Serialize, Debug, Clone, Copy)] |
1902 | | #[serde(deny_unknown_fields)] |
1903 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1904 | | pub struct GlobalConfig { |
1905 | | /// Maximum number of open files that can be opened at one time. |
1906 | | /// This value is not strictly enforced, it is a best effort. Some internal libraries |
1907 | | /// open files or read metadata from a files which do not obey this limit, however |
1908 | | /// the vast majority of cases will have this limit be honored. |
1909 | | /// This value must be larger than `ulimit -n` to have any effect. |
1910 | | /// Any network open file descriptors is not counted in this limit, but is counted |
1911 | | /// in the kernel limit. It is a good idea to set a very large `ulimit -n`. |
1912 | | /// Note: This value must be greater than 10. |
1913 | | /// |
1914 | | /// Default: 24576 (= 24 * 1024) |
1915 | | #[serde(deserialize_with = "convert_numeric_with_shellexpand")] |
1916 | | pub max_open_files: usize, |
1917 | | |
1918 | | /// Default hash function to use while uploading blobs to the CAS when not set |
1919 | | /// by client. |
1920 | | /// |
1921 | | /// Default: `ConfigDigestHashFunction::sha256` |
1922 | | pub default_digest_hash_function: Option<ConfigDigestHashFunction>, |
1923 | | |
1924 | | /// Default digest size to use for health check when running |
1925 | | /// diagnostics checks. Health checks are expected to use this |
1926 | | /// size for filling a buffer that is used for creation of |
1927 | | /// digest. |
1928 | | /// |
1929 | | /// Default: 1024*1024 (1MiB) |
1930 | | #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")] |
1931 | | pub default_digest_size_health_check: usize, |
1932 | | } |
1933 | | |
1934 | | pub type StoreConfig = NamedConfig<StoreSpec>; |
1935 | | pub type SchedulerConfig = NamedConfig<SchedulerSpec>; |
1936 | | |
1937 | | #[derive(Deserialize, Serialize, Debug)] |
1938 | | #[serde(deny_unknown_fields)] |
1939 | | #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] |
1940 | | pub struct CasConfig { |
1941 | | /// List of stores available to use in this config. |
1942 | | /// The keys can be used in other configs when needing to reference a store. |
1943 | | pub stores: Vec<StoreConfig>, |
1944 | | |
1945 | | /// Worker configurations used to execute jobs. |
1946 | | pub workers: Option<Vec<WorkerConfig>>, |
1947 | | |
1948 | | /// List of schedulers available to use in this config. |
1949 | | /// The keys can be used in other configs when needing to reference a |
1950 | | /// scheduler. |
1951 | | pub schedulers: Option<Vec<SchedulerConfig>>, |
1952 | | |
1953 | | /// Servers to setup for this process. |
1954 | | pub servers: Vec<ServerConfig>, |
1955 | | |
1956 | | /// Experimental - Origin events configuration. This is the service that will |
1957 | | /// collect and publish nativelink events to a store for processing by an |
1958 | | /// external service. |
1959 | | pub experimental_origin_events: Option<OriginEventsSpec>, |
1960 | | |
1961 | | /// Any global configurations that apply to all modules live here. |
1962 | | pub global: Option<GlobalConfig>, |
1963 | | } |
1964 | | |
1965 | | impl CasConfig { |
1966 | | const ZSTD_COMPRESSION_DOCS_URL: &'static str = |
1967 | | "https://docs.nativelink.com/configuration/compression"; |
1968 | | |
1969 | | /// # Errors |
1970 | | /// |
1971 | | /// Will return `Err` if we can't load the file. |
1972 | 20 | pub fn try_from_json5_file(config_file: &str) -> Result<Self, Error> { |
1973 | 20 | let json_contents = std::fs::read_to_string(config_file) |
1974 | 20 | .err_tip(|| format!0 ("Could not open config file {config_file}"))?0 ; |
1975 | 20 | Self::try_from_json5_str(&json_contents) |
1976 | 20 | } |
1977 | | |
1978 | 38 | pub fn try_from_json5_str(json_contents: &str) -> Result<Self, Error> { |
1979 | 38 | let mut config: Self = serde_json5::from_str(json_contents)?0 ; |
1980 | 38 | for server26 in &config.servers { |
1981 | 26 | if let Some(services) = &server.services { |
1982 | 26 | Self::check_store_conflict(services)?1 ; |
1983 | 0 | } |
1984 | | } |
1985 | 37 | config.validate_single_use_worker()?0 ; |
1986 | 37 | config.apply_zstd_grpc_store_defaults(); |
1987 | 37 | Ok(config) |
1988 | 38 | } |
1989 | | |
1990 | 53 | fn validate_single_use_worker(&self) -> Result<(), Error> { |
1991 | 53 | let workers = self.workers.as_deref().unwrap_or_default(); |
1992 | 53 | for WorkerConfig::Local(worker22 ) in workers { |
1993 | 22 | if let Some(capture12 ) = &worker.experimental_buck2_file_capture { |
1994 | 12 | if !worker.single_use |
1995 | 11 | || !worker.entrypoint.is_empty() |
1996 | 10 | || worker.use_mount_namespace == Some(true) |
1997 | | { |
1998 | 3 | return Err(make_input_err!( |
1999 | 3 | "Buck2 file capture requires a single_use worker inside the execution container, without an entrypoint wrapper or separate action mount namespace" |
2000 | 3 | )); |
2001 | 9 | } |
2002 | 24 | for path in [ |
2003 | 9 | &capture.executable, |
2004 | 9 | &capture.token_file, |
2005 | 9 | &capture.state_directory, |
2006 | 9 | ] |
2007 | 9 | .into_iter() |
2008 | 9 | .chain(capture.protected_paths.iter()) |
2009 | 9 | .chain(capture.internal_paths.iter()) |
2010 | | { |
2011 | | // These paths belong to the Linux execution container, |
2012 | | // even when an operator validates the config on Windows. |
2013 | 24 | if !path.starts_with('/') |
2014 | 46 | || path.split('/')22 .all22 (|part| part.is_empty() || part == "."20 ) |
2015 | 19 | || path.contains('\0') |
2016 | 48 | || path.split('/')18 .any18 (|part| part == "..") |
2017 | | { |
2018 | 7 | return Err(make_input_err!( |
2019 | 7 | "Buck2 capture paths must be absolute and cannot select the filesystem root" |
2020 | 7 | )); |
2021 | 17 | } |
2022 | | } |
2023 | 2 | if capture.gateway.is_empty() |
2024 | 2 | || capture.container_id.is_empty() |
2025 | 2 | || capture.finalize_timeout_s > 3600 |
2026 | | { |
2027 | 0 | return Err(make_input_err!( |
2028 | 0 | "Buck2 capture requires a gateway, container identity, and finalization timeout no greater than one hour" |
2029 | 0 | )); |
2030 | 2 | } |
2031 | 10 | } |
2032 | | } |
2033 | 43 | if !workers.iter().any(|worker| match worker11 { |
2034 | 11 | WorkerConfig::Local(worker) => worker.single_use, |
2035 | 11 | }) { |
2036 | 37 | return Ok(()); |
2037 | 6 | } |
2038 | 6 | if workers.len() != 1 || self.schedulers5 .as_ref5 ().is_some_and5 (|s| !s0 .is_empty0 ()) { |
2039 | 1 | return Err(make_input_err!( |
2040 | 1 | "single_use requires exactly one worker and no scheduler in the process" |
2041 | 1 | )); |
2042 | 5 | } |
2043 | 5 | for services2 in self |
2044 | 5 | .servers |
2045 | 5 | .iter() |
2046 | 5 | .filter_map(|server| server.services2 .as_ref2 ()) |
2047 | | { |
2048 | | let ServicesConfig { |
2049 | 2 | cas, |
2050 | 2 | ac, |
2051 | 2 | capabilities, |
2052 | 2 | execution, |
2053 | 2 | bytestream, |
2054 | 2 | fetch, |
2055 | 2 | push, |
2056 | 2 | worker_api, |
2057 | 2 | experimental_bep, |
2058 | 2 | admin, |
2059 | | health: _, |
2060 | 2 | } = services; |
2061 | 2 | if cas.is_some() |
2062 | 1 | || ac.is_some() |
2063 | 1 | || capabilities.is_some() |
2064 | 1 | || execution.is_some() |
2065 | 1 | || bytestream.is_some() |
2066 | 1 | || fetch.is_some() |
2067 | 1 | || push.is_some() |
2068 | 1 | || worker_api.is_some() |
2069 | 1 | || experimental_bep.is_some() |
2070 | 1 | || admin.is_some() |
2071 | | { |
2072 | 1 | return Err(make_input_err!( |
2073 | 1 | "single_use worker processes may expose only health services" |
2074 | 1 | )); |
2075 | 1 | } |
2076 | | } |
2077 | 4 | Ok(()) |
2078 | 53 | } |
2079 | | |
2080 | 37 | fn zstd_wire_compression_enabled_anywhere(&self) -> bool { |
2081 | 37 | if self |
2082 | 37 | .servers |
2083 | 37 | .iter() |
2084 | 37 | .filter_map(|server| server.services25 .as_ref25 ()) |
2085 | 37 | .filter_map(|services| services.capabilities25 .as_deref25 ()) |
2086 | 37 | .flatten() |
2087 | 37 | .any(|capabilities| capabilities19 .remote_cache_compression) |
2088 | | { |
2089 | 1 | return true; |
2090 | 36 | } |
2091 | | |
2092 | 67 | self.stores.iter()36 .any36 (|store| { |
2093 | 67 | let mut enabled = false; |
2094 | 67 | store.spec.visit_grpc_specs(&mut |grpc| {5 |
2095 | 5 | enabled |= matches!2 (grpc.store_type, StoreType::Cas) |
2096 | 3 | && grpc.experimental_remote_cache_compression == Some(true); |
2097 | 5 | }); |
2098 | 67 | enabled |
2099 | 67 | }) |
2100 | 37 | } |
2101 | | |
2102 | 37 | fn apply_zstd_grpc_store_defaults(&mut self) { |
2103 | 37 | let zstd_enabled_anywhere = self.zstd_wire_compression_enabled_anywhere(); |
2104 | | |
2105 | 69 | for store in &mut self.stores37 { |
2106 | 69 | let store_name = store.name.as_str(); |
2107 | 69 | store.spec.visit_grpc_specs_mut(&mut |grpc| {9 |
2108 | 9 | if !matches!3 (grpc.store_type, StoreType::Cas) { |
2109 | 3 | if grpc.experimental_remote_cache_compression == Some(true) { |
2110 | 1 | warn!( |
2111 | | store = store_name, |
2112 | | instance_name = grpc.instance_name, |
2113 | | "'experimental_remote_cache_compression' is enabled on a non-CAS \ |
2114 | | gRPC store, where it has no effect: REAPI zstd wire compression \ |
2115 | | only applies to CAS blob transfers. Enable it on the CAS gRPC \ |
2116 | | stores or a capabilities instance instead. See {}", |
2117 | | Self::ZSTD_COMPRESSION_DOCS_URL, |
2118 | | ); |
2119 | 2 | } |
2120 | 3 | return; |
2121 | 6 | } |
2122 | | |
2123 | 6 | if !zstd_enabled_anywhere { |
2124 | 2 | return; |
2125 | 4 | } |
2126 | | |
2127 | 4 | match grpc.experimental_remote_cache_compression { |
2128 | 2 | None => grpc.experimental_remote_cache_compression = Some(true), |
2129 | 1 | Some(false) => warn!( |
2130 | | store = store_name, |
2131 | | instance_name = grpc.instance_name, |
2132 | | "Zstd wire compression is enabled elsewhere, but this eligible CAS gRPC \ |
2133 | | store explicitly disables it. Unless this upstream cannot use zstd, \ |
2134 | | enable 'experimental_remote_cache_compression' for substantially faster \ |
2135 | | transfers of compressible artifacts. See {}", |
2136 | | Self::ZSTD_COMPRESSION_DOCS_URL, |
2137 | | ), |
2138 | 1 | Some(true) => {} |
2139 | | } |
2140 | 9 | }); |
2141 | | } |
2142 | 37 | } |
2143 | | |
2144 | 26 | fn check_store_conflict(services: &ServicesConfig) -> Result<(), Error> { |
2145 | 26 | if let Some(cas_config18 ) = &services.cas |
2146 | 18 | && let Some(ac_config) = &services.ac |
2147 | | { |
2148 | | // Create a hashmap from the CAS configuration for quick lookup |
2149 | 18 | let cas_store_map: HashMap<_, _> = cas_config |
2150 | 18 | .iter() |
2151 | 20 | .map18 (|with_instance_name| { |
2152 | 20 | ( |
2153 | 20 | &with_instance_name.instance_name, |
2154 | 20 | &with_instance_name.cas_store, |
2155 | 20 | ) |
2156 | 20 | }) |
2157 | 18 | .collect(); |
2158 | | |
2159 | 20 | for with_instance_name in ac_config18 { |
2160 | 20 | if let Some(cas_store) = cas_store_map.get(&with_instance_name.instance_name) |
2161 | 20 | && cas_store == &&with_instance_name.ac_store |
2162 | | { |
2163 | 1 | return Err(make_err!( |
2164 | 1 | Code::InvalidArgument, |
2165 | 1 | "CAS and AC use the same store '{}' in the config", |
2166 | 1 | cas_store |
2167 | 1 | )); |
2168 | 19 | } |
2169 | | } |
2170 | 8 | } |
2171 | 25 | Ok(()) |
2172 | 26 | } |
2173 | | } |
2174 | | |
2175 | | #[cfg(test)] |
2176 | | mod tests { |
2177 | | use tracing_test::traced_test; |
2178 | | |
2179 | | use super::*; |
2180 | | |
2181 | | #[test] |
2182 | 1 | fn single_use_worker_defaults_off_and_rejects_shared_processes() { |
2183 | 1 | assert!(!LocalWorkerConfig::default().single_use); |
2184 | 1 | let mut config = CasConfig::try_from_json5_str("{ stores: [], servers: [] }").unwrap(); |
2185 | 1 | config.workers = Some(vec![WorkerConfig::Local(LocalWorkerConfig { |
2186 | 1 | single_use: true, |
2187 | 1 | ..Default::default() |
2188 | 1 | })]); |
2189 | 1 | config.validate_single_use_worker().unwrap(); |
2190 | 1 | config |
2191 | 1 | .workers |
2192 | 1 | .as_mut() |
2193 | 1 | .unwrap() |
2194 | 1 | .push(WorkerConfig::Local(LocalWorkerConfig::default())); |
2195 | 1 | assert!(config.validate_single_use_worker().is_err()); |
2196 | 1 | config.workers.as_mut().unwrap().pop(); |
2197 | 1 | config.servers = serde_json5::from_str( |
2198 | 1 | r#"[{ |
2199 | 1 | listener: { http: { socket_address: "127.0.0.1:50061" } }, |
2200 | 1 | services: { health: { path: "/health" } } |
2201 | 1 | }]"#, |
2202 | | ) |
2203 | 1 | .unwrap(); |
2204 | 1 | config.validate_single_use_worker().unwrap(); |
2205 | 1 | config.servers[0].services.as_mut().unwrap().cas = Some(vec![]); |
2206 | 1 | assert!(config.validate_single_use_worker().is_err()); |
2207 | 1 | } |
2208 | | |
2209 | | #[test] |
2210 | 1 | fn buck2_capture_requires_an_isolated_execution_container() { |
2211 | 1 | assert!( |
2212 | 1 | LocalWorkerConfig::default() |
2213 | 1 | .experimental_buck2_file_capture |
2214 | 1 | .is_none() |
2215 | | ); |
2216 | 1 | let capture = Buck2FileCaptureConfig { |
2217 | 1 | executable: "/shared/buck2-file-capture".to_string(), |
2218 | 1 | gateway: "http://capture/api-auth/v1/buck2-files".to_string(), |
2219 | 1 | token_file: "/capture-token/token".to_string(), |
2220 | 1 | state_directory: "/capture-state".to_string(), |
2221 | 1 | container_id: "pod-uid".to_string(), |
2222 | 1 | protected_paths: vec!["/worker-config".to_string()], |
2223 | 1 | internal_paths: vec!["/cas".to_string()], |
2224 | 1 | finalize_timeout_s: 120, |
2225 | 1 | }; |
2226 | 5 | for (single_use, entrypoint, mount_namespace, valid) in [ |
2227 | 1 | (true, "", None, true), |
2228 | 1 | (true, "", Some(false), true), |
2229 | 1 | (false, "", None, false), |
2230 | 1 | (true, "/outer-container-wrapper", None, false), |
2231 | 1 | (true, "", Some(true), false), |
2232 | 1 | ] { |
2233 | 5 | let mut config = CasConfig::try_from_json5_str("{ stores: [], servers: [] }").unwrap(); |
2234 | 5 | config.workers = Some(vec![WorkerConfig::Local(LocalWorkerConfig { |
2235 | 5 | single_use, |
2236 | 5 | entrypoint: entrypoint.to_string(), |
2237 | 5 | use_mount_namespace: mount_namespace, |
2238 | 5 | experimental_buck2_file_capture: Some(capture.clone()), |
2239 | 5 | ..Default::default() |
2240 | 5 | })]); |
2241 | 5 | let result = config.validate_single_use_worker(); |
2242 | 5 | assert_eq!(result.is_ok(), valid, "{result:?}"); |
2243 | | } |
2244 | 7 | for invalid in [ |
2245 | 1 | "relative/helper", |
2246 | 1 | "/", |
2247 | 1 | "/./", |
2248 | 1 | "//", |
2249 | 1 | "/tmp/../token", |
2250 | 1 | "C:\\capture\\token", |
2251 | 1 | "/tmp/\0token", |
2252 | 1 | ] { |
2253 | 7 | let mut invalid_capture = capture.clone(); |
2254 | 7 | invalid_capture.token_file = invalid.to_string(); |
2255 | 7 | let mut config = CasConfig::try_from_json5_str("{ stores: [], servers: [] }").unwrap(); |
2256 | 7 | config.workers = Some(vec![WorkerConfig::Local(LocalWorkerConfig { |
2257 | 7 | single_use: true, |
2258 | 7 | experimental_buck2_file_capture: Some(invalid_capture), |
2259 | 7 | ..Default::default() |
2260 | 7 | })]); |
2261 | 7 | assert!( |
2262 | 7 | config.validate_single_use_worker().is_err(), |
2263 | | "accepted {invalid:?}" |
2264 | | ); |
2265 | | } |
2266 | 1 | } |
2267 | | |
2268 | 4 | fn grpc_compression_configs(config: &CasConfig) -> Vec<(bool, Option<bool>)> { |
2269 | 4 | let mut configs = Vec::new(); |
2270 | 6 | for store in &config.stores4 { |
2271 | 8 | store.spec6 .visit_grpc_specs6 (&mut |grpc| { |
2272 | 8 | configs.push(( |
2273 | 8 | matches!2 (grpc.store_type, StoreType::Cas), |
2274 | 8 | grpc.experimental_remote_cache_compression, |
2275 | | )); |
2276 | 8 | }); |
2277 | | } |
2278 | 4 | configs |
2279 | 4 | } |
2280 | | |
2281 | | #[test] |
2282 | 1 | fn capabilities_config_remote_cache_compression_deserializes_true() { |
2283 | 1 | let config: CapabilitiesConfig = |
2284 | 1 | serde_json5::from_str(r#"{"remote_cache_compression": true}"#).unwrap(); |
2285 | | |
2286 | 1 | assert!(config.remote_cache_compression); |
2287 | 1 | } |
2288 | | |
2289 | | #[test] |
2290 | 1 | fn capabilities_config_remote_cache_compression_defaults_false() { |
2291 | 1 | let config: CapabilitiesConfig = serde_json5::from_str("{}").unwrap(); |
2292 | | |
2293 | 1 | assert!(!config.remote_cache_compression); |
2294 | 1 | } |
2295 | | |
2296 | | #[test] |
2297 | 1 | fn omitted_grpc_compression_stays_disabled_without_zstd_intent() { |
2298 | 1 | let config = CasConfig::try_from_json5_str( |
2299 | 1 | r#"{ |
2300 | 1 | stores: [{ |
2301 | 1 | name: "upstream", |
2302 | 1 | grpc: { |
2303 | 1 | endpoints: [{ address: "http://localhost:1234" }], |
2304 | 1 | store_type: "cas", |
2305 | 1 | }, |
2306 | 1 | }], |
2307 | 1 | servers: [], |
2308 | 1 | }"#, |
2309 | | ) |
2310 | 1 | .unwrap(); |
2311 | | |
2312 | 1 | assert_eq!(grpc_compression_configs(&config), vec![(true, None)]); |
2313 | 1 | } |
2314 | | |
2315 | | #[test] |
2316 | 1 | fn explicit_grpc_zstd_intent_enables_other_eligible_grpc_stores() { |
2317 | 1 | let config = CasConfig::try_from_json5_str( |
2318 | 1 | r#"{ |
2319 | 1 | stores: [ |
2320 | 1 | { |
2321 | 1 | name: "enabled", |
2322 | 1 | grpc: { |
2323 | 1 | endpoints: [{ address: "http://localhost:1234" }], |
2324 | 1 | store_type: "cas", |
2325 | 1 | experimental_remote_cache_compression: true, |
2326 | 1 | }, |
2327 | 1 | }, |
2328 | 1 | { |
2329 | 1 | name: "inherited", |
2330 | 1 | grpc: { |
2331 | 1 | endpoints: [{ address: "http://localhost:5678" }], |
2332 | 1 | store_type: "cas", |
2333 | 1 | }, |
2334 | 1 | }, |
2335 | 1 | ], |
2336 | 1 | servers: [], |
2337 | 1 | }"#, |
2338 | | ) |
2339 | 1 | .unwrap(); |
2340 | | |
2341 | 1 | assert_eq!( |
2342 | 1 | grpc_compression_configs(&config), |
2343 | 1 | vec![(true, Some(true)), (true, Some(true))] |
2344 | | ); |
2345 | 1 | } |
2346 | | |
2347 | | #[test] |
2348 | | #[traced_test] |
2349 | 1 | fn ac_grpc_compression_warns_and_expresses_no_cas_intent() { |
2350 | 1 | let config = CasConfig::try_from_json5_str( |
2351 | 1 | r#"{ |
2352 | 1 | stores: [ |
2353 | 1 | { |
2354 | 1 | name: "action-cache", |
2355 | 1 | grpc: { |
2356 | 1 | instance_name: "ac", |
2357 | 1 | endpoints: [{ address: "http://localhost:1234" }], |
2358 | 1 | store_type: "ac", |
2359 | 1 | experimental_remote_cache_compression: true, |
2360 | 1 | }, |
2361 | 1 | }, |
2362 | 1 | { |
2363 | 1 | name: "cas", |
2364 | 1 | grpc: { |
2365 | 1 | endpoints: [{ address: "http://localhost:5678" }], |
2366 | 1 | store_type: "cas", |
2367 | 1 | }, |
2368 | 1 | }, |
2369 | 1 | ], |
2370 | 1 | servers: [], |
2371 | 1 | }"#, |
2372 | | ) |
2373 | 1 | .unwrap(); |
2374 | | |
2375 | | // The AC-store setting is inert: it neither compresses AC RPCs nor |
2376 | | // expresses process-wide zstd intent, so the CAS store stays unset. |
2377 | 1 | assert_eq!( |
2378 | 1 | grpc_compression_configs(&config), |
2379 | 1 | vec![(false, Some(true)), (true, None)] |
2380 | | ); |
2381 | 1 | assert!(logs_contain("store=\"action-cache\"")); |
2382 | 1 | assert!(logs_contain("no effect")); |
2383 | 1 | } |
2384 | | |
2385 | | #[test] |
2386 | | #[traced_test] |
2387 | 1 | fn capabilities_zstd_intent_defaults_nested_cas_grpc_and_warns_on_opt_out() { |
2388 | 1 | let config = CasConfig::try_from_json5_str( |
2389 | 1 | r#"{ |
2390 | 1 | stores: [{ |
2391 | 1 | name: "nested-upstreams", |
2392 | 1 | fast_slow: { |
2393 | 1 | fast: { |
2394 | 1 | grpc: { |
2395 | 1 | instance_name: "auto", |
2396 | 1 | endpoints: [{ address: "http://localhost:1234" }], |
2397 | 1 | store_type: "cas", |
2398 | 1 | }, |
2399 | 1 | }, |
2400 | 1 | slow: { |
2401 | 1 | shard: { |
2402 | 1 | stores: [ |
2403 | 1 | { |
2404 | 1 | store: { |
2405 | 1 | grpc: { |
2406 | 1 | instance_name: "opt-out", |
2407 | 1 | endpoints: [{ |
2408 | 1 | address: "http://localhost:5678", |
2409 | 1 | }], |
2410 | 1 | store_type: "cas", |
2411 | 1 | experimental_remote_cache_compression: false, |
2412 | 1 | }, |
2413 | 1 | }, |
2414 | 1 | weight: 1, |
2415 | 1 | }, |
2416 | 1 | { |
2417 | 1 | store: { |
2418 | 1 | grpc: { |
2419 | 1 | instance_name: "action-cache", |
2420 | 1 | endpoints: [{ |
2421 | 1 | address: "http://localhost:9012", |
2422 | 1 | }], |
2423 | 1 | store_type: "ac", |
2424 | 1 | }, |
2425 | 1 | }, |
2426 | 1 | weight: 1, |
2427 | 1 | }, |
2428 | 1 | ], |
2429 | 1 | }, |
2430 | 1 | }, |
2431 | 1 | }, |
2432 | 1 | }], |
2433 | 1 | servers: [{ |
2434 | 1 | listener: { |
2435 | 1 | http: { socket_address: "127.0.0.1:50051" }, |
2436 | 1 | }, |
2437 | 1 | services: { |
2438 | 1 | capabilities: [{ |
2439 | 1 | instance_name: "main", |
2440 | 1 | remote_cache_compression: true, |
2441 | 1 | }], |
2442 | 1 | }, |
2443 | 1 | }], |
2444 | 1 | }"#, |
2445 | | ) |
2446 | 1 | .unwrap(); |
2447 | | |
2448 | 1 | assert_eq!( |
2449 | 1 | grpc_compression_configs(&config), |
2450 | 1 | vec![(true, Some(true)), (true, Some(false)), (false, None),] |
2451 | | ); |
2452 | 1 | assert!(logs_contain("store=\"nested-upstreams\"")); |
2453 | 1 | assert!(logs_contain("instance_name=\"opt-out\"")); |
2454 | 1 | assert!(logs_contain(CasConfig::ZSTD_COMPRESSION_DOCS_URL)); |
2455 | 1 | } |
2456 | | } |