Skip to main content

xet_data/deduplication/
file_deduplication.rs

1use std::result::Result;
2
3use more_asserts::{debug_assert_le, debug_assert_lt};
4use xet_core_structures::MerkleHashMap;
5use xet_core_structures::merklehash::{ChunkHashList, MerkleHash, file_hash};
6use xet_core_structures::metadata_shard::file_structs::{
7    FileDataSequenceEntry, FileDataSequenceHeader, FileMetadataExt, FileVerificationEntry, MDBFileInfo,
8};
9use xet_core_structures::metadata_shard::hash_is_global_dedup_eligible;
10use xet_runtime::core::XetContext;
11
12use super::constants::{MAX_XORB_BYTES, MAX_XORB_CHUNKS};
13use super::data_aggregator::DataAggregator;
14use super::dedup_metrics::DeduplicationMetrics;
15use super::defrag_prevention::DefragPrevention;
16use super::interface::DeduplicationDataInterface;
17use super::{Chunk, RawXorbData};
18use crate::progress_tracking::upload_tracking::FileXorbDependency;
19
20pub struct FileDeduper<DataInterfaceType: DeduplicationDataInterface> {
21    #[cfg_attr(not(feature = "simulation"), allow(dead_code))]
22    ctx: XetContext,
23
24    data_mng: DataInterfaceType,
25
26    /// A tag for tracking the file externally
27    file_id: u64,
28
29    /// The new data here that hasn't yet been deduplicated.
30    new_data: Vec<Chunk>,
31
32    /// The amount of new data we have.
33    new_data_size: usize,
34
35    /// A hashmap allowing deduplication against the current chunk.
36    new_data_hash_lookup: MerkleHashMap<usize>,
37
38    /// The current chunk hashes for this file.
39    chunk_hashes: ChunkHashList,
40
41    /// The current file data entries.
42    file_info: Vec<FileDataSequenceEntry>,
43
44    /// The list of indices in which the file entry references the current data
45    internally_referencing_entries: Vec<usize>,
46
47    /// Tracking the defragmentation of the file specification.
48    defrag_tracker: DefragPrevention,
49
50    /// The minimum number of chunks to wait for between generating global
51    /// dedup queries.  Can be changed by testing code.
52    min_spacing_between_global_dedup_queries: usize,
53
54    /// The next chunk index that is eligible for global dedup queries
55    next_chunk_index_eligible_for_global_dedup_query: usize,
56
57    /// The tracked deduplication metrics for this file.
58    deduplication_metrics: DeduplicationMetrics,
59}
60
61impl<DataInterfaceType: DeduplicationDataInterface> FileDeduper<DataInterfaceType> {
62    pub fn new(data_manager: DataInterfaceType, file_id: u64, ctx: XetContext) -> Self {
63        Self {
64            ctx: ctx.clone(),
65            data_mng: data_manager,
66            file_id,
67            new_data: Vec::new(),
68            new_data_size: 0,
69            new_data_hash_lookup: MerkleHashMap::new(),
70            chunk_hashes: Vec::new(),
71            file_info: Vec::new(),
72            internally_referencing_entries: Vec::new(),
73            defrag_tracker: DefragPrevention::new(&ctx),
74            min_spacing_between_global_dedup_queries: 0,
75            next_chunk_index_eligible_for_global_dedup_query: 0,
76            deduplication_metrics: DeduplicationMetrics::default(),
77        }
78    }
79
80    pub async fn process_chunks(
81        &mut self,
82        chunks: &[Chunk],
83    ) -> Result<DeduplicationMetrics, DataInterfaceType::ErrorType> {
84        // track the different deduplication statistics.
85        let mut dedup_metrics = DeduplicationMetrics::default();
86
87        // Track new xorb dependencies
88        let mut xorb_dependencies = Vec::new();
89
90        // All the previous chunk are stored here, use it as the global chunk index start.
91        let global_chunk_index_start = self.chunk_hashes.len();
92
93        #[cfg(feature = "simulation")]
94        let xorb_cut_bytes = self
95            .ctx
96            .config
97            .xorb
98            .simulation_max_bytes
99            .map(|bs| bs.as_u64().min(*MAX_XORB_BYTES as u64) as usize)
100            .unwrap_or(*MAX_XORB_BYTES);
101        #[cfg(not(feature = "simulation"))]
102        let xorb_cut_bytes = *MAX_XORB_BYTES;
103        #[cfg(feature = "simulation")]
104        let xorb_cut_chunks = self
105            .ctx
106            .config
107            .xorb
108            .simulation_max_chunks
109            .unwrap_or(*MAX_XORB_CHUNKS)
110            .min(*MAX_XORB_CHUNKS);
111        #[cfg(not(feature = "simulation"))]
112        let xorb_cut_chunks = *MAX_XORB_CHUNKS;
113
114        let chunk_hashes = Vec::from_iter(chunks.iter().map(|c| c.hash));
115
116        // Now, parallelize the querying of potential new shards on the server end with
117        // querying for dedup information of the chunks, which are the two most expensive
118        // parts of the process.  Then when we go into the next section, everything is essentially
119        // a local lookup table so the remaining work should be quite fast.
120
121        // This holds the results of the dedup queries.
122        let mut deduped_blocks = vec![None; chunks.len()];
123
124        // Do at most two passes; 1) with global dedup querying possibly enabled, and 2) possibly rerunning
125        // if the global dedup query came back with a new shard.
126
127        for first_pass in [true, false] {
128            // Now, go through and test all of these for whether or not they can be deduplicated.
129            let mut local_chunk_index = 0;
130            while local_chunk_index < chunks.len() {
131                let global_chunk_index = global_chunk_index_start + local_chunk_index;
132
133                // First check to see if we don't already know what these blocks are from a previous pass.
134                if let Some((n_deduped, _, _)) = &deduped_blocks[local_chunk_index] {
135                    local_chunk_index += n_deduped;
136                } else if let Some((n_deduped, fse, is_uploaded_shard)) =
137                    self.data_mng.chunk_hash_dedup_query(&chunk_hashes[local_chunk_index..]).await?
138                {
139                    if !first_pass {
140                        // This means new shards were discovered; so these are global dedup eligible.  We'll record
141                        // the rest later on
142                        dedup_metrics.deduped_chunks_by_global_dedup += n_deduped as u64;
143                        dedup_metrics.deduped_bytes_by_global_dedup += fse.unpacked_segment_bytes as u64;
144                    }
145
146                    deduped_blocks[local_chunk_index] = Some((n_deduped, fse, is_uploaded_shard));
147                    local_chunk_index += n_deduped;
148
149                    // Now see if we can issue a background query against the global dedup server to see if
150                    // any shards are present that give us more dedup ability.
151                    //
152                    // If we've already queried these against the global dedup, then we can proceed on without
153                    // re-querying anything.  Only doing this on the first pass also guarantees that in the case of
154                    // errors on shard retrieval, we don't get stuck in a loop trying to download
155                    // and reprocess.
156                } else {
157                    // Check for global deduplication.
158                    if
159                    // Only do this query on the first pass.
160                    first_pass
161                        // The first hash of every file and those matching a pattern are eligible. 
162                        && (global_chunk_index == 0
163                            || hash_is_global_dedup_eligible(&chunk_hashes[local_chunk_index]))
164                        // Limit by enforcing at least 4MB between chunk queries.
165                        && global_chunk_index >= self.next_chunk_index_eligible_for_global_dedup_query
166                    {
167                        self.data_mng
168                            .register_global_dedup_query(chunk_hashes[local_chunk_index])
169                            .await?;
170
171                        self.next_chunk_index_eligible_for_global_dedup_query =
172                            global_chunk_index + self.min_spacing_between_global_dedup_queries;
173                    }
174
175                    local_chunk_index += 1;
176                }
177            }
178
179            // Now, see if any of the chunk queries have completed.
180            let new_shards_added = self.data_mng.complete_global_dedup_queries().await?;
181
182            if !new_shards_added {
183                break;
184            }
185        }
186
187        // Now, go through and process the result of the query.
188        let mut cur_idx = 0;
189
190        while cur_idx < chunks.len() {
191            let mut dedupe_query = deduped_blocks[cur_idx].take();
192
193            if dedupe_query.is_none() {
194                // In this case, do a second query against the local xorb to see if we're just repeating previous
195                // information in the xorb.
196                dedupe_query = self.dedup_query_against_local_data(&chunk_hashes[cur_idx..]);
197            }
198
199            if let Some((n_deduped, fse, is_external)) = dedupe_query {
200                dedup_metrics.deduped_chunks += n_deduped as u64;
201                dedup_metrics.deduped_bytes += fse.unpacked_segment_bytes as u64;
202                dedup_metrics.total_chunks += n_deduped as u64;
203                dedup_metrics.total_bytes += fse.unpacked_segment_bytes as u64;
204
205                // check the fragmentation state and if it is pretty fragmented,
206                // we skip dedupe.  However, continuing the previous is always fine.
207                if self.file_data_sequence_continues_current(&fse)
208                    || self.defrag_tracker.allow_dedup_on_next_range(n_deduped)
209                {
210                    // Report this as a dependency
211                    // The case where it's dededuped against the present xorb is handled
212                    // when the xorb gets cut and we know the hash.
213                    if fse.xorb_hash != MerkleHash::marker() {
214                        xorb_dependencies.push(FileXorbDependency {
215                            file_id: self.file_id,
216                            xorb_hash: fse.xorb_hash,
217                            n_bytes: fse.unpacked_segment_bytes as u64,
218                            is_external,
219                        });
220                    }
221
222                    // We found one or more chunk hashes present
223                    self.add_file_data_sequence_entry(fse, n_deduped);
224
225                    cur_idx += n_deduped;
226                    continue;
227                } else {
228                    dedup_metrics.defrag_prevented_dedup_chunks += n_deduped as u64;
229                    dedup_metrics.defrag_prevented_dedup_bytes += fse.unpacked_segment_bytes as u64;
230                }
231            }
232
233            // Okay, now we need to add new data.
234            let n_bytes = chunks[cur_idx].data.len();
235
236            dedup_metrics.total_chunks += 1;
237            dedup_metrics.total_bytes += n_bytes as u64;
238            dedup_metrics.new_bytes += n_bytes as u64;
239            dedup_metrics.new_chunks += 1;
240
241            // Do we need to cut a new xorb first?
242            if self.new_data_size + n_bytes > xorb_cut_bytes || self.new_data.len() + 1 > xorb_cut_chunks {
243                let new_xorb = self.cut_new_xorb();
244                xorb_dependencies.push(FileXorbDependency {
245                    file_id: self.file_id,
246                    xorb_hash: new_xorb.hash(),
247                    n_bytes: new_xorb.num_bytes() as u64,
248                    is_external: false,
249                });
250                self.data_mng.register_new_xorb(new_xorb).await?;
251            }
252
253            if !self.file_info.is_empty()
254                && self.file_info.last().unwrap().xorb_hash == MerkleHash::marker()
255                && self.file_info.last().unwrap().chunk_index_end as usize == self.new_data.len()
256            {
257                // This is the next chunk in the CAS block we're building,
258                // in which case we can just modify the previous entry.
259                let last_entry = self.file_info.last_mut().unwrap();
260                last_entry.unpacked_segment_bytes += n_bytes as u32;
261                last_entry.chunk_index_end += 1;
262                self.defrag_tracker.increment_last_range_in_fragmentation_estimate(1);
263            } else {
264                // This block is unrelated to the previous one.
265                // This chunk will get the CAS hash updated when the local CAS block
266                // is full and registered.
267                let file_info_len = self.file_info.len();
268                self.internally_referencing_entries.push(file_info_len);
269                let chunk_idx = self.new_data.len();
270
271                self.file_info.push(FileDataSequenceEntry::new(
272                    MerkleHash::marker(),
273                    n_bytes,
274                    chunk_idx,
275                    chunk_idx + 1,
276                ));
277                self.defrag_tracker.add_range_to_fragmentation_estimate(1);
278            }
279
280            let chunk = chunks[cur_idx].clone();
281            self.new_data_size += chunk.data.len();
282            self.new_data_hash_lookup.insert(chunk.hash, self.new_data.len());
283            self.new_data.push(chunk);
284
285            // Next round.
286            cur_idx += 1;
287        }
288
289        self.deduplication_metrics.merge_in(&dedup_metrics);
290        self.chunk_hashes.extend(chunks.iter().map(|c| (c.hash, c.data.len() as u64)));
291
292        // Register the xorb dependencies as needed.
293        if !xorb_dependencies.is_empty() {
294            self.data_mng.register_xorb_dependencies(&xorb_dependencies).await;
295        }
296
297        Ok(dedup_metrics)
298    }
299
300    fn file_data_sequence_continues_current(&self, fse: &FileDataSequenceEntry) -> bool {
301        !self.file_info.is_empty()
302            && self.file_info.last().unwrap().xorb_hash == fse.xorb_hash
303            && self.file_info.last().unwrap().chunk_index_end == fse.chunk_index_start
304    }
305
306    /// Add a new file data sequence entry to the current process, possibly merging with the
307    /// previous entry.
308    fn add_file_data_sequence_entry(&mut self, fse: FileDataSequenceEntry, n_deduped: usize) {
309        // Do we modify the previous entry as this is the next logical chunk, or do we
310        // start a new entry?
311        if self.file_data_sequence_continues_current(&fse) {
312            // This block is the contiguous continuation of the last entry
313            let last_entry = self.file_info.last_mut().unwrap();
314            last_entry.unpacked_segment_bytes += fse.unpacked_segment_bytes;
315            last_entry.chunk_index_end = fse.chunk_index_end;
316
317            // Update the fragmentation estimation window
318            self.defrag_tracker.increment_last_range_in_fragmentation_estimate(n_deduped);
319        } else {
320            // Make sure we're tracking any that we need to fill in later.
321            if fse.xorb_hash == MerkleHash::marker() {
322                self.internally_referencing_entries.push(self.file_info.len());
323            }
324            // This block is new
325            self.file_info.push(fse);
326            self.defrag_tracker.add_range_to_fragmentation_estimate(n_deduped);
327        }
328    }
329
330    /// Cut a new xorb from the existing data.  
331    fn cut_new_xorb(&mut self) -> RawXorbData {
332        // Cut the new xorb.
333        let new_xorb = RawXorbData::from_chunks(&self.new_data[..], vec![0]);
334
335        let xorb_hash = new_xorb.hash();
336
337        // Go through and replace all the indices in the file sequence entries with
338        // the new xorb if referenced.
339        for &idx in self.internally_referencing_entries.iter() {
340            let fse = &mut self.file_info[idx];
341            debug_assert_eq!(fse.xorb_hash, MerkleHash::marker());
342            debug_assert_lt!(fse.chunk_index_start as usize, self.new_data.len());
343            debug_assert_le!(fse.chunk_index_end as usize, self.new_data.len());
344
345            fse.xorb_hash = xorb_hash;
346        }
347
348        #[cfg(debug_assertions)]
349        {
350            // For bookkeeping checks, make sure we have everything.
351            for fse in self.file_info.iter() {
352                debug_assert_ne!(fse.xorb_hash, MerkleHash::marker());
353            }
354        }
355
356        // Clear out the old data.
357        self.new_data.clear();
358        self.new_data_hash_lookup.clear();
359        self.new_data_size = 0;
360        self.internally_referencing_entries.clear();
361
362        new_xorb
363    }
364
365    /// Do a query against the local data; this would return an entry with MerkleHash::marker(), which
366    /// would need to get filled in.
367    fn dedup_query_against_local_data(
368        &mut self,
369        chunks: &[MerkleHash],
370    ) -> Option<(usize, FileDataSequenceEntry, bool)> {
371        // It's important for the defrag prevention to have a good estimate of the number of chunks in
372        // a row that can be deduplicated, so this pulls through the
373        if let Some(&base_idx) = self.new_data_hash_lookup.get(&chunks[0]) {
374            let mut n_bytes = self.new_data[base_idx].data.len();
375
376            let mut end_idx = base_idx + 1;
377            for (i, chunk) in chunks.iter().enumerate().skip(1) {
378                if let Some(&idx) = self.new_data_hash_lookup.get(chunk)
379                    && idx == base_idx + i
380                {
381                    end_idx = idx + 1;
382                    n_bytes += self.new_data[idx].data.len();
383                    continue;
384                }
385                break;
386            }
387
388            Some((
389                end_idx - base_idx,
390                FileDataSequenceEntry::new(MerkleHash::marker(), n_bytes, base_idx, end_idx),
391                false,
392            ))
393        } else {
394            None
395        }
396    }
397
398    /// Finalize the internal state, converting remaining data to a DataAggregator object that contains the file info
399    /// and remaining data.  Also returns the aggregated deduplication metrics and the list of xorb hashes that were
400    /// registered as part of this run.
401    ///
402    /// Returns (file hash, chunk_hashes, data aggregation, deduplication metrics)
403    pub fn finalize(
404        self,
405        metadata_ext: Option<FileMetadataExt>,
406    ) -> (MerkleHash, ChunkHashList, DataAggregator, DeduplicationMetrics) {
407        let file_hash = file_hash(&self.chunk_hashes);
408
409        let metadata = FileDataSequenceHeader::new(file_hash, self.file_info.len(), true, metadata_ext.is_some());
410
411        let mut chunk_idx = 0;
412
413        // Create the file verification stamp.
414        let verification = self
415            .file_info
416            .iter()
417            .map(|entry| {
418                let n_chunks = (entry.chunk_index_end - entry.chunk_index_start) as usize;
419                let chunk_hashes: Vec<_> = self.chunk_hashes[chunk_idx..chunk_idx + n_chunks]
420                    .iter()
421                    .map(|(hash, _)| *hash)
422                    .collect();
423                let range_hash =
424                    xet_core_structures::metadata_shard::chunk_verification::range_hash_from_chunks(&chunk_hashes);
425                chunk_idx += n_chunks;
426
427                FileVerificationEntry::new(range_hash)
428            })
429            .collect();
430
431        let fi = MDBFileInfo {
432            metadata,
433            segments: self.file_info,
434            verification,
435            metadata_ext,
436        };
437
438        let remaining_data = DataAggregator::new(self.new_data, fi, self.internally_referencing_entries, self.file_id);
439
440        (file_hash, self.chunk_hashes, remaining_data, self.deduplication_metrics)
441    }
442}