/build/source/nativelink-store/src/compression_store.rs
Line | Count | Source |
1 | | // Copyright 2024 Trace Machina, Inc. 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::cmp; |
16 | | use core::pin::Pin; |
17 | | use std::sync::Arc; |
18 | | |
19 | | use async_trait::async_trait; |
20 | | use byteorder::{ByteOrder, LittleEndian}; |
21 | | use bytes::{Buf, BufMut, BytesMut}; |
22 | | use futures::future::FutureExt; |
23 | | use lz4_flex::block::{compress_into, decompress_into, get_maximum_output_size}; |
24 | | use nativelink_config::stores::CompressionSpec; |
25 | | use nativelink_error::{Code, Error, ResultExt, error_if, make_err}; |
26 | | use nativelink_metric::MetricsComponent; |
27 | | use nativelink_util::buf_channel::{ |
28 | | DropCloserReadHalf, DropCloserWriteHalf, make_buf_channel_pair, |
29 | | }; |
30 | | use nativelink_util::health_utils::{HealthStatusIndicator, default_health_status_indicator}; |
31 | | use nativelink_util::spawn; |
32 | | use nativelink_util::store_trait::{ |
33 | | RemoveCallback, Store, StoreDriver, StoreKey, StoreLike, UploadSizeInfo, |
34 | | }; |
35 | | use serde::{Deserialize, Serialize}; |
36 | | use wincode::{SchemaRead, SchemaWrite}; |
37 | | |
38 | | use crate::cas_utils::is_zero_digest; |
39 | | |
40 | | // In the event the bytestream format changes this number should be incremented to prevent |
41 | | // backwards compatibility issues. |
42 | | pub const CURRENT_STREAM_FORMAT_VERSION: u8 = 1; |
43 | | |
44 | | // Default block size that will be used to slice stream into. |
45 | | pub const DEFAULT_BLOCK_SIZE: u32 = 64 * 1024; |
46 | | |
47 | | const U32_SZ: u64 = size_of::<u8>() as u64; |
48 | | |
49 | | // We use a custom frame format here because I wanted the ability in the future to: |
50 | | // * Read a random part of the data without needing to parse entire file. |
51 | | // * Compress the data on the fly without needing to know the exact input size. |
52 | | // |
53 | | // The frame format that LZ4 uses does not contain an index of where the different |
54 | | // blocks are located. This would mean in the event we only wanted the last byte of |
55 | | // a file, we'd need to seek to the header of each block to find where the next block |
56 | | // offset is until we got to the last block then decompress it. |
57 | | // |
58 | | // By using this custom frame format we keep the ability to have each block reference |
59 | | // the next block (for efficiency), but also after all blocks we have a footer frame |
60 | | // which contains an index of each block in the stream. This means that we can read a |
61 | | // fixed number of bytes of the file, which always contain the size of the index, then |
62 | | // with that size we know the exact size of the footer frame, which contains the entire |
63 | | // index. Once this footer frame is loaded, we could then do some math to figure out |
64 | | // which block each byte is in and the offset of each block within the compressed file. |
65 | | // |
66 | | // The frame format is as follows: |
67 | | // |----------------------------------HEADER-----------------------------------------| |
68 | | // | version(u8) | block_size (u32) | upload_size_type (u32) | upload_size (u32) | |
69 | | // |----------------------------------BLOCK------------------------------------------| |
70 | | // | frame_type(u8) 0x00 | compressed_data_size (u32) | ...DATA... | |
71 | | // | ...DATA... | |
72 | | // | [Possibly repeat block] | |
73 | | // |----------------------------------FOOTER-----------------------------------------| |
74 | | // | frame_type(u8) 0x01 | footer_size (u32) | index_count1 (u64) | |
75 | | // | ...[pos_from_prev_index (u32) - repeat for count {index_count*}]... | |
76 | | // | index_count2 (u32) | uncompressed_data_sz (u64) | block_size (u32) | |
77 | | // | version (u8) |------------------------------------------------------------------| |
78 | | // |---------------------------------------------------------------------------------| |
79 | | // |
80 | | // version - A constant number used to define what version of this format is being |
81 | | // used. Version in header and footer must match. |
82 | | // block_size - Size of each block uncompressed except for last block. This means that |
83 | | // every block uncompressed will be a constant size except last block may |
84 | | // be variable size. Block size in header and footer must match. |
85 | | // upload_size_type - Value of 0 = UploadSizeInfo::ExactSize, 1 = UploadSizeInfo::MaxSize. |
86 | | // This is for debug reasons only. |
87 | | // upload_size - The size of the data. WARNING: Do not rely on this being the uncompressed |
88 | | // payload size. It is a debug field and a "best guess" on how large the data |
89 | | // is. The header does not contain the upload data size. This value is the |
90 | | // value counter part to what the `upload_size_type` field. |
91 | | // frame_type - Type of each frame. 0 = BLOCK frame, 1 = FOOTER frame. Header frame will |
92 | | // always start with the first byte of the stream, so no magic number for it. |
93 | | // compressed_data_size - The size of this block. The bytes after this field should be read |
94 | | // in sequence to get all of the block's data in this block. |
95 | | // footer_size - Size of the footer for bytes after this field. |
96 | | // index_count1 - Number of items in the index. ({index_count1} * 4) represents the number |
97 | | // of bytes that should be read after this field in order to get all index |
98 | | // data. |
99 | | // pos_from_prev_index - Index of an individual block in the stream relative to the previous |
100 | | // block. This field may repeat {index_count} times. |
101 | | // index_count2 - Same as {index_count1} and should always be equal. This field might be |
102 | | // useful if you want to read a random byte from this stream, have random |
103 | | // access to it and know the size of the stream during reading, because |
104 | | // this field is always in the exact same place in the stream relative to |
105 | | // the last byte. |
106 | | // uncompressed_data_sz - Size of the original uncompressed data. |
107 | | // |
108 | | // Note: All fields fields little-endian. |
109 | | |
110 | | /// Number representing a chunk. |
111 | | pub const CHUNK_FRAME_TYPE: u8 = 0; |
112 | | |
113 | | /// Number representing the footer. |
114 | | pub const FOOTER_FRAME_TYPE: u8 = 1; |
115 | | |
116 | | /// This is a partial mirror of `nativelink_config::stores::Lz4Config`. |
117 | | /// We cannot use that natively here because it could cause our |
118 | | /// serialized format to change if we added more configs. |
119 | | #[derive( |
120 | 0 | Serialize, Deserialize, PartialEq, Eq, Debug, Default, Copy, Clone, SchemaWrite, SchemaRead, |
121 | | )] |
122 | | pub struct Lz4Config { |
123 | | pub block_size: u32, |
124 | | } |
125 | | |
126 | 0 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, SchemaWrite, SchemaRead)] |
127 | | pub struct Header { |
128 | | pub version: u8, |
129 | | pub config: Lz4Config, |
130 | | pub upload_size: UploadSizeInfo, |
131 | | } |
132 | | |
133 | | #[derive( |
134 | 0 | Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone, Copy, SchemaWrite, SchemaRead, |
135 | | )] |
136 | | pub struct SliceIndex { |
137 | | pub position_from_prev_index: u32, |
138 | | } |
139 | | |
140 | 0 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, SchemaWrite, SchemaRead)] |
141 | | pub struct Footer { |
142 | | pub indexes: Vec<SliceIndex>, |
143 | | pub index_count: u32, |
144 | | pub uncompressed_data_size: u64, |
145 | | pub config: Lz4Config, |
146 | | pub version: u8, |
147 | | } |
148 | | |
149 | | /// `lz4_flex::block::get_maximum_output_size()` way over estimates, so we use the |
150 | | /// one provided here: <https://github.com/torvalds/linux/blob/master/include/linux/lz4.h#L61> |
151 | | /// Local testing shows this gives quite accurate worst case given random input. |
152 | 17 | const fn lz4_compress_bound(input_size: u64) -> u64 { |
153 | 17 | input_size + (input_size / 255) + 16 |
154 | 17 | } |
155 | | |
156 | 1.30k | fn serialized_size<C, V>(value: &V, config: C) -> Result<u64, Error> |
157 | 1.30k | where |
158 | 1.30k | C: wincode::config::Config, |
159 | 1.30k | V: SchemaWrite<C, Src = V>, |
160 | | { |
161 | 1.30k | wincode::config::serialized_size(value, config).map_err(|e| {0 |
162 | 0 | make_err!( |
163 | 0 | Code::Internal, |
164 | | "Failed to calculate serialized size: {:?}", |
165 | | e |
166 | | ) |
167 | 0 | }) |
168 | 1.30k | } |
169 | | |
170 | | struct UploadState { |
171 | | header: Header, |
172 | | footer: Footer, |
173 | | max_output_size: u64, |
174 | | input_max_size: u64, |
175 | | /// The most blocks this upload may write. |
176 | | max_index_count: u64, |
177 | | } |
178 | | |
179 | | impl UploadState { |
180 | 17 | pub(crate) fn new( |
181 | 17 | store: &CompressionStore, |
182 | 17 | upload_size: UploadSizeInfo, |
183 | 17 | ) -> Result<Self, Error> { |
184 | 17 | let input_max_size = match upload_size { |
185 | 17 | UploadSizeInfo::MaxSize(sz4 ) | UploadSizeInfo::ExactSize(sz13 ) => sz, |
186 | | }; |
187 | | |
188 | 17 | let max_index_count = (input_max_size / u64::from(store.config.block_size)) + 1; |
189 | | // The footer counts blocks in a u32, so no stream holds more. An exact |
190 | | // size past that is refused; an upper bound past it (a caller that |
191 | | // does not know the size announces u64::MAX) only bounds the loop. |
192 | 17 | let max_index_count = match upload_size { |
193 | 13 | UploadSizeInfo::ExactSize(_) => u64::from( |
194 | 13 | u32::try_from(max_index_count) |
195 | 13 | .err_tip(|| "Could not convert max_index_count to u32")?0 , |
196 | | ), |
197 | 4 | UploadSizeInfo::MaxSize(_) => max_index_count.min(u64::from(u32::MAX)), |
198 | | }; |
199 | | |
200 | 17 | let header = Header { |
201 | 17 | version: CURRENT_STREAM_FORMAT_VERSION, |
202 | 17 | config: Lz4Config { |
203 | 17 | block_size: store.config.block_size, |
204 | 17 | }, |
205 | 17 | upload_size, |
206 | 17 | }; |
207 | | // The index grows as blocks are written. Sizing it up front from the |
208 | | // announced size allocated one entry per possible block before any |
209 | | // data arrived, which for an unknown size (u64::MAX) asked the |
210 | | // allocator for a petabyte and aborted the process. |
211 | 17 | let footer = Footer { |
212 | 17 | indexes: Vec::new(), |
213 | 17 | index_count: 0, // Updated later. |
214 | 17 | uncompressed_data_size: 0, // Updated later. |
215 | 17 | config: header.config, |
216 | 17 | version: CURRENT_STREAM_FORMAT_VERSION, |
217 | 17 | }; |
218 | | |
219 | | // This is more accurate of an estimate than what get_maximum_output_size calculates. |
220 | 17 | let max_block_size = lz4_compress_bound(u64::from(store.config.block_size)) + U32_SZ + 1; |
221 | | |
222 | | // Saturating: an unknown size makes the bound u64::MAX, which the |
223 | | // inner store reads as "no useful bound". |
224 | 17 | let max_output_size = { |
225 | | // Room for the longest encoding of the index count (a u64, ten |
226 | | // bytes as a varint). |
227 | | const MAX_COUNT_SZ: u64 = 10; |
228 | 17 | let header_size = serialized_size(&header, store.wincode_config)?0 ; |
229 | 17 | let max_content_size = max_block_size.saturating_mul(max_index_count); |
230 | 17 | let empty_footer_size = serialized_size(&footer, store.wincode_config)?0 ; |
231 | 17 | let max_index_entry_size = serialized_size( |
232 | 17 | &SliceIndex { |
233 | 17 | position_from_prev_index: u32::MAX, |
234 | 17 | }, |
235 | 17 | store.wincode_config, |
236 | 0 | )?; |
237 | 17 | let max_footer_size = (U32_SZ + 1 + empty_footer_size + MAX_COUNT_SZ) |
238 | 17 | .saturating_add(max_index_entry_size.saturating_mul(max_index_count)); |
239 | 17 | header_size |
240 | 17 | .saturating_add(max_content_size) |
241 | 17 | .saturating_add(max_footer_size) |
242 | | }; |
243 | | |
244 | 17 | Ok(Self { |
245 | 17 | header, |
246 | 17 | footer, |
247 | 17 | max_output_size, |
248 | 17 | input_max_size, |
249 | 17 | max_index_count, |
250 | 17 | }) |
251 | 17 | } |
252 | | } |
253 | | |
254 | | // Based off of an older Bincode config |
255 | | pub type WincodeConfig = wincode::config::Configuration< |
256 | | true, |
257 | | { wincode::config::PREALLOCATION_SIZE_LIMIT_DISABLED }, |
258 | | wincode::len::BincodeLen, |
259 | | wincode::int_encoding::LittleEndian, |
260 | | wincode::int_encoding::FixInt, |
261 | | u32, |
262 | | >; |
263 | | |
264 | | /// This store will compress data before sending it on to the inner store. |
265 | | /// Note: Currently using `get_part()` and trying to read part of the data will |
266 | | /// result in the entire contents being read from the inner store but will |
267 | | /// only send the contents requested. |
268 | | #[derive(MetricsComponent)] |
269 | | pub struct CompressionStore { |
270 | | #[metric(group = "inner_store")] |
271 | | inner_store: Store, |
272 | | config: nativelink_config::stores::Lz4Config, |
273 | | wincode_config: WincodeConfig, |
274 | | } |
275 | | |
276 | | impl core::fmt::Debug for CompressionStore { |
277 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
278 | 0 | f.debug_struct("CompressionStore") |
279 | 0 | .field("inner_store", &self.inner_store) |
280 | 0 | .field("config", &self.config) |
281 | 0 | .finish_non_exhaustive() |
282 | 0 | } |
283 | | } |
284 | | |
285 | | impl CompressionStore { |
286 | 21 | pub fn new(spec: &CompressionSpec, inner_store: Store) -> Result<Arc<Self>, Error> { |
287 | 21 | let lz4_config = match spec.compression_algorithm { |
288 | 21 | nativelink_config::stores::CompressionAlgorithm::Lz4(mut lz4_config) => { |
289 | 21 | if lz4_config.block_size == 0 { |
290 | 13 | lz4_config.block_size = DEFAULT_BLOCK_SIZE; |
291 | 13 | }8 |
292 | 21 | if lz4_config.max_decode_block_size == 0 { |
293 | 21 | lz4_config.max_decode_block_size = lz4_config.block_size; |
294 | 21 | }0 |
295 | 21 | lz4_config |
296 | | } |
297 | | }; |
298 | 21 | Ok(Arc::new(Self { |
299 | 21 | inner_store, |
300 | 21 | config: lz4_config, |
301 | 21 | wincode_config: WincodeConfig::new(), |
302 | 21 | })) |
303 | 21 | } |
304 | | |
305 | 9 | async fn inner_has_with_results( |
306 | 9 | self: Pin<&Self>, |
307 | 9 | digests: &[StoreKey<'_>], |
308 | 9 | results: &mut [Option<u64>], |
309 | 9 | ) -> Result<(), Error> { |
310 | 12 | if !digests.iter()9 .any9 (|digest| is_zero_digest(digest.borrow())) { |
311 | 6 | return self.inner_store.has_with_results(digests, results).await; |
312 | 3 | } |
313 | | |
314 | 10 | for (digest, result) in digests3 .iter3 ().zip3 (results3 .iter_mut3 ()) { |
315 | 10 | if is_zero_digest(digest.borrow()) { |
316 | 5 | *result = Some(0); |
317 | 5 | } |
318 | | } |
319 | | |
320 | 3 | let nonzero_digests = digests |
321 | 3 | .iter() |
322 | 10 | .filter3 (|digest| !is_zero_digest(digest.borrow())) |
323 | 3 | .map(StoreKey::borrow) |
324 | 3 | .collect::<Vec<_>>(); |
325 | 3 | if nonzero_digests.is_empty() { |
326 | 1 | return Ok(()); |
327 | 2 | } |
328 | | |
329 | 2 | let mut nonzero_results = vec![None; nonzero_digests.len()]; |
330 | 2 | self.inner_store |
331 | 2 | .has_with_results(&nonzero_digests, &mut nonzero_results) |
332 | 2 | .await?0 ; |
333 | 5 | for (result, inner_result) in digests2 |
334 | 2 | .iter() |
335 | 2 | .zip(results.iter_mut()) |
336 | 8 | .filter_map2 (|(digest, result)| (!is_zero_digest(digest.borrow())).then_some(result)) |
337 | 2 | .zip(nonzero_results) |
338 | 5 | { |
339 | 5 | *result = inner_result; |
340 | 5 | } |
341 | 2 | Ok(()) |
342 | 9 | } |
343 | | } |
344 | | |
345 | | #[async_trait] |
346 | | impl StoreDriver for CompressionStore { |
347 | 0 | async fn post_init(self: Arc<Self>) -> Result<(), Error> { |
348 | | self.inner_store.clone().into_inner().post_init().await?; |
349 | | Ok(()) |
350 | 0 | } |
351 | | |
352 | | async fn has_with_results( |
353 | | self: Pin<&Self>, |
354 | | digests: &[StoreKey<'_>], |
355 | | results: &mut [Option<u64>], |
356 | 9 | ) -> Result<(), Error> { |
357 | | self.inner_has_with_results(digests, results).await?; |
358 | | // The inner store holds the framed, compressed bytes, so the sizes it |
359 | | // reports are the compressed ones. Callers size fast-tier writes and |
360 | | // buffers from this answer, so for a digest key report the length the |
361 | | // digest declares, which is what a read of this store delivers. A |
362 | | // string key declares no length and keeps the stored size; reading |
363 | | // the uncompressed one back from the frame header is a follow-up. |
364 | | for (key, result) in digests.iter().zip(results.iter_mut()) { |
365 | | if let (StoreKey::Digest(digest), Some(_)) = (key.borrow(), result.as_ref()) { |
366 | | *result = Some(digest.size_bytes()); |
367 | | } |
368 | | } |
369 | | Ok(()) |
370 | 9 | } |
371 | | |
372 | | async fn update( |
373 | | self: Pin<&Self>, |
374 | | key: StoreKey<'_>, |
375 | | mut reader: DropCloserReadHalf, |
376 | | upload_size: UploadSizeInfo, |
377 | 21 | ) -> Result<u64, Error> { |
378 | | if is_zero_digest(key.borrow()) { |
379 | 4 | return reader.recv().await.and_then(|should_be_empty| { |
380 | 4 | if should_be_empty.is_empty() { |
381 | 2 | Ok(0) |
382 | | } else { |
383 | 2 | Err(make_err!(Code::Internal, "Zero byte hash not empty")) |
384 | | } |
385 | 4 | }); |
386 | | } |
387 | | |
388 | | let mut output_state = UploadState::new(&self, upload_size)?; |
389 | | |
390 | | let (mut tx, rx) = make_buf_channel_pair(); |
391 | | |
392 | | let inner_store = self.inner_store.clone(); |
393 | | let key = key.into_owned(); |
394 | 17 | let update_fut = spawn!("compression_store_update_spawn", async move { |
395 | 17 | inner_store |
396 | 17 | .update( |
397 | 17 | key, |
398 | 17 | rx, |
399 | 17 | UploadSizeInfo::MaxSize(output_state.max_output_size), |
400 | 17 | ) |
401 | 17 | .await |
402 | 17 | .err_tip(|| "Inner store update in compression store failed") |
403 | 17 | }) |
404 | | .map( |
405 | 17 | |result| match result.err_tip(|| "Failed to run compression update spawn") { |
406 | 17 | Ok(inner_result) => { |
407 | 17 | inner_result.err_tip(|| "Compression underlying store update failed") |
408 | | } |
409 | 0 | Err(e) => Err(e), |
410 | 17 | }, |
411 | | ); |
412 | | |
413 | 17 | let write_fut = async move { |
414 | | { |
415 | | // Write Header. |
416 | 17 | let serialized_header = |
417 | 17 | wincode::config::serialize(&output_state.header, self.wincode_config).map_err( |
418 | 0 | |e| make_err!(Code::Internal, "Failed to serialize header : {:?}", e), |
419 | 0 | )?; |
420 | 17 | tx.send(serialized_header.into()) |
421 | 17 | .await |
422 | 17 | .err_tip(|| "Failed to write compression header on upload")?0 ; |
423 | | } |
424 | | |
425 | 17 | let mut received_amt = 0; |
426 | 17 | let mut index_count: u32 = 0; |
427 | 149 | while u64::from(index_count) < output_state.max_index_count { |
428 | 142 | let chunk = reader |
429 | 142 | .consume(Some(self.config.block_size as usize)) |
430 | 142 | .await |
431 | 142 | .err_tip(|| "Failed to read take in update in compression store")?0 ; |
432 | 142 | if chunk.is_empty() { |
433 | 10 | break; // EOF. |
434 | 132 | } |
435 | | |
436 | 132 | received_amt += u64::try_from(chunk.len()) |
437 | 132 | .err_tip(|| "Could not convert chunk.len() to u64")?0 ; |
438 | 0 | error_if!( |
439 | 132 | received_amt > output_state.input_max_size, |
440 | | "Got more data than stated in compression store upload request" |
441 | | ); |
442 | | |
443 | 132 | let max_output_size = get_maximum_output_size(self.config.block_size as usize); |
444 | 132 | let mut compressed_data_buf = BytesMut::with_capacity(max_output_size); |
445 | 132 | compressed_data_buf.put_u8(CHUNK_FRAME_TYPE); |
446 | 132 | compressed_data_buf.put_u32_le(0); // Filled later. |
447 | | |
448 | | // For efficiency reasons we do some raw slice manipulation so we can write directly |
449 | | // into our buffer instead of having to do another allocation. |
450 | 132 | let raw_compressed_data = unsafe { |
451 | 132 | core::slice::from_raw_parts_mut( |
452 | 132 | compressed_data_buf.chunk_mut().as_mut_ptr(), |
453 | 132 | max_output_size, |
454 | 132 | ) |
455 | | }; |
456 | | |
457 | 132 | let compressed_data_sz = compress_into(&chunk, raw_compressed_data) |
458 | 132 | .map_err(|e| make_err!0 (Code::Internal0 , "Compression error {:?}", e))?0 ; |
459 | 132 | unsafe { |
460 | 132 | compressed_data_buf.advance_mut(compressed_data_sz); |
461 | 132 | } |
462 | | |
463 | | // Now fill the size in our slice. |
464 | 132 | LittleEndian::write_u32( |
465 | 132 | &mut compressed_data_buf[1..5], |
466 | 132 | u32::try_from(compressed_data_sz).unwrap_or(u32::MAX), |
467 | | ); |
468 | | |
469 | | // Now send our chunk. |
470 | 132 | tx.send(compressed_data_buf.freeze()) |
471 | 132 | .await |
472 | 132 | .err_tip(|| "Failed to write chunk to inner store in compression store")?0 ; |
473 | | |
474 | 132 | output_state.footer.indexes.push(SliceIndex { |
475 | 132 | position_from_prev_index: u32::try_from(compressed_data_sz).unwrap_or(u32::MAX), |
476 | 132 | }); |
477 | | |
478 | 132 | index_count += 1; |
479 | | } |
480 | 17 | upload_size.validate_size(received_amt)?2 ; |
481 | 15 | if !reader.recv().await?0 .is_empty() { |
482 | 0 | return Err(make_err!( |
483 | 0 | Code::InvalidArgument, |
484 | 0 | "Data after the declared compression input size" |
485 | 0 | )); |
486 | 15 | } |
487 | | // Index 0 is actually a pointer to the second chunk. This is because we don't need |
488 | | // an index for the first item, since it starts at position `{header_len}`. |
489 | | // The code above causes us to create 1 more index than we actually need, so we |
490 | | // remove the last index from our vector here, because at this point we are always |
491 | | // one index too many. |
492 | | // Note: We need to be careful that if we don't have any data (zero bytes) it |
493 | | // doesn't go to -1. |
494 | 15 | index_count = index_count.saturating_sub(1); |
495 | 15 | output_state |
496 | 15 | .footer |
497 | 15 | .indexes |
498 | 15 | .resize(index_count as usize, SliceIndex::default()); |
499 | 15 | output_state.footer.index_count = |
500 | 15 | u32::try_from(output_state.footer.indexes.len()).unwrap_or(u32::MAX); |
501 | 15 | output_state.footer.uncompressed_data_size = received_amt; |
502 | | { |
503 | | // Write Footer. |
504 | 15 | let serialized_footer = |
505 | 15 | wincode::config::serialize(&output_state.footer, self.wincode_config).map_err( |
506 | 0 | |e| make_err!(Code::Internal, "Failed to serialize header : {:?}", e), |
507 | 0 | )?; |
508 | | |
509 | 15 | let mut footer = BytesMut::with_capacity(1 + 4 + serialized_footer.len()); |
510 | 15 | footer.put_u8(FOOTER_FRAME_TYPE); |
511 | 15 | footer.put_u32_le(u32::try_from(serialized_footer.len()).unwrap_or(u32::MAX)); |
512 | 15 | footer.extend_from_slice(&serialized_footer); |
513 | | |
514 | 15 | tx.send(footer.freeze()) |
515 | 15 | .await |
516 | 15 | .err_tip(|| "Failed to write footer to inner store in compression store")?0 ; |
517 | 15 | tx.send_eof() |
518 | 15 | .err_tip(|| "Failed writing EOF in compression store update")?0 ; |
519 | | } |
520 | | |
521 | 15 | Result::<u64, Error>::Ok(received_amt) |
522 | 17 | }; |
523 | | let (write_result, update_result) = tokio::join!(write_fut, update_fut); |
524 | | match (write_result, update_result) { |
525 | | (Ok(size), Ok(_)) => Ok(size), |
526 | | (Err(e), _) | (_, Err(e)) => Err(e), |
527 | | } |
528 | 21 | } |
529 | | |
530 | | async fn get_part( |
531 | | self: Pin<&Self>, |
532 | | key: StoreKey<'_>, |
533 | | writer: &mut DropCloserWriteHalf, |
534 | | offset: u64, |
535 | | length: Option<u64>, |
536 | 1.25k | ) -> Result<(), Error> { |
537 | | if is_zero_digest(key.borrow()) { |
538 | | writer |
539 | | .send_eof() |
540 | | .err_tip(|| "Failed to send zero EOF in filesystem store get_part")?; |
541 | | return Ok(()); |
542 | | } |
543 | | |
544 | | let (tx, mut rx) = make_buf_channel_pair(); |
545 | | |
546 | | let inner_store = self.inner_store.clone(); |
547 | | let key = key.into_owned(); |
548 | 1.25k | let get_part_fut = spawn!("compression_store_get_part_spawn", async move { |
549 | 1.25k | inner_store |
550 | 1.25k | .get_part(key, tx, 0, None) |
551 | 1.25k | .await |
552 | 1.25k | .err_tip(|| "Inner store get in compression store failed") |
553 | 1.25k | }) |
554 | | .map( |
555 | 1.25k | |result| match result.err_tip(|| "Failed to run compression get spawn") { |
556 | 1.25k | Ok(inner_result) => { |
557 | 1.25k | inner_result.err_tip(|| "Compression underlying store get failed") |
558 | | } |
559 | 0 | Err(e) => Err(e), |
560 | 1.25k | }, |
561 | | ); |
562 | 1.25k | let read_fut = async move { |
563 | 1.25k | let header = { |
564 | | // Read header. |
565 | | const EMPTY_HEADER: Header = Header { |
566 | | version: CURRENT_STREAM_FORMAT_VERSION, |
567 | | config: Lz4Config { block_size: 0 }, |
568 | | upload_size: UploadSizeInfo::ExactSize(0), |
569 | | }; |
570 | 1.25k | let header_size = serialized_size(&EMPTY_HEADER, self.wincode_config)?0 ; |
571 | 1.25k | let chunk = rx |
572 | 1.25k | .consume(Some(usize::try_from(header_size).unwrap_or(usize::MAX))) |
573 | 1.25k | .await |
574 | 1.25k | .err_tip(|| "Failed to read header in get_part compression store")?0 ; |
575 | 0 | error_if!( |
576 | 1.25k | chunk.len() as u64 != header_size, |
577 | | "Expected inner store to return the proper amount of data in compression store {} != {}", |
578 | 0 | chunk.len(), |
579 | | header_size, |
580 | | ); |
581 | | |
582 | 1.25k | wincode::config::deserialize::<Header, WincodeConfig>( |
583 | 1.25k | &chunk, |
584 | 1.25k | self.wincode_config, |
585 | | ) |
586 | 1.25k | .map_err(|e| make_err!0 (Code::Internal0 , "Failed to deserialize header : {:?}", e))?0 |
587 | | }; |
588 | | |
589 | 0 | error_if!( |
590 | 1.25k | header.version != CURRENT_STREAM_FORMAT_VERSION, |
591 | | "Expected header version to match in get compression, got {}, want {}", |
592 | | header.version, |
593 | | CURRENT_STREAM_FORMAT_VERSION |
594 | | ); |
595 | 0 | error_if!( |
596 | 1.25k | header.config.block_size > self.config.max_decode_block_size, |
597 | | "Block size is too large in compression, got {} > {}", |
598 | | header.config.block_size, |
599 | 0 | self.config.max_decode_block_size |
600 | | ); |
601 | | |
602 | 1.25k | let mut chunk = rx |
603 | 1.25k | .consume(Some(1 + 4)) |
604 | 1.25k | .await |
605 | 1.25k | .err_tip(|| "Failed to read init frame info in compression store")?0 ; |
606 | 0 | error_if!( |
607 | 1.25k | chunk.len() < 1 + 4, |
608 | | "Received EOF too early while reading init frame info in compression store" |
609 | | ); |
610 | | |
611 | 1.25k | let mut frame_type = chunk.get_u8(); |
612 | 1.25k | let mut frame_sz = chunk.get_u32_le(); |
613 | | |
614 | 1.25k | let mut uncompressed_data_sz: u64 = 0; |
615 | 1.25k | let mut remaining_bytes_to_send: u64 = length.unwrap_or(u64::MAX); |
616 | 1.25k | let mut chunks_count: u32 = 0; |
617 | 5.09k | while frame_type != FOOTER_FRAME_TYPE { |
618 | 0 | error_if!( |
619 | 3.84k | frame_type != CHUNK_FRAME_TYPE, |
620 | | "Expected frame to be BODY in compression store, got {} at {}", |
621 | | frame_type, |
622 | | chunks_count |
623 | | ); |
624 | | |
625 | 3.84k | let chunk = rx |
626 | 3.84k | .consume(Some(frame_sz as usize)) |
627 | 3.84k | .await |
628 | 3.84k | .err_tip(|| "Failed to read chunk in get_part compression store")?0 ; |
629 | 3.84k | if chunk.len() < frame_sz as usize { |
630 | 0 | return Err(make_err!( |
631 | 0 | Code::Internal, |
632 | 0 | "Got EOF earlier than expected. Maybe the data is not compressed or different format?" |
633 | 0 | )); |
634 | 3.84k | } |
635 | | { |
636 | 3.84k | let max_output_size = |
637 | 3.84k | get_maximum_output_size(header.config.block_size as usize); |
638 | 3.84k | let mut uncompressed_data = BytesMut::with_capacity(max_output_size); |
639 | | |
640 | | // For efficiency reasons we do some raw slice manipulation so we can write directly |
641 | | // into our buffer instead of having to do another allocation. |
642 | 3.84k | let raw_decompressed_data = unsafe { |
643 | 3.84k | core::slice::from_raw_parts_mut( |
644 | 3.84k | uncompressed_data.chunk_mut().as_mut_ptr(), |
645 | 3.84k | max_output_size, |
646 | 3.84k | ) |
647 | | }; |
648 | | |
649 | 3.84k | let uncompressed_chunk_sz = decompress_into(&chunk, raw_decompressed_data) |
650 | 3.84k | .map_err(|e| make_err!0 (Code::Internal0 , "Decompression error {:?}", e))?0 ; |
651 | 3.84k | unsafe { uncompressed_data.advance_mut(uncompressed_chunk_sz) }; |
652 | 3.84k | let new_uncompressed_data_sz = |
653 | 3.84k | uncompressed_data_sz + uncompressed_chunk_sz as u64; |
654 | 3.84k | if new_uncompressed_data_sz >= offset && remaining_bytes_to_send > 02.36k { |
655 | 2.02k | let start_pos = |
656 | 2.02k | usize::try_from(offset.saturating_sub(uncompressed_data_sz)) |
657 | 2.02k | .unwrap_or(usize::MAX); |
658 | 2.02k | let end_pos = cmp::min( |
659 | 2.02k | start_pos.saturating_add( |
660 | 2.02k | usize::try_from(remaining_bytes_to_send).unwrap_or(usize::MAX), |
661 | | ), |
662 | 2.02k | uncompressed_chunk_sz, |
663 | | ); |
664 | 2.02k | if end_pos != start_pos { |
665 | | // Make sure we don't send an EOF by accident. |
666 | 1.90k | writer |
667 | 1.90k | .send(uncompressed_data.freeze().slice(start_pos..end_pos)) |
668 | 1.90k | .await |
669 | 1.90k | .err_tip(|| "Failed sending chunk in compression store")?0 ; |
670 | 112 | } |
671 | 2.02k | remaining_bytes_to_send -= (end_pos - start_pos) as u64; |
672 | 1.82k | } |
673 | 3.84k | uncompressed_data_sz = new_uncompressed_data_sz; |
674 | | } |
675 | 3.84k | chunks_count += 1; |
676 | | |
677 | 3.84k | let mut chunk = rx |
678 | 3.84k | .consume(Some(1 + 4)) |
679 | 3.84k | .await |
680 | 3.84k | .err_tip(|| "Failed to read frame info in compression store")?0 ; |
681 | 0 | error_if!( |
682 | 3.84k | chunk.len() < 1 + 4, |
683 | | "Received EOF too early while reading frame info in compression store" |
684 | | ); |
685 | | |
686 | 3.84k | frame_type = chunk.get_u8(); |
687 | 3.84k | frame_sz = chunk.get_u32_le(); |
688 | | } |
689 | | // Index count will always be +1 (unless it is zero bytes long). |
690 | 1.25k | chunks_count = chunks_count.saturating_sub(1); |
691 | | { |
692 | | // Read and validate footer. |
693 | 1.25k | let chunk = rx |
694 | 1.25k | .consume(Some(frame_sz as usize)) |
695 | 1.25k | .await |
696 | 1.25k | .err_tip(|| "Failed to read chunk in get_part compression store")?0 ; |
697 | 0 | error_if!( |
698 | 1.25k | chunk.len() < frame_sz as usize, |
699 | | "Unexpected EOF when reading footer in compression store get_part" |
700 | | ); |
701 | | |
702 | 1.25k | let footer = wincode::config::deserialize::<Footer, WincodeConfig>( |
703 | 1.25k | &chunk, |
704 | 1.25k | self.wincode_config, |
705 | | ) |
706 | 1.25k | .map_err(|e| make_err!0 (Code::Internal0 , "Failed to deserialize footer : {:?}", e))?0 ; |
707 | | |
708 | 0 | error_if!( |
709 | 1.25k | header.version != footer.version, |
710 | | "Expected header and footer versions to match compression store get_part, {} != {}", |
711 | | header.version, |
712 | | footer.version |
713 | | ); |
714 | 0 | error_if!( |
715 | 1.25k | footer.indexes.len() != footer.index_count as usize, |
716 | | "Expected index counts to match in compression store footer in get_part, {} != {}", |
717 | 0 | footer.indexes.len(), |
718 | | footer.index_count |
719 | | ); |
720 | 0 | error_if!( |
721 | 1.25k | footer.index_count != chunks_count, |
722 | | concat!( |
723 | | "Expected index counts to match received chunks count ", |
724 | | "in compression store footer in get_part, {} != {}" |
725 | | ), |
726 | | footer.index_count, |
727 | | chunks_count |
728 | | ); |
729 | 0 | error_if!( |
730 | 1.25k | footer.uncompressed_data_size != uncompressed_data_sz, |
731 | | "Expected uncompressed data sizes to match in compression store footer in get_part, {} != {}", |
732 | | footer.uncompressed_data_size, |
733 | | uncompressed_data_sz |
734 | | ); |
735 | | } |
736 | | |
737 | 1.25k | writer |
738 | 1.25k | .send_eof() |
739 | 1.25k | .err_tip(|| "Failed to send eof in compression store write")?0 ; |
740 | 1.25k | Ok(()) |
741 | 1.25k | }; |
742 | | |
743 | | let (read_result, get_part_fut_result) = tokio::join!(read_fut, get_part_fut); |
744 | | if let Err(mut e) = read_result { |
745 | | // We may need to propagate the error from reading the data through first. |
746 | | if let Err(err) = get_part_fut_result { |
747 | | e = err.merge(e); |
748 | | } |
749 | | return Err(e); |
750 | | } |
751 | | Ok(()) |
752 | 1.25k | } |
753 | | |
754 | 4 | fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver { |
755 | 4 | self |
756 | 4 | } |
757 | | |
758 | 0 | fn as_any(&self) -> &(dyn core::any::Any + Sync + Send + 'static) { |
759 | 0 | self |
760 | 0 | } |
761 | | |
762 | 0 | fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> { |
763 | 0 | self |
764 | 0 | } |
765 | | |
766 | 0 | fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> { |
767 | 0 | self.inner_store.register_remove_callback(callback) |
768 | 0 | } |
769 | | } |
770 | | |
771 | | default_health_status_indicator!(CompressionStore); |