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/dedup_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 futures::stream::{self, FuturesOrdered, StreamExt, TryStreamExt};
21
use futures::try_join;
22
use nativelink_config::stores::DedupSpec;
23
use nativelink_error::{Code, Error, ResultExt, make_err};
24
use nativelink_metric::MetricsComponent;
25
use nativelink_util::buf_channel::{DropCloserReadHalf, DropCloserWriteHalf};
26
use nativelink_util::common::DigestInfo;
27
use nativelink_util::fastcdc::FastCDC;
28
use nativelink_util::health_utils::{HealthStatusIndicator, default_health_status_indicator};
29
use nativelink_util::store_trait::{
30
    RemoveCallback, Store, StoreDriver, StoreKey, StoreLike, UploadSizeInfo,
31
};
32
use serde::{Deserialize, Serialize};
33
use tokio_util::codec::FramedRead;
34
use tokio_util::io::StreamReader;
35
use tracing::warn;
36
37
use crate::cas_utils::is_zero_digest;
38
use crate::compression_store::WincodeConfig;
39
40
// NOTE: If these change update the comments in `stores.rs` to reflect
41
// the new defaults.
42
const DEFAULT_MIN_SIZE: u64 = 64 * 1024;
43
const DEFAULT_NORM_SIZE: u64 = 256 * 1024;
44
const DEFAULT_MAX_SIZE: u64 = 512 * 1024;
45
const DEFAULT_MAX_CONCURRENT_FETCH_PER_GET: usize = 10;
46
47
#[derive(
48
    Serialize,
49
    Deserialize,
50
    PartialEq,
51
    Eq,
52
    Debug,
53
    Default,
54
    Clone,
55
    wincode::SchemaRead,
56
0
    wincode::SchemaWrite,
57
)]
58
pub struct DedupIndex {
59
    pub entries: Vec<DigestInfo>,
60
}
61
62
#[derive(MetricsComponent)]
63
pub struct DedupStore {
64
    #[metric(group = "index_store")]
65
    index_store: Store,
66
    #[metric(group = "content_store")]
67
    content_store: Store,
68
    fast_cdc_decoder: FastCDC,
69
    #[metric(help = "Maximum number of concurrent fetches per get")]
70
    max_concurrent_fetch_per_get: usize,
71
    wincode_config: WincodeConfig,
72
}
73
74
impl core::fmt::Debug for DedupStore {
75
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
76
0
        f.debug_struct("DedupStore")
77
0
            .field("index_store", &self.index_store)
78
0
            .field("content_store", &self.content_store)
79
0
            .field("fast_cdc_decoder", &self.fast_cdc_decoder)
80
0
            .field(
81
0
                "max_concurrent_fetch_per_get",
82
0
                &self.max_concurrent_fetch_per_get,
83
0
            )
84
0
            .finish_non_exhaustive()
85
0
    }
86
}
87
88
impl DedupStore {
89
10
    pub fn new(
90
10
        spec: &DedupSpec,
91
10
        index_store: Store,
92
10
        content_store: Store,
93
10
    ) -> Result<Arc<Self>, Error> {
94
10
        let min_size = if spec.min_size == 0 {
95
0
            DEFAULT_MIN_SIZE
96
        } else {
97
10
            u64::from(spec.min_size)
98
        };
99
10
        let normal_size = if spec.normal_size == 0 {
100
0
            DEFAULT_NORM_SIZE
101
        } else {
102
10
            u64::from(spec.normal_size)
103
        };
104
10
        let max_size = if spec.max_size == 0 {
105
0
            DEFAULT_MAX_SIZE
106
        } else {
107
10
            u64::from(spec.max_size)
108
        };
109
10
        let max_concurrent_fetch_per_get = if spec.max_concurrent_fetch_per_get == 0 {
110
0
            DEFAULT_MAX_CONCURRENT_FETCH_PER_GET
111
        } else {
112
10
            spec.max_concurrent_fetch_per_get as usize
113
        };
114
10
        Ok(Arc::new(Self {
115
10
            index_store,
116
10
            content_store,
117
10
            fast_cdc_decoder: FastCDC::new(
118
10
                usize::try_from(min_size).err_tip(|| "Could not convert min_size to usize")
?0
,
119
10
                usize::try_from(normal_size)
120
10
                    .err_tip(|| "Could not convert normal_size to usize")
?0
,
121
10
                usize::try_from(max_size).err_tip(|| "Could not convert max_size to usize")
?0
,
122
            ),
123
10
            max_concurrent_fetch_per_get,
124
10
            wincode_config: WincodeConfig::new(),
125
        }))
126
10
    }
127
128
4
    async fn has(self: Pin<&Self>, key: StoreKey<'_>) -> Result<Option<u64>, Error> {
129
        // First we need to load the index that contains where the individual parts actually
130
        // can be fetched from.
131
3
        let index_entries = {
132
4
            let maybe_data = self
133
4
                .index_store
134
4
                .get_part_unchunked(key.borrow(), 0, None)
135
4
                .await
136
4
                .err_tip(|| "Failed to read index store in dedup store");
137
4
            let 
data3
= match maybe_data {
138
1
                Err(e) => {
139
1
                    if e.code == Code::NotFound {
140
1
                        return Ok(None);
141
0
                    }
142
0
                    return Err(e);
143
                }
144
3
                Ok(data) => data,
145
            };
146
147
3
            match wincode::config::deserialize::<DedupIndex, WincodeConfig>(
148
3
                &data,
149
3
                self.wincode_config,
150
3
            ) {
151
3
                Ok(dedup_index) => dedup_index,
152
0
                Err(err) => {
153
0
                    warn!(?key, ?err, "Failed to deserialize index in dedup store",);
154
                    // We return the equivalent of NotFound here so the client is happy.
155
0
                    return Ok(None);
156
                }
157
            }
158
        };
159
160
3
        let digests: Vec<_> = index_entries
161
3
            .entries
162
3
            .into_iter()
163
3
            .map(StoreKey::Digest)
164
3
            .collect();
165
3
        let mut sum = 0;
166
8
        for size in 
self.content_store3
.
has_many3
(&digests).await
?0
{
167
8
            let Some(
size7
) = size else {
168
                // A part is missing so return None meaning not-found.
169
                // This will abort all in-flight queries related to this request.
170
1
                return Ok(None);
171
            };
172
7
            sum += size;
173
        }
174
2
        Ok(Some(sum))
175
4
    }
176
}
177
178
#[async_trait]
179
impl StoreDriver for DedupStore {
180
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
181
        try_join!(
182
            self.index_store.clone().into_inner().post_init(),
183
            self.content_store.clone().into_inner().post_init(),
184
        )?;
185
        Ok(())
186
0
    }
187
188
    async fn has_with_results(
189
        self: Pin<&Self>,
190
        digests: &[StoreKey<'_>],
191
        results: &mut [Option<u64>],
192
5
    ) -> Result<(), Error> {
193
        digests
194
            .iter()
195
            .zip(results.iter_mut())
196
5
            .map(|(key, result)| async move {
197
5
                if is_zero_digest(key.borrow()) {
198
1
                    *result = Some(0);
199
1
                    return Ok(());
200
4
                }
201
202
4
                match self.has(key.borrow()).await {
203
4
                    Ok(maybe_size) => {
204
4
                        *result = maybe_size;
205
4
                        Ok(())
206
                    }
207
0
                    Err(err) => Err(err),
208
                }
209
10
            })
210
            .collect::<FuturesOrdered<_>>()
211
            .try_collect()
212
            .await
213
5
    }
214
215
    async fn update(
216
        self: Pin<&Self>,
217
        key: StoreKey<'_>,
218
        mut reader: DropCloserReadHalf,
219
        size_info: UploadSizeInfo,
220
11
    ) -> Result<u64, Error> {
221
        if is_zero_digest(key.borrow()) {
222
            let size = reader.drain().await?;
223
            UploadSizeInfo::ExactSize(0).validate_size(size)?;
224
            size_info.validate_size(size)?;
225
            return Ok(0);
226
        }
227
        let mut bytes_reader = StreamReader::new(reader);
228
        let frame_reader = FramedRead::new(&mut bytes_reader, self.fast_cdc_decoder.clone());
229
        let index_entries: Vec<_> = frame_reader
230
84
            .map(|r| r.err_tip(|| "Failed to decode frame from fast_cdc"))
231
84
            .map_ok(|frame| async move {
232
84
                let hash = blake3::hash(&frame[..]).into();
233
84
                let index_entry = DigestInfo::new(hash, frame.len() as u64);
234
84
                if self
235
84
                    .content_store
236
84
                    .has(index_entry)
237
84
                    .await
238
84
                    .err_tip(|| "Failed to call .has() in DedupStore::update()")
?0
239
84
                    .is_some()
240
                {
241
                    // If our store has this digest, we don't need to upload it.
242
0
                    return Result::<_, Error>::Ok(index_entry);
243
84
                }
244
84
                self.content_store
245
84
                    .update_oneshot(index_entry, frame)
246
84
                    .await
247
84
                    .err_tip(|| "Failed to update content store in dedup_store")
?0
;
248
84
                Ok(index_entry)
249
168
            })
250
            .try_buffered(self.max_concurrent_fetch_per_get)
251
            .try_collect()
252
            .await?;
253
254
        let total_size = index_entries.iter().map(DigestInfo::size_bytes).sum();
255
        size_info.validate_size(total_size)?;
256
257
        let serialized_index = wincode::config::serialize(
258
            &DedupIndex {
259
                entries: index_entries,
260
            },
261
            self.wincode_config,
262
        )
263
0
        .map_err(|e| {
264
0
            make_err!(
265
0
                Code::Internal,
266
                "Failed to serialize index in dedup_store : {:?}",
267
                e
268
            )
269
0
        })?;
270
271
        self.index_store
272
            .update_oneshot(key, serialized_index.into())
273
            .await
274
            .err_tip(|| "Failed to insert our index entry to index_store in dedup_store")?;
275
276
        Ok(total_size)
277
11
    }
278
279
    async fn get_part(
280
        self: Pin<&Self>,
281
        key: StoreKey<'_>,
282
        writer: &mut DropCloserWriteHalf,
283
        offset: u64,
284
        length: Option<u64>,
285
935
    ) -> Result<(), Error> {
286
        // Special case for if a client tries to read zero bytes.
287
        if length == Some(0) {
288
            writer
289
                .send_eof()
290
                .err_tip(|| "Failed to write EOF out from get_part dedup")?;
291
            return Ok(());
292
        }
293
        // First we need to download the index that contains where the individual parts actually
294
        // can be fetched from.
295
        let index_entries = {
296
            let data = self
297
                .index_store
298
                .get_part_unchunked(key, 0, None)
299
                .await
300
                .err_tip(|| "Failed to read index store in dedup store")?;
301
            wincode::config::deserialize::<DedupIndex, WincodeConfig>(&data, self.wincode_config)
302
0
                .map_err(|e| {
303
0
                    make_err!(
304
0
                        Code::Internal,
305
                        "Failed to deserialize index in dedup_store::get_part : {:?}",
306
                        e
307
                    )
308
0
                })?
309
        };
310
311
        let mut start_byte_in_stream: u64 = 0;
312
        let entries = {
313
            if offset == 0 && length.is_none() {
314
                index_entries.entries
315
            } else {
316
                let mut current_entries_sum = 0;
317
                let mut entries = Vec::with_capacity(index_entries.entries.len());
318
                for entry in index_entries.entries {
319
                    let first_byte = current_entries_sum;
320
                    let entry_size = entry.size_bytes();
321
                    current_entries_sum += entry_size;
322
                    // Filter any items who's end byte is before the first requested byte.
323
                    if current_entries_sum <= offset {
324
                        start_byte_in_stream = current_entries_sum;
325
                        continue;
326
                    }
327
                    // If we are not going to read any bytes past the length we are done.
328
                    if let Some(length) = length
329
                        && first_byte >= offset + length
330
                    {
331
                        break;
332
                    }
333
                    entries.push(entry);
334
                }
335
                entries
336
            }
337
        };
338
339
        // Second we we create a stream of futures for each chunk, but buffer/limit them so only
340
        // `max_concurrent_fetch_per_get` will be executed at a time.
341
        // The results will be streamed out in the same order they are in the entries table.
342
        // The results will execute in a "window-like" fashion, meaning that if we limit to
343
        // 5 requests at a time, and request 3 is stalled, request 1 & 2 can be output and
344
        // request 4 & 5 can be executing (or finished) while waiting for 3 to finish.
345
        // Note: We will buffer our data here up to:
346
        // `spec.max_size * spec.max_concurrent_fetch_per_get` per `get_part()` request.
347
        let mut entries_stream = stream::iter(entries)
348
2.11k
            .map(move |index_entry| async move {
349
2.11k
                let 
data2.11k
= self
350
2.11k
                    .content_store
351
2.11k
                    .get_part_unchunked(index_entry, 0, None)
352
2.11k
                    .await
353
2.11k
                    .err_tip(|| "Failed to get_part in content_store in dedup_store")
?1
;
354
355
2.11k
                Result::<_, Error>::Ok(data)
356
4.22k
            })
357
            .buffered(self.max_concurrent_fetch_per_get);
358
359
        // Stream out the buffered data one at a time and write the data to our writer stream.
360
        // In the event any of these error, we will abort early and abandon all the rest of the
361
        // streamed data.
362
        // Note: Need to take special care to ensure we send the proper slice of data requested.
363
        let mut bytes_to_skip = usize::try_from(offset - start_byte_in_stream)
364
            .err_tip(|| "Could not convert (offset - start_byte_in_stream) to usize")?;
365
        let mut bytes_to_send = usize::try_from(length.unwrap_or(u64::MAX - offset))
366
            .err_tip(|| "Could not convert length to usize")?;
367
        while let Some(result) = entries_stream.next().await {
368
            let mut data = result.err_tip(|| "Inner store iterator closed early in DedupStore")?;
369
            assert!(
370
                bytes_to_skip <= data.len(),
371
                "Formula above must be wrong, {} > {}",
372
                bytes_to_skip,
373
                data.len()
374
            );
375
            let end_pos = cmp::min(data.len(), bytes_to_send + bytes_to_skip);
376
            if bytes_to_skip != 0 || data.len() > bytes_to_send {
377
                data = data.slice(bytes_to_skip..end_pos);
378
            }
379
            writer
380
                .send(data)
381
                .await
382
                .err_tip(|| "Failed to write data to get_part dedup")?;
383
            bytes_to_send -= end_pos - bytes_to_skip;
384
            bytes_to_skip = 0;
385
        }
386
387
        // Finish our stream by writing our EOF and shutdown the stream.
388
        writer
389
            .send_eof()
390
            .err_tip(|| "Failed to write EOF out from get_part dedup")?;
391
        Ok(())
392
935
    }
393
394
0
    fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver {
395
0
        self
396
0
    }
397
398
0
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
399
0
        self
400
0
    }
401
402
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
403
0
        self
404
0
    }
405
406
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
407
0
        self.index_store
408
0
            .register_remove_callback(callback.clone())?;
409
0
        self.content_store.register_remove_callback(callback)?;
410
0
        Ok(())
411
0
    }
412
}
413
414
default_health_status_indicator!(DedupStore);