From e6587194901ebf9225bb49d659e82412923933be Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Tue, 3 Nov 2026 03:16:30 +0000 Subject: [PATCH 1/2] fix container smoke shutdown persistence Graft-Changeset: c42142df7af0d6dd4499fef30e1a7430ca8f5693cba4d7f6dd73458651a186820 Graft-Session: ses_01M6FK3MBEFJEHGJ5GNCFWDFBM Graft-Work-Item: WI-3114 Graft-Batch: 65 --- Dockerfile | 3 ++- crates/librtbit/src/session/mod.rs | 10 +++++++++ crates/rtbit/src/main.rs | 22 ++++++++++++++----- .../s6-overlay/s6-rc.d/init-rtbit-config/run | 5 +++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index da5a017f..792bcc24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,8 @@ LABEL org.opencontainers.image.description="A modern BitTorrent client written i COPY --from=builder /bin/rtbit /bin/rtbit COPY root/ / -RUN mkdir -p /home/rtbit/db /home/rtbit/cache /home/rtbit/downloads /home/rtbit/completed +RUN mkdir -p /home/rtbit/db /home/rtbit/cache /home/rtbit/downloads /home/rtbit/completed \ + && ln -s com.rtbit.session /home/rtbit/db/session WORKDIR /home/rtbit diff --git a/crates/librtbit/src/session/mod.rs b/crates/librtbit/src/session/mod.rs index f74845cb..7491561b 100644 --- a/crates/librtbit/src/session/mod.rs +++ b/crates/librtbit/src/session/mod.rs @@ -1648,6 +1648,16 @@ impl Session { for torrent in &torrents { self.try_update_persistence_metadata(torrent).await; } + // A fresh session has no torrent metadata to trigger a persistence + // flush. Still write the session database during graceful shutdown so + // an empty, healthy state is durable and observable. + if torrents.is_empty() + && let Some(persistence) = self.persistence.as_ref() + && let Ok(settings) = persistence.load_session_settings().await + && let Err(error) = persistence.store_session_settings(&settings).await + { + warn!(?error, "error flushing empty session database"); + } } /// Save every torrent's state on a timer, so upload totals reach disk even if diff --git a/crates/rtbit/src/main.rs b/crates/rtbit/src/main.rs index 22f3fad9..aa752f80 100644 --- a/crates/rtbit/src/main.rs +++ b/crates/rtbit/src/main.rs @@ -1,3 +1,4 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::HashSet, io, @@ -788,9 +789,11 @@ fn main() -> anyhow::Result<()> { .build()?; let token = tokio_util::sync::CancellationToken::new(); + let shutdown_complete = Arc::new(AtomicBool::new(false)); #[cfg(not(target_os = "windows"))] { let token = token.clone(); + let shutdown_complete = shutdown_complete.clone(); let shutdown_timeout = opts.shutdown_timeout; use signal_hook::{consts::SIGINT, consts::SIGTERM, iterator::Signals}; let mut signals = Signals::new([SIGINT, SIGTERM])?; @@ -808,19 +811,23 @@ fn main() -> anyhow::Result<()> { token.cancel(); cancel_triggered = true; + let shutdown_complete = shutdown_complete.clone(); std::thread::spawn(move || { std::thread::sleep(shutdown_timeout); - warn!( - "could not shut down within {:?}, killing myself", - shutdown_timeout - ); - std::process::exit(1) + if !shutdown_complete.load(Ordering::Acquire) { + warn!( + "could not shut down within {:?}, killing myself", + shutdown_timeout + ); + std::process::exit(1) + } }); } }); } let result = rt.block_on(async_main(opts, token.clone())); + shutdown_complete.store(true, Ordering::Release); match result.as_ref() { Err(e) if librtbit_core::spawn_utils::is_cancelled(e) => { info!("rtbit shut down: cancelled"); @@ -831,6 +838,11 @@ fn main() -> anyhow::Result<()> { rt.shutdown_timeout(Duration::from_secs(1)); match result { Ok(_) => std::process::exit(0), + // SIGTERM/SIGINT is the normal container shutdown path. The signal + // cancels the HTTP server, which is reported as an internal + // cancellation error, but it must still produce a successful process + // exit after the session has flushed its state. + Err(e) if librtbit_core::spawn_utils::is_cancelled(&e) => std::process::exit(0), Err(_) => std::process::exit(1), } } diff --git a/root/etc/s6-overlay/s6-rc.d/init-rtbit-config/run b/root/etc/s6-overlay/s6-rc.d/init-rtbit-config/run index a87fcd58..0e363962 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-rtbit-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-rtbit-config/run @@ -3,10 +3,15 @@ # Ensure directories exist mkdir -p \ /home/rtbit/db \ + /home/rtbit/db/com.rtbit.session \ /home/rtbit/cache \ /home/rtbit/downloads \ /home/rtbit/completed +# Keep the legacy container-facing session path available while the daemon +# stores its state in the current application-data directory. +ln -sfn com.rtbit.session /home/rtbit/db/session + # Set ownership (lsiown handles edge cases on various filesystems) lsiown abc:abc /home/rtbit lsiown -R abc:abc /home/rtbit/db /home/rtbit/cache From 62652e175f947fdee505982ed122b38d906acbb6 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Tue, 3 Nov 2026 03:23:30 +0000 Subject: [PATCH 2/2] Preserve legacy session directory migration Graft-Changeset: c9629bcba9633c308a4db5c66da480a9e0cdc33e0958ed4b6a9f044ec0ca4c51d Graft-Session: ses_01M6FPREN4TN0NBVVFHG7NXQGB Graft-Work-Item: WI-3118 Co-authored-by: gpt-5.6-sol Graft-Batch: 65 --- .github/workflows/container.yml | 4 +- Cargo.lock | 1 + Dockerfile | 3 +- crates/librtbit-core/Cargo.toml | 1 + crates/librtbit-core/src/directories.rs | 71 +++++++++++++------ .../s6-overlay/s6-rc.d/init-rtbit-config/run | 5 -- 6 files changed, 54 insertions(+), 31 deletions(-) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index ce18ca7f..3fa7dc39 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -115,7 +115,7 @@ jobs: deadline=$((SECONDS + 30)) setup_token='' while (( SECONDS < deadline )); do - if ! setup_token=$(docker exec "$container" cat /home/rtbit/db/session/setup-token 2>/dev/null); then + if ! setup_token=$(docker exec "$container" cat /home/rtbit/db/com.rtbit.session/setup-token 2>/dev/null); then setup_token='' fi if [[ -n "$setup_token" ]] && curl --fail --silent --show-error \ @@ -140,4 +140,4 @@ jobs: docker stop --time 30 "$container" >/dev/null [[ "$(docker inspect --format '{{.State.ExitCode}}' "$container")" == 0 ]] docker run --rm --volume "$volume:/data" --entrypoint sh "$IMAGE" \ - -c 'test -s /data/session/session.json' + -c 'test -s /data/com.rtbit.session/session.json' diff --git a/Cargo.lock b/Cargo.lock index dc4e289f..81e12c6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8919,6 +8919,7 @@ dependencies = [ "swarmforge-buffers", "swarmforge-clone-to-owned", "swarmforge-sha1-wrapper", + "tempfile", "thiserror 2.0.21", "tokio", "tokio-util", diff --git a/Dockerfile b/Dockerfile index 792bcc24..da5a017f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,7 @@ LABEL org.opencontainers.image.description="A modern BitTorrent client written i COPY --from=builder /bin/rtbit /bin/rtbit COPY root/ / -RUN mkdir -p /home/rtbit/db /home/rtbit/cache /home/rtbit/downloads /home/rtbit/completed \ - && ln -s com.rtbit.session /home/rtbit/db/session +RUN mkdir -p /home/rtbit/db /home/rtbit/cache /home/rtbit/downloads /home/rtbit/completed WORKDIR /home/rtbit diff --git a/crates/librtbit-core/Cargo.toml b/crates/librtbit-core/Cargo.toml index 5901de9a..40da37a0 100644 --- a/crates/librtbit-core/Cargo.toml +++ b/crates/librtbit-core/Cargo.toml @@ -43,6 +43,7 @@ encoding_rs = "0.8" [dev-dependencies] serde_json = "1" +tempfile.workspace = true [lints.clippy] all = { level = "warn", priority = -1 } diff --git a/crates/librtbit-core/src/directories.rs b/crates/librtbit-core/src/directories.rs index e0d8b3db..3d7782fe 100644 --- a/crates/librtbit-core/src/directories.rs +++ b/crates/librtbit-core/src/directories.rs @@ -1,6 +1,32 @@ use anyhow::Context; +use std::path::Path; use tracing::{info, warn}; +fn migrate_directory(old: &Path, new: &Path) { + match (old.exists(), new.exists()) { + (true, true) => { + warn!( + ?old, + ?new, + "can't migrate configuration as both directories exist, not sure what to do" + ) + } + (true, false) => { + info!( + ?old, + ?new, + "migrating configuration directories as rtbit was upgraded" + ); + if let Err(e) = std::fs::rename(old, new) { + warn!(?old, ?new, "error migrating: {e:#}"); + } + } + // In these cases, nothing to migrate, so do nothing. + (false, true) => {} + (false, false) => {} + } +} + pub fn get_configuration_directory_linux( application: &str, ) -> anyhow::Result { @@ -21,32 +47,33 @@ pub fn get_configuration_directory_linux( (legacy_dir.config_dir(), new_dir.config_dir()), (legacy_dir.data_dir(), new_dir.data_dir()), ] { - match (old.exists(), new.exists()) { - (true, true) => { - warn!( - ?old, - ?new, - "can't migrate configuration as both directories exist, not sure what to do" - ) - } - (true, false) => { - info!( - ?old, - ?new, - "migrating configuration directories as rtbit was upgraded" - ); - if let Err(e) = std::fs::rename(old, new) { - warn!(?old, ?new, "error migrating: {e:#}"); - } - } - // In these cases, nothing to migrate, so do nothing. - (false, true) => {} - (false, false) => {} - } + migrate_directory(old, new); } Ok(new_dir) } +#[cfg(test)] +mod tests { + use super::migrate_directory; + + #[test] + fn migrates_legacy_directory_when_current_directory_is_absent() { + let temp = tempfile::tempdir().unwrap(); + let legacy = temp.path().join("session"); + let current = temp.path().join("com.rtbit.session"); + std::fs::create_dir(&legacy).unwrap(); + std::fs::write(legacy.join("session.json"), "legacy state").unwrap(); + + migrate_directory(&legacy, ¤t); + + assert!(!legacy.exists()); + assert_eq!( + std::fs::read_to_string(current.join("session.json")).unwrap(), + "legacy state" + ); + } +} + pub fn get_configuration_directory(application: &str) -> anyhow::Result { #[cfg(not(any(target_os = "macos", target_os = "windows")))] { diff --git a/root/etc/s6-overlay/s6-rc.d/init-rtbit-config/run b/root/etc/s6-overlay/s6-rc.d/init-rtbit-config/run index 0e363962..a87fcd58 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-rtbit-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-rtbit-config/run @@ -3,15 +3,10 @@ # Ensure directories exist mkdir -p \ /home/rtbit/db \ - /home/rtbit/db/com.rtbit.session \ /home/rtbit/cache \ /home/rtbit/downloads \ /home/rtbit/completed -# Keep the legacy container-facing session path available while the daemon -# stores its state in the current application-data directory. -ln -sfn com.rtbit.session /home/rtbit/db/session - # Set ownership (lsiown handles edge cases on various filesystems) lsiown abc:abc /home/rtbit lsiown -R abc:abc /home/rtbit/db /home/rtbit/cache