Coverage Report

Created: 2026-08-14 20:52

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
178
    pub(crate) fn visit_grpc_specs(&self, visitor: &mut dyn FnMut(&GrpcSpec)) {
651
178
        match self {
652
1
            Self::CacheMetrics(spec) => spec.backend.visit_grpc_specs(visitor),
653
8
            Self::Verify(spec) => spec.backend.visit_grpc_specs(visitor),
654
11
            Self::Compression(spec) => spec.backend.visit_grpc_specs(visitor),
655
8
            Self::Dedup(spec) => {
656
8
                spec.index_store.visit_grpc_specs(visitor);
657
8
                spec.content_store.visit_grpc_specs(visitor);
658
8
            }
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
28
            Self::FastSlow(spec) => {
665
28
                spec.fast.visit_grpc_specs(visitor);
666
28
                spec.slow.visit_grpc_specs(visitor);
667
28
            }
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
100
            | Self::ExperimentalMongo(_) => {}
686
        }
687
178
    }
688
689
174
    pub(crate) fn visit_grpc_specs_mut(&mut self, visitor: &mut dyn FnMut(&mut GrpcSpec)) {
690
174
        match self {
691
1
            Self::CacheMetrics(spec) => spec.backend.visit_grpc_specs_mut(visitor),
692
8
            Self::Verify(spec) => spec.backend.visit_grpc_specs_mut(visitor),
693
11
            Self::Compression(spec) => spec.backend.visit_grpc_specs_mut(visitor),
694
8
            Self::Dedup(spec) => {
695
8
                spec.index_store.visit_grpc_specs_mut(visitor);
696
8
                spec.content_store.visit_grpc_specs_mut(visitor);
697
8
            }
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
28
            Self::FastSlow(spec) => {
704
28
                spec.fast.visit_grpc_specs_mut(visitor);
705
28
                spec.slow.visit_grpc_specs_mut(visitor);
706
28
            }
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
100
            | Self::ExperimentalMongo(_) => {}
725
        }
726
174
    }
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
    /// Common retry and upload configuration
1246
    #[serde(flatten)]
1247
    pub common: CommonObjectSpec,
1248
}
1249
1250
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1251
#[serde(deny_unknown_fields)]
1252
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1253
pub struct ExperimentalGcsSpec {
1254
    /// Bucket name to use as the backend.
1255
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1256
    pub bucket: String,
1257
1258
    /// Chunk size for resumable uploads.
1259
    ///
1260
    /// Default: 2MB
1261
    #[serde(
1262
        default,
1263
        deserialize_with = "convert_optional_data_size_with_shellexpand"
1264
    )]
1265
    pub resumable_chunk_size: Option<usize>,
1266
1267
    /// Common retry and upload configuration
1268
    #[serde(flatten)]
1269
    pub common: CommonObjectSpec,
1270
1271
    /// Error if authentication was not found.
1272
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1273
    pub authentication_required: bool,
1274
1275
    /// Connection timeout in milliseconds.
1276
    /// Default: 3000
1277
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1278
    pub connection_timeout_s: u64,
1279
1280
    /// Read timeout in milliseconds.
1281
    /// Default: 3000
1282
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1283
    pub read_timeout_s: u64,
1284
}
1285
1286
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1287
#[serde(deny_unknown_fields)]
1288
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1289
pub struct ExperimentalAzureSpec {
1290
    /// The Azure Storage account name. Used to build the default container URL
1291
    /// `https://{account_name}.blob.core.windows.net/{container}` when `sas_url`
1292
    /// is not provided.
1293
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1294
    pub account_name: String,
1295
1296
    /// The container name to use as the backend.
1297
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1298
    pub container: String,
1299
1300
    /// Optional blob endpoint host override (for example an Azurite emulator host
1301
    /// such as `http://127.0.0.1:10000/devstoreaccount1`). When set, this replaces
1302
    /// the default `https://{account_name}.blob.core.windows.net` endpoint. The
1303
    /// container is always appended to form the final container URL. Ignored when
1304
    /// `sas_url` is set.
1305
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1306
    pub endpoint: Option<String>,
1307
1308
    /// Optional pre-formed SAS URL pointing at the container. When set, the store
1309
    /// uses it directly as the container URL with no credential (the SAS token is
1310
    /// expected to already be present in the URL), and `account_name`, `container`,
1311
    /// and `endpoint` are ignored for URL construction.
1312
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1313
    pub sas_url: Option<String>,
1314
1315
    /// Common retry and upload configuration.
1316
    #[serde(flatten)]
1317
    pub common: CommonObjectSpec,
1318
}
1319
1320
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
1321
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1322
pub struct CommonObjectSpec {
1323
    /// If you wish to prefix the location in the bucket. If None, no prefix will be used.
1324
    #[serde(default)]
1325
    pub key_prefix: Option<String>,
1326
1327
    /// Retry configuration to use when a network request fails.
1328
    #[serde(default)]
1329
    pub retry: Retry,
1330
1331
    /// If the number of seconds since the `last_modified` time of the object
1332
    /// is greater than this value, the object will not be considered
1333
    /// "existing". This allows for external tools to delete objects that
1334
    /// have not been uploaded in a long time. If a client receives a `NotFound`
1335
    /// the client should re-upload the object.
1336
    ///
1337
    /// There should be sufficient buffer time between how long the expiration
1338
    /// configuration of the external tool is and this value. Keeping items
1339
    /// around for a few days is generally a good idea.
1340
    ///
1341
    /// Default: 0. Zero means never consider an object expired.
1342
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1343
    pub consider_expired_after_s: u32,
1344
1345
    /// The maximum buffer size to retain in case of a retryable error
1346
    /// during upload. Setting this to zero will disable upload buffering;
1347
    /// this means that in the event of a failure during upload, the entire
1348
    /// upload will be aborted and the client will likely receive an error.
1349
    ///
1350
    /// Default: 5MB.
1351
    #[serde(
1352
        default,
1353
        deserialize_with = "convert_optional_data_size_with_shellexpand"
1354
    )]
1355
    pub max_retry_buffer_per_request: Option<usize>,
1356
1357
    /// Maximum number of concurrent `UploadPart` requests per `MultipartUpload`.
1358
    ///
1359
    /// Default: 10.
1360
    ///
1361
    #[serde(
1362
        default,
1363
        deserialize_with = "convert_optional_numeric_with_shellexpand"
1364
    )]
1365
    pub multipart_max_concurrent_uploads: Option<usize>,
1366
1367
    /// Allow unencrypted HTTP connections. Only use this for local testing.
1368
    ///
1369
    /// Default: false
1370
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1371
    pub insecure_allow_http: bool,
1372
1373
    /// Disable HTTP/2 connections and only use HTTP/1.1. Default client
1374
    /// configuration will have HTTP/1.1 and HTTP/2 enabled for connection
1375
    /// schemes. HTTP/2 should be disabled if environments have poor support
1376
    /// or performance related to HTTP/2. Safe to keep default unless
1377
    /// underlying network environment, S3, or GCS API servers specify otherwise.
1378
    ///
1379
    /// Default: false
1380
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1381
    pub disable_http2: bool,
1382
}
1383
1384
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
1385
#[serde(rename_all = "snake_case")]
1386
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1387
pub enum StoreType {
1388
    /// The store is content addressable storage.
1389
    Cas,
1390
    /// The store is an action cache.
1391
    Ac,
1392
}
1393
1394
#[derive(Serialize, Deserialize, Debug, Clone)]
1395
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1396
pub struct ClientTlsConfig {
1397
    /// Path to the certificate authority to use to validate the remote.
1398
    ///
1399
    /// Default: None
1400
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1401
    pub ca_file: Option<String>,
1402
1403
    /// Path to the certificate file for client authentication.
1404
    ///
1405
    /// Default: None
1406
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1407
    pub cert_file: Option<String>,
1408
1409
    /// Path to the private key file for client authentication.
1410
    ///
1411
    /// Default: None
1412
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1413
    pub key_file: Option<String>,
1414
1415
    /// If set the client will use the native roots for TLS connections.
1416
    ///
1417
    /// Default: false
1418
    #[serde(default)]
1419
    pub use_native_roots: Option<bool>,
1420
}
1421
1422
#[derive(Serialize, Deserialize, Debug, Clone)]
1423
#[serde(deny_unknown_fields)]
1424
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1425
pub struct GrpcEndpoint {
1426
    /// The endpoint address (i.e. `grpc(s)://example.com:443`).
1427
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
1428
    pub address: String,
1429
    /// The TLS configuration to use to connect to the endpoint (if grpcs).
1430
    pub tls_config: Option<ClientTlsConfig>,
1431
    /// The maximum concurrency to allow on this endpoint.
1432
    #[serde(
1433
        default,
1434
        deserialize_with = "convert_optional_numeric_with_shellexpand"
1435
    )]
1436
    pub concurrency_limit: Option<usize>,
1437
1438
    /// Timeout for establishing a TCP connection to the endpoint (seconds).
1439
    /// If not set or 0, defaults to 30 seconds.
1440
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1441
    pub connect_timeout_s: u64,
1442
1443
    /// TCP keepalive interval (seconds). Sends TCP keepalive probes at this
1444
    /// interval to detect dead connections at the OS level.
1445
    /// If not set or 0, defaults to 30 seconds.
1446
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1447
    pub tcp_keepalive_s: u64,
1448
1449
    /// HTTP/2 keepalive interval (seconds). Sends HTTP/2 PING frames at this
1450
    /// interval to detect dead connections at the application level.
1451
    /// If not set or 0, defaults to 30 seconds.
1452
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1453
    pub http2_keepalive_interval_s: u64,
1454
1455
    /// HTTP/2 keepalive timeout (seconds). If a PING response is not received
1456
    /// within this duration, the connection is considered dead.
1457
    /// If not set or 0, defaults to 20 seconds.
1458
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1459
    pub http2_keepalive_timeout_s: u64,
1460
}
1461
1462
#[derive(Serialize, Deserialize, Debug, Clone)]
1463
#[serde(deny_unknown_fields)]
1464
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1465
pub struct GrpcSpec {
1466
    /// Instance name for gRPC calls. Proxy calls will have the `instance_name` changed to this.
1467
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1468
    pub instance_name: String,
1469
1470
    /// The endpoint of the grpc connection.
1471
    pub endpoints: Vec<GrpcEndpoint>,
1472
1473
    /// The type of the upstream store, this ensures that the correct server calls are made.
1474
    pub store_type: StoreType,
1475
1476
    /// Retry configuration to use when a network request fails.
1477
    #[serde(default)]
1478
    pub retry: Retry,
1479
1480
    /// Limit the number of simultaneous upstream requests to this many. A
1481
    /// value of zero is treated as unlimited. If the limit is reached the
1482
    /// request is queued.
1483
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1484
    pub max_concurrent_requests: usize,
1485
1486
    /// The number of connections to make to each specified endpoint to balance
1487
    /// the load over multiple TCP connections.
1488
    /// Default: 1.
1489
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1490
    pub connections_per_endpoint: usize,
1491
1492
    /// Maximum time (seconds) allowed for a single RPC request (e.g. a
1493
    /// `ByteStream.Write` call) before it is cancelled.
1494
    ///
1495
    /// A value of 0 (the default) disables the per-RPC timeout. Dead
1496
    /// connections are still detected by the HTTP/2 and TCP keepalive
1497
    /// mechanisms configured on each endpoint.
1498
    ///
1499
    /// For large uploads (multi-GB), either leave this at 0 or set it
1500
    /// large enough to accommodate the full transfer time.
1501
    ///
1502
    /// Default: 0 (disabled)
1503
    #[serde(default, deserialize_with = "convert_duration_with_shellexpand")]
1504
    pub rpc_timeout_s: u64,
1505
1506
    /// Use legacy `ByteStream` resource name format, omitting the digest
1507
    /// function component from the path.
1508
    ///
1509
    /// Modern `NativeLink` generates resource names like:
1510
    ///   `{instance}/blobs/{digest_function}/{hash}/{size}`
1511
    ///
1512
    /// Older backends (e.g. Buildbarn pre-v0.3) expect the original format:
1513
    ///   `{instance}/blobs/{hash}/{size}`
1514
    ///
1515
    /// Set this to `true` when connecting to such backends to avoid
1516
    /// `InvalidArgument: Unsupported digest function` errors.
1517
    ///
1518
    /// Default: false
1519
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1520
    pub use_legacy_resource_names: bool,
1521
1522
    /// Static headers to attach to every outgoing gRPC request sent to this
1523
    /// store's upstream endpoints. Useful for fixed authentication tokens
1524
    /// (e.g. `{"authorization": "Bearer <token>"}`) and other static metadata.
1525
    #[serde(default)]
1526
    pub headers: HashMap<String, String>,
1527
1528
    /// Header names to forward from the incoming client request to every
1529
    /// outgoing upstream request. The header value is taken from the client
1530
    /// request that triggered this store operation. Use this to pass through
1531
    /// dynamic credentials such as JWT tokens sent by build clients.
1532
    ///
1533
    /// Example: `["authorization", "x-custom-token"]`
1534
    ///
1535
    /// `NativeLink` also automatically injects the current OpenTelemetry trace
1536
    /// context (`traceparent` / `tracestate`) into every outgoing request.
1537
    #[serde(default)]
1538
    pub forward_headers: Vec<String>,
1539
1540
    /// Optional and experimental: coalesce small-blob reads into
1541
    /// `BatchReadBlobs` RPCs instead of issuing one `ByteStream` `Read`
1542
    /// stream per blob. Each `ByteStream` read carries a fixed per-RPC cost,
1543
    /// so batching many small reads into a single `BatchReadBlobs` request
1544
    /// can dramatically reduce read latency for small blobs.
1545
    ///
1546
    /// Only full reads (offset 0, whole blob) of blobs at or below
1547
    /// `max_blob_size_bytes` are batched; everything else continues to use
1548
    /// the `ByteStream` `Read` path.
1549
    ///
1550
    /// Incompatible with `forward_headers`: batched reads share one upstream
1551
    /// RPC across many client requests, so per-client forwarded headers
1552
    /// (e.g. credentials) cannot be attached. Configuring both is rejected
1553
    /// at startup.
1554
    ///
1555
    /// Default: unset (disabled). When unset there is zero behavior change.
1556
    #[serde(default)]
1557
    pub experimental_read_batching: Option<GrpcReadBatchingConfig>,
1558
1559
    /// Compress this store's own blob transfers on the wire with REAPI
1560
    /// `compressed-blobs/zstd`. Uploads and full-blob downloads of blobs at
1561
    /// or above 64 KiB are zstd-compressed; smaller blobs and ranged reads
1562
    /// keep the identity path.
1563
    ///
1564
    /// The upstream instance must have
1565
    /// `capabilities.remote_cache_compression` enabled, otherwise compressed
1566
    /// requests fail with `InvalidArgument`. This setting is what makes
1567
    /// NativeLink-to-NativeLink hops (for example worker to CAS) benefit
1568
    /// from wire compression; it is independent of what external clients
1569
    /// such as Bazel negotiate for themselves.
1570
    ///
1571
    /// Compressed uploads do not resume mid-stream (mirroring the REAPI
1572
    /// server contract): a transport failure part-way through a compressed
1573
    /// upload surfaces immediately to the caller instead of retrying, and
1574
    /// outer callers retry the whole upload.
1575
    ///
1576
    /// When combined with `experimental_chunked_uploads`, chunked uploads
1577
    /// take precedence for blobs at or above the chunking threshold.
1578
    ///
1579
    /// When zstd wire compression is enabled elsewhere in the process,
1580
    /// omitting this setting enables it automatically for CAS gRPC stores.
1581
    /// Set it explicitly to `false` to opt this store out.
1582
    ///
1583
    /// Default: inherited from the process-wide zstd wire-compression intent.
1584
    #[serde(
1585
        default,
1586
        skip_serializing_if = "Option::is_none",
1587
        deserialize_with = "convert_boolean_with_shellexpand"
1588
    )]
1589
    pub experimental_remote_cache_compression: Option<bool>,
1590
}
1591
1592
impl GrpcSpec {
1593
    /// Whether this store should use REAPI zstd wire compression.
1594
    #[must_use]
1595
0
    pub fn remote_cache_compression_enabled(&self) -> bool {
1596
0
        self.experimental_remote_cache_compression.unwrap_or(false)
1597
0
    }
1598
}
1599
1600
/// Configuration for experimental small-blob read coalescing in a gRPC
1601
/// store. See [`GrpcSpec::experimental_read_batching`].
1602
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
1603
#[serde(deny_unknown_fields)]
1604
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1605
pub struct GrpcReadBatchingConfig {
1606
    /// Only blobs at or below this size (in bytes) are eligible for
1607
    /// batching. Larger blobs always use the `ByteStream` `Read` path.
1608
    ///
1609
    /// Default: 131072 (128 KiB).
1610
    #[serde(
1611
        default = "default_read_batching_max_blob_size_bytes",
1612
        deserialize_with = "convert_data_size_with_shellexpand"
1613
    )]
1614
    pub max_blob_size_bytes: u64,
1615
1616
    /// Maximum total payload bytes packed into a single `BatchReadBlobs`
1617
    /// request. This should leave headroom under the 4 MiB default gRPC
1618
    /// message limit for protobuf framing overhead.
1619
    ///
1620
    /// Default: 3145728 (3 MiB).
1621
    #[serde(
1622
        default = "default_read_batching_max_batch_bytes",
1623
        deserialize_with = "convert_data_size_with_shellexpand"
1624
    )]
1625
    pub max_batch_bytes: u64,
1626
1627
    /// Maximum number of concurrent `BatchReadBlobs` RPCs dispatched by the
1628
    /// coalescer. Must be greater than zero.
1629
    ///
1630
    /// Default: 4.
1631
    #[serde(
1632
        default = "default_read_batching_dispatch_slots",
1633
        deserialize_with = "convert_numeric_with_shellexpand"
1634
    )]
1635
    pub dispatch_slots: usize,
1636
1637
    /// Bound on the number of payload bytes waiting in the coalescer queue.
1638
    /// When exceeded, new read requests bypass batching and fall back to the
1639
    /// regular `ByteStream` `Read` path instead of blocking.
1640
    ///
1641
    /// Default: 33554432 (32 MiB).
1642
    #[serde(
1643
        default = "default_read_batching_max_queued_bytes",
1644
        deserialize_with = "convert_data_size_with_shellexpand"
1645
    )]
1646
    pub max_queued_bytes: u64,
1647
}
1648
1649
0
const fn default_read_batching_max_blob_size_bytes() -> u64 {
1650
0
    128 * 1024 // 128 KiB.
1651
0
}
1652
1653
0
const fn default_read_batching_max_batch_bytes() -> u64 {
1654
0
    3 * 1024 * 1024 // 3 MiB.
1655
0
}
1656
1657
0
const fn default_read_batching_dispatch_slots() -> usize {
1658
0
    4
1659
0
}
1660
1661
0
const fn default_read_batching_max_queued_bytes() -> u64 {
1662
0
    32 * 1024 * 1024 // 32 MiB.
1663
0
}
1664
1665
/// The possible error codes that might occur on an upstream request.
1666
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
1667
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1668
pub enum ErrorCode {
1669
    Cancelled = 1,
1670
    Unknown = 2,
1671
    InvalidArgument = 3,
1672
    DeadlineExceeded = 4,
1673
    NotFound = 5,
1674
    AlreadyExists = 6,
1675
    PermissionDenied = 7,
1676
    ResourceExhausted = 8,
1677
    FailedPrecondition = 9,
1678
    Aborted = 10,
1679
    OutOfRange = 11,
1680
    Unimplemented = 12,
1681
    Internal = 13,
1682
    Unavailable = 14,
1683
    DataLoss = 15,
1684
    Unauthenticated = 16,
1685
    // Note: This list is duplicated from nativelink-error/lib.rs.
1686
}
1687
1688
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1689
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1690
pub struct RedisSpec {
1691
    /// The hostname or IP address of the Redis server.
1692
    /// Ex: `["redis://username:password@redis-server-url:6380/99"]`
1693
    /// 99 Represents database ID, 6380 represents the port.
1694
    #[serde(deserialize_with = "convert_vec_string_with_shellexpand")]
1695
    pub addresses: Vec<String>,
1696
1697
    /// DEPRECATED: use `command_timeout_ms`
1698
    /// The response timeout for the Redis connection in seconds.
1699
    ///
1700
    /// Default: 10
1701
    #[serde(default)]
1702
    pub response_timeout_s: u64,
1703
1704
    /// DEPRECATED: use `connection_timeout_ms`
1705
    ///
1706
    /// The connection timeout for the Redis connection in seconds.
1707
    ///
1708
    /// Default: 10
1709
    #[serde(default)]
1710
    pub connection_timeout_s: u64,
1711
1712
    /// An optional and experimental Redis channel to publish write events to.
1713
    ///
1714
    /// If set, every time a write operation is made to a Redis node
1715
    /// then an event will be published to a Redis channel with the given name.
1716
    /// If unset, the writes will still be made,
1717
    /// but the write events will not be published.
1718
    ///
1719
    /// Default: (Empty String / No Channel)
1720
    #[serde(default)]
1721
    pub experimental_pub_sub_channel: Option<String>,
1722
1723
    /// An optional prefix to prepend to all keys in this store.
1724
    ///
1725
    /// Setting this value can make it convenient to query or
1726
    /// organize your data according to the shared prefix.
1727
    ///
1728
    /// Default: (Empty String / No Prefix)
1729
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1730
    pub key_prefix: String,
1731
1732
    /// Set the mode Redis is operating in.
1733
    ///
1734
    /// Available options are "cluster" for
1735
    /// [cluster mode](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/),
1736
    /// "sentinel" for [sentinel mode](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/),
1737
    /// or "standard" if Redis is operating in neither cluster nor sentinel mode.
1738
    ///
1739
    /// Default: standard,
1740
    #[serde(default)]
1741
    pub mode: RedisMode,
1742
1743
    /// Deprecated as redis-rs doesn't use it
1744
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1745
    pub broadcast_channel_capacity: usize,
1746
1747
    /// The amount of time in milliseconds until the redis store considers the
1748
    /// command to be timed out. This will trigger a retry of the command and
1749
    /// potentially a reconnection to the redis server.
1750
    ///
1751
    /// Default: 10000 (10 seconds)
1752
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1753
    pub command_timeout_ms: u64,
1754
1755
    /// The amount of time in milliseconds until the redis store considers the
1756
    /// connection to unresponsive. This will trigger a reconnection to the
1757
    /// redis server.
1758
    ///
1759
    /// Default: 3000 (3 seconds)
1760
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1761
    pub connection_timeout_ms: u64,
1762
1763
    /// Per-call ceiling for the `check_health` PING in milliseconds.
1764
    ///
1765
    /// Default: 4000 (4 seconds)
1766
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1767
    pub health_check_timeout_ms: u64,
1768
1769
    /// The amount of data to read from the redis server at a time.
1770
    /// This is used to limit the amount of memory used when reading
1771
    /// large objects from the redis server as well as limiting the
1772
    /// amount of time a single read operation can take.
1773
    ///
1774
    /// IMPORTANT: If this value is too high, the `command_timeout_ms`
1775
    /// might be triggered if the latency or throughput to the redis
1776
    /// server is too low.
1777
    ///
1778
    /// Default: 64KiB
1779
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1780
    pub read_chunk_size: usize,
1781
1782
    /// The number of connections to keep open to the redis servers.
1783
    ///
1784
    /// Default: 3
1785
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1786
    pub connection_pool_size: usize,
1787
1788
    /// Expire keys written by this store after this many seconds.
1789
    ///
1790
    /// Redis does not expire these on its own, so a store whose consumer
1791
    /// stops keeps every key it ever wrote. A BEP store is the case this
1792
    /// exists for: if the ETL stops consuming, the backlog grows until the
1793
    /// Redis node runs out of memory.
1794
    ///
1795
    /// Set this per store, not globally. The same store type backs the CAS
1796
    /// fast tier and the scheduler, and expiring scheduler state would drop
1797
    /// in-flight actions.
1798
    ///
1799
    /// This trades data for a bound. Anything not consumed within the window
1800
    /// is deleted, so the value has to exceed the longest consumer outage you
1801
    /// intend to survive, and it must also exceed how long a single upload can
1802
    /// take: the temp key an upload builds carries this same TTL, so a value
1803
    /// below the upload duration would expire the write in flight.
1804
    ///
1805
    /// Zero disables expiry. One second is the smallest value that enables it,
1806
    /// and any value that small is almost certainly a mistake.
1807
    ///
1808
    /// Default: 0 (keys never expire)
1809
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1810
    pub key_ttl_s: u64,
1811
1812
    /// The maximum number of upload chunks to allow per update.
1813
    /// This is used to limit the amount of memory used when uploading
1814
    /// large objects to the redis server. A good rule of thumb is to
1815
    /// think of the data as:
1816
    /// `AVAIL_MEMORY / (read_chunk_size * max_chunk_uploads_per_update) = THORETICAL_MAX_CONCURRENT_UPLOADS`
1817
    /// (note: it's a good idea to divide `AVAIL_MAX_MEMORY` by ~10 to account for other memory usage)
1818
    ///
1819
    /// Default: 10
1820
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1821
    pub max_chunk_uploads_per_update: usize,
1822
1823
    /// The COUNT value passed when scanning keys in Redis.
1824
    /// This is used to hint the amount of work that should be done per response.
1825
    ///
1826
    /// Default: 10000
1827
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1828
    pub scan_count: usize,
1829
1830
    /// Retry configuration to use when a network request fails.
1831
    #[serde(default)]
1832
    pub retry: Retry,
1833
1834
    /// Maximum number of permitted actions to the Redis store at any one time
1835
    /// This stops problems with timeouts due to many, many inflight actions
1836
    /// Default: 500
1837
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1838
    pub max_client_permits: usize,
1839
1840
    /// Maximum number of items returned per cursor for the search indexes
1841
    /// May reduce thundering herd issues with worker provisioner at higher node counts,
1842
    /// Default: 1500
1843
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1844
    pub max_count_per_cursor: u64,
1845
}
1846
1847
#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
1848
#[serde(rename_all = "snake_case")]
1849
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1850
pub enum RedisMode {
1851
    /// Use Redis Cluster.
1852
    Cluster,
1853
1854
    /// Use Redis Sentinel.
1855
    Sentinel,
1856
1857
    /// Use a standalone Redis server.
1858
    #[default]
1859
    Standard,
1860
}
1861
1862
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
1863
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1864
pub struct NoopSpec {}
1865
1866
/// Retry configuration. This configuration is exponential and each iteration
1867
/// a jitter as a percentage is applied of the calculated delay. For example:
1868
/// ```haskell
1869
/// Retry{
1870
///   max_retries: 7,
1871
///   delay: 0.1,
1872
///   jitter: 0.5,
1873
/// }
1874
/// ```
1875
/// will result in:
1876
/// Attempt - Delay
1877
/// 1         0ms
1878
/// 2         75ms - 125ms
1879
/// 3         150ms - 250ms
1880
/// 4         300ms - 500ms
1881
/// 5         600ms - 1s
1882
/// 6         1.2s - 2s
1883
/// 7         2.4s - 4s
1884
/// 8         4.8s - 8s
1885
/// Remember that to get total results is additive, meaning the above results
1886
/// would mean a single request would have a total delay of 9.525s - 15.875s.
1887
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
1888
#[serde(deny_unknown_fields)]
1889
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1890
pub struct Retry {
1891
    /// Maximum number of retries until retrying stops.
1892
    /// Setting this to zero will always attempt 1 time, but not retry.
1893
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1894
    pub max_retries: usize,
1895
1896
    /// Delay in seconds for exponential back off.
1897
    #[serde(default)]
1898
    pub delay: f32,
1899
1900
    /// Amount of jitter to add as a percentage in decimal form. This will
1901
    /// change the formula like:
1902
    /// ```haskell
1903
    /// random(
1904
    ///    (2 ^ {attempt_number}) * {delay} * (1 - (jitter / 2)),
1905
    ///    (2 ^ {attempt_number}) * {delay} * (1 + (jitter / 2)),
1906
    /// )
1907
    /// ```
1908
    #[serde(default)]
1909
    pub jitter: f32,
1910
1911
    /// A list of error codes to retry on, if this isn't set then the default
1912
    /// error codes to retry on are used. These default codes are the most
1913
    /// likely to be non-permanent.
1914
    ///  - `Unknown`
1915
    ///  - `Cancelled`
1916
    ///  - `DeadlineExceeded`
1917
    ///  - `ResourceExhausted`
1918
    ///  - `Aborted`
1919
    ///  - `Internal`
1920
    ///  - `Unavailable`
1921
    ///  - `DataLoss`
1922
    #[serde(default)]
1923
    pub retry_on_errors: Option<Vec<ErrorCode>>,
1924
}
1925
1926
/// Configuration for `ExperimentalMongoDB` store.
1927
#[derive(Serialize, Deserialize, Debug, Clone)]
1928
#[serde(deny_unknown_fields)]
1929
#[cfg_attr(feature = "dev-schema", derive(JsonSchema))]
1930
pub struct ExperimentalMongoSpec {
1931
    /// `ExperimentalMongoDB` connection string.
1932
    /// Example: <mongodb://localhost:27017> or <mongodb+srv://cluster.mongodb.net>
1933
    #[serde(deserialize_with = "convert_string_with_shellexpand")]
1934
    pub connection_string: String,
1935
1936
    /// The database name to use.
1937
    /// Default: "nativelink"
1938
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1939
    pub database: String,
1940
1941
    /// The collection name for CAS data.
1942
    /// Default: "cas"
1943
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1944
    pub cas_collection: String,
1945
1946
    /// The collection name for scheduler data.
1947
    /// Default: "scheduler"
1948
    #[serde(default, deserialize_with = "convert_string_with_shellexpand")]
1949
    pub scheduler_collection: String,
1950
1951
    /// Prefix to prepend to all keys stored in `MongoDB`.
1952
    /// Default: ""
1953
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1954
    pub key_prefix: Option<String>,
1955
1956
    /// The maximum amount of data to read from `MongoDB` in a single chunk (in bytes).
1957
    /// Default: 65536 (64KB)
1958
    #[serde(default, deserialize_with = "convert_data_size_with_shellexpand")]
1959
    pub read_chunk_size: usize,
1960
1961
    /// Deprecated, unused
1962
    /// Maximum number of concurrent uploads allowed.
1963
    /// Default: 10
1964
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1965
    pub max_concurrent_uploads: usize,
1966
1967
    /// Connection timeout in milliseconds.
1968
    /// Default: 3000
1969
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1970
    pub connection_timeout_ms: u64,
1971
1972
    /// Command timeout in milliseconds.
1973
    /// Default: 10000
1974
    #[serde(default, deserialize_with = "convert_numeric_with_shellexpand")]
1975
    pub command_timeout_ms: u64,
1976
1977
    /// Enable `MongoDB` change streams for real-time updates.
1978
    /// Required for scheduler subscriptions.
1979
    /// Default: false
1980
    #[serde(default, deserialize_with = "convert_boolean_with_shellexpand")]
1981
    pub enable_change_streams: bool,
1982
1983
    /// Write concern 'w' parameter.
1984
    /// Can be a number (e.g., 1) or string (e.g., "majority").
1985
    /// Default: None (uses `MongoDB` default)
1986
    #[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
1987
    pub write_concern_w: Option<String>,
1988
1989
    /// Write concern 'j' parameter (journal acknowledgment).
1990
    /// Default: None (uses `MongoDB` default)
1991
    #[serde(default)]
1992
    pub write_concern_j: Option<bool>,
1993
1994
    /// Write concern timeout in milliseconds.
1995
    /// Default: None (uses `MongoDB` default)
1996
    #[serde(
1997
        default,
1998
        deserialize_with = "convert_optional_numeric_with_shellexpand"
1999
    )]
2000
    pub write_concern_timeout_ms: Option<u32>,
2001
2002
    /// Limits the number of requests at any one time
2003
    /// Default: Unlimited
2004
    #[serde(
2005
        default,
2006
        deserialize_with = "convert_optional_numeric_with_shellexpand"
2007
    )]
2008
    pub max_requests: Option<usize>,
2009
}
2010
2011
impl Retry {
2012
49
    pub fn make_jitter_fn(&self) -> Arc<dyn Fn(Duration) -> Duration + Send + Sync> {
2013
49
        if self.jitter == 0f32 {
2014
49
            Arc::new(move |delay: Duration| delay)
2015
        } else {
2016
0
            let local_jitter = self.jitter;
2017
0
            Arc::new(move |delay: Duration| {
2018
0
                delay.mul_f32(local_jitter.mul_add(rand::rng().random::<f32>() - 0.5, 1.))
2019
0
            })
2020
        }
2021
49
    }
2022
}