-
Notifications
You must be signed in to change notification settings - Fork 0
Add SMB connection pool, pipelined reads, and TCP tuning #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -427,7 +427,7 @@ async fn handle_get_object( | |
| // ── Fast path: compound Create+Read+Close for small files ─────── | ||
| // Tries to read the entire file in one SMB round trip. Falls back to | ||
| // streaming for large files or range requests. | ||
| let max_read = share.max_read_size(); | ||
| let max_read = share.compound_max_read_size(); | ||
| let no_range = range_header.is_none(); | ||
|
|
||
| if no_range { | ||
|
|
@@ -595,19 +595,26 @@ async fn handle_get_object( | |
| let (body, tx) = SpiceioBody::channel(4); | ||
| let chunk_size = handle.max_chunk; | ||
|
|
||
| // Spawn background task to stream SMB reads into the channel | ||
| // Spawn background task to stream pipelined SMB reads into the channel. | ||
| // Sends batches of read requests to fill the network pipe, then pushes | ||
| // each chunk to the HTTP response body as it arrives. | ||
| tokio::spawn(async move { | ||
| let mut offset = start; | ||
| let stream_end = end + 1; | ||
| while offset < stream_end { | ||
| let to_read = ((stream_end - offset) as u32).min(chunk_size); | ||
| match handle.read_chunk(offset, to_read).await { | ||
| Ok(chunk) if chunk.is_empty() => break, | ||
| Ok(chunk) => { | ||
| offset += chunk.len() as u64; | ||
| if tx.send(chunk).await.is_err() { | ||
| crate::serr!("[spiceio] getobject client disconnected"); | ||
| break; | ||
| 'outer: while offset < stream_end { | ||
| let remaining = stream_end - offset; | ||
| match handle.read_pipeline(offset, chunk_size, remaining).await { | ||
|
Comment on lines
+604
to
+606
|
||
| Ok(chunks) if chunks.is_empty() => break, | ||
| Ok(chunks) => { | ||
| for chunk in chunks { | ||
| if chunk.is_empty() { | ||
| break 'outer; | ||
| } | ||
| offset += chunk.len() as u64; | ||
| if tx.send(chunk).await.is_err() { | ||
| crate::serr!("[spiceio] getobject client disconnected"); | ||
| break 'outer; | ||
| } | ||
| } | ||
| } | ||
| Err(e) => { | ||
|
|
@@ -671,7 +678,7 @@ async fn handle_put_object( | |
| // ── Fast path: collect small bodies and use compound write ────── | ||
| let content_length: Option<u64> = | ||
| get_header(hdrs, "content-length").and_then(|s| s.parse().ok()); | ||
| let max_write = share.max_write_size() as u64; | ||
| let max_write = share.compound_max_write_size() as u64; | ||
|
|
||
| if let Some(cl) = content_length | ||
| && cl <= max_write | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -22,6 +22,8 @@ pub struct SmbConfig { | |||||
| pub password: String, | ||||||
| pub domain: String, | ||||||
| pub workstation: String, | ||||||
| /// Cap for standalone read/write I/O (0 = use DEFAULT_MAX_IO). | ||||||
| pub max_io_size: u32, | ||||||
| } | ||||||
|
|
||||||
| impl SmbConfig { | ||||||
|
|
@@ -30,14 +32,28 @@ impl SmbConfig { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Default I/O cap for standalone (non-compound) read/write operations. | ||||||
| /// Many NAS servers advertise multi-MB maximums in negotiate but fail at sizes | ||||||
| /// well below the advertised limit. 64 KB is the safe conservative default; | ||||||
| /// override via `SPICEIO_SMB_MAX_IO` for servers that handle larger I/O | ||||||
| /// (e.g., Windows Server, enterprise NAS). Even at 64 KB the connection pool | ||||||
| /// and pipelined reads still deliver major throughput gains. | ||||||
| const DEFAULT_MAX_IO: u32 = 65536; | ||||||
|
|
||||||
| /// An authenticated SMB2 session. | ||||||
| pub struct SmbClient { | ||||||
| stream: Mutex<TcpStream>, | ||||||
| message_id: AtomicU64, | ||||||
| session_id: u64, | ||||||
| config: SmbConfig, | ||||||
| /// Effective max read size for standalone (non-compound) reads. | ||||||
| pub max_read_size: u32, | ||||||
| /// Effective max write size for standalone (non-compound) writes. | ||||||
| pub max_write_size: u32, | ||||||
| /// Capped max for compound operations (64KB — some NAS servers reject | ||||||
| /// larger payloads inside compound requests). | ||||||
| pub compound_max_read_size: u32, | ||||||
| pub compound_max_write_size: u32, | ||||||
| /// 16-byte client GUID | ||||||
| client_guid: [u8; 16], | ||||||
| /// SMB 3.1.1 signing key (derived after auth) | ||||||
|
|
@@ -60,6 +76,34 @@ impl SmbClient { | |||||
| }; | ||||||
| stream.set_nodelay(true)?; | ||||||
|
|
||||||
| // Enlarge socket buffers to 1 MB for large read/write throughput. | ||||||
| { | ||||||
| use std::os::fd::AsRawFd; | ||||||
|
|
||||||
| unsafe extern "C" { | ||||||
| fn setsockopt( | ||||||
| socket: i32, | ||||||
| level: i32, | ||||||
| option_name: i32, | ||||||
| option_value: *const u8, | ||||||
| option_len: u32, | ||||||
| ) -> i32; | ||||||
| } | ||||||
|
|
||||||
| const SOL_SOCKET: i32 = 0xffff; | ||||||
| const SO_SNDBUF: i32 = 0x1001; | ||||||
| const SO_RCVBUF: i32 = 0x1002; | ||||||
|
|
||||||
| let fd = stream.as_raw_fd(); | ||||||
| let buf_size: i32 = 1024 * 1024; | ||||||
| let ptr = std::ptr::from_ref(&buf_size).cast(); | ||||||
| let len = size_of::<i32>() as u32; | ||||||
|
||||||
| let len = size_of::<i32>() as u32; | |
| let len = std::mem::size_of::<i32>() as u32; |
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pipelined_read assumes responses arrive in the same order requests were sent (it just pushes each chunk). SMB2 responses can be returned out-of-order, so this can reorder/corrupt the byte stream (and can also prematurely stop if an EOF response for a later offset arrives early). Track message_id→request index/offset when sending and place each decoded chunk into the correct slot before returning (or otherwise reorder by offset).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field comment says the default max I/O is 1MB, but
SPICEIO_SMB_MAX_IOdefaults to0here (which the SMB client interprets as the internal default of 64KB). Update the comment to match the actual default behavior to avoid confusion.