Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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);