mirror of https://github.com/fafhrd91/actix-web
feat(actix-files): opt-in filesize threshold for faster synchronous reads (#3706)
Co-authored-by: Rob Ede <robjtede@icloud.com>
This commit is contained in:
parent
3c2907da41
commit
00b0f8f700
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Opt-In filesize threshold for faster synchronus reads that allow for 20x better performance.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.75.
|
- Minimum supported Rust version (MSRV) is now 1.75.
|
||||||
|
|
||||||
## 0.6.6
|
## 0.6.6
|
||||||
|
|
|
@ -24,6 +24,7 @@ pin_project! {
|
||||||
state: ChunkedReadFileState<Fut>,
|
state: ChunkedReadFileState<Fut>,
|
||||||
counter: u64,
|
counter: u64,
|
||||||
callback: F,
|
callback: F,
|
||||||
|
read_sync: bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +58,7 @@ pub(crate) fn new_chunked_read(
|
||||||
size: u64,
|
size: u64,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
file: File,
|
file: File,
|
||||||
|
size_threshold: u64,
|
||||||
) -> impl Stream<Item = Result<Bytes, Error>> {
|
) -> impl Stream<Item = Result<Bytes, Error>> {
|
||||||
ChunkedReadFile {
|
ChunkedReadFile {
|
||||||
size,
|
size,
|
||||||
|
@ -69,18 +71,18 @@ pub(crate) fn new_chunked_read(
|
||||||
},
|
},
|
||||||
counter: 0,
|
counter: 0,
|
||||||
callback: chunked_read_file_callback,
|
callback: chunked_read_file_callback,
|
||||||
|
read_sync: size < size_threshold,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "experimental-io-uring"))]
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
async fn chunked_read_file_callback(
|
fn chunked_read_file_callback_sync(
|
||||||
mut file: File,
|
mut file: File,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
max_bytes: usize,
|
max_bytes: usize,
|
||||||
) -> Result<(File, Bytes), Error> {
|
) -> Result<(File, Bytes), io::Error> {
|
||||||
use io::{Read as _, Seek as _};
|
use io::{Read as _, Seek as _};
|
||||||
|
|
||||||
let res = actix_web::web::block(move || {
|
|
||||||
let mut buf = Vec::with_capacity(max_bytes);
|
let mut buf = Vec::with_capacity(max_bytes);
|
||||||
|
|
||||||
file.seek(io::SeekFrom::Start(offset))?;
|
file.seek(io::SeekFrom::Start(offset))?;
|
||||||
|
@ -92,8 +94,22 @@ async fn chunked_read_file_callback(
|
||||||
} else {
|
} else {
|
||||||
Ok((file, Bytes::from(buf)))
|
Ok((file, Bytes::from(buf)))
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.await??;
|
|
||||||
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
#[inline]
|
||||||
|
async fn chunked_read_file_callback(
|
||||||
|
file: File,
|
||||||
|
offset: u64,
|
||||||
|
max_bytes: usize,
|
||||||
|
read_sync: bool,
|
||||||
|
) -> Result<(File, Bytes), Error> {
|
||||||
|
let res = if read_sync {
|
||||||
|
chunked_read_file_callback_sync(file, offset, max_bytes)?
|
||||||
|
} else {
|
||||||
|
actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes))
|
||||||
|
.await??
|
||||||
|
};
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
@ -171,7 +187,7 @@ where
|
||||||
#[cfg(not(feature = "experimental-io-uring"))]
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||||
where
|
where
|
||||||
F: Fn(File, u64, usize) -> Fut,
|
F: Fn(File, u64, usize, bool) -> Fut,
|
||||||
Fut: Future<Output = Result<(File, Bytes), Error>>,
|
Fut: Future<Output = Result<(File, Bytes), Error>>,
|
||||||
{
|
{
|
||||||
type Item = Result<Bytes, Error>;
|
type Item = Result<Bytes, Error>;
|
||||||
|
@ -193,7 +209,7 @@ where
|
||||||
.take()
|
.take()
|
||||||
.expect("ChunkedReadFile polled after completion");
|
.expect("ChunkedReadFile polled after completion");
|
||||||
|
|
||||||
let fut = (this.callback)(file, offset, max_bytes);
|
let fut = (this.callback)(file, offset, max_bytes, *this.read_sync);
|
||||||
|
|
||||||
this.state
|
this.state
|
||||||
.project_replace(ChunkedReadFileState::Future { fut });
|
.project_replace(ChunkedReadFileState::Future { fut });
|
||||||
|
|
|
@ -49,6 +49,7 @@ pub struct Files {
|
||||||
use_guards: Option<Rc<dyn Guard>>,
|
use_guards: Option<Rc<dyn Guard>>,
|
||||||
guards: Vec<Rc<dyn Guard>>,
|
guards: Vec<Rc<dyn Guard>>,
|
||||||
hidden_files: bool,
|
hidden_files: bool,
|
||||||
|
size_threshold: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Files {
|
impl fmt::Debug for Files {
|
||||||
|
@ -73,6 +74,7 @@ impl Clone for Files {
|
||||||
use_guards: self.use_guards.clone(),
|
use_guards: self.use_guards.clone(),
|
||||||
guards: self.guards.clone(),
|
guards: self.guards.clone(),
|
||||||
hidden_files: self.hidden_files,
|
hidden_files: self.hidden_files,
|
||||||
|
size_threshold: self.size_threshold,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +121,7 @@ impl Files {
|
||||||
use_guards: None,
|
use_guards: None,
|
||||||
guards: Vec::new(),
|
guards: Vec::new(),
|
||||||
hidden_files: false,
|
hidden_files: false,
|
||||||
|
size_threshold: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +207,18 @@ impl Files {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the async file-size threshold.
|
||||||
|
///
|
||||||
|
/// When a file is larger than the threshold, the reader
|
||||||
|
/// will switch from faster blocking file-reads to slower async reads
|
||||||
|
/// to avoid blocking the main-thread when processing large files.
|
||||||
|
///
|
||||||
|
/// Default is 0, meaning all files are read asyncly.
|
||||||
|
pub fn set_size_threshold(mut self, size: u64) -> Self {
|
||||||
|
self.size_threshold = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Specifies whether to use ETag or not.
|
/// Specifies whether to use ETag or not.
|
||||||
///
|
///
|
||||||
/// Default is true.
|
/// Default is true.
|
||||||
|
@ -367,6 +382,7 @@ impl ServiceFactory<ServiceRequest> for Files {
|
||||||
file_flags: self.file_flags,
|
file_flags: self.file_flags,
|
||||||
guards: self.use_guards.clone(),
|
guards: self.use_guards.clone(),
|
||||||
hidden_files: self.hidden_files,
|
hidden_files: self.hidden_files,
|
||||||
|
size_threshold: self.size_threshold,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref default) = *self.default.borrow() {
|
if let Some(ref default) = *self.default.borrow() {
|
||||||
|
|
|
@ -80,6 +80,7 @@ pub struct NamedFile {
|
||||||
pub(crate) content_type: Mime,
|
pub(crate) content_type: Mime,
|
||||||
pub(crate) content_disposition: ContentDisposition,
|
pub(crate) content_disposition: ContentDisposition,
|
||||||
pub(crate) encoding: Option<ContentEncoding>,
|
pub(crate) encoding: Option<ContentEncoding>,
|
||||||
|
pub(crate) size_threshold: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "experimental-io-uring"))]
|
#[cfg(not(feature = "experimental-io-uring"))]
|
||||||
|
@ -200,6 +201,7 @@ impl NamedFile {
|
||||||
encoding,
|
encoding,
|
||||||
status_code: StatusCode::OK,
|
status_code: StatusCode::OK,
|
||||||
flags: Flags::default(),
|
flags: Flags::default(),
|
||||||
|
size_threshold: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,6 +355,18 @@ impl NamedFile {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the async file-size threshold.
|
||||||
|
///
|
||||||
|
/// When a file is larger than the threshold, the reader
|
||||||
|
/// will switch from faster blocking file-reads to slower async reads
|
||||||
|
/// to avoid blocking the main-thread when processing large files.
|
||||||
|
///
|
||||||
|
/// Default is 0, meaning all files are read asyncly.
|
||||||
|
pub fn set_size_threshold(mut self, size: u64) -> Self {
|
||||||
|
self.size_threshold = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Specifies whether to return `ETag` header in response.
|
/// Specifies whether to return `ETag` header in response.
|
||||||
///
|
///
|
||||||
/// Default is true.
|
/// Default is true.
|
||||||
|
@ -440,7 +454,8 @@ impl NamedFile {
|
||||||
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
|
let reader =
|
||||||
|
chunked::new_chunked_read(self.md.len(), 0, self.file, self.size_threshold);
|
||||||
|
|
||||||
return res.streaming(reader);
|
return res.streaming(reader);
|
||||||
}
|
}
|
||||||
|
@ -577,7 +592,7 @@ impl NamedFile {
|
||||||
.map_into_boxed_body();
|
.map_into_boxed_body();
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader = chunked::new_chunked_read(length, offset, self.file);
|
let reader = chunked::new_chunked_read(length, offset, self.file, self.size_threshold);
|
||||||
|
|
||||||
if offset != 0 || length != self.md.len() {
|
if offset != 0 || length != self.md.len() {
|
||||||
res.status(StatusCode::PARTIAL_CONTENT);
|
res.status(StatusCode::PARTIAL_CONTENT);
|
||||||
|
|
|
@ -39,6 +39,7 @@ pub struct FilesServiceInner {
|
||||||
pub(crate) file_flags: named::Flags,
|
pub(crate) file_flags: named::Flags,
|
||||||
pub(crate) guards: Option<Rc<dyn Guard>>,
|
pub(crate) guards: Option<Rc<dyn Guard>>,
|
||||||
pub(crate) hidden_files: bool,
|
pub(crate) hidden_files: bool,
|
||||||
|
pub(crate) size_threshold: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for FilesServiceInner {
|
impl fmt::Debug for FilesServiceInner {
|
||||||
|
@ -70,7 +71,9 @@ impl FilesService {
|
||||||
named_file.flags = self.file_flags;
|
named_file.flags = self.file_flags;
|
||||||
|
|
||||||
let (req, _) = req.into_parts();
|
let (req, _) = req.into_parts();
|
||||||
let res = named_file.into_response(&req);
|
let res = named_file
|
||||||
|
.set_size_threshold(self.size_threshold)
|
||||||
|
.into_response(&req);
|
||||||
ServiceResponse::new(req, res)
|
ServiceResponse::new(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,17 +172,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match NamedFile::open_async(&path).await {
|
match NamedFile::open_async(&path).await {
|
||||||
Ok(mut named_file) => {
|
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
||||||
if let Some(ref mime_override) = this.mime_override {
|
|
||||||
let new_disposition = mime_override(&named_file.content_type.type_());
|
|
||||||
named_file.content_disposition.disposition = new_disposition;
|
|
||||||
}
|
|
||||||
named_file.flags = this.file_flags;
|
|
||||||
|
|
||||||
let (req, _) = req.into_parts();
|
|
||||||
let res = named_file.into_response(&req);
|
|
||||||
Ok(ServiceResponse::new(req, res))
|
|
||||||
}
|
|
||||||
Err(err) => this.handle_err(err, req).await,
|
Err(err) => this.handle_err(err, req).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue