/build/source/nativelink-service/src/wire_compression.rs
Line | Count | Source |
1 | | // Copyright 2026 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 | | //! Wire compression utilities for REAPI compressed-blobs. |
16 | | //! |
17 | | //! This module handles compression/decompression of blob data on the gRPC wire |
18 | | //! between client and server, per the REAPI compressed-blobs specification. |
19 | | //! This is orthogonal to at-rest compression (`CompressionStore` with LZ4). |
20 | | |
21 | | use std::collections::HashSet; |
22 | | |
23 | | use bytes::Bytes; |
24 | | use nativelink_config::cas_server::{CapabilitiesConfig, InstanceName, WithInstanceName}; |
25 | | use nativelink_error::{Code, Error, make_err, make_input_err}; |
26 | | use nativelink_proto::build::bazel::remote::execution::v2::compressor; |
27 | | use nativelink_util::spawn_blocking; |
28 | | // The codecs are shared with client-side GrpcStore transfers; re-export so |
29 | | // existing service callers keep their import paths. |
30 | | pub use nativelink_util::wire_compression::{ |
31 | | ZSTD_COMPRESSION_LEVEL, compress, decompress, stream_decode_compressed_upload, |
32 | | stream_encode_compressed_download, |
33 | | }; |
34 | | use tracing::warn; |
35 | | |
36 | | /// Which instances accept and advertise REAPI compressed-blobs (zstd). |
37 | | /// |
38 | | /// Derived from `CapabilitiesConfig.remote_cache_compression` so that |
39 | | /// advertisement (capabilities) and acceptance (ByteStream/CAS) cannot drift. |
40 | | #[derive(Debug, Clone, Default)] |
41 | | pub struct RemoteCacheCompressionInstances(HashSet<InstanceName>); |
42 | | |
43 | | impl RemoteCacheCompressionInstances { |
44 | | #[must_use] |
45 | 3 | pub fn from_capabilities_configs(configs: &[WithInstanceName<CapabilitiesConfig>]) -> Self { |
46 | | Self( |
47 | 3 | configs |
48 | 3 | .iter() |
49 | 3 | .filter(|config| config.remote_cache_compression) |
50 | 3 | .map(|config| config.instance_name2 .clone2 ()) |
51 | 3 | .collect(), |
52 | | ) |
53 | 3 | } |
54 | | |
55 | | #[must_use] |
56 | 16 | pub fn from_enabled_instance_names( |
57 | 16 | instance_names: impl IntoIterator<Item = InstanceName>, |
58 | 16 | ) -> Self { |
59 | 16 | Self(instance_names.into_iter().collect()) |
60 | 16 | } |
61 | | |
62 | | #[must_use] |
63 | 52 | pub fn enabled_for(&self, instance_name: &str) -> bool { |
64 | 52 | self.0.contains(instance_name) |
65 | 52 | } |
66 | | } |
67 | | |
68 | | /// Resolve a wire compressor from a URI compressor string (as it appears in |
69 | | /// the `compressed-blobs/{compressor}/...` resource name) and validate it |
70 | | /// against whether the instance supports remote cache compression. |
71 | | /// |
72 | | /// Returns `compressor::Value::Identity` when `compressor_str` is `None` or |
73 | | /// `"identity"`. This is the single source of truth for URI-to-compressor |
74 | | /// parsing so that `ByteStream` and any other callers stay consistent. |
75 | 52 | pub fn resolve_wire_compressor( |
76 | 52 | compressor_str: Option<&str>, |
77 | 52 | remote_cache_compression_enabled: bool, |
78 | 52 | ) -> Result<compressor::Value, Error> { |
79 | 52 | match compressor_str { |
80 | 34 | None | Some("identity"21 ) => Ok(compressor::Value::Identity), |
81 | 18 | Some("zstd") => { |
82 | 17 | if remote_cache_compression_enabled { |
83 | 13 | Ok(compressor::Value::Zstd) |
84 | | } else { |
85 | 4 | Err(make_input_err!( |
86 | 4 | "Remote cache compression is not supported by this instance" |
87 | 4 | )) |
88 | | } |
89 | | } |
90 | 1 | Some(other) => Err(make_input_err!("Unsupported wire compressor: '{}'", other)), |
91 | | } |
92 | 52 | } |
93 | | |
94 | | #[must_use] |
95 | 2 | pub fn compress_for_batch_read(data: Bytes) -> (Bytes, compressor::Value) { |
96 | 2 | match compress(data.clone(), compressor::Value::Zstd) { |
97 | 2 | Ok(compressed) => { |
98 | 2 | if compressed.len() < data.len() { |
99 | 1 | (compressed, compressor::Value::Zstd) |
100 | | } else { |
101 | 1 | (data, compressor::Value::Identity) |
102 | | } |
103 | | } |
104 | 0 | Err(err) => { |
105 | 0 | warn!("Wire compression failed, falling back to identity: {}", err); |
106 | 0 | (data, compressor::Value::Identity) |
107 | | } |
108 | | } |
109 | 2 | } |
110 | | |
111 | 6 | fn validate_identity_batch_update(data: Bytes, expected_size: usize) -> Result<Bytes, Error> { |
112 | 6 | if data.len() != expected_size { |
113 | 0 | return Err(make_err!( |
114 | 0 | Code::InvalidArgument, |
115 | 0 | "Identity data size {} does not match expected size {}", |
116 | 0 | data.len(), |
117 | 0 | expected_size |
118 | 0 | )); |
119 | 6 | } |
120 | 6 | Ok(data) |
121 | 6 | } |
122 | | |
123 | 8 | pub async fn decompress_batch_update( |
124 | 8 | data: Bytes, |
125 | 8 | compressor_i32: i32, |
126 | 8 | expected_size: usize, |
127 | 8 | remote_cache_compression_enabled: bool, |
128 | 8 | ) -> Result<Bytes, Error> { |
129 | 8 | let request_compressor = compressor::Value::try_from(compressor_i32) |
130 | 8 | .map_err(|_| make_input_err!0 ("Unknown compressor value: {}", compressor_i32))?0 ; |
131 | | |
132 | 8 | match request_compressor { |
133 | 6 | compressor::Value::Identity => validate_identity_batch_update(data, expected_size), |
134 | | compressor::Value::Zstd => { |
135 | 2 | if !remote_cache_compression_enabled { |
136 | 1 | return Err(make_input_err!( |
137 | 1 | "Remote cache compression is not supported by this instance" |
138 | 1 | )); |
139 | 1 | } |
140 | 1 | spawn_blocking!("cas_decode_compressed_upload", move || { |
141 | 1 | decompress(&data, compressor::Value::Zstd, expected_size) |
142 | 1 | }) |
143 | 1 | .await |
144 | 1 | .map_err(|e| make_err!0 (Code::Internal0 , "Decompression task failed: {}", e))?0 |
145 | | } |
146 | 0 | other => Err(make_input_err!("Unsupported wire compressor: {:?}", other)), |
147 | | } |
148 | 8 | } |