1
+ use crate :: { error:: DownloadError , Error } ;
1
2
use reqwest:: { Client , Url } ;
2
- use std:: { path:: PathBuf , sync:: Arc } ;
3
+ use std:: { path:: PathBuf , sync:: Arc , time :: Duration } ;
3
4
use tokio:: {
4
5
fs:: File ,
5
6
io:: AsyncWriteExt ,
6
7
sync:: { mpsc, oneshot, watch, Semaphore } ,
7
8
} ;
8
9
9
- use crate :: Error ;
10
-
11
10
const QUEUE_SIZE : usize = 100 ;
12
11
const MAX_RETRIES : usize = 3 ;
13
12
@@ -18,7 +17,6 @@ struct DownloadRequest {
18
17
result : oneshot:: Sender < Result < File , Error > > ,
19
18
status : watch:: Sender < Status > ,
20
19
cancel : oneshot:: Receiver < ( ) > ,
21
- remaining_retries : usize ,
22
20
}
23
21
24
22
#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
@@ -67,7 +65,6 @@ pub enum Status {
67
65
InProgress ( DownloadProgress ) ,
68
66
Completed ,
69
67
Retrying ,
70
- Cancelled ,
71
68
Failed ,
72
69
}
73
70
@@ -121,7 +118,6 @@ impl DownloadManager {
121
118
result : result_tx,
122
119
status : status_tx,
123
120
cancel : cancel_rx,
124
- remaining_retries : MAX_RETRIES ,
125
121
} ;
126
122
127
123
let _ = self . queue . try_send ( req) ;
@@ -139,7 +135,7 @@ async fn dispatcher_thread(
139
135
mut rx : mpsc:: Receiver < DownloadRequest > ,
140
136
sem : Arc < Semaphore > ,
141
137
) {
142
- while let Some ( mut request) = rx. recv ( ) . await {
138
+ while let Some ( request) = rx. recv ( ) . await {
143
139
let permit = match sem. clone ( ) . acquire_owned ( ) . await {
144
140
Ok ( permit) => permit,
145
141
Err ( _) => break ,
@@ -148,40 +144,79 @@ async fn dispatcher_thread(
148
144
tokio:: spawn ( async move {
149
145
// Move the permit into the worker thread so it's automatically released when the thread finishes
150
146
let _permit = permit;
151
- loop {
152
- match download_thread ( client. clone ( ) , & mut request) . await {
153
- Ok ( file) => {
154
- let _ = request. status . send ( Status :: Completed ) ;
155
- let _ = request. result . send ( Ok ( file) ) ;
156
- break ;
157
- }
158
- Err ( e) => {
159
- if request. remaining_retries > 0 {
160
- let _ = request. status . send ( Status :: Retrying ) ;
161
- request. remaining_retries -= 1 ;
162
- } else {
163
- let status = match e {
164
- Error :: Io ( ref io_err) => {
165
- if io_err. kind ( ) == std:: io:: ErrorKind :: Interrupted {
166
- Status :: Cancelled
167
- } else {
168
- Status :: Failed
169
- }
170
- }
171
- _ => Status :: Failed ,
172
- } ;
173
- let _ = request. status . send ( status) ;
174
- let _ = request. result . send ( Err ( e) ) ;
175
- break ;
176
- }
177
- }
147
+ download_thread ( client. clone ( ) , request) . await ;
148
+ } ) ;
149
+ }
150
+ }
151
+
152
+ async fn download_thread ( client : Client , mut req : DownloadRequest ) {
153
+ fn should_retry ( e : & Error ) -> bool {
154
+ match e {
155
+ Error :: Reqwest ( network_err) => {
156
+ network_err. is_timeout ( )
157
+ || network_err. is_connect ( )
158
+ || network_err. is_request ( )
159
+ || network_err
160
+ . status ( )
161
+ . map ( |status_code| status_code. is_server_error ( ) )
162
+ . unwrap_or ( true )
163
+ }
164
+ Error :: Download ( DownloadError :: Cancelled ) | Error :: Io ( _) => false ,
165
+ _ => false ,
166
+ }
167
+ }
168
+
169
+ let mut last_error = None ;
170
+ for attempt in 0 ..=( MAX_RETRIES ) {
171
+ if attempt > MAX_RETRIES {
172
+ req. status . send ( Status :: Failed ) . ok ( ) ;
173
+ req. result
174
+ . send ( Err ( Error :: Download ( DownloadError :: RetriesExhausted {
175
+ last_error_msg : last_error
176
+ . as_ref ( )
177
+ . map ( |e : & crate :: Error | e. to_string ( ) )
178
+ . unwrap_or_else ( || "Unknown Error" . to_string ( ) ) ,
179
+ } ) ) )
180
+ . ok ( ) ;
181
+ return ;
182
+ }
183
+
184
+ if attempt > 0 {
185
+ req. status . send ( Status :: Retrying ) . ok ( ) ;
186
+ // Basic exponential backoff
187
+ let delay_ms = 1000 * 2u64 . pow ( attempt as u32 - 1 ) ;
188
+ let delay = Duration :: from_millis ( delay_ms) ;
189
+
190
+ tokio:: select! {
191
+ _ = tokio:: time:: sleep( delay) => { } ,
192
+ _ = & mut req. cancel => {
193
+ req. status. send( Status :: Failed ) . ok( ) ;
194
+ req. result. send( Err ( Error :: Download ( DownloadError :: Cancelled ) ) ) . ok( ) ;
195
+ return ;
178
196
}
179
197
}
180
- } ) ;
198
+ }
199
+
200
+ match download ( client. clone ( ) , & mut req) . await {
201
+ Ok ( file) => {
202
+ req. status . send ( Status :: Completed ) . ok ( ) ;
203
+ req. result . send ( Ok ( file) ) . ok ( ) ;
204
+ return ;
205
+ }
206
+ Err ( e) => {
207
+ if should_retry ( & e) {
208
+ last_error = Some ( e) ;
209
+ continue ;
210
+ }
211
+ req. status . send ( Status :: Failed ) . ok ( ) ;
212
+ req. result . send ( Err ( e) ) . ok ( ) ;
213
+ return ;
214
+ }
215
+ }
181
216
}
182
217
}
183
218
184
- async fn download_thread ( client : Client , req : & mut DownloadRequest ) -> Result < File , Error > {
219
+ async fn download ( client : Client , req : & mut DownloadRequest ) -> Result < File , Error > {
185
220
let update_progress = |bytes_downloaded : u64 , total_bytes : Option < u64 > | {
186
221
req. status
187
222
. send ( Status :: InProgress ( DownloadProgress {
@@ -211,10 +246,7 @@ async fn download_thread(client: Client, req: &mut DownloadRequest) -> Result<Fi
211
246
_ = & mut req. cancel => {
212
247
drop( file) ; // Manually drop the file handle to ensure that deletion doesn't fail
213
248
tokio:: fs:: remove_file( & req. destination) . await ?;
214
- return Err ( Error :: Io ( std:: io:: Error :: new(
215
- std:: io:: ErrorKind :: Interrupted ,
216
- "Download cancelled" ,
217
- ) ) ) ;
249
+ return Err ( Error :: Download ( DownloadError :: Cancelled ) ) ;
218
250
}
219
251
chunk = response. chunk( ) => {
220
252
match chunk {
@@ -230,7 +262,6 @@ async fn download_thread(client: Client, req: &mut DownloadRequest) -> Result<Fi
230
262
return Err ( Error :: Reqwest ( e) )
231
263
} ,
232
264
}
233
-
234
265
}
235
266
}
236
267
}
0 commit comments