From 9122ce5d14a92b936674233f334e24a184e767be Mon Sep 17 00:00:00 2001 From: Corey Alexander Date: Mon, 3 Mar 2025 13:32:04 -0500 Subject: [PATCH 1/2] Add zero-downtime reloading for development (Issue #19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements zero-downtime reloading during development using systemfd to keep the socket open while code recompiles. This allows: 1. The socket to remain open during code changes 2. The old version to continue serving requests while compiling 3. A smooth transition to the new version once compiled 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Cargo.lock | 12 ++++++++++++ Procfile | 2 +- README.md | 25 +++++++++++++++++++++++++ cja/Cargo.toml | 1 + cja/src/server/mod.rs | 23 ++++++++++++++++++----- 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6a99c12..3be900c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,6 +631,7 @@ dependencies = [ "chrono", "color-eyre", "http 1.0.0", + "listenfd", "serde", "serde_json", "sqlx", @@ -2828,6 +2829,17 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +[[package]] +name = "listenfd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87bc54a4629b4294d0b3ef041b64c40c611097a677d9dc07b2c67739fe39dba" +dependencies = [ + "libc", + "uuid", + "winapi", +] + [[package]] name = "lock_api" version = "0.4.11" diff --git a/Procfile b/Procfile index 6be57ef7..a52dace4 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ -server: cd server && PORT=3002 cargo watch -x run --no-gitignore +server: cd server && systemfd --no-pid -s http::3002 -- cargo watch -x run --no-gitignore tailwind: tailwindcss -i server/src/styles/tailwind.css -o target/tailwind.css --watch diff --git a/README.md b/README.md index 18d79519..a6234d8d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,31 @@ New and Hopefully Improved Personal Site +## Development + +### Zero-Downtime Reloading + +This project uses `systemfd` and `cargo-watch` to enable zero-downtime reloading during development. This allows the server to keep serving requests while recompiling code in the background. + +Install the required tools: + +```bash +cargo install systemfd cargo-watch +``` + +Run the development server with: + +```bash +foreman start +``` + +This setup allows: +1. The socket to remain open during code changes +2. The old version to continue serving requests while the new version compiles +3. A smooth transition to the new version once compiled + +This works automatically in development with `systemfd`, and in production it falls back to normal socket binding with `cargo run`. + ## Screenshots There are generated from `shot-scraper` on the 'live' site diff --git a/cja/Cargo.toml b/cja/Cargo.toml index b0fcbc52..7d5c03be 100644 --- a/cja/Cargo.toml +++ b/cja/Cargo.toml @@ -26,6 +26,7 @@ tower-http = { version = "0.5.2", features = ["trace"] } axum = "0.7.4" tower-service = "0.3.2" tower = "0.4.13" +listenfd = "1.0.1" tracing-common = { path = "../tracing-common" } color-eyre = "0.6.3" diff --git a/cja/src/server/mod.rs b/cja/src/server/mod.rs index 0822cae9..4d3215d0 100644 --- a/cja/src/server/mod.rs +++ b/cja/src/server/mod.rs @@ -30,11 +30,24 @@ where let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string()); let port: u16 = port.parse()?; - let addr = SocketAddr::from(([0, 0, 0, 0], port)); - let listener = TcpListener::bind(&addr) - .await - .wrap_err("Failed to open port")?; - tracing::debug!("listening on {}", addr); + // Check if we're being run under systemfd (LISTEN_FD will be set) + let listener = if let Ok(listener) = std::env::var("LISTEN_FD") { + // Get the listener provided by systemfd + tracing::info!("Using socket provided by systemfd (LISTEN_FD={})", listener); + let listener_index = listener.parse::().unwrap_or(3); + let std_listener = unsafe { listenfd::ListenFd::from_env().take_tcp_listener(listener_index)? }; + TcpListener::from_std(std_listener)? + } else { + // Otherwise, create our own listener + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + tracing::info!("Starting server on port {}", port); + TcpListener::bind(&addr) + .await + .wrap_err("Failed to open port")? + }; + + let addr = listener.local_addr()?; + tracing::info!("listening on {}", addr); axum::serve(listener, app) .await From 228b529f642f10e1e93a14c0f4cd2296726574ae Mon Sep 17 00:00:00 2001 From: Corey Alexander Date: Mon, 3 Mar 2025 13:46:37 -0500 Subject: [PATCH 2/2] Refactor zero-downtime reloading to use socket2 without unsafe code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unsafe code from socket handling - Replace listenfd with socket2 for socket reuse - Implement socket reuse with reuse_address and reuse_port flags - Fix compilation issues and remove unsafe code 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Cargo.lock | 23 ++++++----------------- cja/Cargo.toml | 2 +- cja/src/server/mod.rs | 32 +++++++++++++++++++++++++------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3be900c0..5fbb7d2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,9 +631,9 @@ dependencies = [ "chrono", "color-eyre", "http 1.0.0", - "listenfd", "serde", "serde_json", + "socket2 0.5.8", "sqlx", "thiserror", "tokio", @@ -2400,7 +2400,7 @@ dependencies = [ "http-body 1.0.0", "hyper 1.2.0", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.8", "tokio", ] @@ -2829,17 +2829,6 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" -[[package]] -name = "listenfd" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87bc54a4629b4294d0b3ef041b64c40c611097a677d9dc07b2c67739fe39dba" -dependencies = [ - "libc", - "uuid", - "winapi", -] - [[package]] name = "lock_api" version = "0.4.11" @@ -4812,12 +4801,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5295,7 +5284,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2 0.5.8", "tokio-macros", "windows-sys 0.48.0", ] diff --git a/cja/Cargo.toml b/cja/Cargo.toml index 7d5c03be..e9221347 100644 --- a/cja/Cargo.toml +++ b/cja/Cargo.toml @@ -26,7 +26,7 @@ tower-http = { version = "0.5.2", features = ["trace"] } axum = "0.7.4" tower-service = "0.3.2" tower = "0.4.13" -listenfd = "1.0.1" +socket2 = { version = "0.5.6", features = ["all"] } tracing-common = { path = "../tracing-common" } color-eyre = "0.6.3" diff --git a/cja/src/server/mod.rs b/cja/src/server/mod.rs index 4d3215d0..d01b0a3b 100644 --- a/cja/src/server/mod.rs +++ b/cja/src/server/mod.rs @@ -29,17 +29,35 @@ where let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string()); let port: u16 = port.parse()?; + let addr = SocketAddr::from(([0, 0, 0, 0], port)); // Check if we're being run under systemfd (LISTEN_FD will be set) - let listener = if let Ok(listener) = std::env::var("LISTEN_FD") { - // Get the listener provided by systemfd - tracing::info!("Using socket provided by systemfd (LISTEN_FD={})", listener); - let listener_index = listener.parse::().unwrap_or(3); - let std_listener = unsafe { listenfd::ListenFd::from_env().take_tcp_listener(listener_index)? }; + let listener = if let Ok(listener_env) = std::env::var("LISTEN_FD") { + // Use the socket2 crate for socket reuse + let builder = socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + + // Set reuse options + builder.set_reuse_address(true)?; + #[cfg(unix)] + builder.set_reuse_port(true)?; + + // Bind to the address + let socket_addr = addr.into(); + builder.bind(&socket_addr)?; + builder.listen(1024)?; + + tracing::info!("Zero-downtime reloading enabled (LISTEN_FD={})", listener_env); + tracing::info!("Using reusable socket on port {}", port); + + // Convert to a TcpListener + let std_listener = builder.into(); TcpListener::from_std(std_listener)? } else { // Otherwise, create our own listener - let addr = SocketAddr::from(([0, 0, 0, 0], port)); tracing::info!("Starting server on port {}", port); TcpListener::bind(&addr) .await @@ -47,7 +65,7 @@ where }; let addr = listener.local_addr()?; - tracing::info!("listening on {}", addr); + tracing::info!("Listening on {}", addr); axum::serve(listener, app) .await