Skip to main content

nectar_primitives/file/
splitter_parallel.rs

1//! Sync parallel file splitter using random-access data sources.
2
3use std::marker::PhantomData;
4
5use rayon::prelude::*;
6
7use crate::bmt::DEFAULT_BODY_SIZE;
8use crate::chunk::AnyChunk;
9
10use super::constants::{LEVEL_LIMIT, compute_spans_inline};
11use super::error::{FileError, Result};
12use super::mode::{PlainMode, SplitMode};
13use super::read_at::ReadAt;
14use super::tree::TreeParams;
15
16#[cfg(feature = "encryption")]
17use super::mode::EncryptedMode;
18
19/// Parallel file splitter using random-access data sources.
20///
21/// Produces chunks by reading data at known offsets in parallel, then building
22/// intermediate levels. The caller decides where produced chunks go.
23pub struct GenericParallelSplitter<M: SplitMode, const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
24    _mode: PhantomData<M>,
25}
26
27/// Plain (unencrypted) parallel splitter.
28pub type ParallelSplitter<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> =
29    GenericParallelSplitter<PlainMode, BODY_SIZE>;
30
31/// Encrypted parallel splitter.
32#[cfg(feature = "encryption")]
33pub type EncryptedParallelSplitter<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> =
34    GenericParallelSplitter<EncryptedMode, BODY_SIZE>;
35
36impl<M, const BODY_SIZE: usize> std::fmt::Debug for GenericParallelSplitter<M, BODY_SIZE>
37where
38    M: SplitMode,
39{
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.debug_struct("GenericParallelSplitter")
42            .finish_non_exhaustive()
43    }
44}
45
46impl<M, const BODY_SIZE: usize> GenericParallelSplitter<M, BODY_SIZE>
47where
48    M: SplitMode + Send + Sync,
49{
50    /// Split `source`, invoking `sink` for every produced chunk.
51    ///
52    /// `sink` is called from rayon worker threads, so it must be `Fn + Sync`.
53    /// Returns the root reference.
54    pub fn split_into<R, F>(source: &R, sink: F) -> Result<M::RootRef>
55    where
56        R: ReadAt + Sync,
57        F: Fn(AnyChunk<BODY_SIZE>) + Sync,
58    {
59        const { super::constants::assert_valid_body_size::<BODY_SIZE>() };
60        let size = source.len();
61        let tree = TreeParams::<BODY_SIZE>::new(size);
62
63        if size == 0 {
64            let (chunk, root) = M::empty_chunk::<BODY_SIZE>()?;
65            sink(chunk.into());
66            return Ok(root);
67        }
68
69        let spans = compute_spans_inline(BODY_SIZE / M::REF_SIZE);
70
71        // Level 0: produce data chunks in parallel.
72        let level0_refs = Self::create_data_chunks(source, &tree, &sink)?;
73
74        // Build intermediate levels.
75        Self::build_intermediate_levels(level0_refs, size, &spans, &sink)
76    }
77
78    /// Split `source`, collecting every produced chunk into a `Vec`.
79    ///
80    /// Chunk order is irrelevant; callers key by address. Returns the root
81    /// reference and the produced chunks.
82    pub fn split_to_vec<R: ReadAt + Sync>(
83        source: &R,
84    ) -> Result<(M::RootRef, Vec<AnyChunk<BODY_SIZE>>)> {
85        let chunks = std::sync::Mutex::new(Vec::new());
86        let root = Self::split_into(source, |chunk| chunks.lock().unwrap().push(chunk))?;
87        Ok((root, chunks.into_inner().unwrap()))
88    }
89
90    fn create_data_chunks<R, F>(
91        source: &R,
92        tree: &TreeParams<BODY_SIZE>,
93        sink: &F,
94    ) -> Result<Vec<M::RefBytes>>
95    where
96        R: ReadAt + Sync,
97        F: Fn(AnyChunk<BODY_SIZE>) + Sync,
98    {
99        let data_chunks = tree.data_chunks();
100        let size = tree.size();
101
102        let results: Vec<Result<M::RefBytes>> = (0..data_chunks)
103            .into_par_iter()
104            .map(|i| {
105                let offset = i * BODY_SIZE as u64;
106                let chunk_size = ((size - offset) as usize).min(BODY_SIZE);
107
108                let mut buf = vec![0u8; chunk_size];
109                source
110                    .read_at(offset, &mut buf)
111                    .map_err(|e| FileError::Store(Box::new(e)))?;
112
113                let span = if i + 1 == data_chunks {
114                    size - offset
115                } else {
116                    BODY_SIZE as u64
117                };
118
119                let chunk_bytes = super::helpers::build_intermediate_payload(span, &buf);
120
121                let (chunk, ref_bytes) = M::prepare_chunk::<BODY_SIZE>(chunk_bytes)?;
122                sink(chunk.into());
123                Ok(ref_bytes)
124            })
125            .collect();
126
127        results.into_iter().collect()
128    }
129
130    fn build_intermediate_levels<F>(
131        mut refs: Vec<M::RefBytes>,
132        total_size: u64,
133        spans: &[u64; LEVEL_LIMIT],
134        sink: &F,
135    ) -> Result<M::RootRef>
136    where
137        F: Fn(AnyChunk<BODY_SIZE>) + Sync,
138    {
139        let mut level = 1;
140
141        while refs.len() > 1 {
142            refs = Self::build_level(&refs, level, total_size, spans, sink)?;
143            level += 1;
144        }
145
146        // Extract root reference from the single remaining ref.
147        M::extract_root(refs[0].as_ref())
148    }
149
150    fn build_level<F>(
151        refs: &[M::RefBytes],
152        level: usize,
153        total_size: u64,
154        spans: &[u64; LEVEL_LIMIT],
155        sink: &F,
156    ) -> Result<Vec<M::RefBytes>>
157    where
158        F: Fn(AnyChunk<BODY_SIZE>) + Sync,
159    {
160        let refs_per_chunk = M::refs_per_chunk(BODY_SIZE);
161        let chunks_at_level = refs.len().div_ceil(refs_per_chunk);
162        let max_span = spans[level] * BODY_SIZE as u64;
163
164        let results: Vec<Result<M::RefBytes>> = (0..chunks_at_level)
165            .into_par_iter()
166            .map(|i| {
167                let start = i * refs_per_chunk;
168                let end = (start + refs_per_chunk).min(refs.len());
169                let child_refs = &refs[start..end];
170
171                // Single reference: carry up without wrapping (dangling chunk optimization).
172                if child_refs.len() == 1 {
173                    return Ok(child_refs[0].clone());
174                }
175
176                let span = if i + 1 == chunks_at_level {
177                    total_size.saturating_sub(i as u64 * max_span)
178                } else {
179                    max_span
180                };
181
182                let ref_data: Vec<u8> = child_refs
183                    .iter()
184                    .flat_map(|r| r.as_ref())
185                    .copied()
186                    .collect();
187                let chunk_bytes = super::helpers::build_intermediate_payload(span, &ref_data);
188
189                let (chunk, ref_bytes) = M::prepare_chunk::<BODY_SIZE>(chunk_bytes)?;
190                sink(chunk.into());
191                Ok(ref_bytes)
192            })
193            .collect();
194
195        results.into_iter().collect()
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::file::join;
203    use crate::store::MemoryStore;
204
205    fn split_and_store(
206        data: &[u8],
207    ) -> (crate::chunk::ChunkAddress, MemoryStore<DEFAULT_BODY_SIZE>) {
208        let (root, chunks) = ParallelSplitter::<DEFAULT_BODY_SIZE>::split_to_vec(&data).unwrap();
209        (root, MemoryStore::from_chunks(chunks))
210    }
211
212    generate_plain_splitter_tests!(split_and_store);
213
214    #[test]
215    fn test_parallel_splitter_varying_data() {
216        let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 123)
217            .map(|i| (i % 256) as u8)
218            .collect();
219
220        let (root, store) = split_and_store(&data);
221
222        let (seq_root, _) = crate::file::split::<DEFAULT_BODY_SIZE>(&data).unwrap();
223        assert_eq!(root, seq_root);
224
225        let recovered = futures::executor::block_on(join(&store, root)).unwrap();
226        assert_eq!(recovered, data);
227    }
228
229    #[test]
230    fn test_split_into_lock_free_sink() {
231        use std::sync::atomic::{AtomicUsize, Ordering};
232
233        let data = vec![0xAB; DEFAULT_BODY_SIZE + 1];
234        let count = AtomicUsize::new(0);
235        let root = ParallelSplitter::<DEFAULT_BODY_SIZE>::split_into(&data.as_slice(), |_chunk| {
236            count.fetch_add(1, Ordering::Relaxed);
237        })
238        .unwrap();
239        assert!(!root.is_zero());
240        assert_eq!(count.load(Ordering::Relaxed), 3);
241    }
242
243    #[cfg(feature = "encryption")]
244    mod encrypted {
245        use super::*;
246        use crate::file::{EncryptedParallelSplitter, split_encrypted};
247        use crate::store::MemoryStore;
248
249        fn encrypted_split_and_store(
250            data: &[u8],
251        ) -> (
252            crate::chunk::encryption::EncryptedChunkRef,
253            MemoryStore<DEFAULT_BODY_SIZE>,
254        ) {
255            let (root_ref, chunks) =
256                EncryptedParallelSplitter::<DEFAULT_BODY_SIZE>::split_to_vec(&data).unwrap();
257            (root_ref, MemoryStore::from_chunks(chunks))
258        }
259
260        generate_encrypted_splitter_tests!(encrypted_split_and_store);
261
262        #[test]
263        fn test_encrypted_parallel_matches_sequential() {
264            let data: Vec<u8> = (0..DEFAULT_BODY_SIZE * 5 + 123)
265                .map(|i| (i % 256) as u8)
266                .collect();
267
268            let (par_ref, par_store) = encrypted_split_and_store(&data);
269            let (seq_ref, seq_store) = split_encrypted::<DEFAULT_BODY_SIZE>(&data).unwrap();
270
271            assert_eq!(par_store.len(), seq_store.len());
272
273            let par_recovered = futures::executor::block_on(join(&par_store, par_ref)).unwrap();
274            assert_eq!(par_recovered, data);
275
276            let seq_recovered = futures::executor::block_on(join(&seq_store, seq_ref)).unwrap();
277            assert_eq!(seq_recovered, data);
278        }
279
280        #[test]
281        fn test_encrypted_parallel_nondeterministic() {
282            let data = b"test determinism";
283            let (ref1, _) = encrypted_split_and_store(data);
284            let (ref2, _) = encrypted_split_and_store(data);
285
286            // Different random keys each time.
287            assert_ne!(ref1.address(), ref2.address());
288        }
289    }
290}