Skip to main content

rapidgzip_core/
reader.rs

1use crate::backend::{
2    LineCounter, Output, SequentialDecoder, SequentialItem, decode_source,
3    decode_source_with_index, validate_initial_stream,
4};
5use crate::config::Config;
6use crate::gzip::StreamCursor;
7use crate::index::{DeflateIndex, IndexCollector, IndexOptions};
8use crate::indexed_parallel::IndexedPlan;
9use crate::runtime::{AuxiliaryKind, RuntimeState};
10use crate::{
11    DecodeError, DecodeReport, DecoderHandle, DecoderStats, IndexedDecodeReport, IndexingError,
12    ReadAt, WorkerLimitError,
13};
14use std::io::{self, IoSliceMut, Read};
15use std::sync::Arc;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::mpsc::{self, Receiver, SyncSender, TrySendError};
18use std::thread::{self, JoinHandle};
19use std::time::Duration;
20
21enum Message {
22    Data(Vec<u8>),
23    Finished(Completion),
24    Failed(Failure),
25}
26
27enum Completion {
28    Decode(DecodeReport),
29    Indexed(IndexedDecodeReport),
30}
31
32enum Failure {
33    Decode(DecodeError),
34    Indexing(IndexingError),
35}
36
37struct ChannelOutput {
38    sender: SyncSender<Message>,
39    cancelled: Arc<AtomicBool>,
40    runtime: Arc<RuntimeState>,
41}
42
43impl ChannelOutput {
44    fn send(&self, mut message: Message) -> Result<(), DecodeError> {
45        let mut observed_full = false;
46        loop {
47            if self.cancelled.load(Ordering::Relaxed) {
48                return Err(DecodeError::Cancelled);
49            }
50            match self.sender.try_send(message) {
51                Ok(()) => {
52                    if !observed_full {
53                        self.runtime.set_consumer_blocked(false);
54                    }
55                    return Ok(());
56                }
57                Err(TrySendError::Disconnected(_)) => return Err(DecodeError::Cancelled),
58                Err(TrySendError::Full(returned)) => {
59                    observed_full = true;
60                    self.runtime.set_consumer_blocked(true);
61                    message = returned;
62                    thread::park_timeout(Duration::from_millis(1));
63                }
64            }
65        }
66    }
67}
68
69impl Output for ChannelOutput {
70    fn emit(&mut self, chunk: Vec<u8>) -> Result<(), DecodeError> {
71        let byte_count = chunk.len();
72        self.send(Message::Data(chunk))?;
73        self.runtime.add_decompressed_bytes(byte_count);
74        Ok(())
75    }
76}
77
78enum Terminal {
79    Open,
80    Finished(Completion),
81    Failed(Failure),
82}
83
84/// Owned parallel decoder output implementing [`Read`] and [`Send`].
85///
86/// Reaching EOF means the selected container passed every available check and
87/// makes the final [`DecodeReport`] available through
88/// [`DecoderReader::report`]. A decoding
89/// failure is returned as an [`io::Error`] whose source is a [`DecodeError`].
90///
91/// Dropping this value cancels the background pipeline. It does not verify
92/// unread compressed data; use [`DecoderReader::finish`] to discard unread
93/// output while still verifying the complete stream.
94///
95/// A positional source is decoded by a background pipeline that is cancelled
96/// and joined on drop. A non-seekable source is decoded synchronously as this
97/// reader is pulled, so dropping it immediately drops the source and never
98/// leaves a blocked coordinator thread behind.
99#[must_use]
100pub struct DecoderReader {
101    mode: ReaderMode,
102    cancelled: Arc<AtomicBool>,
103    handle: DecoderHandle,
104    current: Vec<u8>,
105    current_offset: usize,
106    terminal: Terminal,
107}
108
109enum ReaderMode {
110    Coordinator {
111        receiver: Option<Receiver<Message>>,
112        worker: Option<JoinHandle<()>>,
113    },
114    Streaming {
115        decoder: Box<SequentialDecoder<StreamCursor<Box<dyn Read + Send>>>>,
116        collector: Option<Arc<IndexCollector>>,
117        line_counter: LineCounter,
118    },
119}
120
121/// Launches a coordinator thread around `decode` and wires it to the reader.
122///
123/// `configured_workers` seeds the runtime's immutable worker maximum.
124fn spawn_coordinator<F>(
125    decode: F,
126    in_flight_chunks: usize,
127    configured_workers: usize,
128) -> Result<DecoderReader, DecodeError>
129where
130    F: FnOnce(
131            &AtomicBool,
132            &mut ChannelOutput,
133            &Arc<RuntimeState>,
134        ) -> Result<Completion, IndexingError>
135        + Send
136        + 'static,
137{
138    let (sender, receiver) = mpsc::sync_channel(in_flight_chunks);
139    let cancelled = Arc::new(AtomicBool::new(false));
140    let runtime = RuntimeState::new(configured_workers);
141    let handle = DecoderHandle::new(Arc::clone(&runtime));
142    let worker_cancelled = Arc::clone(&cancelled);
143    let worker_runtime = Arc::clone(&runtime);
144    let worker = thread::Builder::new()
145        .name("rapidgzip-coordinator".to_owned())
146        .spawn(move || {
147            let _registration = worker_runtime.register_auxiliary(AuxiliaryKind::Coordinator);
148            let mut output = ChannelOutput {
149                sender,
150                cancelled: Arc::clone(&worker_cancelled),
151                runtime: Arc::clone(&worker_runtime),
152            };
153            let terminal = match decode(&worker_cancelled, &mut output, &worker_runtime) {
154                Ok(completion) => {
155                    worker_runtime.set_member_count(match &completion {
156                        Completion::Decode(report) => report.member_count,
157                        Completion::Indexed(report) => report.decode.member_count,
158                    });
159                    Message::Finished(completion)
160                }
161                Err(IndexingError::Decode(DecodeError::Cancelled))
162                    if worker_cancelled.load(Ordering::Relaxed) =>
163                {
164                    return;
165                }
166                Err(IndexingError::Decode(error)) => Message::Failed(Failure::Decode(error)),
167                Err(error) => Message::Failed(Failure::Indexing(error)),
168            };
169            let _ = output.send(terminal);
170        })
171        .map_err(DecodeError::output_io)?;
172
173    Ok(DecoderReader {
174        mode: ReaderMode::Coordinator {
175            receiver: Some(receiver),
176            worker: Some(worker),
177        },
178        cancelled,
179        handle,
180        current: Vec::new(),
181        current_offset: 0,
182        terminal: Terminal::Open,
183    })
184}
185
186pub(crate) fn spawn<R>(source: R, config: Config) -> Result<DecoderReader, DecodeError>
187where
188    R: ReadAt + 'static,
189{
190    let in_flight_chunks = config.in_flight_chunks;
191    let configured_workers = config.decoder_threads;
192    spawn_coordinator(
193        move |cancelled, output, runtime| {
194            decode_source(&source, &config, cancelled, output, runtime)
195                .map(Completion::Decode)
196                .map_err(IndexingError::from)
197        },
198        in_flight_chunks,
199        configured_workers,
200    )
201}
202
203pub(crate) fn spawn_indexed<R>(
204    source: R,
205    config: Config,
206    options: IndexOptions,
207) -> Result<IndexingDecoderReader, DecodeError>
208where
209    R: ReadAt + 'static,
210{
211    let in_flight_chunks = config.in_flight_chunks;
212    let configured_workers = config.decoder_threads;
213    spawn_coordinator(
214        move |cancelled, output, runtime| {
215            decode_source_with_index(&source, &config, cancelled, output, runtime, options)
216                .map(Completion::Indexed)
217        },
218        in_flight_chunks,
219        configured_workers,
220    )
221    .map(|inner| IndexingDecoderReader { inner })
222}
223
224pub(crate) fn spawn_from_index<R>(
225    source: R,
226    config: Config,
227    index: Arc<DeflateIndex>,
228    plan: IndexedPlan,
229) -> Result<DecoderReader, DecodeError>
230where
231    R: ReadAt + 'static,
232{
233    let in_flight_chunks = config.in_flight_chunks;
234    let configured_workers = config.decoder_threads;
235    spawn_coordinator(
236        move |cancelled, output, runtime| {
237            crate::indexed_parallel::decode(
238                &source, &config, cancelled, output, &index, &plan, runtime,
239            )
240            .map(Completion::Decode)
241            .map_err(IndexingError::from)
242        },
243        in_flight_chunks,
244        configured_workers,
245    )
246}
247
248/// Creates a pull-driven decoder for a non-seekable source.
249///
250/// One initial read provides best-effort fail-fast header validation. The
251/// resumable decoder then reads only from `DecoderReader::read`; no coordinator
252/// or decoder-worker thread is created for this path.
253pub(crate) fn spawn_stream<R>(source: R, config: Config) -> Result<DecoderReader, DecodeError>
254where
255    R: Read + Send + 'static,
256{
257    let source: Box<dyn Read + Send> = Box::new(source);
258    let mut cursor = StreamCursor::new(source, config.input_page_size);
259    validate_initial_stream(&mut cursor, &config)?;
260    let runtime = RuntimeState::new(config.decoder_threads);
261    let handle = DecoderHandle::new(Arc::clone(&runtime));
262    let decoder = SequentialDecoder::new(
263        cursor,
264        &config,
265        0,
266        0,
267        config.decoder_threads,
268        &runtime,
269        None,
270    );
271    Ok(DecoderReader {
272        mode: ReaderMode::Streaming {
273            decoder: Box::new(decoder),
274            collector: None,
275            line_counter: LineCounter::new(config.count_lines),
276        },
277        cancelled: Arc::new(AtomicBool::new(false)),
278        handle,
279        current: Vec::new(),
280        current_offset: 0,
281        terminal: Terminal::Open,
282    })
283}
284
285pub(crate) fn spawn_stream_indexed<R>(
286    source: R,
287    config: Config,
288    options: IndexOptions,
289) -> Result<IndexingDecoderReader, DecodeError>
290where
291    R: Read + Send + 'static,
292{
293    let source: Box<dyn Read + Send> = Box::new(source);
294    let mut cursor = StreamCursor::new(source, config.input_page_size);
295    validate_initial_stream(&mut cursor, &config)?;
296    let runtime = RuntimeState::new(config.decoder_threads);
297    let handle = DecoderHandle::new(Arc::clone(&runtime));
298    let collector = IndexCollector::new(options, config.count_lines);
299    let decoder = SequentialDecoder::new(
300        cursor,
301        &config,
302        0,
303        0,
304        config.decoder_threads,
305        &runtime,
306        Some(&collector),
307    );
308    Ok(IndexingDecoderReader {
309        inner: DecoderReader {
310            mode: ReaderMode::Streaming {
311                decoder: Box::new(decoder),
312                collector: Some(collector),
313                line_counter: LineCounter::new(config.count_lines),
314            },
315            cancelled: Arc::new(AtomicBool::new(false)),
316            handle,
317            current: Vec::new(),
318            current_offset: 0,
319            terminal: Terminal::Open,
320        },
321    })
322}
323
324impl DecoderReader {
325    /// Returns a cloneable telemetry and runtime-control handle.
326    ///
327    /// The handle can be retained after moving this reader into a parser or a
328    /// `Box<dyn Read + Send>`.
329    pub fn handle(&self) -> DecoderHandle {
330        self.handle.clone()
331    }
332
333    /// Returns an approximate lock-free snapshot of decoder activity.
334    pub fn stats(&self) -> DecoderStats {
335        self.handle.stats()
336    }
337
338    /// Changes the maximum number of workers that may accept decoder tasks.
339    ///
340    /// This is a convenience forwarding method for
341    /// [`DecoderHandle::set_worker_limit`]. Retain a handle when the reader
342    /// will be moved into another component.
343    ///
344    /// # Errors
345    ///
346    /// Returns [`WorkerLimitError`] for zero or a value above the configured
347    /// worker budget.
348    pub fn set_worker_limit(&self, workers: usize) -> Result<(), WorkerLimitError> {
349        self.handle.set_worker_limit(workers)
350    }
351
352    /// Returns the report after verified EOF has been observed.
353    ///
354    /// This is `None` while decoding is open and after a terminal failure.
355    pub const fn report(&self) -> Option<&DecodeReport> {
356        match &self.terminal {
357            Terminal::Finished(Completion::Decode(report)) => Some(report),
358            Terminal::Finished(Completion::Indexed(report)) => Some(&report.decode),
359            Terminal::Open | Terminal::Failed(_) => None,
360        }
361    }
362
363    fn join_worker(&mut self) -> Result<(), DecodeError> {
364        let worker = match &mut self.mode {
365            ReaderMode::Coordinator { worker, .. } => worker.take(),
366            ReaderMode::Streaming { .. } => None,
367        };
368        if let Some(worker) = worker {
369            if worker.join().is_err() {
370                return Err(DecodeError::WorkerPanicked);
371            }
372        }
373        Ok(())
374    }
375
376    fn receive(&mut self) {
377        let mut reusable = std::mem::take(&mut self.current);
378        reusable.clear();
379        let message = match &mut self.mode {
380            ReaderMode::Coordinator { receiver, .. } => receiver
381                .as_ref()
382                .expect("receiver remains present until shutdown")
383                .recv()
384                .ok(),
385            ReaderMode::Streaming {
386                decoder,
387                collector,
388                line_counter,
389            } => {
390                let runtime = Arc::clone(&self.handle.state);
391                let result = {
392                    let _busy = runtime.begin_task();
393                    decoder.next_chunk(&self.cancelled, reusable)
394                };
395                match result {
396                    Ok(SequentialItem::Chunk(data)) => {
397                        line_counter.note_output(&data, collector.as_deref());
398                        runtime.add_decompressed_bytes(data.len());
399                        Some(Message::Data(data))
400                    }
401                    Ok(SequentialItem::Finished(report)) => {
402                        let report = line_counter.finish_report(report);
403                        if let Some(collector) = collector {
404                            match collector.finish(
405                                report.compressed_bytes,
406                                report.decompressed_bytes,
407                                report.line_count,
408                            ) {
409                                Ok(index) => Some(Message::Finished(Completion::Indexed(
410                                    IndexedDecodeReport {
411                                        decode: report,
412                                        index,
413                                    },
414                                ))),
415                                Err(error) => Some(Message::Failed(Failure::Indexing(
416                                    IndexingError::Index(error),
417                                ))),
418                            }
419                        } else {
420                            Some(Message::Finished(Completion::Decode(report)))
421                        }
422                    }
423                    Err(error) => Some(Message::Failed(Failure::Decode(error))),
424                }
425            }
426        };
427        match message {
428            Some(Message::Data(data)) => {
429                self.current = data;
430                self.current_offset = 0;
431            }
432            Some(Message::Finished(completion)) => {
433                self.handle.state.mark_terminal();
434                let terminal = match self.join_worker() {
435                    Ok(()) => Terminal::Finished(completion),
436                    Err(error) => Terminal::Failed(Failure::Decode(error)),
437                };
438                self.terminal = terminal;
439            }
440            Some(Message::Failed(error)) => {
441                self.handle.state.mark_terminal();
442                let terminal = match self.join_worker() {
443                    Ok(()) => Terminal::Failed(error),
444                    Err(join_error) => Terminal::Failed(Failure::Decode(join_error)),
445                };
446                self.terminal = terminal;
447            }
448            None => {
449                self.handle.state.mark_terminal();
450                let error = self
451                    .join_worker()
452                    .err()
453                    .unwrap_or(DecodeError::WorkerPanicked);
454                self.terminal = Terminal::Failed(Failure::Decode(error));
455            }
456        }
457    }
458
459    /// Discards unread output, verifies the remaining stream, and returns its
460    /// final report.
461    ///
462    /// # Errors
463    ///
464    /// Returns the first decoding, verification, input, or worker failure.
465    pub fn finish(mut self) -> Result<DecodeReport, DecodeError> {
466        self.current.clear();
467        loop {
468            match &self.terminal {
469                Terminal::Finished(Completion::Decode(report)) => return Ok(*report),
470                Terminal::Finished(Completion::Indexed(report)) => return Ok(report.decode),
471                Terminal::Failed(Failure::Decode(error)) => return Err(error.clone()),
472                Terminal::Failed(Failure::Indexing(IndexingError::Decode(error))) => {
473                    return Err(error.clone());
474                }
475                Terminal::Failed(Failure::Indexing(IndexingError::Index(_))) => {
476                    return Err(DecodeError::WorkerPanicked);
477                }
478                Terminal::Open => self.receive(),
479            }
480            self.current.clear();
481        }
482    }
483
484    fn finish_indexed(mut self) -> Result<IndexedDecodeReport, IndexingError> {
485        self.current.clear();
486        while matches!(self.terminal, Terminal::Open) {
487            self.receive();
488            self.current.clear();
489        }
490        match std::mem::replace(&mut self.terminal, Terminal::Open) {
491            Terminal::Finished(Completion::Indexed(report)) => Ok(report),
492            Terminal::Finished(Completion::Decode(_)) | Terminal::Open => {
493                Err(IndexingError::Decode(DecodeError::WorkerPanicked))
494            }
495            Terminal::Failed(Failure::Decode(error)) => Err(IndexingError::Decode(error)),
496            Terminal::Failed(Failure::Indexing(error)) => Err(error),
497        }
498    }
499}
500
501/// Owned decoded output that publishes a random-access index at verified EOF.
502///
503/// This reader has the same `Read + Send` behavior, runtime telemetry, dynamic
504/// worker controls, backpressure, and cancellation semantics as
505/// [`DecoderReader`]. Index construction is explicit in the type so the normal
506/// reader does not pay for checkpoint windows or lose the small, [`Copy`]
507/// [`DecodeReport`] result.
508///
509/// Reaching [`Read`] EOF means both the compressed stream and collected index
510/// have been validated. [`Self::report`] then borrows the complete result, while
511/// [`Self::finish`] consumes the reader and returns ownership of it.
512#[must_use]
513pub struct IndexingDecoderReader {
514    inner: DecoderReader,
515}
516
517impl IndexingDecoderReader {
518    /// Returns a cloneable telemetry and runtime-control handle.
519    pub fn handle(&self) -> DecoderHandle {
520        self.inner.handle()
521    }
522
523    /// Returns an approximate lock-free snapshot of decoder activity.
524    pub fn stats(&self) -> DecoderStats {
525        self.inner.stats()
526    }
527
528    /// Changes the maximum number of workers that may accept decoder tasks.
529    ///
530    /// # Errors
531    ///
532    /// Returns [`WorkerLimitError`] for zero or a value above the configured
533    /// worker budget.
534    pub fn set_worker_limit(&self, workers: usize) -> Result<(), WorkerLimitError> {
535        self.inner.set_worker_limit(workers)
536    }
537
538    /// Returns the indexed result after verified EOF has been observed.
539    ///
540    /// This is `None` while decoding is open and after a terminal failure.
541    #[must_use]
542    pub const fn report(&self) -> Option<&IndexedDecodeReport> {
543        match &self.inner.terminal {
544            Terminal::Finished(Completion::Indexed(report)) => Some(report),
545            Terminal::Open | Terminal::Finished(Completion::Decode(_)) | Terminal::Failed(_) => {
546                None
547            }
548        }
549    }
550
551    /// Discards unread output, verifies the remaining stream, finalizes the
552    /// index, and returns both the scalar report and index.
553    ///
554    /// # Errors
555    ///
556    /// Returns the first decoding, verification, input, worker, or index
557    /// construction failure.
558    pub fn finish(self) -> Result<IndexedDecodeReport, IndexingError> {
559        self.inner.finish_indexed()
560    }
561}
562
563impl Read for IndexingDecoderReader {
564    fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
565        self.inner.read(output)
566    }
567
568    fn read_vectored(&mut self, buffers: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
569        self.inner.read_vectored(buffers)
570    }
571}
572
573impl Read for DecoderReader {
574    fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
575        if output.is_empty() {
576            return Ok(0);
577        }
578
579        loop {
580            if self.current_offset < self.current.len() {
581                let count = output
582                    .len()
583                    .min(self.current.len().saturating_sub(self.current_offset));
584                output[..count].copy_from_slice(
585                    &self.current[self.current_offset..self.current_offset + count],
586                );
587                self.current_offset += count;
588                if self.current_offset == self.current.len() {
589                    self.current.clear();
590                    self.current_offset = 0;
591                }
592                self.handle.state.add_consumed_bytes(count);
593                return Ok(count);
594            }
595
596            match &self.terminal {
597                Terminal::Finished(_) => return Ok(0),
598                Terminal::Failed(Failure::Decode(error)) => return Err(error.to_io_error()),
599                Terminal::Failed(Failure::Indexing(error)) => return Err(error.to_io_error()),
600                Terminal::Open => self.receive(),
601            }
602        }
603    }
604
605    fn read_vectored(&mut self, buffers: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
606        let mut total = 0;
607        for buffer in buffers {
608            if buffer.is_empty() {
609                continue;
610            }
611            match self.read(buffer) {
612                Ok(0) => break,
613                Ok(read) => {
614                    total += read;
615                    if read < buffer.len() {
616                        break;
617                    }
618                }
619                Err(_) if total > 0 => break,
620                Err(error) => return Err(error),
621            }
622        }
623        Ok(total)
624    }
625}
626
627impl Drop for DecoderReader {
628    fn drop(&mut self) {
629        self.cancelled.store(true, Ordering::Relaxed);
630        self.handle.state.mark_terminal();
631        if let ReaderMode::Coordinator { receiver, .. } = &mut self.mode {
632            receiver.take();
633            let _ = self.join_worker();
634        }
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::{DecoderReader, IndexingDecoderReader};
641    use std::io::Read;
642
643    fn assert_traits<T: Read + Send + Unpin>() {}
644
645    #[test]
646    fn decoder_reader_is_read_send_and_unpin() {
647        assert_traits::<DecoderReader>();
648        assert_traits::<IndexingDecoderReader>();
649    }
650}