Skip to main content

kv_extsort/
sorter.rs

1use std::{
2    error::Error,
3    sync::{atomic::AtomicBool, Arc},
4};
5
6use bytemuck::Pod;
7use crossbeam_channel::{bounded, unbounded, Receiver, Select, Sender};
8use log::debug;
9
10use crate::{
11    chunk::{FileChunk, FileChunkDir, MemChunk},
12    merge::merge_chunks_with_binary_heap,
13    Result,
14};
15
16pub struct SortConfig {
17    pub(crate) max_chunk_bytes: usize,
18    pub(crate) concurrency: usize,
19    pub(crate) merge_k: usize,
20    pub(crate) canceled: Arc<AtomicBool>,
21}
22
23impl Default for SortConfig {
24    fn default() -> Self {
25        Self {
26            max_chunk_bytes: 512 * 1024 * 1024,
27            concurrency: 8,
28            merge_k: 16,
29            canceled: Arc::new(AtomicBool::new(false)),
30        }
31    }
32}
33
34impl SortConfig {
35    pub fn new() -> Self {
36        Default::default()
37    }
38
39    pub fn set_cancel_flag(self, canceled: Arc<AtomicBool>) -> Self {
40        Self { canceled, ..self }
41    }
42
43    pub fn get_cancel_flag(&self) -> Arc<AtomicBool> {
44        self.canceled.clone()
45    }
46
47    pub(crate) fn ensure_not_canceled<E>(&self) -> Result<(), E> {
48        if self.canceled.load(std::sync::atomic::Ordering::Relaxed) {
49            Err(crate::Error::Canceled)
50        } else {
51            Ok(())
52        }
53    }
54
55    pub fn max_chunk_bytes(self, max_chunk_bytes: usize) -> Self {
56        assert!(
57            max_chunk_bytes > 0,
58            "max_chunk_bytes must be greater than 0"
59        );
60        Self {
61            max_chunk_bytes,
62            ..self
63        }
64    }
65
66    pub fn concurrency(self, concurrency: usize) -> Self {
67        assert!(concurrency > 0, "concurrency must be greater than 0");
68        Self {
69            concurrency,
70            ..self
71        }
72    }
73
74    pub fn merge_k(self, merge_k: usize) -> Self {
75        assert!(merge_k >= 2, "merge_k must not be less than 2");
76        Self { merge_k, ..self }
77    }
78}
79
80pub fn sort<K, E>(
81    source: impl Iterator<Item = std::result::Result<(K, Vec<u8>), E>> + Send,
82    config: SortConfig,
83) -> SortedIter<K, E>
84where
85    K: Ord + Pod + Copy + Send + Sync,
86    E: Error + Send + 'static,
87{
88    let (output_tx, output_rx) = bounded(config.concurrency * 16);
89    let chunk_dir = match FileChunkDir::<K>::new() {
90        Ok(chunk_dir) => Arc::new(chunk_dir),
91        Err(e) => {
92            let _ = output_tx.send(Err(e));
93            return SortedIter::new(output_rx, None);
94        }
95    };
96    let (file_chunk_tx, file_chunk_rx) = unbounded();
97    let chunk_dir = chunk_dir.clone();
98
99    {
100        let chunk_dir = chunk_dir.clone();
101        rayon::ThreadPoolBuilder::new()
102            .num_threads(config.concurrency + 1)
103            .build()
104            .unwrap()
105            .install(|| {
106                start_sorting_stage(&config, source, chunk_dir.clone(), file_chunk_tx);
107                start_merging_stage(&config, file_chunk_rx, chunk_dir.clone(), output_tx);
108            });
109    }
110
111    SortedIter::new(output_rx, Some(chunk_dir))
112}
113
114pub struct SortedIter<K: Pod, E> {
115    output_rx: Receiver<Result<(K, Vec<u8>), E>>,
116    done: bool,
117    #[allow(dead_code)]
118    chunk_dir: Option<Arc<FileChunkDir<K>>>,
119}
120
121impl<K: Pod, E> SortedIter<K, E> {
122    fn new(
123        output_rx: Receiver<Result<(K, Vec<u8>), E>>,
124        chunk_dir: Option<Arc<FileChunkDir<K>>>,
125    ) -> Self {
126        SortedIter {
127            output_rx,
128            chunk_dir,
129            done: false,
130        }
131    }
132}
133
134impl<K: Pod, E> Iterator for SortedIter<K, E> {
135    type Item = Result<(K, Vec<u8>), E>;
136
137    fn next(&mut self) -> Option<Self::Item> {
138        if self.done {
139            return None;
140        }
141        match self.output_rx.recv() {
142            Ok(Ok(v)) => Some(Ok(v)),
143            Ok(Err(e)) => {
144                self.done = true;
145                Some(Err(e))
146            }
147            Err(_) => {
148                self.done = true;
149                None
150            }
151        }
152    }
153}
154
155fn mem_to_file_chunk<K: Pod + Ord, E>(
156    buffer: Vec<(K, Vec<u8>)>,
157    chunk_dir: Arc<FileChunkDir<K>>,
158) -> Result<FileChunk<K>, E> {
159    let mem_chunk = MemChunk::from_unsorted(buffer);
160    let mut file_chunk = chunk_dir.add_chunk()?;
161    mem_chunk.write_to_file(&mut file_chunk)?;
162    Ok(file_chunk.finalize())
163}
164
165fn start_sorting_stage<K, E>(
166    config: &SortConfig,
167    source: impl Iterator<Item = std::result::Result<(K, Vec<u8>), E>> + Send,
168    chunk_dir: Arc<FileChunkDir<K>>,
169    chunk_tx: Sender<Result<FileChunk<K>, E>>,
170) where
171    K: Ord + Pod + Copy + Send + Sync,
172    E: Send + 'static,
173{
174    debug!("Sorting stage started.");
175
176    let item_header_size = std::mem::size_of::<Vec<u8>>();
177    let mut chunk_size = 0;
178
179    let mut buffer = Vec::new();
180
181    for res in source {
182        match res {
183            Ok((key, value)) => {
184                let item_size = item_header_size + value.len();
185                if chunk_size + item_size >= config.max_chunk_bytes {
186                    let buffer = std::mem::take(&mut buffer);
187                    let chunk_dir = chunk_dir.clone();
188                    let chunk_tx = chunk_tx.clone();
189                    if let Err(e) = config.ensure_not_canceled() {
190                        let _ = chunk_tx.send(Err(e));
191                        return;
192                    }
193                    rayon::spawn(move || {
194                        let _ = chunk_tx.send(mem_to_file_chunk(buffer, chunk_dir));
195                    });
196                    chunk_size = 0;
197                }
198                chunk_size += item_size;
199                buffer.push((key, value));
200            }
201            Err(e) => {
202                let _ = chunk_tx.send(Err(crate::Error::Source(e)));
203            }
204        }
205    }
206
207    // last chunk
208    if !buffer.is_empty() {
209        if let Err(e) = config.ensure_not_canceled() {
210            let _ = chunk_tx.send(Err(e));
211            return;
212        }
213        rayon::spawn(move || {
214            let _ = chunk_tx.send(mem_to_file_chunk(buffer, chunk_dir));
215        });
216    }
217}
218
219fn start_merging_stage<K, E>(
220    config: &SortConfig,
221    chunk_rx: Receiver<Result<FileChunk<K>, E>>,
222    chunk_dir: Arc<FileChunkDir<K>>,
223    output_tx: Sender<Result<(K, Vec<u8>), E>>,
224) where
225    K: Ord + Pod + Copy + Send + Sync,
226    E: Send + 'static,
227{
228    debug!("Merging stage started.");
229
230    let (merged_tx, merged_rx) = unbounded::<Result<FileChunk<K>, E>>();
231    let mut pending = Vec::new();
232    let mut source_finished = false;
233    let mut num_running_merges = 0;
234
235    let mut recv_select = Select::new();
236    recv_select.recv(&chunk_rx); // select index=0
237    recv_select.recv(&merged_rx); // select index=1
238
239    loop {
240        let idx = recv_select.ready();
241        match idx {
242            // Receive chunks from the sorting stage
243            0 => match chunk_rx.try_recv() {
244                Ok(Ok(chunk)) => {
245                    debug!("Received chunk: items={}", chunk.len());
246                    pending.push(chunk)
247                }
248                Ok(Err(e)) => {
249                    let _ = output_tx.send(Err(e));
250                    break;
251                }
252                Err(_) => {
253                    debug!("All chunks received from the sorting stage");
254                    source_finished = true;
255                    recv_select.remove(0);
256                }
257            },
258            // Receive merged chunks
259            1 => match merged_rx.try_recv() {
260                Ok(Ok(chunk)) => {
261                    debug!("Received merged chunk: items={}", chunk.len());
262                    num_running_merges -= 1;
263                    pending.push(chunk)
264                }
265                Ok(Err(e)) => {
266                    let _ = output_tx.send(Err(e));
267                    break;
268                }
269                Err(_) => {
270                    panic!("merged_rx should not be closed at this point")
271                }
272            },
273            _ => unreachable!(),
274        }
275
276        if let Err(e) = config.ensure_not_canceled() {
277            let _ = output_tx.send(Err(e));
278        }
279
280        // Plan to merge
281        let total_chunks = pending.len() + num_running_merges;
282        let num_merge = if source_finished {
283            if pending.len() > config.merge_k {
284                (total_chunks - config.merge_k + 1).min(config.merge_k)
285            } else if num_running_merges == 0 {
286                break;
287            } else {
288                continue;
289            }
290        } else if total_chunks >= config.merge_k * 2 - 1 {
291            if pending.len() >= config.merge_k {
292                config.merge_k
293            } else {
294                continue;
295            }
296        } else {
297            continue;
298        };
299
300        pending.sort_by_key(|chunk| chunk.len());
301        let remaining = pending.split_off(num_merge.min(pending.len()));
302        let merging = std::mem::replace(&mut pending, remaining);
303
304        let merged_tx = merged_tx.clone();
305        let mut chunk_writer = match chunk_dir.add_chunk() {
306            Ok(chunk_writer) => chunk_writer,
307            Err(e) => {
308                let _ = output_tx.send(Err(e));
309                break;
310            }
311        };
312
313        // Start merging
314        debug!("Start merging {} chunks", merging.len());
315        num_running_merges += 1;
316        let canceled = config.canceled.clone();
317        rayon::spawn(move || {
318            match merge_chunks_with_binary_heap(canceled, merging, |(key, value)| {
319                chunk_writer.push(&key, &value)
320            }) {
321                Ok(()) => {
322                    let _ = merged_tx.send(Ok(chunk_writer.finalize()));
323                }
324                Err(e) => {
325                    let _ = merged_tx.send(Err(e));
326                }
327            }
328        });
329    }
330
331    let canceled = config.canceled.clone();
332    if canceled.load(std::sync::atomic::Ordering::Relaxed) {
333        return;
334    }
335
336    debug!("Start iteration by merging {} chunks", pending.len());
337    rayon::spawn(move || {
338        if let Err(e) = merge_chunks_with_binary_heap(canceled, pending, |(key, value)| {
339            let _ = output_tx.send(Ok((key, value)));
340            Ok(())
341        }) {
342            let _ = merged_tx.send(Err(e));
343        }
344        drop(chunk_dir);
345    });
346}