Skip to content

Commit 00b0f8f

Browse files
imgurbot12robjtede
andauthored
feat(actix-files): opt-in filesize threshold for faster synchronous reads (#3706)
Co-authored-by: Rob Ede <robjtede@icloud.com>
1 parent 3c2907d commit 00b0f8f

File tree

5 files changed

+70
-29
lines changed

5 files changed

+70
-29
lines changed

actix-files/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Opt-In filesize threshold for faster synchronus reads that allow for 20x better performance.
56
- Minimum supported Rust version (MSRV) is now 1.75.
67

78
## 0.6.6

actix-files/src/chunked.rs

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pin_project! {
2424
state: ChunkedReadFileState<Fut>,
2525
counter: u64,
2626
callback: F,
27+
read_sync: bool,
2728
}
2829
}
2930

@@ -57,6 +58,7 @@ pub(crate) fn new_chunked_read(
5758
size: u64,
5859
offset: u64,
5960
file: File,
61+
size_threshold: u64,
6062
) -> impl Stream<Item = Result<Bytes, Error>> {
6163
ChunkedReadFile {
6264
size,
@@ -69,31 +71,45 @@ pub(crate) fn new_chunked_read(
6971
},
7072
counter: 0,
7173
callback: chunked_read_file_callback,
74+
read_sync: size < size_threshold,
7275
}
7376
}
7477

7578
#[cfg(not(feature = "experimental-io-uring"))]
76-
async fn chunked_read_file_callback(
79+
fn chunked_read_file_callback_sync(
7780
mut file: File,
7881
offset: u64,
7982
max_bytes: usize,
80-
) -> Result<(File, Bytes), Error> {
83+
) -> Result<(File, Bytes), io::Error> {
8184
use io::{Read as _, Seek as _};
8285

83-
let res = actix_web::web::block(move || {
84-
let mut buf = Vec::with_capacity(max_bytes);
86+
let mut buf = Vec::with_capacity(max_bytes);
8587

86-
file.seek(io::SeekFrom::Start(offset))?;
88+
file.seek(io::SeekFrom::Start(offset))?;
8789

88-
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
90+
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
8991

90-
if n_bytes == 0 {
91-
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
92-
} else {
93-
Ok((file, Bytes::from(buf)))
94-
}
95-
})
96-
.await??;
92+
if n_bytes == 0 {
93+
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
94+
} else {
95+
Ok((file, Bytes::from(buf)))
96+
}
97+
}
98+
99+
#[cfg(not(feature = "experimental-io-uring"))]
100+
#[inline]
101+
async fn chunked_read_file_callback(
102+
file: File,
103+
offset: u64,
104+
max_bytes: usize,
105+
read_sync: bool,
106+
) -> Result<(File, Bytes), Error> {
107+
let res = if read_sync {
108+
chunked_read_file_callback_sync(file, offset, max_bytes)?
109+
} else {
110+
actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes))
111+
.await??
112+
};
97113

98114
Ok(res)
99115
}
@@ -171,7 +187,7 @@ where
171187
#[cfg(not(feature = "experimental-io-uring"))]
172188
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
173189
where
174-
F: Fn(File, u64, usize) -> Fut,
190+
F: Fn(File, u64, usize, bool) -> Fut,
175191
Fut: Future<Output = Result<(File, Bytes), Error>>,
176192
{
177193
type Item = Result<Bytes, Error>;
@@ -193,7 +209,7 @@ where
193209
.take()
194210
.expect("ChunkedReadFile polled after completion");
195211

196-
let fut = (this.callback)(file, offset, max_bytes);
212+
let fut = (this.callback)(file, offset, max_bytes, *this.read_sync);
197213

198214
this.state
199215
.project_replace(ChunkedReadFileState::Future { fut });

actix-files/src/files.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub struct Files {
4949
use_guards: Option<Rc<dyn Guard>>,
5050
guards: Vec<Rc<dyn Guard>>,
5151
hidden_files: bool,
52+
size_threshold: u64,
5253
}
5354

5455
impl fmt::Debug for Files {
@@ -73,6 +74,7 @@ impl Clone for Files {
7374
use_guards: self.use_guards.clone(),
7475
guards: self.guards.clone(),
7576
hidden_files: self.hidden_files,
77+
size_threshold: self.size_threshold,
7678
}
7779
}
7880
}
@@ -119,6 +121,7 @@ impl Files {
119121
use_guards: None,
120122
guards: Vec::new(),
121123
hidden_files: false,
124+
size_threshold: 0,
122125
}
123126
}
124127

@@ -204,6 +207,18 @@ impl Files {
204207
self
205208
}
206209

210+
/// Sets the async file-size threshold.
211+
///
212+
/// When a file is larger than the threshold, the reader
213+
/// will switch from faster blocking file-reads to slower async reads
214+
/// to avoid blocking the main-thread when processing large files.
215+
///
216+
/// Default is 0, meaning all files are read asyncly.
217+
pub fn set_size_threshold(mut self, size: u64) -> Self {
218+
self.size_threshold = size;
219+
self
220+
}
221+
207222
/// Specifies whether to use ETag or not.
208223
///
209224
/// Default is true.
@@ -367,6 +382,7 @@ impl ServiceFactory<ServiceRequest> for Files {
367382
file_flags: self.file_flags,
368383
guards: self.use_guards.clone(),
369384
hidden_files: self.hidden_files,
385+
size_threshold: self.size_threshold,
370386
};
371387

372388
if let Some(ref default) = *self.default.borrow() {

actix-files/src/named.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ pub struct NamedFile {
8080
pub(crate) content_type: Mime,
8181
pub(crate) content_disposition: ContentDisposition,
8282
pub(crate) encoding: Option<ContentEncoding>,
83+
pub(crate) size_threshold: u64,
8384
}
8485

8586
#[cfg(not(feature = "experimental-io-uring"))]
@@ -200,6 +201,7 @@ impl NamedFile {
200201
encoding,
201202
status_code: StatusCode::OK,
202203
flags: Flags::default(),
204+
size_threshold: 0,
203205
})
204206
}
205207

@@ -353,6 +355,18 @@ impl NamedFile {
353355
self
354356
}
355357

358+
/// Sets the async file-size threshold.
359+
///
360+
/// When a file is larger than the threshold, the reader
361+
/// will switch from faster blocking file-reads to slower async reads
362+
/// to avoid blocking the main-thread when processing large files.
363+
///
364+
/// Default is 0, meaning all files are read asyncly.
365+
pub fn set_size_threshold(mut self, size: u64) -> Self {
366+
self.size_threshold = size;
367+
self
368+
}
369+
356370
/// Specifies whether to return `ETag` header in response.
357371
///
358372
/// Default is true.
@@ -440,7 +454,8 @@ impl NamedFile {
440454
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
441455
}
442456

443-
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
457+
let reader =
458+
chunked::new_chunked_read(self.md.len(), 0, self.file, self.size_threshold);
444459

445460
return res.streaming(reader);
446461
}
@@ -577,7 +592,7 @@ impl NamedFile {
577592
.map_into_boxed_body();
578593
}
579594

580-
let reader = chunked::new_chunked_read(length, offset, self.file);
595+
let reader = chunked::new_chunked_read(length, offset, self.file, self.size_threshold);
581596

582597
if offset != 0 || length != self.md.len() {
583598
res.status(StatusCode::PARTIAL_CONTENT);

actix-files/src/service.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub struct FilesServiceInner {
3939
pub(crate) file_flags: named::Flags,
4040
pub(crate) guards: Option<Rc<dyn Guard>>,
4141
pub(crate) hidden_files: bool,
42+
pub(crate) size_threshold: u64,
4243
}
4344

4445
impl fmt::Debug for FilesServiceInner {
@@ -70,7 +71,9 @@ impl FilesService {
7071
named_file.flags = self.file_flags;
7172

7273
let (req, _) = req.into_parts();
73-
let res = named_file.into_response(&req);
74+
let res = named_file
75+
.set_size_threshold(self.size_threshold)
76+
.into_response(&req);
7477
ServiceResponse::new(req, res)
7578
}
7679

@@ -169,17 +172,7 @@ impl Service<ServiceRequest> for FilesService {
169172
}
170173
} else {
171174
match NamedFile::open_async(&path).await {
172-
Ok(mut named_file) => {
173-
if let Some(ref mime_override) = this.mime_override {
174-
let new_disposition = mime_override(&named_file.content_type.type_());
175-
named_file.content_disposition.disposition = new_disposition;
176-
}
177-
named_file.flags = this.file_flags;
178-
179-
let (req, _) = req.into_parts();
180-
let res = named_file.into_response(&req);
181-
Ok(ServiceResponse::new(req, res))
182-
}
175+
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
183176
Err(err) => this.handle_err(err, req).await,
184177
}
185178
}

0 commit comments

Comments
 (0)