From 641b6853adf6bff26961650536221a096f195001 Mon Sep 17 00:00:00 2001 From: Dionisio Date: Mon, 2 Mar 2026 00:12:33 +0100 Subject: [PATCH] perf: add socket2 TCP_NODELAY + socket tuning for low-latency responses - Replace basic TcpListener::bind with socket2 tuned socket - TCP_NODELAY: disable Nagle's algorithm (-5 to 40ms latency on small responses) - SO_REUSEADDR: port available immediately after server restart - SO_REUSEPORT: ready for multi-worker scaling (Linux) - TCP_KEEPALIVE: detect dead connections within 60s/10s interval - listen(2048): high backlog for WebDAV connection bursts - Eliminate redundant create_dir_all calls from upload hot path --- Cargo.lock | 1 + Cargo.toml | 1 + src/infrastructure/services/dedup_service.rs | 19 ++----------- src/interfaces/api/handlers/file_handler.rs | 2 +- src/main.rs | 29 ++++++++++++++++++-- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c7d193c..b75a192e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1847,6 +1847,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "socket2", "sqlx", "tempfile", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index d64c1c16..93456abd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ infer = "0.19" async-compression = { version = "0.4", features = ["tokio", "gzip"] } async_zip = { version = "0.0.18", features = ["tokio", "deflate"] } dashmap = "6" +socket2 = { version = "0.6.2", features = ["all"] } [features] default = [] diff --git a/src/infrastructure/services/dedup_service.rs b/src/infrastructure/services/dedup_service.rs index 6ec28fa6..a5135b33 100644 --- a/src/infrastructure/services/dedup_service.rs +++ b/src/infrastructure/services/dedup_service.rs @@ -210,15 +210,7 @@ impl DedupService { // same filesystem; if it fails because the other writer won, // we just discard our temp file — the blob is already there. if !blob_path.exists() { - if let Some(parent) = blob_path.parent() { - fs::create_dir_all(parent).await.map_err(|e| { - DomainError::internal_error( - "Dedup", - format!("Failed to create blob directory: {}", e), - ) - })?; - } - + // Parent directory (xx/) guaranteed to exist — created by initialize() let temp_path = self.temp_root.join(format!("{}.tmp", uuid::Uuid::new_v4())); fs::write(&temp_path, content).await.map_err(|e| { DomainError::internal_error("Dedup", format!("Failed to write temp blob: {}", e)) @@ -308,14 +300,7 @@ impl DedupService { // Blob already on disk — discard the source file let _ = fs::remove_file(source_path).await; } else { - if let Some(parent) = blob_path.parent() { - fs::create_dir_all(parent).await.map_err(|e| { - DomainError::internal_error( - "Dedup", - format!("Failed to create blob directory: {}", e), - ) - })?; - } + // Parent directory (xx/) guaranteed to exist — created by initialize() // rename is atomic on the same filesystem. If source and blob // dirs live on different filesystems (rare), this falls back to diff --git a/src/interfaces/api/handlers/file_handler.rs b/src/interfaces/api/handlers/file_handler.rs index 31c57004..026aa8f9 100644 --- a/src/interfaces/api/handlers/file_handler.rs +++ b/src/interfaces/api/handlers/file_handler.rs @@ -119,8 +119,8 @@ impl FileHandler { } // ── Spool multipart field to temp file + hash-on-write ── + // .dedup_temp is created once by DedupService::initialize() at startup let temp_dir = state.core.path_service.get_root_path().join(".dedup_temp"); - let _ = tokio::fs::create_dir_all(&temp_dir).await; let temp_path = temp_dir.join(format!("upload-{}", uuid::Uuid::new_v4())); let mut total_size: u64 = 0; diff --git a/src/main.rs b/src/main.rs index a61dec3b..bc49c497 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,9 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; +use std::time::Duration; + +use socket2::{Domain, Protocol, Socket, TcpKeepalive, Type}; use axum::Router; use axum::extract::DefaultBodyLimit; @@ -269,15 +272,37 @@ async fn main() -> Result<(), Box> { // Without this Axum caps Multipart bodies at 2 MB. app = app.layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)); - // Start server + // Start server — tuned socket for low-latency responses let addr = SocketAddr::from(([0, 0, 0, 0], 8086)); tracing::info!("Starting OxiCloud server on http://{}", addr); - let listener = tokio::net::TcpListener::bind(addr).await?; + let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?; + socket.set_reuse_address(true)?; + // Allow multiple workers on the same port (future-ready) + #[cfg(not(windows))] + socket.set_reuse_port(true)?; + // Disable Nagle's algorithm — send small responses (JSON, PROPFIND) + // immediately instead of waiting up to 40ms for coalescing. + socket.set_tcp_nodelay(true)?; + // Detect dead connections within 60s instead of hours + socket.set_keepalive(true)?; + socket.set_tcp_keepalive( + &TcpKeepalive::new() + .with_time(Duration::from_secs(60)) + .with_interval(Duration::from_secs(10)), + )?; + socket.set_nonblocking(true)?; + socket.bind(&addr.into())?; + // High backlog for connection bursts (WebDAV clients open many parallel connections) + socket.listen(2048)?; + + let listener = tokio::net::TcpListener::from_std(socket.into())?; // Provide the fully-built state to the router let app = app.with_state(app_state); + // TCP_NODELAY is inherited from the listening socket on Linux, + // so every accepted connection already has Nagle disabled. axum::serve(listener, app).await?; tracing::info!("Server shutdown completed");