Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}