Coverage Report

Created: 2026-10-16 16:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-config/src/stores.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 core::time::Duration;
16
use std::collections::HashMap;
17
use std::sync::Arc;
18
19
use rand::Rng;
20
#[cfg(feature = "dev-schema")]
21
use schemars::JsonSchema;
22
use serde::{Deserialize, Serialize};
23
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_data_size_with_shellexpand, convert_optional_numeric_with_shellexpand,
28
    convert_optional_string_with_shellexpand, convert_string_with_shellexpand,
29
    convert_vec_string_with_shellexpand,
30
};
31
32
/// Name of the store. This type will be used when referencing a store
33
/// in the `CasConfig::stores`'s map key.
34
pub type StoreRefName = String;
35
36
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
37
#[serde(rename_all = "snake_case")]
38
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
39
pub enum ConfigDigestHashFunction {
40
    /// Use the sha256 hash function.
41
    /// <https://en.wikipedia.org/wiki/SHA-2>
42
    Sha256,
43
44
    /// Use the blake3 hash function.
45
    /// <https://en.wikipedia.org/wiki/BLAKE_(hash_function)>
46
    Blake3,
47
}
48
49
#[derive(Serialize, Deserialize, Debug, Clone)]
50
#[serde(rename_all = "snake_case")]
51
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
52
pub enum StoreSpec {
53
    /// Cache metrics store wraps another store and emits low-cardinality
54
    /// OpenTelemetry cache operation metrics for the wrapped store.
55
    ///
56
    /// This wrapper is opt-in. Stores that are not explicitly wrapped by
57
    /// `cache_metrics` are constructed exactly as they are without this
58
    /// wrapper and do not pay its hot-path timing or recording cost.
59
    ///
60
    /// **Example JSON Config:**
61
    /// ```json
62
    /// "cache_metrics": {
63
    ///   "cache_type": "cas",
64
    ///   "backend": {
65
    ///     "filesystem": {
66
    ///       "content_path": "~/.cache/nativelink/content_path-cas",
67
    ///       "temp_path": "~/.cache/nativelink/tmp_path-cas"
68
    ///     }
69
    ///   }
70
    /// }
71
    /// ```
72
    ///
73
    CacheMetrics(Box<CacheMetricsSpec>),
74
75
    /// Memory store will store all data in a hashmap in memory.
76
    ///
77
    /// **Example JSON Config:**
78
    /// ```json
79
    /// "memory": {
80
    ///   "eviction_policy": {
81
    ///     "max_bytes": "10mb",
82
    ///   }
83
    /// }
84
    /// ```
85
    ///
86
    Memory(MemorySpec),
87
88
    /// A generic blob store that will store files on the cloud
89
    /// provider. This configuration will never delete files, so you are
90
    /// responsible for purging old files in other ways.
91
    /// It supports the following backends:
92
    ///
93
    /// 1. **Amazon S3:**
94
    ///    S3 store will use Amazon's S3 service as a backend to store
95
    ///    the files. This configuration can be used to share files
96
    ///    across multiple instances. Uses system certificates for TLS
97
    ///    verification via `rustls-platform-verifier`.
98
    ///
99
    ///   **Example JSON Config:**
100
    ///   ```json
101
    ///   "experimental_cloud_object_store": {
102
    ///     "provider": "aws",
103
    ///     "region": "eu-north-1",
104
    ///     "bucket": "crossplane-bucket-af79aeca9",
105
    ///     "key_prefix": "test-prefix-index/",
106
    ///     "retry": {
107
    ///       "max_retries": 6,
108
    ///       "delay": 0.3,
109
    ///       "jitter": 0.5
110
    ///     },
111
    ///     "multipart_max_concurrent_uploads": 10
112
    ///   }
113
    ///   ```
114
    ///
115
    /// 2. **Google Cloud Storage:**
116
    ///    GCS store uses Google's GCS service as a backend to store
117
    ///    the files. This configuration can be used to share files
118
    ///    across multiple instances.
119
    ///
120
    ///   **Example JSON Config:**
121
    ///   ```json
122
    ///   "experimental_cloud_object_store": {
123
    ///     "provider": "gcs",
124
    ///     "bucket": "test-bucket",
125
    ///     "key_prefix": "test-prefix-index/",
126
    ///     "retry": {
127
    ///       "max_retries": 6,
128
    ///       "delay": 0.3,
129
    ///       "jitter": 0.5
130
    ///     },
131
    ///     "multipart_max_concurrent_uploads": 10
132
    ///   }
133
    ///   ```
134
    ///
135
    /// 3. **Azure Blob Store:**
136
    ///    Azure Blob store will use Microsoft's Azure Blob service as a
137
    ///    backend to store the files. This configuration can be used to
138
    ///    share files across multiple instances.
139
    ///
140
    ///   **Example JSON Config:**
141
    ///   ```json
142
    ///   "experimental_cloud_object_store": {
143
    ///     "provider": "azure",
144
    ///     "account_name": "cloudshell1393657559",
145
    ///     "container": "simple-test-container",
146
    ///     "key_prefix": "folder/",
147
    ///     "retry": {
148
    ///         "max_retries": 6,
149
    ///         "delay": 0.3,
150
    ///         "jitter": 0.5
151
    ///     },
152
    ///     "multipart_max_concurrent_uploads": 10
153
    ///   }
154
    ///   ```
155
    ///
156
    /// 4. **NetApp ONTAP S3:**
157
    ///    NetApp ONTAP S3 store will use ONTAP's S3-compatible storage as a backend
158
    ///    to store files. This store is specifically configured for ONTAP's S3 requirements
159
    ///    including custom TLS configuration, credentials management, and proper vserver
160
    ///    configuration.
161
    ///
162
    ///    This store uses AWS environment variables for credentials:
163
    ///    - `AWS_ACCESS_KEY_ID`
164
    ///    - `AWS_SECRET_ACCESS_KEY`
165
    ///    - `AWS_DEFAULT_REGION`
166
    ///
167
    ///    **Example JSON Config:**
168
    ///    ```json
169
    ///    "experimental_cloud_object_store": {
170
    ///      "provider": "ontap",
171
    ///      "endpoint": "https://ontap-s3-endpoint:443",
172
    ///      "vserver_name": "your-vserver",
173
    ///      "bucket": "your-bucket",
174
    ///      "root_certificates": "/path/to/certs.pem",  // Optional
175
    ///      "key_prefix": "test-prefix/",               // Optional
176
    ///      "retry": {
177
    ///        "max_retries": 6,
178
    ///        "delay": 0.3,
179
    ///        "jitter": 0.5
180
    ///      },
181
    ///      "multipart_max_concurrent_uploads": 10
182
    ///    }
183
    ///    ```
184
    ///
185
    /// 5. **Cloudflare R2:**
186
    ///    R2 store uses Cloudflare's R2 service as a backend. R2 speaks the
187
    ///    S3 API, so this is a thin wrapper that derives the account-scoped
188
    ///    endpoint (`https://{account_id}.r2.cloudflarestorage.com`) for you.
189
    ///
190
    ///    **Example JSON Config:**
191
    ///    ```json
192
    ///    "experimental_cloud_object_store": {
193
    ///      "provider": "r2",
194
    ///      "account_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
195
    ///      "bucket": "nativelink-cas",
196
    ///      "key_prefix": "test-prefix/",
197
    ///      "retry": {
198
    ///        "max_retries": 6,
199
    ///        "delay": 0.3,
200
    ///        "jitter": 0.5
201
    ///      },
202
    ///      "multipart_max_concurrent_uploads": 10
203
    ///    }
204
    ///    ```
205
    ///
206
    /// 6. **Oracle Cloud Infrastructure (OCI) Object Storage:**
207
    ///    OCI store uses Oracle Cloud Infrastructure's S3-compatible Object
208
    ///    Storage API. The path-style endpoint is derived from your Object
209
    ///    Storage `namespace` and `region` as
210
    ///    `https://{namespace}.compat.objectstorage.{region}.oci.customer-oci.com`.
211
    ///    Authenticate with a Customer Secret Key (Access Key/Secret Key pair
212
    ///    created under User Settings -> Customer secret keys in the OCI
213
    ///    console); the secret cannot be retrieved after generation, so read
214
    ///    it from an env var via shellexpand.
215
    ///
216
    ///    **Example JSON Config:**
217
    ///    ```json
218
    ///    "experimental_cloud_object_store": {
219
    ///      "provider": "oci",
220
    ///      "namespace": "your-object-storage-namespace",
221
    ///      "region": "us-phoenix-1",
222
    ///      "bucket": "nativelink-cas",
223
    ///      "access_key_id": "oci_access_key_id",
224
    ///      "secret_access_key": "oci_secret_access_key",
225
    ///      "key_prefix": "test-prefix/",
226
    ///      "retry": {
227
    ///        "max_retries": 6,
228
    ///        "delay": 0.3,
229
    ///        "jitter": 0.5
230
    ///      }
231
    ///    }
232
    ///    ```
233
    ExperimentalCloudObjectStore(ExperimentalCloudObjectSpec),
234
235
    /// ONTAP S3 Existence Cache provides a caching layer on top of the ONTAP S3 store
236
    /// to optimize repeated existence checks. It maintains an in-memory cache of object
237
    /// digests and periodically syncs this cache to disk for persistence.
238
    ///
239
    /// The cache helps reduce latency for repeated calls to check object existence,
240
    /// while still ensuring eventual consistency with the underlying ONTAP S3 store.
241
    ///
242
    /// Example JSON Config:
243
    /// ```json
244
    /// "ontap_s3_existence_cache": {
245
    ///   "index_path": "/path/to/cache/index.json",
246
    ///   "sync_interval_seconds": 300,
247
    ///   "backend": {
248
    ///     "endpoint": "https://ontap-s3-endpoint:443",
249
    ///     "vserver_name": "your-vserver",
250
    ///     "bucket": "your-bucket",
251
    ///     "key_prefix": "test-prefix/"
252
    ///   }
253
    /// }
254
    /// ```
255
    ///
256
    OntapS3ExistenceCache(Box<OntapS3ExistenceCacheSpec>),
257
258
    /// Verify store is used to apply verifications to an underlying
259
    /// store implementation. It is strongly encouraged to validate
260
    /// as much data as you can before accepting data from a client,
261
    /// failing to do so may cause the data in the store to be
262
    /// populated with invalid data causing all kinds of problems.
263
    ///
264
    /// The suggested configuration is to have the CAS validate the
265
    /// hash and size and the AC validate nothing.
266
    ///
267
    /// **Example JSON Config:**
268
    /// ```json
269
    /// "verify": {
270
    ///   "backend": {
271
    ///     "memory": {
272
    ///       "eviction_policy": {
273
    ///         "max_bytes": "500mb"
274
    ///       }
275
    ///     },
276
    ///   },
277
    ///   "verify_size": true,
278
    ///   "verify_hash": true
279
    /// }
280
    /// ```
281
    ///
282
    Verify(Box<VerifySpec>),
283
284
    /// Completeness checking store verifies if the
285
    /// output files & folders exist in the CAS before forwarding
286
    /// the request to the underlying store.
287
    /// Note: This store should only be used on AC stores.
288
    ///
289
    /// **Example JSON Config:**
290
    /// ```json
291
    /// "completeness_checking": {
292
    ///   "backend": {
293
    ///     "filesystem": {
294
    ///       "content_path": "~/.cache/nativelink/content_path-ac",
295
    ///       "temp_path": "~/.cache/nativelink/tmp_path-ac",
296
    ///       "eviction_policy": {
297
    ///         "max_bytes": "500mb",
298
    ///       }
299
    ///     }
300
    ///   },
301
    ///   "cas_store": {
302
    ///     "ref_store": {
303
    ///       "name": "CAS_MAIN_STORE"
304
    ///     }
305
    ///   }
306
    /// }
307
    /// ```
308
    ///
309
    CompletenessChecking(Box<CompletenessCheckingSpec>),
310
311
    /// A compression store that will compress the data inbound and
312
    /// outbound. There will be a non-trivial cost to compress and
313
    /// decompress the data, but in many cases if the final store is
314
    /// a store that requires network transport and/or storage space
315
    /// is a concern it is often faster and more efficient to use this
316
    /// store before those stores.
317
    ///
318
    /// **Example JSON Config:**
319
    /// ```json
320
    /// "compression": {
321
    ///   "compression_algorithm": {
322
    ///     "lz4": {}
323
    ///   },
324
    ///   "backend": {
325
    ///     "filesystem": {
326
    ///       "content_path": "/tmp/nativelink/data/content_path-cas",
327
    ///       "temp_path": "/tmp/nativelink/data/tmp_path-cas",
328
    ///       "eviction_policy": {
329
    ///         "max_bytes": "2gb",
330
    ///       }
331
    ///     }
332
    ///   }
333
    /// }
334
    /// ```
335
    ///
336
    Compression(Box<CompressionSpec>),
337
338
    /// A dedup store will take the inputs and run a rolling hash
339
    /// algorithm on them to slice the input into smaller parts then
340
    /// run a sha256 algorithm on the slice and if the object doesn't
341
    /// already exist, upload the slice to the `content_store` using
342
    /// a new digest of just the slice. Once all parts exist, an
343
    /// Action-Cache-like digest will be built and uploaded to the
344
    /// `index_store` which will contain a reference to each
345
    /// chunk/digest of the uploaded file. Downloading a request will
346
    /// first grab the index from the `index_store`, and forward the
347
    /// download content of each chunk as if it were one file.
348
    ///
349
    /// This store is exceptionally good when the following conditions
350
    /// are met:
351
    /// * Content is mostly the same (inserts, updates, deletes are ok)
352
    /// * Content is not compressed or encrypted
353
    /// * Uploading or downloading from `content_store` is the bottleneck.
354
    ///
355
    /// Note: This store pairs well when used with `CompressionSpec` as
356
    /// the `content_store`, but never put `DedupSpec` as the backend of
357
    /// `CompressionSpec` as it will negate all the gains.
358
    ///
359
    /// Note: When running `.has()` on this store, it will only check
360
    /// to see if the entry exists in the `index_store` and not check
361
    /// if the individual chunks exist in the `content_store`.
362
    ///
363
    /// **Example JSON Config:**
364
    /// ```json
365
    /// "dedup": {
366
    ///   "index_store": {
367
    ///     "memory": {
368
    ///       "eviction_policy": {
369
    ///          "max_bytes": "1GB",
370
    ///       }
371
    ///     }
372
    ///   },
373
    ///   "content_store": {
374
    ///     "compression": {
375
    ///       "compression_algorithm": {
376
    ///         "lz4": {}
377
    ///       },
378
    ///       "backend": {
379
    ///         "fast_slow": {
380
    ///           "fast": {
381
    ///             "memory": {
382
    ///               "eviction_policy": {
383
    ///                 "max_bytes": "500MB",
384
    ///               }
385
    ///             }
386
    ///           },
387
    ///           "slow": {
388
    ///             "filesystem": {
389
    ///               "content_path": "/tmp/nativelink/data/content_path-content",
390
    ///               "temp_path": "/tmp/nativelink/data/tmp_path-content",
391
    ///               "eviction_policy": {
392
    ///                 "max_bytes": "2gb"
393
    ///               }
394
    ///             }
395
    ///           }
396
    ///         }
397
    ///       }
398
    ///     }
399
    ///   }
400
    /// }
401
    /// ```
402
    ///
403
    Dedup(Box<DedupSpec>),
404
405
    /// Existence store will wrap around another store and cache calls
406
    /// to has so that subsequent `has_with_results` calls will be
407
    /// faster. This is useful for cases when you have a store that
408
    /// is slow to respond to has calls.
409
    /// Note: This store should only be used on CAS stores.
410
    ///
411
    /// **Example JSON Config:**
412
    /// ```json
413
    /// "existence_cache": {
414
    ///   "backend": {
415
    ///     "memory": {
416
    ///       "eviction_policy": {
417
    ///         "max_bytes": "500mb",
418
    ///       }
419
    ///     }
420
    ///   },
421
    ///   // Note this is the existence store policy, not the backend policy
422
    ///   "eviction_policy": {
423
    ///     "max_seconds": 100,
424
    ///   }
425
    /// }
426
    /// ```
427
    ///
428
    ExistenceCache(Box<ExistenceCacheSpec>),
429
430
    /// `FastSlow` store will first try to fetch the data from the `fast`
431
    /// store and then if it does not exist try the `slow` store.
432
    /// When the object does exist in the `slow` store, it will copy
433
    /// the data to the `fast` store while returning the data.
434
    /// This store should be thought of as a store that "buffers"
435
    /// the data to the `fast` store.
436
    /// On uploads it will mirror data to both `fast` and `slow` stores.
437
    ///
438
    /// WARNING: If you need data to always exist in the `slow` store
439
    /// for something like remote execution, be careful because this
440
    /// store will never check to see if the objects exist in the
441
    /// `slow` store if it exists in the `fast` store (i.e. it assumes
442
    /// that if an object exists in the `fast` store it will exist in
443
    /// the `slow` store).
444
    ///
445
    /// ***Example JSON Config:***
446
    /// ```json
447
    /// "fast_slow": {
448
    ///   "fast": {
449
    ///     "filesystem": {
450
    ///       "content_path": "/tmp/nativelink/data/content_path-index",
451
    ///       "temp_path": "/tmp/nativelink/data/tmp_path-index",
452
    ///       "eviction_policy": {
453
    ///         "max_bytes": "500mb",
454
    ///       }
455
    ///     }
456
    ///   },
457
    ///   "slow": {
458
    ///     "filesystem": {
459
    ///       "content_path": "/tmp/nativelink/data/content_path-index",
460
    ///       "temp_path": "/tmp/nativelink/data/tmp_path-index",
461
    ///       "eviction_policy": {
462
    ///         "max_bytes": "500mb",
463
    ///       }
464
    ///     }
465
    ///   }
466
    /// }
467
    /// ```
468
    ///
469
    FastSlow(Box<FastSlowSpec>),
470
471
    /// Shards the data to multiple stores. This is useful for cases
472
    /// when you want to distribute the load across multiple stores.
473
    /// The digest hash is used to determine which store to send the
474
    /// data to.
475
    ///
476
    /// **Example JSON Config:**
477
    /// ```json
478
    /// "shard": {
479
    ///   "stores": [
480
    ///    {
481
    ///     "store": {
482
    ///       "memory": {
483
    ///         "eviction_policy": {
484
    ///             "max_bytes": "10mb"
485
    ///         },
486
    ///       },
487
    ///     },
488
    ///     "weight": 1
489
    ///   }]
490
    /// }
491
    /// ```
492
    ///
493
    Shard(ShardSpec),
494
495
    /// Stores the data on the filesystem. This store is designed for
496
    /// local persistent storage. Restarts of this program should restore
497
    /// the previous state, meaning anything uploaded will be persistent
498
    /// as long as the filesystem integrity holds.
499
    ///
500
    /// **Example JSON Config:**
501
    /// ```json
502
    /// "filesystem": {
503
    ///   "content_path": "/tmp/nativelink/data-worker-test/content_path-cas",
504
    ///   "temp_path": "/tmp/nativelink/data-worker-test/tmp_path-cas",
505
    ///   "eviction_policy": {
506
    ///     "max_bytes": "10gb",
507
    ///   }
508
    /// }
509
    /// ```
510
    ///
511
    Filesystem(FilesystemSpec),
512
513
    /// Store used to reference a store in the root store manager.
514
    /// This is useful for cases when you want to share a store in different
515
    /// nested stores. Example, you may want to share the same memory store
516
    /// used for the action cache, but use a `FastSlowSpec` and have the fast
517
    /// store also share the memory store for efficiency.
518
    ///
519
    /// **Example JSON Config:**
520
    /// ```json
521
    /// "ref_store": {
522
    ///   "name": "FS_CONTENT_STORE"
523
    /// }
524
    /// ```
525
    ///
526
    RefStore(RefSpec),
527
528
    /// Uses the size field of the digest to separate which store to send the
529
    /// data. This is useful for cases when you'd like to put small objects
530
    /// in one store and large objects in another store. This should only be
531
    /// used if the size field is the real size of the content, in other
532
    /// words, don't use on AC (Action Cache) stores. Any store where you can
533
    /// safely use `VerifySpec.verify_size = true`, this store should be safe
534
    /// to use (i.e. CAS stores).
535
    ///
536
    /// **Example JSON Config:**
537
    /// ```json
538
    /// "size_partitioning": {
539
    ///   "size": "128mib",
540
    ///   "lower_store": {
541
    ///     "memory": {
542
    ///       "eviction_policy": {
543
    ///         "max_bytes": "${NATIVELINK_CAS_MEMORY_CONTENT_LIMIT:-100mb}"
544
    ///       }
545
    ///     }
546
    ///   },
547
    ///   "upper_store": {
548
    ///     /// This store discards data larger than 128mib.
549
    ///     "noop": {}
550
    ///   }
551
    /// }
552
    /// ```
553
    ///
554
    SizePartitioning(Box<SizePartitioningSpec>),
555
556
    /// This store will pass-through calls to another gRPC store. This store
557
    /// is not designed to be used as a sub-store of another store, but it
558
    /// does satisfy the interface and will likely work.
559
    ///
560
    /// One major GOTCHA is that some stores use a special function on this
561
    /// store to get the size of the underlying object, which is only reliable
562
    /// when this store is serving the a CAS store, not an AC store. If using
563
    /// this store directly without being a child of any store there are no
564
    /// side effects and is the most efficient way to use it.
565
    ///
566
    /// **Example JSON Config:**
567
    /// ```json
568
    /// "grpc": {
569
    ///   "instance_name": "main",
570
    ///   "endpoints": [
571
    ///     {"address": "grpc://${CAS_ENDPOINT:-127.0.0.1}:50051"}
572
    ///   ],
573
    ///   "connections_per_endpoint": "5",
574
    ///   "rpc_timeout_s": "5m",
575
    ///   "store_type": "ac",
576
    ///   // Static headers attached to every outgoing request to the upstream
577
    ///   // remote cache. Useful for fixed service-account credentials.
578
    ///   "headers": {
579
    ///     "authorization": "Bearer my-static-token"
580
    ///   },
581
    ///   // Header names to copy from the inbound client request and forward to
582
    ///   // the upstream remote cache. Use this to pass through dynamic
583
    ///   // credentials such as a JWT sent by the build client.
584
    ///   "forward_headers": ["authorization", "x-custom-token"]
585
    /// }
586
    /// ```
587
    ///
588
    Grpc(GrpcSpec),
589
590
    /// Stores data in any stores compatible with Redis APIs.
591
    ///
592
    /// Pairs well with `SizePartitioning` and/or `FastSlow` stores.
593
    /// Ideal for accepting small object sizes as most redis store
594
    /// services have a max file upload of between 256Mb-512Mb.
595
    ///
596
    /// If you are using Redis together with any stores above it
597
    /// e.g. existence cache, you will need to configure `notify-keyspace-events`
598
    /// to `KA` as per <https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/#configuration>
599
    /// in order for us to get eviction events. Failing to do so will get you
600
    /// log messages complaining about it, as well as errors like <https://github.com/TraceMachina/nativelink/issues/2436>
601
    ///
602
    /// **Example JSON Config:**
603
    /// ```json
604
    /// "redis_store": {
605
    ///   "addresses": [
606
    ///     "redis://127.0.0.1:6379/",
607
    ///   ],
608
    ///   "max_client_permits": 1000,
609
    /// }
610
    /// ```
611
    ///
612
    RedisStore(RedisSpec),
613
614
    /// Noop store is a store that sends streams into the void and all data
615
    /// retrieval will return 404 (`NotFound`). This can be useful for cases
616
    /// where you may need to partition your data and part of your data needs
617
    /// to be discarded.
618
    ///
619
    /// **Example JSON Config:**
620
    /// ```json
621
    /// "noop": {}
622
    /// ```
623
    ///
624
    Noop(NoopSpec),
625
626
    /// Experimental `MongoDB` store implementation.
627
    ///
628
    /// This store uses `MongoDB` as a backend for storing data. It supports
629
    /// both CAS (Content Addressable Storage) and scheduler data with
630
    /// optional change streams for real-time updates.
631
    ///
632
    /// **Example JSON Config:**
633
    /// ```json
634
    /// "experimental_mongo": {
635
    ///     "connection_string": "mongodb://localhost:27017",
636
    ///     "database": "nativelink",
637
    ///     "cas_collection": "cas",
638
    ///     "key_prefix": "cas:",
639
    ///     "read_chunk_size": 65536,
640
    ///     "max_concurrent_uploads": 10,
641
    ///     "enable_change_streams": false,
642
    ///     "max_requests": "100"
643
    /// }
644
    /// ```
645
    ///
646
    ExperimentalMongo(ExperimentalMongoSpec),
647
}
648
649
impl StoreSpec {
650
190
    pub(crate) fn visit_grpc_specs(&self, visitor: &mut dyn FnMut(&GrpcSpec)) {
651
190
        match self {
652
1
            Self::CacheMetrics(spec) => spec.backend.visit_grpc_specs(visitor),
653
9
            Self::Verify(spec) => spec.backend.visit_grpc_specs(visitor),
654
12
            Self::Compression(spec) => spec.backend.visit_grpc_specs(visitor),
655
9
            Self::Dedup(spec) => {
656
9
                spec.index_store.visit_grpc_specs(visitor);
657
9
                spec.content_store.visit_grpc_specs(visitor);
658
9
            }
659
2
            Self::ExistenceCache(spec) => spec.backend.visit_grpc_specs(visitor),
660
2
            Self::CompletenessChecking(spec) => {
661
2
                spec.backend.visit_grpc_specs(visitor);
662
2
                spec.cas_store.visit_grpc_specs(visitor);
663
2
            }
664
31
            Self::FastSlow(spec) => {
665
31
                spec.fast.visit_grpc_specs(visitor);
666
31
                spec.slow.visit_grpc_specs(visitor);
667
31
            }
668
2
            Self::Shard(spec) => {
669
3
                for shard in 
&spec.stores2
{
670
3
                    shard.store.visit_grpc_specs(visitor);
671
3
                }
672
            }
673
3
            Self::SizePartitioning(spec) => {
674
3
                spec.lower_store.visit_grpc_specs(visitor);
675
3
                spec.upper_store.visit_grpc_specs(visitor);
676
3
            }
677
13
            Self::Grpc(spec) => visitor(spec),
678
            Self::Memory(_)
679
            | Self::ExperimentalCloudObjectStore(_)
680
            | Self::OntapS3ExistenceCache(_)
681
            | Self::Filesystem(_)
682
            | Self::RefStore(_)
683
            | Self::RedisStore(_)
684
            | Self::Noop(_)
685
106
            | Self::ExperimentalMongo(_) => {}
686
        }
687
190
    }
688
689
186
    pub(crate) fn visit_grpc_specs_mut(&mut self, visitor: &mut dyn FnMut(&mut GrpcSpec)) {
690
186
        match self {
691
1
            Self::CacheMetrics(spec) => spec.backend.visit_grpc_specs_mut(visitor),
692
9
            Self::Verify(spec) => spec.backend.visit_grpc_specs_mut(visitor),
693
12
            Self::Compression(spec) => spec.backend.visit_grpc_specs_mut(visitor),
694
9
            Self::Dedup(spec) => {
695
9
                spec.index_store.visit_grpc_specs_mut(visitor);
696
9
                spec.content_store.visit_grpc_specs_mut(visitor);
697
9
            }
698
2
            Self::ExistenceCache(spec) => spec.backend.visit_grpc_specs_mut(visitor),
699
2
            Self::CompletenessChecking(spec) => {
700
2
                spec.backend.visit_grpc_specs_mut(visitor);
701
2
                spec.cas_store.visit_grpc_specs_mut(visitor);
702
2
            }
703
31
            Self::FastSlow(spec) => {
704
31
                spec.fast.visit_grpc_specs_mut(visitor);
705
31
                spec.slow.visit_grpc_specs_mut(visitor);
706
31
            }
707
2
            Self::Shard(spec) => {
708
3
                for shard in 
&mut spec.stores2
{
709
3
                    shard.store.visit_grpc_specs_mut(visitor);
710
3
                }
711
            }
712
3
            Self::SizePartitioning(spec) => {
713
3
                spec.lower_store.visit_grpc_specs_mut(visitor);
714
3
                spec.upper_store.visit_grpc_specs_mut(visitor);
715
3
            }
716
9
            Self::Grpc(spec) => visitor(spec),
717
            Self::Memory(_)
718
            | Self::ExperimentalCloudObjectStore(_)
719
            | Self::OntapS3ExistenceCache(_)
720
            | Self::Filesystem(_)
721
            | Self::RefStore(_)
722
            | Self::RedisStore(_)
723
            | Self::Noop(_)
724
106
            | Self::ExperimentalMongo(_) => {}
725
        }
726
186
    }
727
}
728
729
/// Configuration for an individual shard of the store.
730
#[derive(Serialize, Deserialize, Debug, Clone)]
731
#[serde(deny_unknown_fields)]
732
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
733
pub struct ShardConfig {
734
    /// Store to shard the data to.
735
    pub store: StoreSpec,
736
737
    /// The weight of the store. This is used to determine how much data
738
    /// should be sent to the store. The actual percentage is the sum of
739
    /// all the store's weights divided by the individual store's weight.
740
    ///
741
    /// Default: 1
742
    #[serde(deserialize_with = "convert_optional_numeric_with_shellexpand")]
743
    pub weight: Option<u32>,
744
}
745
746
#[derive(Serialize, Deserialize, Debug, Clone)]
747
#[serde(deny_unknown_fields)]
748
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
749
pub struct ShardSpec {
750
    /// Stores to shard the data to.
751
    pub stores: Vec<ShardConfig>,
752
}
753
754
#[derive(Serialize, Deserialize, Debug, Clone)]
755
#[serde(deny_unknown_fields)]
756
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
757
pub struct CacheMetricsSpec {
758
    /// Low-cardinality cache type label for metrics, for example `cas` or `ac`.
759
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
760
    pub cache_type: String,
761
762
    /// Store to wrap with cache operation metrics.
763
    pub backend: StoreSpec,
764
}
765
766
#[derive(Serialize, Deserialize, Debug, Clone)]
767
#[serde(deny_unknown_fields)]
768
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
769
pub struct SizePartitioningSpec {
770
    /// Size to partition the data on.
771
    #[serde(deserialize_with = "convert_data_size_with_shellexpand")]
772
    pub size: u64,
773
774
    /// Store to send data when object is < (less than) size.
775
    pub lower_store: StoreSpec,
776
777
    /// Store to send data when object is >= (less than eq) size.
778
    pub upper_store: StoreSpec,
779
}
780
781
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
782
#[serde(deny_unknown_fields)]
783
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
784
pub struct RefSpec {
785
    /// Name of the store under the root "stores" config object.
786
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
787
    pub name: String,
788
}
789
790
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
791
#[serde(deny_unknown_fields)]
792
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
793
pub struct FilesystemSpec {
794
    /// Path on the system where to store the actual content. This is where
795
    /// the bulk of the data will be placed.
796
    /// On service boot this folder will be scanned and all files will be
797
    /// added to the cache. In the event one of the files doesn't match the
798
    /// criteria, the file will be deleted.
799
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
800
    pub content_path: String,
801
802
    /// A temporary location of where files that are being uploaded or
803
    /// deleted will be placed while the content cannot be guaranteed to be
804
    /// accurate. This location must be on the same block device as
805
    /// `content_path` so atomic moves can happen (i.e. move without copy).
806
    /// All files in this folder will be deleted on every startup.
807
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
808
    pub temp_path: String,
809
810
    /// Buffer size to use when reading files. Generally this should be left
811
    /// to the default value except for testing.
812
    /// Default: 32k.
813
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
814
    pub read_buffer_size: u32,
815
816
    /// Policy used to evict items out of the store. Failure to set this
817
    /// value will cause items to never be removed from the store causing
818
    /// infinite memory usage.
819
    pub eviction_policy: Option<EvictionPolicy>,
820
821
    /// The block size of the filesystem for the running machine
822
    /// value is used to determine an entry's actual size on disk consumed
823
    /// For a 4KB block size filesystem, a 1B file actually consumes 4KB
824
    /// Default: 4kb
825
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
826
    pub block_size: u64,
827
828
    /// Maximum number of concurrent write operations allowed.
829
    /// Each write involves streaming data to a temp file and calling `sync_all()`,
830
    /// which can saturate disk I/O when many writes happen simultaneously.
831
    /// Limiting concurrency prevents disk saturation from blocking the async
832
    /// runtime.
833
    /// A value of 0 means unlimited (no concurrency limit).
834
    /// Default: unlimited
835
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
836
    pub max_concurrent_writes: usize,
837
838
    /// When true, advise the kernel to drop the page cache for each blob after
839
    /// it is written or read (`posix_fadvise` with `POSIX_FADV_DONTNEED`). On
840
    /// real filesystems this takes a globally serialized, all-CPU kernel path
841
    /// that stalls on many-core hosts, and it evicts the page-cache tier that
842
    /// fast-disk deployments rely on. Leave off unless you specifically want to
843
    /// keep this store's I/O out of the page cache.
844
    /// Default: false
845
    #[serde(default)]
846
    pub evict_page_cache: bool,
847
}
848
849
// NetApp ONTAP S3 Spec
850
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
851
#[serde(deny_unknown_fields)]
852
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
853
pub struct ExperimentalOntapS3Spec {
854
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
855
    pub endpoint: String,
856
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
857
    pub vserver_name: String,
858
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
859
    pub bucket: String,
860
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
861
    pub root_certificates: Option<String>,
862
863
    /// Common retry and upload configuration
864
    #[serde(flatten)]
865
    pub common: CommonObjectSpec,
866
}
867
868
// Cloudflare R2 Spec
869
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
870
#[serde(deny_unknown_fields)]
871
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
872
pub struct ExperimentalR2Spec {
873
    /// Cloudflare account ID. Endpoint is derived as
874
    /// `https://{account_id}.r2.cloudflarestorage.com`.
875
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
876
    pub account_id: String,
877
878
    /// Bucket name to use as the backend.
879
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
880
    pub bucket: String,
881
882
    /// Explicit R2 access key.
883
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
884
    pub access_key_id: Option<String>,
885
886
    /// Explicit R2 secret key.
887
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
888
    pub secret_access_key: Option<String>,
889
890
    /// Retry and upload settings.
891
    #[serde(flatten)]
892
    pub common: CommonObjectSpec,
893
}
894
895
// Oracle Cloud Infrastructure (OCI) Object Storage Spec.
896
//
897
// Uses the OCI Object Storage Amazon S3 Compatibility API. The store talks to
898
// the path-style compatibility endpoint, which embeds the Object Storage
899
// namespace in the host and the bucket in the request path:
900
// `https://{namespace}.compat.objectstorage.{region}.oci.customer-oci.com/{bucket}/{object}`.
901
// Authentication uses a Customer Secret Key (an Access Key/Secret Key pair
902
// generated under User Settings in the OCI console) signed with AWS SigV4.
903
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
904
#[serde(deny_unknown_fields)]
905
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
906
pub struct ExperimentalOciSpec {
907
    /// OCI Object Storage namespace. This is the immutable, system-generated
908
    /// top-level container assigned to the tenancy (the same name in every
909
    /// region). It is the host prefix of the derived path-style endpoint:
910
    /// `https://{namespace}.compat.objectstorage.{region}.oci.customer-oci.com`.
911
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
912
    pub namespace: String,
913
914
    /// OCI region identifier, for example `us-phoenix-1` or `us-ashburn-1`.
915
    /// Used both to build the endpoint host and as the AWS `SigV4` signing
916
    /// region. If your tooling cannot set an OCI region identifier, OCI also
917
    /// accepts `us-east-1` to target the tenancy home region.
918
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
919
    pub region: String,
920
921
    /// Bucket name to use as the backend. Bucket names must be unique within
922
    /// the Object Storage namespace.
923
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
924
    pub bucket: String,
925
926
    /// Customer Secret Key access key. When omitted (along with
927
    /// `secret_access_key`), the default AWS credential chain is used instead.
928
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
929
    pub access_key_id: Option<String>,
930
931
    /// Customer Secret Key secret. OCI does not allow retrieving a secret key
932
    /// after generation, so store it securely (for example via `${ENV_VAR}`
933
    /// shell expansion).
934
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
935
    pub secret_access_key: Option<String>,
936
937
    /// Retry and upload settings.
938
    #[serde(flatten)]
939
    pub common: CommonObjectSpec,
940
}
941
942
#[derive(Serialize, Deserialize, Debug, Clone)]
943
#[serde(deny_unknown_fields)]
944
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
945
pub struct OntapS3ExistenceCacheSpec {
946
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
947
    pub index_path: String,
948
    #[serde(deserialize_with = "convert_numeric_with_shellexpand")]
949
    pub sync_interval_seconds: u32,
950
    pub backend: Box<ExperimentalOntapS3Spec>,
951
}
952
953
#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy, PartialEq, Eq)]
954
#[serde(rename_all = "snake_case")]
955
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
956
pub enum StoreDirection {
957
    /// The store operates normally and all get and put operations are
958
    /// handled by it.
959
    #[default]
960
    Both,
961
    /// Update operations will cause persistence to this store, but Get
962
    /// operations will be ignored.
963
    /// This only makes sense on the fast store as the slow store will
964
    /// never get written to on Get anyway.
965
    Update,
966
    /// Get operations will cause persistence to this store, but Update
967
    /// operations will be ignored.
968
    Get,
969
    /// Operate as a read only store, only really makes sense if there's
970
    /// another way to write to it.
971
    ReadOnly,
972
}
973
974
#[derive(Serialize, Deserialize, Debug, Clone)]
975
#[serde(deny_unknown_fields)]
976
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
977
pub struct FastSlowSpec {
978
    /// Fast store that will be attempted to be contacted before reaching
979
    /// out to the `slow` store.
980
    pub fast: StoreSpec,
981
982
    /// How to handle the fast store. This can be useful to set to Get for
983
    /// worker nodes such that results are persisted to the slow store only.
984
    #[serde(default)]
985
    pub fast_direction: StoreDirection,
986
987
    /// If the object does not exist in the `fast` store it will try to
988
    /// get it from this store.
989
    pub slow: StoreSpec,
990
991
    /// How to handle the slow store. This can be useful if creating a diode
992
    /// and you wish to have an upstream read only store.
993
    #[serde(default)]
994
    pub slow_direction: StoreDirection,
995
996
    /// Reads of blobs at or above this size skip the leader/follower dedup
997
    /// map and stream straight from the slow store without populating the
998
    /// fast tier. `0` (the default) disables the bypass: every read goes
999
    /// through dedup, matching the prior behaviour. Enable it by setting a
1000
    /// threshold — 256 MiB is a reasonable starting point for backends where
1001
    /// large-blob dedup is a net loss (followers tend to time out anyway),
1002
    /// but the right value is workload-dependent.
1003
    /// Default: disabled (0)
1004
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1005
    pub bypass_dedup_threshold_bytes: u64,
1006
}
1007
1008
#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy)]
1009
#[serde(deny_unknown_fields)]
1010
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1011
pub struct MemorySpec {
1012
    /// Policy used to evict items out of the store. Failure to set this
1013
    /// value will cause items to never be removed from the store causing
1014
    /// infinite memory usage.
1015
    pub eviction_policy: Option<EvictionPolicy>,
1016
}
1017
1018
#[derive(Serialize, Deserialize, Debug, Clone)]
1019
#[serde(deny_unknown_fields)]
1020
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1021
pub struct DedupSpec {
1022
    /// Store used to store the index of each dedup slice. This store
1023
    /// should generally be fast and small.
1024
    pub index_store: StoreSpec,
1025
1026
    /// The store where the individual chunks will be uploaded. This
1027
    /// store should generally be the slower & larger store.
1028
    pub content_store: StoreSpec,
1029
1030
    /// Minimum size that a chunk will be when slicing up the content.
1031
    /// Note: This setting can be increased to improve performance
1032
    /// because it will actually not check this number of bytes when
1033
    /// deciding where to partition the data.
1034
    ///
1035
    /// Default: 64k
1036
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1037
    pub min_size: u32,
1038
1039
    /// A best-effort attempt will be made to keep the average size
1040
    /// of the chunks to this number. It is not a guarantee, but a
1041
    /// slight attempt will be made.
1042
    ///
1043
    /// This value will also be about the threshold used to determine
1044
    /// if we should even attempt to dedup the entry or just forward
1045
    /// it directly to the `content_store` without an index. The actual
1046
    /// value will be about `normal_size * 1.3` due to implementation
1047
    /// details.
1048
    ///
1049
    /// Default: 256k
1050
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1051
    pub normal_size: u32,
1052
1053
    /// Maximum size a chunk is allowed to be.
1054
    ///
1055
    /// Default: 512k
1056
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1057
    pub max_size: u32,
1058
1059
    /// Due to implementation detail, we want to prefer to download
1060
    /// the first chunks of the file so we can stream the content
1061
    /// out and free up some of our buffers. This configuration
1062
    /// will be used to restrict the number of concurrent chunk
1063
    /// downloads at a time per `get()` request.
1064
    ///
1065
    /// This setting will also affect how much memory might be used
1066
    /// per `get()` request. Estimated worst case memory per `get()`
1067
    /// request is: `max_concurrent_fetch_per_get * max_size`.
1068
    ///
1069
    /// Default: 10
1070
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1071
    pub max_concurrent_fetch_per_get: u32,
1072
}
1073
1074
#[derive(Serialize, Deserialize, Debug, Clone)]
1075
#[serde(deny_unknown_fields)]
1076
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1077
pub struct ExistenceCacheSpec {
1078
    /// The underlying store wrap around. All content will first flow
1079
    /// through self before forwarding to backend. In the event there
1080
    /// is an error detected in self, the connection to the backend
1081
    /// will be terminated, and early termination should always cause
1082
    /// updates to fail on the backend.
1083
    pub backend: StoreSpec,
1084
1085
    /// Policy used to evict items out of the store. Failure to set this
1086
    /// value will cause items to never be removed from the store causing
1087
    /// infinite memory usage.
1088
    pub eviction_policy: Option<EvictionPolicy>,
1089
}
1090
1091
#[derive(Serialize, Deserialize, Debug, Clone)]
1092
#[serde(deny_unknown_fields)]
1093
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1094
pub struct VerifySpec {
1095
    /// The underlying store wrap around. All content will first flow
1096
    /// through self before forwarding to backend. In the event there
1097
    /// is an error detected in self, the connection to the backend
1098
    /// will be terminated, and early termination should always cause
1099
    /// updates to fail on the backend.
1100
    pub backend: StoreSpec,
1101
1102
    /// If set the store will verify the size of the data before accepting
1103
    /// an upload of data.
1104
    ///
1105
    /// This should be set to false for AC, but true for CAS stores.
1106
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1107
    pub verify_size: bool,
1108
1109
    /// If the data should be hashed and verify that the key matches the
1110
    /// computed hash. The hash function is automatically determined based
1111
    /// request and if not set will use the global default.
1112
    ///
1113
    /// This should be set to false for AC, but true for CAS stores.
1114
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1115
    pub verify_hash: bool,
1116
}
1117
1118
#[derive(Serialize, Deserialize, Debug, Clone)]
1119
#[serde(deny_unknown_fields)]
1120
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1121
pub struct CompletenessCheckingSpec {
1122
    /// The underlying store that will have it's results validated before sending to client.
1123
    pub backend: StoreSpec,
1124
1125
    /// When a request is made, the results are decoded and all output digests/files are verified
1126
    /// to exist in this CAS store before returning success.
1127
    pub cas_store: StoreSpec,
1128
}
1129
1130
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)]
1131
#[serde(deny_unknown_fields)]
1132
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1133
pub struct Lz4Config {
1134
    /// Size of the blocks to compress.
1135
    /// Higher values require more ram, but might yield slightly better
1136
    /// compression ratios.
1137
    ///
1138
    /// Default: 65536 (64k).
1139
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1140
    pub block_size: u32,
1141
1142
    /// Maximum size allowed to attempt to deserialize data into.
1143
    /// This is needed because the `block_size` is embedded into the data
1144
    /// so if there was a bad actor, they could upload an extremely large
1145
    /// `block_size`'ed entry and we'd allocate a large amount of memory
1146
    /// when retrieving the data. To prevent this from happening, we
1147
    /// allow you to specify the maximum that we'll attempt to deserialize.
1148
    ///
1149
    /// Default: value in `block_size`.
1150
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1151
    pub max_decode_block_size: u32,
1152
}
1153
1154
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
1155
#[serde(rename_all = "snake_case")]
1156
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1157
pub enum CompressionAlgorithm {
1158
    /// LZ4 compression algorithm is extremely fast for compression and
1159
    /// decompression, however does not perform very well in compression
1160
    /// ratio. In most cases build artifacts are highly compressible, however
1161
    /// lz4 is quite good at aborting early if the data is not deemed very
1162
    /// compressible.
1163
    ///
1164
    /// see: <https://lz4.github.io/lz4/>
1165
    Lz4(Lz4Config),
1166
}
1167
1168
#[derive(Serialize, Deserialize, Debug, Clone)]
1169
#[serde(deny_unknown_fields)]
1170
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1171
pub struct CompressionSpec {
1172
    /// The underlying store wrap around. All content will first flow
1173
    /// through self before forwarding to backend. In the event there
1174
    /// is an error detected in self, the connection to the backend
1175
    /// will be terminated, and early termination should always cause
1176
    /// updates to fail on the backend.
1177
    pub backend: StoreSpec,
1178
1179
    /// The compression algorithm to use.
1180
    pub compression_algorithm: CompressionAlgorithm,
1181
}
1182
1183
/// Eviction policy always works on LRU (Least Recently Used). Any time an entry
1184
/// is touched it updates the timestamp. Inserts and updates will execute the
1185
/// eviction policy removing any expired entries and/or the oldest entries
1186
/// until the store size becomes smaller than `max_bytes`.
1187
#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy)]
1188
#[serde(deny_unknown_fields)]
1189
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1190
pub struct EvictionPolicy {
1191
    /// Maximum number of bytes before eviction takes place.
1192
    /// Default: 0. Zero means never evict based on size.
1193
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1194
    pub max_bytes: usize,
1195
1196
    /// When eviction starts based on hitting `max_bytes`, continue until
1197
    /// `max_bytes - evict_bytes` is met to create a low watermark. This stops
1198
    /// operations from thrashing when the store is close to the limit.
1199
    /// Default: 0
1200
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1201
    pub evict_bytes: usize,
1202
1203
    /// Maximum number of seconds for an entry to live since it was last
1204
    /// accessed before it is evicted.
1205
    /// Default: 0. Zero means never evict based on time.
1206
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1207
    pub max_seconds: u32,
1208
1209
    /// Maximum size of the store before an eviction takes place.
1210
    /// Default: 0. Zero means never evict based on count.
1211
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1212
    pub max_count: u64,
1213
}
1214
1215
#[derive(Serialize, Deserialize, Debug, Clone)]
1216
#[serde(tag = "provider", rename_all = "snake_case")]
1217
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1218
pub enum ExperimentalCloudObjectSpec {
1219
    Aws(ExperimentalAwsSpec),
1220
    Gcs(ExperimentalGcsSpec),
1221
    Azure(ExperimentalAzureSpec),
1222
    Ontap(ExperimentalOntapS3Spec),
1223
    R2(ExperimentalR2Spec),
1224
    Oci(ExperimentalOciSpec),
1225
}
1226
1227
impl Default for ExperimentalCloudObjectSpec {
1228
0
    fn default() -> Self {
1229
0
        Self::Aws(ExperimentalAwsSpec::default())
1230
0
    }
1231
}
1232
1233
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1234
#[serde(deny_unknown_fields)]
1235
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1236
pub struct ExperimentalAwsSpec {
1237
    /// S3 region. Usually us-east-1, us-west-2, af-south-1, exc...
1238
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1239
    pub region: String,
1240
1241
    /// Bucket name to use as the backend.
1242
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1243
    pub bucket: String,
1244
1245
    /// Endpoint to talk to instead of the AWS regional endpoint. Set this to
1246
    /// point the store at an S3-compatible service, for example
1247
    /// `https://s3.example.com`. When omitted, the SDK resolves the endpoint
1248
    /// from `region`.
1249
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1250
    pub endpoint: Option<String>,
1251
1252
    /// Address objects as `{endpoint}/{bucket}/{key}` rather than
1253
    /// `{bucket}.{endpoint}/{key}`. Many S3-compatible services serve only the
1254
    /// path-style form.
1255
    ///
1256
    /// Default: false
1257
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1258
    pub force_path_style: bool,
1259
1260
    /// Access key to sign requests with. When this and `secret_access_key` are
1261
    /// both omitted, the default AWS credential chain is used instead.
1262
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1263
    pub access_key_id: Option<String>,
1264
1265
    /// Secret key to sign requests with. Prefer `${ENV_VAR}` shell expansion
1266
    /// over writing the secret into the config file.
1267
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1268
    pub secret_access_key: Option<String>,
1269
1270
    /// Common retry and upload configuration
1271
    #[serde(flatten)]
1272
    pub common: CommonObjectSpec,
1273
}
1274
1275
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1276
#[serde(deny_unknown_fields)]
1277
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1278
pub struct ExperimentalGcsSpec {
1279
    /// Bucket name to use as the backend.
1280
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1281
    pub bucket: String,
1282
1283
    /// Chunk size for resumable uploads.
1284
    ///
1285
    /// Default: 2MB
1286
    #[serde(
1287
        default,
1288
        deserialize_with = "convert_optional_data_size_with_shellexpand"
1289
    )]
1290
    pub resumable_chunk_size: Option<usize>,
1291
1292
    /// Common retry and upload configuration
1293
    #[serde(flatten)]
1294
    pub common: CommonObjectSpec,
1295
1296
    /// Error if authentication was not found.
1297
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1298
    pub authentication_required: bool,
1299
1300
    /// Connection timeout in milliseconds.
1301
    /// Default: 3000
1302
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1303
    pub connection_timeout_s: u64,
1304
1305
    /// Read timeout in milliseconds.
1306
    /// Default: 3000
1307
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1308
    pub read_timeout_s: u64,
1309
}
1310
1311
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1312
#[serde(deny_unknown_fields)]
1313
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1314
pub struct ExperimentalAzureSpec {
1315
    /// The Azure Storage account name. Used to build the default container URL
1316
    /// `https://{account_name}.blob.core.windows.net/{container}` when `sas_url`
1317
    /// is not provided.
1318
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1319
    pub account_name: String,
1320
1321
    /// The container name to use as the backend.
1322
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1323
    pub container: String,
1324
1325
    /// Optional blob endpoint host override (for example an Azurite emulator host
1326
    /// such as `http://127.0.0.1:10000/devstoreaccount1`). When set, this replaces
1327
    /// the default `https://{account_name}.blob.core.windows.net` endpoint. The
1328
    /// container is always appended to form the final container URL. Ignored when
1329
    /// `sas_url` is set.
1330
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1331
    pub endpoint: Option<String>,
1332
1333
    /// Optional pre-formed SAS URL pointing at the container. When set, the store
1334
    /// uses it directly as the container URL with no credential (the SAS token is
1335
    /// expected to already be present in the URL), and `account_name`, `container`,
1336
    /// and `endpoint` are ignored for URL construction.
1337
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1338
    pub sas_url: Option<String>,
1339
1340
    /// Common retry and upload configuration.
1341
    #[serde(flatten)]
1342
    pub common: CommonObjectSpec,
1343
}
1344
1345
#[derive(Serialize, Deserialize, Debug, Clone)]
1346
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1347
pub struct CommonObjectSpec {
1348
    /// If you wish to prefix the location in the bucket. If None, no prefix will be used.
1349
    #[serde(default)]
1350
    pub key_prefix: Option<String>,
1351
1352
    /// Retry configuration to use when a network request fails.
1353
    #[serde(default)]
1354
    pub retry: Retry,
1355
1356
    /// If the number of seconds since the `last_modified` time of the object
1357
    /// is greater than this value, the object will not be considered
1358
    /// "existing". This allows for external tools to delete objects that
1359
    /// have not been uploaded in a long time. If a client receives a `NotFound`
1360
    /// the client should re-upload the object.
1361
    ///
1362
    /// There should be sufficient buffer time between how long the expiration
1363
    /// configuration of the external tool is and this value. Keeping items
1364
    /// around for a few days is generally a good idea.
1365
    ///
1366
    /// Default: 0. Zero means never consider an object expired.
1367
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1368
    pub consider_expired_after_s: u32,
1369
1370
    /// The maximum buffer size to retain in case of a retryable error
1371
    /// during upload. Setting this to zero will disable upload buffering;
1372
    /// this means that in the event of a failure during upload, the entire
1373
    /// upload will be aborted and the client will likely receive an error.
1374
    ///
1375
    /// Default: 5MB.
1376
    #[serde(
1377
        default,
1378
        deserialize_with = "convert_optional_data_size_with_shellexpand"
1379
    )]
1380
    pub max_retry_buffer_per_request: Option<usize>,
1381
1382
    /// Maximum number of concurrent `UploadPart` requests per `MultipartUpload`.
1383
    ///
1384
    /// Default: 10.
1385
    ///
1386
    #[serde(
1387
        default,
1388
        deserialize_with = "convert_optional_numeric_with_shellexpand"
1389
    )]
1390
    pub multipart_max_concurrent_uploads: Option<usize>,
1391
1392
    /// Allow unencrypted HTTP connections. Only use this for local testing.
1393
    ///
1394
    /// Default: false
1395
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1396
    pub insecure_allow_http: bool,
1397
1398
    /// Disable HTTP/2 connections and only use HTTP/1.1. Default client
1399
    /// configuration will have HTTP/1.1 and HTTP/2 enabled for connection
1400
    /// schemes. HTTP/2 should be disabled if environments have poor support
1401
    /// or performance related to HTTP/2. Safe to keep default unless
1402
    /// underlying network environment, S3, or GCS API servers specify otherwise.
1403
    ///
1404
    /// Default: false
1405
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1406
    pub disable_http2: bool,
1407
1408
    /// Minimum number of seconds to wait after a response that asked the
1409
    /// client to slow down (HTTP 503 `ServerBusy` or 429) when it carries no
1410
    /// `Retry-After` header.
1411
    ///
1412
    /// Default: 2.0
1413
    #[serde(default = "default_throttle_retry_delay_s")]
1414
    pub throttle_retry_delay_s: f32,
1415
1416
    /// Longest wait honoured from a `Retry-After` header. A server or proxy
1417
    /// asking for more waits this long instead, and the capped wait is
1418
    /// logged. Must not be less than `throttle_retry_delay_s`.
1419
    ///
1420
    /// Default: 60.0
1421
    #[serde(default = "default_throttle_retry_max_delay_s")]
1422
    pub throttle_retry_max_delay_s: f32,
1423
1424
    /// Seconds one operation may spend waiting on throttled responses
1425
    /// without spending any of `retry.max_retries`: while the budget lasts
1426
    /// each throttle waits what the server asked (at least
1427
    /// `throttle_retry_delay_s`, at most `throttle_retry_max_delay_s`) and
1428
    /// the attempt is not counted, so a long throttle is ridden out rather
1429
    /// than failed on the seventh try. Once it is used up, throttles spend
1430
    /// attempts and backoff like any other transient error. 0 turns the
1431
    /// budget off.
1432
    ///
1433
    /// Default: 120.0
1434
    #[serde(default = "default_throttle_retry_budget_s")]
1435
    pub throttle_retry_budget_s: f32,
1436
}
1437
1438
202
const fn default_throttle_retry_delay_s() -> f32 {
1439
202
    2.0
1440
202
}
1441
1442
202
const fn default_throttle_retry_max_delay_s() -> f32 {
1443
202
    60.0
1444
202
}
1445
1446
202
const fn default_throttle_retry_budget_s() -> f32 {
1447
202
    120.0
1448
202
}
1449
1450
impl Default for CommonObjectSpec {
1451
0
    fn default() -> Self {
1452
0
        Self {
1453
0
            key_prefix: None,
1454
0
            retry: Retry::default(),
1455
0
            consider_expired_after_s: 0,
1456
0
            max_retry_buffer_per_request: None,
1457
0
            multipart_max_concurrent_uploads: None,
1458
0
            insecure_allow_http: false,
1459
0
            disable_http2: false,
1460
0
            throttle_retry_delay_s: default_throttle_retry_delay_s(),
1461
0
            throttle_retry_max_delay_s: default_throttle_retry_max_delay_s(),
1462
0
            throttle_retry_budget_s: default_throttle_retry_budget_s(),
1463
0
        }
1464
0
    }
1465
}
1466
1467
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
1468
#[serde(rename_all = "snake_case")]
1469
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1470
pub enum StoreType {
1471
    /// The store is content addressable storage.
1472
    Cas,
1473
    /// The store is an action cache.
1474
    Ac,
1475
    /// A write-only event sink using `ByteStream.Write`. Only string keys and
1476
    /// exact upload sizes are accepted. The resource hash identifies the key,
1477
    /// not the payload; the size describes the payload. The upstream must be an
1478
    /// event receiver, not a CAS. Reads and existence checks are unsupported.
1479
    /// Use this for BEP/origin event forwarding, never for cache data.
1480
    EventSink,
1481
}
1482
1483
#[derive(Serialize, Deserialize, Debug, Clone)]
1484
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1485
pub struct ClientTlsConfig {
1486
    /// Path to the certificate authority to use to validate the remote.
1487
    ///
1488
    /// Default: None
1489
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1490
    pub ca_file: Option<String>,
1491
1492
    /// Path to the certificate file for client authentication.
1493
    ///
1494
    /// Default: None
1495
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1496
    pub cert_file: Option<String>,
1497
1498
    /// Path to the private key file for client authentication.
1499
    ///
1500
    /// Default: None
1501
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1502
    pub key_file: Option<String>,
1503
1504
    /// If set the client will use the native roots for TLS connections.
1505
    ///
1506
    /// Default: false
1507
    #[serde(default)]
1508
    pub use_native_roots: Option<bool>,
1509
}
1510
1511
#[derive(Serialize, Deserialize, Debug, Clone)]
1512
#[serde(deny_unknown_fields)]
1513
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1514
pub struct GrpcEndpoint {
1515
    /// The endpoint address (i.e. `grpc(s)://example.com:443`).
1516
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
1517
    pub address: String,
1518
    /// The TLS configuration to use to connect to the endpoint (if grpcs).
1519
    pub tls_config: Option<ClientTlsConfig>,
1520
    /// The maximum concurrency to allow on this endpoint.
1521
    #[serde(
1522
        default,
1523
        deserialize_with = "convert_optional_numeric_with_shellexpand"
1524
    )]
1525
    pub concurrency_limit: Option<usize>,
1526
1527
    /// Timeout for establishing a TCP connection to the endpoint (seconds).
1528
    /// If not set or 0, defaults to 30 seconds.
1529
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1530
    pub connect_timeout_s: u64,
1531
1532
    /// TCP keepalive interval (seconds). Sends TCP keepalive probes at this
1533
    /// interval to detect dead connections at the OS level.
1534
    /// If not set or 0, defaults to 30 seconds.
1535
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1536
    pub tcp_keepalive_s: u64,
1537
1538
    /// HTTP/2 keepalive interval (seconds). Sends HTTP/2 PING frames at this
1539
    /// interval to detect dead connections at the application level.
1540
    /// If not set or 0, defaults to 30 seconds.
1541
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1542
    pub http2_keepalive_interval_s: u64,
1543
1544
    /// HTTP/2 keepalive timeout (seconds). If a PING response is not received
1545
    /// within this duration, the connection is considered dead.
1546
    /// If not set or 0, defaults to 20 seconds.
1547
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1548
    pub http2_keepalive_timeout_s: u64,
1549
}
1550
1551
#[derive(Serialize, Deserialize, Debug, Clone)]
1552
#[serde(deny_unknown_fields)]
1553
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1554
pub struct GrpcSpec {
1555
    /// Instance name for gRPC calls. Proxy calls will have the `instance_name` changed to this.
1556
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1557
    pub instance_name: String,
1558
1559
    /// The endpoint of the grpc connection.
1560
    pub endpoints: Vec<GrpcEndpoint>,
1561
1562
    /// The type of the upstream store, this ensures that the correct server calls are made.
1563
    pub store_type: StoreType,
1564
1565
    /// Retry configuration to use when a network request fails.
1566
    #[serde(default)]
1567
    pub retry: Retry,
1568
1569
    /// Limit the number of simultaneous upstream requests to this many. A
1570
    /// value of zero is treated as unlimited. If the limit is reached the
1571
    /// request is queued.
1572
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1573
    pub max_concurrent_requests: usize,
1574
1575
    /// The number of connections to make to each specified endpoint to balance
1576
    /// the load over multiple TCP connections.
1577
    /// Default: 1.
1578
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1579
    pub connections_per_endpoint: usize,
1580
1581
    /// Maximum time (seconds) allowed for a single RPC request (e.g. a
1582
    /// `ByteStream.Write` call) before it is cancelled.
1583
    ///
1584
    /// A value of 0 (the default) disables the per-RPC timeout. Dead
1585
    /// connections are still detected by the HTTP/2 and TCP keepalive
1586
    /// mechanisms configured on each endpoint.
1587
    ///
1588
    /// For large uploads (multi-GB), either leave this at 0 or set it
1589
    /// large enough to accommodate the full transfer time.
1590
    ///
1591
    /// Default: 0 (disabled)
1592
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1593
    pub rpc_timeout_s: u64,
1594
1595
    /// Use legacy `ByteStream` resource name format, omitting the digest
1596
    /// function component from the path.
1597
    ///
1598
    /// Modern `NativeLink` generates resource names like:
1599
    ///   `{instance}/blobs/{digest_function}/{hash}/{size}`
1600
    ///
1601
    /// Older backends (e.g. Buildbarn pre-v0.3) expect the original format:
1602
    ///   `{instance}/blobs/{hash}/{size}`
1603
    ///
1604
    /// Set this to `true` when connecting to such backends to avoid
1605
    /// `InvalidArgument: Unsupported digest function` errors.
1606
    ///
1607
    /// Default: false
1608
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1609
    pub use_legacy_resource_names: bool,
1610
1611
    /// Static headers to attach to every outgoing gRPC request sent to this
1612
    /// store's upstream endpoints. Useful for fixed authentication tokens
1613
    /// (e.g. `{"authorization": "Bearer <token>"}`) and other static metadata.
1614
    #[serde(default)]
1615
    pub headers: HashMap<String, String>,
1616
1617
    /// Header names to forward from the incoming client request to every
1618
    /// outgoing upstream request. The header value is taken from the client
1619
    /// request that triggered this store operation. Use this to pass through
1620
    /// dynamic credentials such as JWT tokens sent by build clients.
1621
    ///
1622
    /// Example: `["authorization", "x-custom-token"]`
1623
    ///
1624
    /// `NativeLink` also automatically injects the current OpenTelemetry trace
1625
    /// context (`traceparent` / `tracestate`) into every outgoing request.
1626
    #[serde(default)]
1627
    pub forward_headers: Vec<String>,
1628
1629
    /// Optional and experimental: coalesce small-blob reads into
1630
    /// `BatchReadBlobs` RPCs instead of issuing one `ByteStream` `Read`
1631
    /// stream per blob. Each `ByteStream` read carries a fixed per-RPC cost,
1632
    /// so batching many small reads into a single `BatchReadBlobs` request
1633
    /// can dramatically reduce read latency for small blobs.
1634
    ///
1635
    /// Only full reads (offset 0, whole blob) of blobs at or below
1636
    /// `max_blob_size_bytes` are batched; everything else continues to use
1637
    /// the `ByteStream` `Read` path.
1638
    ///
1639
    /// Incompatible with `forward_headers`: batched reads share one upstream
1640
    /// RPC across many client requests, so per-client forwarded headers
1641
    /// (e.g. credentials) cannot be attached. Configuring both is rejected
1642
    /// at startup.
1643
    ///
1644
    /// Default: unset (disabled). When unset there is zero behavior change.
1645
    #[serde(default)]
1646
    pub experimental_read_batching: Option<GrpcReadBatchingConfig>,
1647
1648
    /// Compress this store's own blob transfers on the wire with REAPI
1649
    /// `compressed-blobs/zstd`. Uploads and full-blob downloads of blobs at
1650
    /// or above 64 KiB are zstd-compressed; smaller blobs and ranged reads
1651
    /// keep the identity path.
1652
    ///
1653
    /// The upstream instance must have
1654
    /// `capabilities.remote_cache_compression` enabled, otherwise compressed
1655
    /// requests fail with `InvalidArgument`. This setting is what makes
1656
    /// NativeLink-to-NativeLink hops (for example worker to CAS) benefit
1657
    /// from wire compression; it is independent of what external clients
1658
    /// such as Bazel negotiate for themselves.
1659
    ///
1660
    /// Compressed uploads do not resume mid-stream (mirroring the REAPI
1661
    /// server contract): a transport failure part-way through a compressed
1662
    /// upload surfaces immediately to the caller instead of retrying, and
1663
    /// outer callers retry the whole upload.
1664
    ///
1665
    /// When combined with `experimental_chunked_uploads`, chunked uploads
1666
    /// take precedence for blobs at or above the chunking threshold.
1667
    ///
1668
    /// When zstd wire compression is enabled elsewhere in the process,
1669
    /// omitting this setting enables it automatically for CAS gRPC stores.
1670
    /// Set it explicitly to `false` to opt this store out.
1671
    ///
1672
    /// Default: inherited from the process-wide zstd wire-compression intent.
1673
    #[serde(
1674
        default,
1675
        skip_serializing_if = "Option::is_none",
1676
        deserialize_with = "convert_boolean_with_shellexpand"
1677
    )]
1678
    pub experimental_remote_cache_compression: Option<bool>,
1679
}
1680
1681
impl GrpcSpec {
1682
    /// Whether this store should use REAPI zstd wire compression.
1683
    #[must_use]
1684
0
    pub fn remote_cache_compression_enabled(&self) -> bool {
1685
0
        self.experimental_remote_cache_compression.unwrap_or(false)
1686
0
    }
1687
}
1688
1689
/// Configuration for experimental small-blob read coalescing in a gRPC
1690
/// store. See [`GrpcSpec::experimental_read_batching`].
1691
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
1692
#[serde(deny_unknown_fields)]
1693
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1694
pub struct GrpcReadBatchingConfig {
1695
    /// Only blobs at or below this size (in bytes) are eligible for
1696
    /// batching. Larger blobs always use the `ByteStream` `Read` path.
1697
    ///
1698
    /// Default: 131072 (128 KiB).
1699
    #[serde(
1700
        default = "default_read_batching_max_blob_size_bytes",
1701
        deserialize_with = "convert_data_size_with_shellexpand"
1702
    )]
1703
    pub max_blob_size_bytes: u64,
1704
1705
    /// Maximum total payload bytes packed into a single `BatchReadBlobs`
1706
    /// request. This should leave headroom under the 4 MiB default gRPC
1707
    /// message limit for protobuf framing overhead.
1708
    ///
1709
    /// Default: 3145728 (3 MiB).
1710
    #[serde(
1711
        default = "default_read_batching_max_batch_bytes",
1712
        deserialize_with = "convert_data_size_with_shellexpand"
1713
    )]
1714
    pub max_batch_bytes: u64,
1715
1716
    /// Maximum number of concurrent `BatchReadBlobs` RPCs dispatched by the
1717
    /// coalescer. Must be greater than zero.
1718
    ///
1719
    /// Default: 4.
1720
    #[serde(
1721
        default = "default_read_batching_dispatch_slots",
1722
        deserialize_with = "convert_numeric_with_shellexpand"
1723
    )]
1724
    pub dispatch_slots: usize,
1725
1726
    /// Bound on the number of payload bytes waiting in the coalescer queue.
1727
    /// When exceeded, new read requests bypass batching and fall back to the
1728
    /// regular `ByteStream` `Read` path instead of blocking.
1729
    ///
1730
    /// Default: 33554432 (32 MiB).
1731
    #[serde(
1732
        default = "default_read_batching_max_queued_bytes",
1733
        deserialize_with = "convert_data_size_with_shellexpand"
1734
    )]
1735
    pub max_queued_bytes: u64,
1736
}
1737
1738
0
const fn default_read_batching_max_blob_size_bytes() -> u64 {
1739
0
    128 * 1024 // 128 KiB.
1740
0
}
1741
1742
0
const fn default_read_batching_max_batch_bytes() -> u64 {
1743
0
    3 * 1024 * 1024 // 3 MiB.
1744
0
}
1745
1746
0
const fn default_read_batching_dispatch_slots() -> usize {
1747
0
    4
1748
0
}
1749
1750
0
const fn default_read_batching_max_queued_bytes() -> u64 {
1751
0
    32 * 1024 * 1024 // 32 MiB.
1752
0
}
1753
1754
/// The possible error codes that might occur on an upstream request.
1755
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
1756
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1757
pub enum ErrorCode {
1758
    Cancelled = 1,
1759
    Unknown = 2,
1760
    InvalidArgument = 3,
1761
    DeadlineExceeded = 4,
1762
    NotFound = 5,
1763
    AlreadyExists = 6,
1764
    PermissionDenied = 7,
1765
    ResourceExhausted = 8,
1766
    FailedPrecondition = 9,
1767
    Aborted = 10,
1768
    OutOfRange = 11,
1769
    Unimplemented = 12,
1770
    Internal = 13,
1771
    Unavailable = 14,
1772
    DataLoss = 15,
1773
    Unauthenticated = 16,
1774
    // Note: This list is duplicated from nativelink-error/lib.rs.
1775
}
1776
1777
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1778
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1779
pub struct RedisSpec {
1780
    /// The hostname or IP address of the Redis server.
1781
    /// Ex: `["redis://username:password@redis-server-url:6380/99"]`
1782
    /// 99 Represents database ID, 6380 represents the port.
1783
    #[serde(deserialize_with = "convert_vec_string_with_shellexpand")]
1784
    pub addresses: Vec<String>,
1785
1786
    /// DEPRECATED: use `command_timeout_ms`
1787
    /// The response timeout for the Redis connection in seconds.
1788
    ///
1789
    /// Default: 10
1790
    #[serde(default)]
1791
    pub response_timeout_s: u64,
1792
1793
    /// DEPRECATED: use `connection_timeout_ms`
1794
    ///
1795
    /// The connection timeout for the Redis connection in seconds.
1796
    ///
1797
    /// Default: 10
1798
    #[serde(default)]
1799
    pub connection_timeout_s: u64,
1800
1801
    /// An optional and experimental Redis channel to publish write events to.
1802
    ///
1803
    /// If set, every time a write operation is made to a Redis node
1804
    /// then an event will be published to a Redis channel with the given name.
1805
    /// If unset, the writes will still be made,
1806
    /// but the write events will not be published.
1807
    ///
1808
    /// Default: (Empty String / No Channel)
1809
    #[serde(default)]
1810
    pub experimental_pub_sub_channel: Option<String>,
1811
1812
    /// An optional prefix to prepend to all keys in this store.
1813
    ///
1814
    /// Setting this value can make it convenient to query or
1815
    /// organize your data according to the shared prefix.
1816
    ///
1817
    /// Default: (Empty String / No Prefix)
1818
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1819
    pub key_prefix: String,
1820
1821
    /// Set the mode Redis is operating in.
1822
    ///
1823
    /// Available options are "cluster" for
1824
    /// [cluster mode](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/),
1825
    /// "sentinel" for [sentinel mode](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/),
1826
    /// or "standard" if Redis is operating in neither cluster nor sentinel mode.
1827
    ///
1828
    /// Default: standard,
1829
    #[serde(default)]
1830
    pub mode: RedisMode,
1831
1832
    /// Deprecated as redis-rs doesn't use it
1833
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1834
    pub broadcast_channel_capacity: usize,
1835
1836
    /// The amount of time in milliseconds until the redis store considers the
1837
    /// command to be timed out. This will trigger a retry of the command and
1838
    /// potentially a reconnection to the redis server.
1839
    ///
1840
    /// Default: 10000 (10 seconds)
1841
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1842
    pub command_timeout_ms: u64,
1843
1844
    /// The amount of time in milliseconds until the redis store considers the
1845
    /// connection to unresponsive. This will trigger a reconnection to the
1846
    /// redis server.
1847
    ///
1848
    /// Default: 3000 (3 seconds)
1849
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1850
    pub connection_timeout_ms: u64,
1851
1852
    /// Per-call ceiling for the `check_health` PING in milliseconds.
1853
    ///
1854
    /// Default: 4000 (4 seconds)
1855
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1856
    pub health_check_timeout_ms: u64,
1857
1858
    /// The amount of data to read from the redis server at a time.
1859
    /// This is used to limit the amount of memory used when reading
1860
    /// large objects from the redis server as well as limiting the
1861
    /// amount of time a single read operation can take.
1862
    ///
1863
    /// IMPORTANT: If this value is too high, the `command_timeout_ms`
1864
    /// might be triggered if the latency or throughput to the redis
1865
    /// server is too low.
1866
    ///
1867
    /// Default: 64KiB
1868
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1869
    pub read_chunk_size: usize,
1870
1871
    /// The number of connections to keep open to the redis servers.
1872
    ///
1873
    /// Default: 3
1874
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1875
    pub connection_pool_size: usize,
1876
1877
    /// Expire keys written by this store after this many seconds.
1878
    ///
1879
    /// Redis does not expire these on its own, so a store whose consumer
1880
    /// stops keeps every key it ever wrote. A BEP store is the case this
1881
    /// exists for: if the ETL stops consuming, the backlog grows until the
1882
    /// Redis node runs out of memory.
1883
    ///
1884
    /// Set this per store, not globally. The same store type backs the CAS
1885
    /// fast tier and the scheduler, and expiring scheduler state would drop
1886
    /// in-flight actions.
1887
    ///
1888
    /// This trades data for a bound. Anything not consumed within the window
1889
    /// is deleted, so the value has to exceed the longest consumer outage you
1890
    /// intend to survive, and it must also exceed how long a single upload can
1891
    /// take: the temp key an upload builds carries this same TTL, so a value
1892
    /// below the upload duration would expire the write in flight.
1893
    ///
1894
    /// Zero disables expiry. One second is the smallest value that enables it,
1895
    /// and any value that small is almost certainly a mistake.
1896
    ///
1897
    /// Default: 0 (keys never expire)
1898
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1899
    pub key_ttl_s: u64,
1900
1901
    /// The maximum number of upload chunks to allow per update.
1902
    /// This is used to limit the amount of memory used when uploading
1903
    /// large objects to the redis server. A good rule of thumb is to
1904
    /// think of the data as:
1905
    /// `AVAIL_MEMORY / (read_chunk_size * max_chunk_uploads_per_update) = THORETICAL_MAX_CONCURRENT_UPLOADS`
1906
    /// (note: it's a good idea to divide `AVAIL_MAX_MEMORY` by ~10 to account for other memory usage)
1907
    ///
1908
    /// Default: 10
1909
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1910
    pub max_chunk_uploads_per_update: usize,
1911
1912
    /// The COUNT value passed when scanning keys in Redis.
1913
    /// This is used to hint the amount of work that should be done per response.
1914
    ///
1915
    /// Default: 10000
1916
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1917
    pub scan_count: usize,
1918
1919
    /// Retry configuration to use when a network request fails.
1920
    #[serde(default)]
1921
    pub retry: Retry,
1922
1923
    /// Maximum number of permitted actions to the Redis store at any one time
1924
    /// This stops problems with timeouts due to many, many inflight actions
1925
    /// Default: 500
1926
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1927
    pub max_client_permits: usize,
1928
1929
    /// Maximum number of items returned per cursor for the search indexes
1930
    /// May reduce thundering herd issues with worker provisioner at higher node counts,
1931
    /// Default: 1500
1932
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1933
    pub max_count_per_cursor: u64,
1934
}
1935
1936
#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
1937
#[serde(rename_all = "snake_case")]
1938
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1939
pub enum RedisMode {
1940
    /// Use Redis Cluster.
1941
    Cluster,
1942
1943
    /// Use Redis Sentinel.
1944
    Sentinel,
1945
1946
    /// Use a standalone Redis server.
1947
    #[default]
1948
    Standard,
1949
}
1950
1951
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
1952
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1953
pub struct NoopSpec {}
1954
1955
/// Retry configuration. This configuration is exponential and each iteration
1956
/// a jitter as a percentage is applied of the calculated delay. For example:
1957
/// ```haskell
1958
/// Retry{
1959
///   max_retries: 7,
1960
///   delay: 0.1,
1961
///   jitter: 0.5,
1962
/// }
1963
/// ```
1964
/// will result in:
1965
/// Attempt - Delay
1966
/// 1         0ms
1967
/// 2         75ms - 125ms
1968
/// 3         150ms - 250ms
1969
/// 4         300ms - 500ms
1970
/// 5         600ms - 1s
1971
/// 6         1.2s - 2s
1972
/// 7         2.4s - 4s
1973
/// 8         4.8s - 8s
1974
/// Remember that to get total results is additive, meaning the above results
1975
/// would mean a single request would have a total delay of 9.525s - 15.875s.
1976
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
1977
#[serde(deny_unknown_fields)]
1978
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1979
pub struct Retry {
1980
    /// Maximum number of retries until retrying stops.
1981
    /// Setting this to zero will always attempt 1 time, but not retry.
1982
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1983
    pub max_retries: usize,
1984
1985
    /// Delay in seconds for exponential back off.
1986
    #[serde(default)]
1987
    pub delay: f32,
1988
1989
    /// Amount of jitter to add as a percentage in decimal form. This will
1990
    /// change the formula like:
1991
    /// ```haskell
1992
    /// random(
1993
    ///    (2 ^ {attempt_number}) * {delay} * (1 - (jitter / 2)),
1994
    ///    (2 ^ {attempt_number}) * {delay} * (1 + (jitter / 2)),
1995
    /// )
1996
    /// ```
1997
    #[serde(default)]
1998
    pub jitter: f32,
1999
2000
    /// A list of error codes to retry on, if this isn't set then the default
2001
    /// error codes to retry on are used. These default codes are the most
2002
    /// likely to be non-permanent.
2003
    ///  - `Unknown`
2004
    ///  - `Cancelled`
2005
    ///  - `DeadlineExceeded`
2006
    ///  - `ResourceExhausted`
2007
    ///  - `Aborted`
2008
    ///  - `Internal`
2009
    ///  - `Unavailable`
2010
    ///  - `DataLoss`
2011
    #[serde(default)]
2012
    pub retry_on_errors: Option<Vec<ErrorCode>>,
2013
}
2014
2015
/// Configuration for `ExperimentalMongoDB` store.
2016
#[derive(Serialize, Deserialize, Debug, Clone)]
2017
#[serde(deny_unknown_fields)]
2018
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
2019
pub struct ExperimentalMongoSpec {
2020
    /// `ExperimentalMongoDB` connection string.
2021
    /// Example: <mongodb://localhost:27017> or <mongodb+srv://cluster.mongodb.net>
2022
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
2023
    pub connection_string: String,
2024
2025
    /// The database name to use.
2026
    /// Default: "nativelink"
2027
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
2028
    pub database: String,
2029
2030
    /// The collection name for CAS data.
2031
    /// Default: "cas"
2032
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
2033
    pub cas_collection: String,
2034
2035
    /// The collection name for scheduler data.
2036
    /// Default: "scheduler"
2037
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
2038
    pub scheduler_collection: String,
2039
2040
    /// Prefix to prepend to all keys stored in `MongoDB`.
2041
    /// Default: ""
2042
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
2043
    pub key_prefix: Option<String>,
2044
2045
    /// The maximum amount of data to read from `MongoDB` in a single chunk (in bytes).
2046
    /// Default: 65536 (64KB)
2047
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
2048
    pub read_chunk_size: usize,
2049
2050
    /// Deprecated, unused
2051
    /// Maximum number of concurrent uploads allowed.
2052
    /// Default: 10
2053
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
2054
    pub max_concurrent_uploads: usize,
2055
2056
    /// Connection timeout in milliseconds.
2057
    /// Default: 3000
2058
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
2059
    pub connection_timeout_ms: u64,
2060
2061
    /// Command timeout in milliseconds.
2062
    /// Default: 10000
2063
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
2064
    pub command_timeout_ms: u64,
2065
2066
    /// Enable `MongoDB` change streams for real-time updates.
2067
    /// Required for scheduler subscriptions.
2068
    /// Default: false
2069
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
2070
    pub enable_change_streams: bool,
2071
2072
    /// Write concern 'w' parameter.
2073
    /// Can be a number (e.g., 1) or string (e.g., "majority").
2074
    /// Default: None (uses `MongoDB` default)
2075
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
2076
    pub write_concern_w: Option<String>,
2077
2078
    /// Write concern 'j' parameter (journal acknowledgment).
2079
    /// Default: None (uses `MongoDB` default)
2080
    #[serde(default)]
2081
    pub write_concern_j: Option<bool>,
2082
2083
    /// Write concern timeout in milliseconds.
2084
    /// Default: None (uses `MongoDB` default)
2085
    #[serde(
2086
        default,
2087
        deserialize_with = "convert_optional_numeric_with_shellexpand"
2088
    )]
2089
    pub write_concern_timeout_ms: Option<u32>,
2090
2091
    /// Limits the number of requests at any one time
2092
    /// Default: Unlimited
2093
    #[serde(
2094
        default,
2095
        deserialize_with = "convert_optional_numeric_with_shellexpand"
2096
    )]
2097
    pub max_requests: Option<usize>,
2098
}
2099
2100
impl Retry {
2101
57
    pub fn make_jitter_fn(&self) -> Arc<dyn Fn(Duration) -> Duration + Send + Sync> {
2102
57
        if self.jitter == 0f32 {
2103
57
            Arc::new(move |delay: Duration| delay)
2104
        } else {
2105
0
            let local_jitter = self.jitter;
2106
0
            Arc::new(move |delay: Duration| {
2107
0
                delay.mul_f32(local_jitter.mul_add(rand::rng().random::<f32>() - 0.5, 1.))
2108
0
            })
2109
        }
2110
57
    }
2111
}