/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 | | } |