@@ -17,19 +17,39 @@ use std::process::{Command, Stdio};
1717
1818/// HTTP range reader that implements Read + Seek.
1919///
20- /// Each `read()` call fetches bytes via `curl -r start-end`.
21- /// Buffered: fetches chunks of `chunk_size` to avoid per-byte HTTP calls.
20+ /// Internally splits chunks into smaller segments (default 64 MB) with
21+ /// retry + exponential backoff. A mid-download failure only refetches
22+ /// the failed segment, not the entire 256 MB chunk.
23+ ///
24+ /// Caches the last `SEGMENT_CACHE_SIZE` segments so backward seeks
25+ /// within the cache window are free (no re-fetch).
2226pub struct HttpRangeReader {
2327 url : String ,
2428 position : u64 ,
2529 total_size : u64 ,
26- buffer : Vec < u8 > ,
27- buf_start : u64 , // file offset where buffer starts
28- buf_len : usize , // valid bytes in buffer
2930 chunk_size : usize ,
3031 bytes_downloaded : u64 ,
32+
33+ // Segmented cache: each entry = (file_offset, data)
34+ segment_size : usize ,
35+ segment_cache : Vec < ( u64 , Vec < u8 > ) > ,
36+ max_cached_segments : usize ,
37+
38+ // Current active segment (for Read trait)
39+ active_segment_start : u64 ,
40+ active_segment_len : usize ,
41+ active_segment_idx : Option < usize > , // index into segment_cache
3142}
3243
44+ /// Maximum retry attempts per segment.
45+ const MAX_RETRIES : u32 = 4 ;
46+ /// Initial backoff delay in milliseconds.
47+ const INITIAL_BACKOFF_MS : u64 = 1000 ;
48+ /// Default segment size: 64 MB (4 segments per 256 MB chunk).
49+ const DEFAULT_SEGMENT_SIZE : usize = 64 * 1024 * 1024 ;
50+ /// Number of segments to cache (192 MB at 64 MB segments).
51+ const SEGMENT_CACHE_SIZE : usize = 3 ;
52+
3353impl HttpRangeReader {
3454 /// Default chunk: 256 MB (fewer HTTP round-trips, fits in RAM easily).
3555 const DEFAULT_CHUNK : usize = 256 * 1024 * 1024 ;
@@ -42,25 +62,33 @@ impl HttpRangeReader {
4262 url,
4363 position : 0 ,
4464 total_size,
45- buffer : vec ! [ 0u8 ; Self :: DEFAULT_CHUNK ] ,
46- buf_start : 0 ,
47- buf_len : 0 ,
4865 chunk_size : Self :: DEFAULT_CHUNK ,
4966 bytes_downloaded : 0 ,
67+ segment_size : DEFAULT_SEGMENT_SIZE ,
68+ segment_cache : Vec :: with_capacity ( SEGMENT_CACHE_SIZE ) ,
69+ max_cached_segments : SEGMENT_CACHE_SIZE ,
70+ active_segment_start : 0 ,
71+ active_segment_len : 0 ,
72+ active_segment_idx : None ,
5073 }
5174 }
5275
5376 /// Create with custom chunk size.
5477 pub fn with_chunk_size ( url : String , total_size : u64 , chunk_size : usize ) -> Self {
78+ // Segment size = chunk_size / 4, minimum 16 MB
79+ let seg = ( chunk_size / 4 ) . max ( 16 * 1024 * 1024 ) ;
5580 Self {
5681 url,
5782 position : 0 ,
5883 total_size,
59- buffer : vec ! [ 0u8 ; chunk_size] ,
60- buf_start : 0 ,
61- buf_len : 0 ,
6284 chunk_size,
6385 bytes_downloaded : 0 ,
86+ segment_size : seg,
87+ segment_cache : Vec :: with_capacity ( SEGMENT_CACHE_SIZE ) ,
88+ max_cached_segments : SEGMENT_CACHE_SIZE ,
89+ active_segment_start : 0 ,
90+ active_segment_len : 0 ,
91+ active_segment_idx : None ,
6492 }
6593 }
6694
@@ -69,56 +97,100 @@ impl HttpRangeReader {
6997 self . bytes_downloaded
7098 }
7199
72- /// Fetch a range of bytes from the URL via curl.
73- fn fetch_range ( & mut self , start : u64 , len : usize ) -> io:: Result < usize > {
100+ /// Fetch a segment with retry + exponential backoff.
101+ ///
102+ /// Returns the fetched bytes. On permanent failure after MAX_RETRIES, returns error.
103+ fn fetch_segment_with_retry ( & mut self , start : u64 , len : usize ) -> io:: Result < Vec < u8 > > {
74104 let end = ( start + len as u64 - 1 ) . min ( self . total_size - 1 ) ;
75105 let range = format ! ( "{}-{}" , start, end) ;
106+ let expected = ( end - start + 1 ) as usize ;
107+
108+ for attempt in 0 ..MAX_RETRIES {
109+ if attempt > 0 {
110+ let delay = INITIAL_BACKOFF_MS * ( 1u64 << ( attempt - 1 ) ) ;
111+ eprintln ! ( " retry {}/{} after {}ms (segment {}-{})" ,
112+ attempt + 1 , MAX_RETRIES , delay, start, end) ;
113+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( delay) ) ;
114+ }
76115
77- let output = Command :: new ( "curl" )
78- . args ( & [ "-sL" , "-r" , & range, & self . url ] )
79- . stdout ( Stdio :: piped ( ) )
80- . stderr ( Stdio :: null ( ) )
81- . output ( )
82- . map_err ( |e| io:: Error :: new ( io:: ErrorKind :: Other , format ! ( "curl failed: {}" , e) ) ) ?;
83-
84- if !output. status . success ( ) {
85- return Err ( io:: Error :: new (
86- io:: ErrorKind :: Other ,
87- format ! ( "curl returned status {}" , output. status) ,
88- ) ) ;
89- }
90-
91- let fetched = output. stdout . len ( ) ;
92- if fetched == 0 {
93- return Ok ( 0 ) ;
116+ let result = Command :: new ( "curl" )
117+ . args ( & [
118+ "-sL" ,
119+ "--retry" , "2" , // curl-level retry for connection drops
120+ "--retry-delay" , "1" ,
121+ "--connect-timeout" , "30" ,
122+ "--max-time" , "300" , // 5 min max per segment
123+ "-r" , & range,
124+ & self . url ,
125+ ] )
126+ . stdout ( Stdio :: piped ( ) )
127+ . stderr ( Stdio :: null ( ) )
128+ . output ( ) ;
129+
130+ match result {
131+ Ok ( output) if output. status . success ( ) && output. stdout . len ( ) == expected => {
132+ self . bytes_downloaded += output. stdout . len ( ) as u64 ;
133+ return Ok ( output. stdout ) ;
134+ }
135+ Ok ( output) if output. status . success ( ) && !output. stdout . is_empty ( ) => {
136+ // Partial read — might be near EOF, accept it
137+ self . bytes_downloaded += output. stdout . len ( ) as u64 ;
138+ if output. stdout . len ( ) >= expected / 2 {
139+ return Ok ( output. stdout ) ;
140+ }
141+ eprintln ! ( " short read: got {}/{} bytes" , output. stdout. len( ) , expected) ;
142+ }
143+ Ok ( output) => {
144+ eprintln ! ( " fetch failed: status={} got={} bytes" ,
145+ output. status, output. stdout. len( ) ) ;
146+ }
147+ Err ( e) => {
148+ eprintln ! ( " curl error: {}" , e) ;
149+ }
150+ }
94151 }
95152
96- // Copy to buffer
97- let copy_len = fetched. min ( self . buffer . len ( ) ) ;
98- self . buffer [ ..copy_len] . copy_from_slice ( & output. stdout [ ..copy_len] ) ;
99- self . buf_start = start;
100- self . buf_len = copy_len;
101- self . bytes_downloaded += fetched as u64 ;
102-
103- Ok ( copy_len)
153+ Err ( io:: Error :: new (
154+ io:: ErrorKind :: Other ,
155+ format ! ( "segment fetch failed after {} retries: bytes {}-{}" , MAX_RETRIES , start, end) ,
156+ ) )
104157 }
105158
106- /// Ensure the buffer covers `self.position` and has data ready .
107- fn ensure_buffered ( & mut self ) -> io:: Result < ( ) > {
159+ /// Find or fetch the segment containing `self.position`.
160+ fn ensure_segment ( & mut self ) -> io:: Result < ( ) > {
108161 if self . position >= self . total_size {
109162 return Ok ( ( ) ) ;
110163 }
111164
112- // Check if position is within current buffer
113- let buf_end = self . buf_start + self . buf_len as u64 ;
114- if self . position >= self . buf_start && self . position < buf_end {
115- return Ok ( ( ) ) ; // already buffered
165+ // Check if position is within any cached segment
166+ for ( idx, ( seg_start, seg_data) ) in self . segment_cache . iter ( ) . enumerate ( ) {
167+ let seg_end = * seg_start + seg_data. len ( ) as u64 ;
168+ if self . position >= * seg_start && self . position < seg_end {
169+ self . active_segment_start = * seg_start;
170+ self . active_segment_len = seg_data. len ( ) ;
171+ self . active_segment_idx = Some ( idx) ;
172+ return Ok ( ( ) ) ;
173+ }
116174 }
117175
118- // Need to fetch
176+ // Not cached — fetch new segment
119177 let remaining = ( self . total_size - self . position ) as usize ;
120- let fetch_len = self . chunk_size . min ( remaining) ;
121- self . fetch_range ( self . position , fetch_len) ?;
178+ let fetch_len = self . segment_size . min ( remaining) ;
179+ let data = self . fetch_segment_with_retry ( self . position , fetch_len) ?;
180+ let data_len = data. len ( ) ;
181+
182+ // Evict oldest segment if cache is full
183+ if self . segment_cache . len ( ) >= self . max_cached_segments {
184+ self . segment_cache . remove ( 0 ) ;
185+ }
186+
187+ self . segment_cache . push ( ( self . position , data) ) ;
188+ let idx = self . segment_cache . len ( ) - 1 ;
189+
190+ self . active_segment_start = self . position ;
191+ self . active_segment_len = data_len;
192+ self . active_segment_idx = Some ( idx) ;
193+
122194 Ok ( ( ) )
123195 }
124196}
@@ -129,17 +201,23 @@ impl Read for HttpRangeReader {
129201 return Ok ( 0 ) ; // EOF
130202 }
131203
132- self . ensure_buffered ( ) ?;
204+ self . ensure_segment ( ) ?;
205+
206+ let idx = match self . active_segment_idx {
207+ Some ( i) if i < self . segment_cache . len ( ) => i,
208+ _ => return Ok ( 0 ) ,
209+ } ;
133210
134- let buf_offset = ( self . position - self . buf_start ) as usize ;
135- let available = self . buf_len - buf_offset;
211+ let ( seg_start, ref seg_data) = self . segment_cache [ idx] ;
212+ let offset = ( self . position - seg_start) as usize ;
213+ let available = seg_data. len ( ) - offset;
136214 let to_copy = buf. len ( ) . min ( available) ;
137215
138216 if to_copy == 0 {
139217 return Ok ( 0 ) ;
140218 }
141219
142- buf[ ..to_copy] . copy_from_slice ( & self . buffer [ buf_offset..buf_offset + to_copy] ) ;
220+ buf[ ..to_copy] . copy_from_slice ( & seg_data [ offset..offset + to_copy] ) ;
143221 self . position += to_copy as u64 ;
144222 Ok ( to_copy)
145223 }
0 commit comments