Coverage Report

Created: 2026-09-18 20:40

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 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use std::collections::HashMap;
16
17
use nativelink_error::{Code, Error, ResultExt, make_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
325
    fn deref(&self) -> &Self::Target {
55
325
        &self.config
56
325
    }
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
    /// The store name referenced in the `stores` map in the main config.
282
    /// This store name referenced here may be reused multiple times.
283
    /// This value must be a CAS store reference.
284
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
285
    pub cas_store: StoreRefName,
286
287
    /// The scheduler name referenced in the `schedulers` map in the main config.
288
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
289
    pub scheduler: SchedulerRefName,
290
}
291
292
#[derive(Deserialize, Serialize, Debug, Clone)]
293
#[serde(deny_unknown_fields)]
294
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
295
pub struct FetchConfig {
296
    /// The store name referenced in the `stores` map in the main config.
297
    /// This store name referenced here may be reused multiple times.
298
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
299
    pub fetch_store: StoreRefName,
300
}
301
302
#[derive(Deserialize, Serialize, Debug, Clone)]
303
#[serde(deny_unknown_fields)]
304
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
305
pub struct PushConfig {
306
    /// The store name referenced in the `stores` map in the main config.
307
    /// This store name referenced here may be reused multiple times.
308
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
309
    pub push_store: StoreRefName,
310
311
    /// Whether the Action Cache store may be written to, this if set to false
312
    /// it is only possible to read from the Action Cache.
313
    #[serde(default)]
314
    pub read_only: bool,
315
}
316
317
// From https://github.com/serde-rs/serde/issues/818#issuecomment-287438544
318
94
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
319
94
    *t == Default::default()
320
94
}
321
322
#[derive(Deserialize, Serialize, Debug, Default, PartialEq, Eq)]
323
#[serde(deny_unknown_fields)]
324
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
325
pub struct ByteStreamConfig {
326
    /// Name of the store in the "stores" configuration.
327
    pub cas_store: StoreRefName,
328
329
    /// Max number of bytes to send on each grpc stream chunk.
330
    /// According to <https://github.com/grpc/grpc.github.io/issues/371>
331
    /// 16KiB - 64KiB is optimal.
332
    ///
333
    ///
334
    /// Default: 64KiB
335
    #[serde(
336
        default,
337
        deserialize_with = "convert_data_size_with_shellexpand",
338
        skip_serializing_if = "is_default"
339
    )]
340
    pub max_bytes_per_stream: usize,
341
342
    /// In the event a client disconnects while uploading a blob, we will hold
343
    /// the internal stream open for this many seconds before closing it.
344
    /// This allows clients that disconnect to reconnect and continue uploading
345
    /// the same blob.
346
    ///
347
    /// Default: 10 seconds
348
    #[serde(
349
        default,
350
        deserialize_with = "convert_duration_with_shellexpand",
351
        skip_serializing_if = "is_default",
352
        alias = "persist_stream_on_disconnect_timeout"
353
    )]
354
    pub persist_stream_on_disconnect_timeout_s: usize,
355
}
356
357
// Older bytestream config. All fields are as per the newer docs, but this requires
358
// the hashed `cas_stores` v.s. the WithInstanceName approach. This should _not_ be updated
359
// with newer fields, and eventually dropped
360
#[derive(Deserialize, Serialize, Debug, Clone)]
361
#[serde(deny_unknown_fields)]
362
pub struct OldByteStreamConfig {
363
    pub cas_stores: HashMap<InstanceName, StoreRefName>,
364
    #[serde(
365
        default,
366
        deserialize_with = "convert_data_size_with_shellexpand",
367
        skip_serializing_if = "is_default"
368
    )]
369
    pub max_bytes_per_stream: usize,
370
    #[serde(
371
        default,
372
        deserialize_with = "convert_data_size_with_shellexpand",
373
        skip_serializing_if = "is_default"
374
    )]
375
    pub max_decoding_message_size: usize,
376
    #[serde(
377
        default,
378
        deserialize_with = "convert_duration_with_shellexpand",
379
        skip_serializing_if = "is_default",
380
        alias = "persist_stream_on_disconnect_timeout"
381
    )]
382
    pub persist_stream_on_disconnect_timeout_s: usize,
383
}
384
385
#[derive(Deserialize, Serialize, Debug)]
386
#[serde(deny_unknown_fields)]
387
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
388
pub struct WorkerApiConfig {
389
    /// The scheduler name referenced in the `schedulers` map in the main config.
390
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
391
    pub scheduler: SchedulerRefName,
392
393
    /// Disable the periodic sweep that tells workers to kill operations
394
    /// the scheduler no longer has executing on them (for example because
395
    /// every client disconnected before the action finished). With the
396
    /// sweep disabled such orphaned actions run to completion and still
397
    /// warm the action cache, which can be preferable for deployments
398
    /// with long or expensive actions whose clients merely retry.
399
    ///
400
    /// Default: false (the sweep runs)
401
    #[serde(default)]
402
    pub disable_kill_revoked_operations: bool,
403
404
    /// How often, in seconds, the scheduler checks for operations that
405
    /// are still running on a worker but are no longer executing
406
    /// according to the state manager, and tells the worker to kill
407
    /// them. Each pass costs one state-manager lookup per running
408
    /// operation, so store-backed deployments with many concurrent
409
    /// actions may want a longer interval.
410
    ///
411
    /// Default: 5 (0 uses the default)
412
    #[serde(default)]
413
    pub kill_revoked_operations_interval_s: u64,
414
}
415
416
#[derive(Deserialize, Serialize, Debug, Default)]
417
#[serde(deny_unknown_fields)]
418
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
419
pub struct AdminConfig {
420
    /// Path to register the admin API. If path is "/admin", and your
421
    /// domain is "example.com", you can reach the endpoint with:
422
    /// <http://example.com/admin>.
423
    ///
424
    /// Default: "/admin"
425
    #[serde(default)]
426
    pub path: String,
427
}
428
429
#[derive(Deserialize, Serialize, Debug, Default)]
430
#[serde(deny_unknown_fields)]
431
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
432
pub struct HealthConfig {
433
    /// Path to register the health status check. If path is "/status", and your
434
    /// domain is "example.com", you can reach the endpoint with:
435
    /// <http://example.com/status>.
436
    ///
437
    /// Default: "/status"
438
    #[serde(default)]
439
    pub path: String,
440
441
    /// Timeout on health checks. Default: 5s.
442
    #[serde(default)]
443
    pub timeout_seconds: u64,
444
}
445
446
#[derive(Deserialize, Serialize, Debug)]
447
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
448
pub struct BepConfig {
449
    /// The store to publish build events to.
450
    /// The store name referenced in the `stores` map in the main config.
451
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
452
    pub store: StoreRefName,
453
}
454
455
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
456
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
457
pub struct IdentityHeaderSpec {
458
    /// The name of the header to look for the identity in.
459
    /// Default: "x-identity"
460
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
461
    pub header_name: Option<String>,
462
463
    /// If the header is required to be set or fail the request.
464
    #[serde(default)]
465
    pub required: bool,
466
}
467
468
#[derive(Deserialize, Serialize, Clone, Debug)]
469
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
470
pub struct OriginEventsPublisherSpec {
471
    /// The store to publish nativelink events to.
472
    /// The store name referenced in the `stores` map in the main config.
473
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
474
    pub store: StoreRefName,
475
}
476
477
#[derive(Deserialize, Serialize, Clone, Debug)]
478
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
479
pub struct OriginEventsSpec {
480
    /// The publisher configuration for origin events.
481
    pub publisher: OriginEventsPublisherSpec,
482
483
    /// The maximum number of events to queue before applying back pressure.
484
    /// IMPORTANT: Backpressure causes all clients to slow down significantly.
485
    /// Zero is default.
486
    ///
487
    /// Default: 65536 (zero defaults to this)
488
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
489
    pub max_event_queue_size: usize,
490
}
491
492
#[derive(Deserialize, Serialize, Debug)]
493
#[serde(deny_unknown_fields)]
494
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
495
pub struct ServicesConfig {
496
    /// The Content Addressable Storage (CAS) backend config.
497
    /// The key is the `instance_name` used in the protocol and the
498
    /// value is the underlying CAS store config.
499
    #[serde(
500
        default,
501
        deserialize_with = "super::backcompat::opt_vec_with_instance_name"
502
    )]
503
    pub cas: Option<Vec<WithInstanceName<CasStoreConfig>>>,
504
505
    /// The Action Cache (AC) backend config.
506
    /// The key is the `instance_name` used in the protocol and the
507
    /// value is the underlying AC store config.
508
    #[serde(
509
        default,
510
        deserialize_with = "super::backcompat::opt_vec_with_instance_name"
511
    )]
512
    pub ac: Option<Vec<WithInstanceName<AcStoreConfig>>>,
513
514
    /// Capabilities service is required in order to use most of the
515
    /// bazel protocol. This service is used to provide the supported
516
    /// features and versions of this bazel gRPC service.
517
    #[serde(
518
        default,
519
        deserialize_with = "super::backcompat::opt_vec_with_instance_name"
520
    )]
521
    pub capabilities: Option<Vec<WithInstanceName<CapabilitiesConfig>>>,
522
523
    /// The remote execution service configuration.
524
    /// NOTE: This service is under development and is currently just a
525
    /// place holder.
526
    #[serde(
527
        default,
528
        deserialize_with = "super::backcompat::opt_vec_with_instance_name"
529
    )]
530
    pub execution: Option<Vec<WithInstanceName<ExecutionConfig>>>,
531
532
    /// This is the service used to stream data to and from the CAS.
533
    /// Bazel's protocol strongly encourages users to use this streaming
534
    /// interface to interact with the CAS when the data is large.
535
    #[serde(default, deserialize_with = "super::backcompat::opt_bytestream")]
536
    pub bytestream: Option<Vec<WithInstanceName<ByteStreamConfig>>>,
537
538
    /// These two are collectively the Remote Asset protocol, but it's
539
    /// defined as two separate services
540
    #[serde(
541
        default,
542
        deserialize_with = "super::backcompat::opt_vec_with_instance_name"
543
    )]
544
    pub fetch: Option<Vec<WithInstanceName<FetchConfig>>>,
545
546
    #[serde(
547
        default,
548
        deserialize_with = "super::backcompat::opt_vec_with_instance_name"
549
    )]
550
    pub push: Option<Vec<WithInstanceName<PushConfig>>>,
551
552
    /// This is the service used for workers to connect and communicate
553
    /// through.
554
    /// NOTE: This service should be served on a different, non-public port.
555
    /// In other words, `worker_api` configuration should not have any other
556
    /// services that are served on the same port. Doing so is a security
557
    /// risk, as workers have a different permission set than a client
558
    /// that makes the remote execution/cache requests.
559
    pub worker_api: Option<WorkerApiConfig>,
560
561
    /// Experimental - Build Event Protocol (BEP) configuration. This is
562
    /// the service that will consume build events from the client and
563
    /// publish them to a store for processing by an external service.
564
    pub experimental_bep: Option<BepConfig>,
565
566
    /// This is the service for any administrative tasks.
567
    /// It provides a REST API endpoint for administrative purposes.
568
    pub admin: Option<AdminConfig>,
569
570
    /// This is the service for health status check.
571
    pub health: Option<HealthConfig>,
572
}
573
574
#[derive(Deserialize, Serialize, Debug)]
575
#[serde(deny_unknown_fields)]
576
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
577
pub struct TlsConfig {
578
    /// Path to the certificate file.
579
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
580
    pub cert_file: String,
581
582
    /// Path to the private key file.
583
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
584
    pub key_file: String,
585
586
    /// Path to the certificate authority for mTLS, if client authentication is
587
    /// required for this endpoint.
588
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
589
    pub client_ca_file: Option<String>,
590
591
    /// Path to the certificate revocation list for mTLS, if client
592
    /// authentication is required for this endpoint.
593
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
594
    pub client_crl_file: Option<String>,
595
}
596
597
/// Advanced Http configurations. These are generally should not be set.
598
/// For documentation on what each of these do, see the hyper documentation:
599
/// See: <https://docs.rs/hyper/latest/hyper/server/conn/struct.Http.html>
600
///
601
/// Note: All of these default to hyper's default values unless otherwise
602
/// specified.
603
#[derive(Deserialize, Serialize, Debug, Default, Clone, Copy)]
604
#[serde(deny_unknown_fields)]
605
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
606
pub struct HttpServerConfig {
607
    /// Interval to send keep-alive pings via HTTP2.
608
    /// Note: This is in seconds.
609
    #[serde(
610
        default,
611
        deserialize_with = "convert_optional_numeric_with_shellexpand"
612
    )]
613
    pub http2_keep_alive_interval: Option<u32>,
614
615
    #[serde(
616
        default,
617
        deserialize_with = "convert_optional_numeric_with_shellexpand"
618
    )]
619
    pub experimental_http2_max_pending_accept_reset_streams: Option<u32>,
620
621
    #[serde(
622
        default,
623
        deserialize_with = "convert_optional_numeric_with_shellexpand"
624
    )]
625
    pub experimental_http2_initial_stream_window_size: Option<u32>,
626
627
    #[serde(
628
        default,
629
        deserialize_with = "convert_optional_numeric_with_shellexpand"
630
    )]
631
    pub experimental_http2_initial_connection_window_size: Option<u32>,
632
633
    #[serde(default)]
634
    pub experimental_http2_adaptive_window: Option<bool>,
635
636
    #[serde(
637
        default,
638
        deserialize_with = "convert_optional_numeric_with_shellexpand"
639
    )]
640
    pub experimental_http2_max_frame_size: Option<u32>,
641
642
    #[serde(
643
        default,
644
        deserialize_with = "convert_optional_numeric_with_shellexpand"
645
    )]
646
    pub experimental_http2_max_concurrent_streams: Option<u32>,
647
648
    #[serde(
649
        default,
650
        deserialize_with = "convert_optional_numeric_with_shellexpand",
651
        alias = "experimental_http2_keep_alive_timeout"
652
    )]
653
    pub experimental_http2_keep_alive_timeout_s: Option<u32>,
654
655
    #[serde(
656
        default,
657
        deserialize_with = "convert_optional_numeric_with_shellexpand"
658
    )]
659
    pub experimental_http2_max_send_buf_size: Option<u32>,
660
661
    #[serde(default)]
662
    pub experimental_http2_enable_connect_protocol: Option<bool>,
663
664
    #[serde(
665
        default,
666
        deserialize_with = "convert_optional_numeric_with_shellexpand"
667
    )]
668
    pub experimental_http2_max_header_list_size: Option<u32>,
669
}
670
671
#[derive(Deserialize, Serialize, Debug)]
672
#[serde(rename_all = "snake_case")]
673
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
674
pub enum ListenerConfig {
675
    /// Listener for HTTP/HTTPS/HTTP2 sockets.
676
    Http(HttpListener),
677
}
678
679
#[derive(Deserialize, Serialize, Debug, Default)]
680
#[serde(deny_unknown_fields)]
681
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
682
pub struct HttpListener {
683
    /// Address to listen on. Example: `127.0.0.1:8080` or `:8080` to listen
684
    /// to all IPs.
685
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
686
    pub socket_address: String,
687
688
    /// Allow binding `socket_address` before it is assigned locally.
689
    ///
690
    /// Default: false
691
    #[serde(default)]
692
    pub freebind: bool,
693
694
    /// Data transport compression configuration to use for this service.
695
    #[serde(default)]
696
    pub compression: HttpCompressionConfig,
697
698
    /// Advanced Http server configuration.
699
    #[serde(default)]
700
    pub advanced_http: HttpServerConfig,
701
702
    /// Maximum number of bytes to decode on each grpc stream chunk.
703
    /// Default: 4 MiB
704
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
705
    pub max_decoding_message_size: usize,
706
707
    /// Tls Configuration for this server.
708
    /// If not set, the server will not use TLS.
709
    ///
710
    /// Default: None
711
    #[serde(default)]
712
    pub tls: Option<TlsConfig>,
713
}
714
715
#[derive(Deserialize, Serialize, Debug)]
716
#[serde(deny_unknown_fields)]
717
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
718
pub struct ServerConfig {
719
    /// Name of the server. This is used to help identify the service
720
    /// for telemetry and logs.
721
    ///
722
    /// Default: {index of server in config}
723
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
724
    pub name: String,
725
726
    /// Configuration
727
    pub listener: ListenerConfig,
728
729
    /// Services to attach to server.
730
    pub services: Option<ServicesConfig>,
731
732
    /// The config related to identifying the client.
733
    /// Default: {see `IdentityHeaderSpec`}
734
    #[serde(default)]
735
    pub experimental_identity_header: IdentityHeaderSpec,
736
}
737
738
#[derive(Deserialize, Serialize, Debug)]
739
#[serde(rename_all = "snake_case")]
740
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
741
pub enum WorkerProperty {
742
    /// List of static values.
743
    /// Note: Generally there should only ever be 1 value, but if the platform
744
    /// property key is `PropertyType::Priority` it may have more than one value.
745
    #[serde(deserialize_with = "convert_vec_string_with_shellexpand")]
746
    Values(Vec<String>),
747
748
    /// A dynamic configuration. The string will be executed as a command
749
    /// (not shell) and will be split by "\n" (new line character).
750
    QueryCmd(String),
751
}
752
753
/// Generic config for an endpoint and associated configs.
754
#[derive(Deserialize, Serialize, Debug, Default)]
755
#[serde(deny_unknown_fields)]
756
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
757
pub struct EndpointConfig {
758
    /// URI of the endpoint.
759
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
760
    pub uri: String,
761
762
    /// Timeout in seconds that a request should take.
763
    /// Default: 5 seconds
764
    pub timeout: Option<f32>,
765
766
    /// The TLS configuration to use to connect to the endpoint.
767
    pub tls_config: Option<ClientTlsConfig>,
768
}
769
770
#[derive(Copy, Clone, Deserialize, Serialize, Debug, Default)]
771
#[serde(rename_all = "snake_case")]
772
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
773
pub enum UploadCacheResultsStrategy {
774
    /// Only upload action results with an exit code of 0.
775
    #[default]
776
    SuccessOnly,
777
778
    /// Don't upload any action results.
779
    Never,
780
781
    /// Upload all action results that complete.
782
    Everything,
783
784
    /// Only upload action results that fail.
785
    FailuresOnly,
786
}
787
788
#[derive(Clone, Deserialize, Serialize, Debug)]
789
#[serde(rename_all = "snake_case")]
790
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
791
pub enum EnvironmentSource {
792
    /// The name of the platform property in the action to get the value from.
793
    Property(String),
794
795
    /// The raw value to set.
796
    Value(#[serde(deserialize_with = "convert_string_with_shellexpand")] String),
797
798
    /// Take the value from the local environment corresponding to the name key
799
    FromEnvironment,
800
801
    /// The max amount of time in milliseconds the command is allowed to run
802
    /// (requested by the client).
803
    TimeoutMillis,
804
805
    /// A special file path will be provided that can be used to communicate
806
    /// with the parent process about out-of-band information. This file
807
    /// will be read after the command has finished executing. Based on the
808
    /// contents of the file, the behavior of the result may be modified.
809
    ///
810
    /// The format of the file contents should be json with the following
811
    /// schema:
812
    /// {
813
    ///   // If set the command will be considered a failure.
814
    ///   // May be one of the following static strings:
815
    ///   // "timeout": Will Consider this task to be a timeout.
816
    ///   "failure": "timeout",
817
    /// }
818
    ///
819
    /// All fields are optional, file does not need to be created and may be
820
    /// empty.
821
    SideChannelFile,
822
823
    /// A "root" directory for the action. This directory can be used to
824
    /// store temporary files that are not needed after the action has
825
    /// completed. This directory will be purged after the action has
826
    /// completed.
827
    ///
828
    /// For example:
829
    /// If an action writes temporary data to a path but nativelink should
830
    /// clean up this path after the job has executed, you may create any
831
    /// directory under the path provided in this variable. A common pattern
832
    /// would be to use `entrypoint` to set a shell script that reads this
833
    /// variable, `mkdir $ENV_VAR_NAME/tmp` and `export TMPDIR=$ENV_VAR_NAME/tmp`.
834
    /// Another example might be to bind-mount the `/tmp` path in a container to
835
    /// this path in `entrypoint`.
836
    ActionDirectory,
837
}
838
839
#[derive(Deserialize, Serialize, Debug, Default)]
840
#[serde(deny_unknown_fields)]
841
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
842
pub struct UploadActionResultConfig {
843
    /// Underlying AC store that the worker will use to publish execution results
844
    /// into. Objects placed in this store should be reachable from the
845
    /// scheduler/client-cas after they have finished updating.
846
    /// Default: {No uploading is done}
847
    pub ac_store: Option<StoreRefName>,
848
849
    /// In which situations should the results be published to the `ac_store`,
850
    /// if set to `SuccessOnly` then only results with an exit code of 0 will be
851
    /// uploaded, if set to Everything all completed results will be uploaded.
852
    ///
853
    /// Default: `SuccessOnly`
854
    #[serde(default)]
855
    pub upload_ac_results_strategy: UploadCacheResultsStrategy,
856
857
    /// Store to upload historical results to. This should be a CAS store if set.
858
    ///
859
    /// Default: {CAS store of parent}
860
    pub historical_results_store: Option<StoreRefName>,
861
862
    /// In which situations should the results be published to the historical CAS.
863
    /// The historical CAS is where failures are published. These messages conform
864
    /// to the CAS key-value lookup format and are always a `HistoricalExecuteResponse`
865
    /// serialized message.
866
    ///
867
    /// Default: `FailuresOnly`
868
    #[serde(default)]
869
    pub upload_historical_results_strategy: Option<UploadCacheResultsStrategy>,
870
871
    /// Template to use for the `ExecuteResponse.message` property. This message
872
    /// is attached to the response before it is sent to the client. The following
873
    /// special variables are supported:
874
    /// - `digest_function`: Digest function used to calculate the action digest.
875
    /// - `action_digest_hash`: Action digest hash.
876
    /// - `action_digest_size`: Action digest size.
877
    /// - `historical_results_hash`: `HistoricalExecuteResponse` digest hash.
878
    /// - `historical_results_size`: `HistoricalExecuteResponse` digest size.
879
    ///
880
    /// A common use case of this is to provide a link to the web page that
881
    /// contains more useful information for the user.
882
    ///
883
    /// An example that is fully compatible with `bb_browser` is:
884
    /// <https://example.com/my-instance-name-here/blobs/{digest_function}/action/{action_digest_hash}-{action_digest_size}/>
885
    ///
886
    /// Default: "" (no message)
887
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
888
    pub success_message_template: String,
889
890
    /// Same as `success_message_template` but for failure case.
891
    ///
892
    /// An example that is fully compatible with `bb_browser` is:
893
    /// <https://example.com/my-instance-name-here/blobs/{digest_function}/historical_execute_response/{historical_results_hash}-{historical_results_size}/>
894
    ///
895
    /// Default: "" (no message)
896
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
897
    pub failure_message_template: String,
898
}
899
900
#[derive(Deserialize, Serialize, Debug, Default)]
901
#[serde(deny_unknown_fields)]
902
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
903
pub struct LocalWorkerConfig {
904
    /// Name of the worker. This is give a more friendly name to a worker for logging
905
    /// and metric publishing. This is also the prefix of the worker id
906
    /// (i.e. "{name}{uuidv6}").
907
    /// Default: {Index position in the workers list}
908
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
909
    pub name: String,
910
911
    /// Endpoint which the worker will connect to the scheduler's `WorkerApiService`.
912
    pub worker_api_endpoint: EndpointConfig,
913
914
    /// The maximum time an action is allowed to run. If a task requests for a timeout
915
    /// longer than this time limit, the task will be rejected. Value in seconds.
916
    ///
917
    /// Default: 20 minutes
918
    #[serde(
919
        default,
920
        deserialize_with = "convert_duration_with_shellexpand",
921
        alias = "max_action_timeout"
922
    )]
923
    pub max_action_timeout_s: usize,
924
925
    /// Maximum time allowed for uploading action results to CAS after execution
926
    /// completes. If upload takes longer than this, the action fails with
927
    /// `DeadlineExceeded` and may be retried by the scheduler. Value in seconds.
928
    ///
929
    /// Default: 10 minutes
930
    #[serde(
931
        default,
932
        deserialize_with = "convert_duration_with_shellexpand",
933
        alias = "max_upload_timeout"
934
    )]
935
    pub max_upload_timeout_s: usize,
936
937
    /// Maximum time to wait for action directory cleanup before timing out.
938
    /// Value in seconds.
939
    ///
940
    /// Default: 30 seconds
941
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
942
    pub max_cleanup_wait_s: usize,
943
944
    /// Maximum backoff duration for exponential backoff when waiting for cleanup.
945
    /// Value in milliseconds.
946
    ///
947
    /// Default: 500 milliseconds
948
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
949
    pub max_cleanup_backoff_ms: usize,
950
951
    /// Maximum number of inflight tasks this worker can cope with.
952
    ///
953
    /// Default: 0 (infinite tasks)
954
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
955
    pub max_inflight_tasks: u64,
956
957
    /// If timeout is handled in `entrypoint` or another wrapper script.
958
    /// If set to true `NativeLink` will not honor the timeout the action requested
959
    /// and instead will always force kill the action after `max_action_timeout`
960
    /// has been reached. If this is set to false, the smaller value of the action's
961
    /// timeout and `max_action_timeout` will be used to which `NativeLink` will kill
962
    /// the action.
963
    ///
964
    /// The real timeout can be received via an environment variable set in:
965
    /// `EnvironmentSource::TimeoutMillis`.
966
    ///
967
    /// Example on where this is useful: `entrypoint` launches the action inside
968
    /// a docker container, but the docker container may need to be downloaded. Thus
969
    /// the timer should not start until the docker container has started executing
970
    /// the action. In this case, action will likely be wrapped in another program,
971
    /// like `timeout` and propagate timeouts via `EnvironmentSource::SideChannelFile`.
972
    ///
973
    /// Default: false (`NativeLink` fully handles timeouts)
974
    #[serde(default)]
975
    pub timeout_handled_externally: bool,
976
977
    /// The command to execute on every execution request. This will be parsed as
978
    /// a command + arguments (not shell).
979
    /// Example: "run.sh" and a job with command: "sleep 5" will result in a
980
    /// command like: "run.sh sleep 5".
981
    /// Default: {Use the command from the job request}.
982
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
983
    pub entrypoint: String,
984
985
    /// An optional script to run before every action is processed on the worker.
986
    /// The value should be the full path to the script to execute and will pause
987
    /// all actions on the worker if it returns an exit code other than 0.
988
    /// If not set, then the worker will never pause and will continue to accept
989
    /// jobs according to the scheduler configuration.
990
    /// This is useful, for example, if the worker should not take any more
991
    /// actions until there is enough resource available on the machine to
992
    /// handle them.
993
    pub experimental_precondition_script: Option<String>,
994
995
    /// Underlying CAS store that the worker will use to download CAS artifacts.
996
    /// This store must be a `FastSlowStore`. The `fast` store must be a
997
    /// `FileSystemStore` because it will use hardlinks when building out the files
998
    /// instead of copying the files. The slow store must eventually resolve to the
999
    /// same store the scheduler/client uses to send job requests.
1000
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
1001
    pub cas_fast_slow_store: StoreRefName,
1002
1003
    /// Configuration for uploading action results.
1004
    #[serde(default)]
1005
    pub upload_action_result: UploadActionResultConfig,
1006
1007
    /// The directory work jobs will be executed from. This directory will be fully
1008
    /// managed by the worker service and will be purged on startup.
1009
    /// This directory and the directory referenced in `local_filesystem_store_ref`'s
1010
    /// `stores::FilesystemStore::content_path` must be on the same filesystem.
1011
    /// Hardlinks will be used when placing files that are accessible to the jobs
1012
    /// that are sourced from `local_filesystem_store_ref`'s `content_path`.
1013
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
1014
    pub work_directory: String,
1015
1016
    /// Properties of this worker. This configuration will be sent to the scheduler
1017
    /// and used to tell the scheduler to restrict what should be executed on this
1018
    /// worker.
1019
    pub platform_properties: HashMap<String, WorkerProperty>,
1020
1021
    /// An optional mapping of environment names to set for the execution
1022
    /// as well as those specified in the action itself. If set, will set each
1023
    /// key as an environment variable before executing the job with the value
1024
    /// of the environment variable being the value of the property of the
1025
    /// action being executed of that name or the fixed value.
1026
    pub additional_environment: Option<HashMap<String, EnvironmentSource>>,
1027
1028
    /// Optional directory cache configuration for improving performance by caching
1029
    /// reconstructed input directories and using hardlinks instead of rebuilding
1030
    /// them from CAS for every action.
1031
    /// Default: None (directory cache disabled)
1032
    pub directory_cache: Option<DirectoryCacheConfig>,
1033
1034
    /// Optional and experimental: lease every digest in an active action's
1035
    /// input Merkle closure in the worker's locally eviction-managed CAS
1036
    /// tiers (the `cas_fast_slow_store`'s filesystem-backed stores) until the
1037
    /// action has finished cleanup. This prevents `Lost inputs no longer
1038
    /// available remotely` failures caused by local fast-tier eviction while
1039
    /// inputs are being materialized under cache pressure.
1040
    ///
1041
    /// Trade-off: while leases are held, a local filesystem tier may
1042
    /// temporarily exceed its configured `max_bytes` / `max_count` eviction
1043
    /// limits; normal eviction resumes once the action's leases are released.
1044
    /// Operators should leave headroom on the underlying disk when enabling
1045
    /// this.
1046
    ///
1047
    /// Default: false (eviction behavior is unchanged)
1048
    #[serde(default)]
1049
    pub experimental_active_input_leases: bool,
1050
1051
    /// Whether to use namespaces to isolate the execution. This is only available
1052
    /// on Linux. It is highly recommended as it avoids a number of issues with
1053
    /// zombie processes and also provides additional hermeticity. If explicitly set
1054
    /// to true and it is not supported the worker will exit with an error.
1055
    ///
1056
    /// Note: this will fail for non-privileged Dockerised workers, as workers in
1057
    /// Docker don't have permissions to make a new user namespace. Privileged
1058
    /// containers can do this.
1059
    ///
1060
    /// Default: False.
1061
    pub use_namespaces: Option<bool>,
1062
1063
    /// Whether to use a mount namespace to isolate the worker root. This is only
1064
    /// available on Linux and when `use_namespaces` is true. It is highly recommended
1065
    /// provides additional hermeticity. If explicitly set to true and it is not
1066
    /// supported or `use_namespaces` is not set to true the worker will exit with an
1067
    /// error.
1068
    /// Default: False.
1069
    pub use_mount_namespace: Option<bool>,
1070
}
1071
1072
#[derive(Deserialize, Serialize, Debug, Clone)]
1073
#[serde(deny_unknown_fields)]
1074
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1075
pub struct DirectoryCacheConfig {
1076
    /// Maximum number of cached directories.
1077
    /// Default: 1000
1078
    #[serde(default = "default_directory_cache_max_entries")]
1079
    pub max_entries: usize,
1080
1081
    /// Maximum total size in bytes for all cached directories (0 = unlimited).
1082
    /// Default: 10737418240 (10 GB)
1083
    #[serde(
1084
        default = "default_directory_cache_max_size_bytes",
1085
        deserialize_with = "convert_data_size_with_shellexpand"
1086
    )]
1087
    pub max_size_bytes: u64,
1088
1089
    /// Base directory for cache storage. This directory will be managed by
1090
    /// the worker and should be on the same filesystem as `work_directory`.
1091
    /// Default: `{work_directory}/../directory_cache`
1092
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1093
    pub cache_root: String,
1094
1095
    /// Optional and experimental: additionally cache every subdirectory by
1096
    /// its own `Directory` digest, not just the root directory. REAPI Merkle
1097
    /// nodes are content-addressed, so a subtree that is byte-identical
1098
    /// between two different roots has the same digest and can be
1099
    /// materialized with a single hardlink pass instead of being rebuilt
1100
    /// from the CAS. This makes the common "one input file changed out of
1101
    /// thousands" case reuse every unchanged subtree.
1102
    ///
1103
    /// Note: subtree caching multiplies the entry COUNT in the cache (every
1104
    /// distinct subdirectory becomes its own entry, subject to the normal
1105
    /// eviction budgets), so operators enabling this should raise
1106
    /// `max_entries` accordingly (for example 10x). To avoid multiplying the
1107
    /// SIZE accounting too, an entry's recorded size covers only bytes not
1108
    /// owned by a descendant entry, so the cache's size total approximates
1109
    /// unique materialized bytes rather than counting each file once per
1110
    /// ancestor level.
1111
    ///
1112
    /// Trade-off: cold constructions pay roughly one extra hardlink pass per
1113
    /// tree level (measured ~10% cold overhead at depth 3, within run
1114
    /// variance) in exchange for the churn-path reuse above.
1115
    ///
1116
    /// Default: false (only root directories are cached; existing behavior)
1117
    #[serde(default)]
1118
    pub experimental_subtree_caching: bool,
1119
1120
    /// Maximum number of concurrent slow-store fetches across ALL directory
1121
    /// constructions of this cache. This bound protects backing stores from
1122
    /// RPC storms: per-level construction concurrency compounds
1123
    /// multiplicatively across tree levels and concurrent actions.
1124
    ///
1125
    /// Interaction with read coalescing: this semaphore fragments coalesced
1126
    /// batches to at most this many items and serializes fetch waves for
1127
    /// trees with many tiny files. Deployments using
1128
    /// `experimental_read_batching` on the underlying grpc store can raise
1129
    /// this substantially (for example 512), since batching collapses the
1130
    /// RPC count. Must be greater than 0.
1131
    ///
1132
    /// Default: 64
1133
    #[serde(default = "default_directory_cache_max_concurrent_fetches")]
1134
    pub max_concurrent_fetches: usize,
1135
    /// On a directory-cache miss, prefetch every `Directory` proto of the
1136
    /// tree with one logical `GetTree` traversal instead of fetching protos
1137
    /// level by level (which costs one round trip per tree DEPTH). The
1138
    /// [REAPI request contract] permits servers to impose their own limit even
1139
    /// when no page size is specified, and the [REAPI response contract]
1140
    /// requires clients to continue with the returned page token. A paginated
1141
    /// traversal may therefore use multiple RPCs. Only takes effect when the
1142
    /// slow tier is a `grpc` store;
1143
    /// any prefetch failure falls back to the per-level path. Worthwhile when
1144
    /// worker-to-CAS latency is non-trivial and trees are deep; measured 5-34x
1145
    /// on the proto phase at 5-25ms RTT. Note the serving CAS pays the tree walk
1146
    /// against its own backend, so its directory protos should be served
1147
    /// from a fast tier.
1148
    ///
1149
    /// [REAPI request contract]: https://github.com/bazelbuild/remote-apis/blob/becdd8f9ff811df88a22d3eadd6341753d51d167/build/bazel/remote/execution/v2/remote_execution.proto#L1932-L1942
1150
    /// [REAPI response contract]: https://github.com/bazelbuild/remote-apis/blob/becdd8f9ff811df88a22d3eadd6341753d51d167/build/bazel/remote/execution/v2/remote_execution.proto#L1961-L1965
1151
    /// Default: false
1152
    #[serde(default)]
1153
    pub experimental_get_tree_prefetch: bool,
1154
}
1155
1156
0
const fn default_directory_cache_max_entries() -> usize {
1157
0
    1000
1158
0
}
1159
1160
0
const fn default_directory_cache_max_concurrent_fetches() -> usize {
1161
0
    64
1162
0
}
1163
1164
0
const fn default_directory_cache_max_size_bytes() -> u64 {
1165
0
    10 * 1024 * 1024 * 1024 // 10 GB
1166
0
}
1167
1168
#[derive(Deserialize, Serialize, Debug)]
1169
#[serde(rename_all = "snake_case")]
1170
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1171
pub enum WorkerConfig {
1172
    /// A worker type that executes jobs locally on this machine.
1173
    Local(LocalWorkerConfig),
1174
}
1175
1176
#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
1177
#[serde(deny_unknown_fields)]
1178
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1179
pub struct GlobalConfig {
1180
    /// Maximum number of open files that can be opened at one time.
1181
    /// This value is not strictly enforced, it is a best effort. Some internal libraries
1182
    /// open files or read metadata from a files which do not obey this limit, however
1183
    /// the vast majority of cases will have this limit be honored.
1184
    /// This value must be larger than `ulimit -n` to have any effect.
1185
    /// Any network open file descriptors is not counted in this limit, but is counted
1186
    /// in the kernel limit. It is a good idea to set a very large `ulimit -n`.
1187
    /// Note: This value must be greater than 10.
1188
    ///
1189
    /// Default: 24576 (= 24 * 1024)
1190
    #[serde(deserialize_with = "convert_numeric_with_shellexpand")]
1191
    pub max_open_files: usize,
1192
1193
    /// Default hash function to use while uploading blobs to the CAS when not set
1194
    /// by client.
1195
    ///
1196
    /// Default: `ConfigDigestHashFunction::sha256`
1197
    pub default_digest_hash_function: Option<ConfigDigestHashFunction>,
1198
1199
    /// Default digest size to use for health check when running
1200
    /// diagnostics checks. Health checks are expected to use this
1201
    /// size for filling a buffer that is used for creation of
1202
    /// digest.
1203
    ///
1204
    /// Default: 1024*1024 (1MiB)
1205
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1206
    pub default_digest_size_health_check: usize,
1207
}
1208
1209
pub type StoreConfig = NamedConfig<StoreSpec>;
1210
pub type SchedulerConfig = NamedConfig<SchedulerSpec>;
1211
1212
#[derive(Deserialize, Serialize, Debug)]
1213
#[serde(deny_unknown_fields)]
1214
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1215
pub struct CasConfig {
1216
    /// List of stores available to use in this config.
1217
    /// The keys can be used in other configs when needing to reference a store.
1218
    pub stores: Vec<StoreConfig>,
1219
1220
    /// Worker configurations used to execute jobs.
1221
    pub workers: Option<Vec<WorkerConfig>>,
1222
1223
    /// List of schedulers available to use in this config.
1224
    /// The keys can be used in other configs when needing to reference a
1225
    /// scheduler.
1226
    pub schedulers: Option<Vec<SchedulerConfig>>,
1227
1228
    /// Servers to setup for this process.
1229
    pub servers: Vec<ServerConfig>,
1230
1231
    /// Experimental - Origin events configuration. This is the service that will
1232
    /// collect and publish nativelink events to a store for processing by an
1233
    /// external service.
1234
    pub experimental_origin_events: Option<OriginEventsSpec>,
1235
1236
    /// Any global configurations that apply to all modules live here.
1237
    pub global: Option<GlobalConfig>,
1238
}
1239
1240
impl CasConfig {
1241
    const ZSTD_COMPRESSION_DOCS_URL: &'static str =
1242
        "https://docs.nativelink.com/configuration/compression";
1243
1244
    /// # Errors
1245
    ///
1246
    /// Will return `Err` if we can't load the file.
1247
20
    pub fn try_from_json5_file(config_file: &str) -> Result<Self, Error> {
1248
20
        let json_contents = std::fs::read_to_string(config_file)
1249
20
            .err_tip(|| 
format!0
("Could not open config file {config_file}"))
?0
;
1250
20
        Self::try_from_json5_str(&json_contents)
1251
20
    }
1252
1253
24
    fn try_from_json5_str(json_contents: &str) -> Result<Self, Error> {
1254
24
        let mut config: Self = serde_json5::from_str(json_contents)
?0
;
1255
25
        for server in 
&config.servers24
{
1256
25
            if let Some(services) = &server.services {
1257
25
                Self::check_store_conflict(services)
?1
;
1258
0
            }
1259
        }
1260
23
        config.apply_zstd_grpc_store_defaults();
1261
23
        Ok(config)
1262
24
    }
1263
1264
23
    fn zstd_wire_compression_enabled_anywhere(&self) -> bool {
1265
23
        if self
1266
23
            .servers
1267
23
            .iter()
1268
24
            .
filter_map23
(|server| server.services.as_ref())
1269
24
            .
filter_map23
(|services| services.capabilities.as_deref())
1270
23
            .flatten()
1271
23
            .any(|capabilities| 
capabilities19
.remote_cache_compression)
1272
        {
1273
1
            return true;
1274
22
        }
1275
1276
67
        
self.stores.iter()22
.
any22
(|store| {
1277
67
            let mut enabled = false;
1278
67
            store.spec.visit_grpc_specs(&mut |grpc| 
{5
1279
5
                enabled |= 
matches!2
(grpc.store_type, StoreType::Cas)
1280
3
                    && grpc.experimental_remote_cache_compression == Some(true);
1281
5
            });
1282
67
            enabled
1283
67
        })
1284
23
    }
1285
1286
23
    fn apply_zstd_grpc_store_defaults(&mut self) {
1287
23
        let zstd_enabled_anywhere = self.zstd_wire_compression_enabled_anywhere();
1288
1289
69
        for store in 
&mut self.stores23
{
1290
69
            let store_name = store.name.as_str();
1291
69
            store.spec.visit_grpc_specs_mut(&mut |grpc| 
{9
1292
9
                if !
matches!3
(grpc.store_type, StoreType::Cas) {
1293
3
                    if grpc.experimental_remote_cache_compression == Some(true) {
1294
1
                        warn!(
1295
                            store = store_name,
1296
                            instance_name = grpc.instance_name,
1297
                            "'experimental_remote_cache_compression' is enabled on a non-CAS \
1298
                             gRPC store, where it has no effect: REAPI zstd wire compression \
1299
                             only applies to CAS blob transfers. Enable it on the CAS gRPC \
1300
                             stores or a capabilities instance instead. See {}",
1301
                            Self::ZSTD_COMPRESSION_DOCS_URL,
1302
                        );
1303
2
                    }
1304
3
                    return;
1305
6
                }
1306
1307
6
                if !zstd_enabled_anywhere {
1308
2
                    return;
1309
4
                }
1310
1311
4
                match grpc.experimental_remote_cache_compression {
1312
2
                    None => grpc.experimental_remote_cache_compression = Some(true),
1313
1
                    Some(false) => warn!(
1314
                        store = store_name,
1315
                        instance_name = grpc.instance_name,
1316
                        "Zstd wire compression is enabled elsewhere, but this eligible CAS gRPC \
1317
                         store explicitly disables it. Unless this upstream cannot use zstd, \
1318
                         enable 'experimental_remote_cache_compression' for substantially faster \
1319
                         transfers of compressible artifacts. See {}",
1320
                        Self::ZSTD_COMPRESSION_DOCS_URL,
1321
                    ),
1322
1
                    Some(true) => {}
1323
                }
1324
9
            });
1325
        }
1326
23
    }
1327
1328
25
    fn check_store_conflict(services: &ServicesConfig) -> Result<(), Error> {
1329
25
        if let Some(
cas_config18
) = &services.cas
1330
18
            && let Some(ac_config) = &services.ac
1331
        {
1332
            // Create a hashmap from the CAS configuration for quick lookup
1333
18
            let cas_store_map: HashMap<_, _> = cas_config
1334
18
                .iter()
1335
20
                .
map18
(|with_instance_name| {
1336
20
                    (
1337
20
                        &with_instance_name.instance_name,
1338
20
                        &with_instance_name.cas_store,
1339
20
                    )
1340
20
                })
1341
18
                .collect();
1342
1343
20
            for with_instance_name in 
ac_config18
{
1344
20
                if let Some(cas_store) = cas_store_map.get(&with_instance_name.instance_name)
1345
20
                    && cas_store == &&with_instance_name.ac_store
1346
                {
1347
1
                    return Err(make_err!(
1348
1
                        Code::InvalidArgument,
1349
1
                        "CAS and AC use the same store '{}' in the config",
1350
1
                        cas_store
1351
1
                    ));
1352
19
                }
1353
            }
1354
7
        }
1355
24
        Ok(())
1356
25
    }
1357
}
1358
1359
#[cfg(test)]
1360
mod tests {
1361
    use tracing_test::traced_test;
1362
1363
    use super::*;
1364
1365
4
    fn grpc_compression_configs(config: &CasConfig) -> Vec<(bool, Option<bool>)> {
1366
4
        let mut configs = Vec::new();
1367
6
        for store in 
&config.stores4
{
1368
8
            
store.spec6
.
visit_grpc_specs6
(&mut |grpc| {
1369
8
                configs.push((
1370
8
                    
matches!2
(grpc.store_type, StoreType::Cas),
1371
8
                    grpc.experimental_remote_cache_compression,
1372
                ));
1373
8
            });
1374
        }
1375
4
        configs
1376
4
    }
1377
1378
    #[test]
1379
1
    fn capabilities_config_remote_cache_compression_deserializes_true() {
1380
1
        let config: CapabilitiesConfig =
1381
1
            serde_json5::from_str(r#"{"remote_cache_compression": true}"#).unwrap();
1382
1383
1
        assert!(config.remote_cache_compression);
1384
1
    }
1385
1386
    #[test]
1387
1
    fn capabilities_config_remote_cache_compression_defaults_false() {
1388
1
        let config: CapabilitiesConfig = serde_json5::from_str("{}").unwrap();
1389
1390
1
        assert!(!config.remote_cache_compression);
1391
1
    }
1392
1393
    #[test]
1394
1
    fn omitted_grpc_compression_stays_disabled_without_zstd_intent() {
1395
1
        let config = CasConfig::try_from_json5_str(
1396
1
            r#"{
1397
1
                stores: [{
1398
1
                    name: "upstream",
1399
1
                    grpc: {
1400
1
                        endpoints: [{ address: "http://localhost:1234" }],
1401
1
                        store_type: "cas",
1402
1
                    },
1403
1
                }],
1404
1
                servers: [],
1405
1
            }"#,
1406
        )
1407
1
        .unwrap();
1408
1409
1
        assert_eq!(grpc_compression_configs(&config), vec![(true, None)]);
1410
1
    }
1411
1412
    #[test]
1413
1
    fn explicit_grpc_zstd_intent_enables_other_eligible_grpc_stores() {
1414
1
        let config = CasConfig::try_from_json5_str(
1415
1
            r#"{
1416
1
                stores: [
1417
1
                    {
1418
1
                        name: "enabled",
1419
1
                        grpc: {
1420
1
                            endpoints: [{ address: "http://localhost:1234" }],
1421
1
                            store_type: "cas",
1422
1
                            experimental_remote_cache_compression: true,
1423
1
                        },
1424
1
                    },
1425
1
                    {
1426
1
                        name: "inherited",
1427
1
                        grpc: {
1428
1
                            endpoints: [{ address: "http://localhost:5678" }],
1429
1
                            store_type: "cas",
1430
1
                        },
1431
1
                    },
1432
1
                ],
1433
1
                servers: [],
1434
1
            }"#,
1435
        )
1436
1
        .unwrap();
1437
1438
1
        assert_eq!(
1439
1
            grpc_compression_configs(&config),
1440
1
            vec![(true, Some(true)), (true, Some(true))]
1441
        );
1442
1
    }
1443
1444
    #[test]
1445
    #[traced_test]
1446
1
    fn ac_grpc_compression_warns_and_expresses_no_cas_intent() {
1447
1
        let config = CasConfig::try_from_json5_str(
1448
1
            r#"{
1449
1
                stores: [
1450
1
                    {
1451
1
                        name: "action-cache",
1452
1
                        grpc: {
1453
1
                            instance_name: "ac",
1454
1
                            endpoints: [{ address: "http://localhost:1234" }],
1455
1
                            store_type: "ac",
1456
1
                            experimental_remote_cache_compression: true,
1457
1
                        },
1458
1
                    },
1459
1
                    {
1460
1
                        name: "cas",
1461
1
                        grpc: {
1462
1
                            endpoints: [{ address: "http://localhost:5678" }],
1463
1
                            store_type: "cas",
1464
1
                        },
1465
1
                    },
1466
1
                ],
1467
1
                servers: [],
1468
1
            }"#,
1469
        )
1470
1
        .unwrap();
1471
1472
        // The AC-store setting is inert: it neither compresses AC RPCs nor
1473
        // expresses process-wide zstd intent, so the CAS store stays unset.
1474
1
        assert_eq!(
1475
1
            grpc_compression_configs(&config),
1476
1
            vec![(false, Some(true)), (true, None)]
1477
        );
1478
1
        assert!(logs_contain("store=\"action-cache\""));
1479
1
        assert!(logs_contain("no effect"));
1480
1
    }
1481
1482
    #[test]
1483
    #[traced_test]
1484
1
    fn capabilities_zstd_intent_defaults_nested_cas_grpc_and_warns_on_opt_out() {
1485
1
        let config = CasConfig::try_from_json5_str(
1486
1
            r#"{
1487
1
                stores: [{
1488
1
                    name: "nested-upstreams",
1489
1
                    fast_slow: {
1490
1
                        fast: {
1491
1
                            grpc: {
1492
1
                                instance_name: "auto",
1493
1
                                endpoints: [{ address: "http://localhost:1234" }],
1494
1
                                store_type: "cas",
1495
1
                            },
1496
1
                        },
1497
1
                        slow: {
1498
1
                            shard: {
1499
1
                                stores: [
1500
1
                                    {
1501
1
                                        store: {
1502
1
                                            grpc: {
1503
1
                                                instance_name: "opt-out",
1504
1
                                                endpoints: [{
1505
1
                                                    address: "http://localhost:5678",
1506
1
                                                }],
1507
1
                                                store_type: "cas",
1508
1
                                                experimental_remote_cache_compression: false,
1509
1
                                            },
1510
1
                                        },
1511
1
                                        weight: 1,
1512
1
                                    },
1513
1
                                    {
1514
1
                                        store: {
1515
1
                                            grpc: {
1516
1
                                                instance_name: "action-cache",
1517
1
                                                endpoints: [{
1518
1
                                                    address: "http://localhost:9012",
1519
1
                                                }],
1520
1
                                                store_type: "ac",
1521
1
                                            },
1522
1
                                        },
1523
1
                                        weight: 1,
1524
1
                                    },
1525
1
                                ],
1526
1
                            },
1527
1
                        },
1528
1
                    },
1529
1
                }],
1530
1
                servers: [{
1531
1
                    listener: {
1532
1
                        http: { socket_address: "127.0.0.1:50051" },
1533
1
                    },
1534
1
                    services: {
1535
1
                        capabilities: [{
1536
1
                            instance_name: "main",
1537
1
                            remote_cache_compression: true,
1538
1
                        }],
1539
1
                    },
1540
1
                }],
1541
1
            }"#,
1542
        )
1543
1
        .unwrap();
1544
1545
1
        assert_eq!(
1546
1
            grpc_compression_configs(&config),
1547
1
            vec![(true, Some(true)), (true, Some(false)), (false, None),]
1548
        );
1549
1
        assert!(logs_contain("store=\"nested-upstreams\""));
1550
1
        assert!(logs_contain("instance_name=\"opt-out\""));
1551
1
        assert!(logs_contain(CasConfig::ZSTD_COMPRESSION_DOCS_URL));
1552
1
    }
1553
}