Skip to content

feat(engine): on-complete hook, queueing, share ratio, disk-space guard, .part files (WI-1730) - #108

Merged
thedancingdeveloper merged 5 commits into
mainfrom
wi-1730-engine-parity
Oct 17, 2026
Merged

thedancingdeveloper merged 5 commits into
mainfrom
wi-1730-engine-parity

Conversation

@thedancingdeveloper

Copy link
Copy Markdown
Collaborator

WP5 engine feature parity (WI-1730, polish review PR #94).

  • On-complete command and webhook: kill_on_drop, timeout, detached; skipped for torrents that were already complete on restore
  • Queue limits for downloads and seeds, per-torrent share-ratio and seeding-time limits, and a low-disk guard with hysteresis, all run by a policy pass
  • Optional .part names for incomplete files; an existing .part is adopted, and the file is renamed when it completes
  • JSON and Postgres persistence of policy flags, share limits, upload totals and the .part choice
  • Saves on session.stop() (the rtbit server now calls it on SIGINT/SIGTERM), on a 60 s timer, and straight away in set_share_limits

Gates at 151a926: fmt ok; cargo test -p swarmforge --lib 159 passed / 5 ignored; cargo check --workspace --all-targets and the postgres feature check pass.

Not yet covered: the signal path has no test, Postgres has not been run against a live database, and the CLI flags and compat endpoints are not wired yet.

🤖 Generated with Claude Code

thedancingdeveloper and others added 5 commits October 17, 2026 10:44
… guard, .part

Adds the engine side of WI-1730 (rustTorrent WP5, polish review H6 and
parity items) in the session and torrent modules. CLI flags, compat
endpoints and the policy settings UI are wired in other packages.

- On-complete hook: optional shell command (RTBIT_* env) and/or a JSON
  webhook POST, run detached when a torrent finishes downloading. Torrents
  already complete on initialize (session restore) do not fire it.
  Configure with Session::set_on_complete_hook.
- Queueing: Session::set_max_active_downloads / set_max_active_seeds.
  A periodic policy pass keeps the oldest torrents running and parks the
  rest in a queue (Torrent::is_queued). User pause/resume overrides it.
- Share limits: Session::set_share_limits with a ratio and/or seeding
  time. Reaching either pauses a seeding torrent. Counters are per
  process, not persisted.
- Low-disk guard: Session::set_min_free_disk_space holds downloads when
  free space falls below the threshold and resumes them at twice the
  threshold, so they do not flap.
- .part files: Session::set_use_part_files writes incomplete files as
  <name>.part and renames each to its final name when it completes.
  An existing final name is always used. Applies to torrents added later.

Pure decision functions (queue plan, share limit, disk hysteresis) and the
storage rename paths have unit tests. The policy pass itself is not
covered by an integration test.

Refs WI-1730

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…starts

The policy pass pauses torrents for three reasons: queue slot, low disk
space, and share limit. Those reasons lived only in memory, so a restart
brought every such torrent back paused with no flag set. The policy pass
never resumes an unflagged paused torrent, so queued and disk-held
downloads stayed paused forever after a restart.

- Add a share_paused flag beside queued and held_for_disk.
- Replace the policy_pause(queued, held) bool pair with a PolicyPause
  reason that sets exactly one flag, and read/write all three through
  PolicyFlags.
- Persist the three flags in the JSON and Postgres session stores. The
  Postgres columns are added with defaults, so existing databases migrate
  in place and older state files still load.
- On restore, reapply the flags once each torrent is added back.
- Add session-level tests that drive policy_pass() directly: queue limit
  and release, queued download across restart, disk hold across restart,
  and share-paused seed across restart.

Refs WI-1730

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
update_db read the torrent's paused and policy flags before taking the
database lock. Two concurrent updates could therefore store in the
reverse order of their reads, and the older snapshot would win. The
share-paused restart test hit this about once in 25 runs: the file
recorded a paused torrent without its share-limit flag.

Read the torrent and insert it under one lock. flush() takes the same
lock, so it runs after the guard is released.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…ime out on-complete command

- Share limits (ratio and seeding time) and the upload total are saved
  with each torrent in the JSON and Postgres session stores. The upload
  total is kept in a base counter that a pause folds the live run into,
  so it accumulates across pauses and restarts. Restore reapplies both,
  so a limit that has not yet tripped still trips after a restart.
- session.stop() persists each torrent before pausing it, so a clean
  restart keeps the upload total and each torrent's running state.
- A torrent's .part choice is saved and reapplied on restore. An existing
  <name>.part file is adopted whatever the session setting says, so a
  partial download is not orphaned and redownloaded.
- The on-complete command runs as a tokio child with kill_on_drop and a
  5 minute timeout, so a hung script cannot hold a blocking thread.
- AddTorrentOptions::part_files (Option, default: session setting) lets a
  caller pin the choice per torrent.

Restart tests cover share limits and uploads (including a restored ratio
limit that trips after uploads continue), the persisted .part choice with
the switch off, and adoption of an existing .part file. A unit test
covers the command timeout.

Refs WI-1730

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…are_limits

Upload totals and share limits were only saved on pause and on a clean
stop, and the binary never called stop(), so a SIGINT/SIGTERM restart lost
them.

- Session::persist_all saves every torrent. stop() uses it before pausing.
- A persist timer saves every torrent each persist_interval (new
  SessionOptions field, default 60 s), so a kill loses at most that much.
- The rtbit server path calls session.stop() after the HTTP API returns on
  cancel, so SIGINT and SIGTERM save before the process exits.
- Session::set_share_limits is async and saves the torrent right away.

Tests: a drop without a final save keeps the upload total (saved by the
timer) and the share limits (saved by set_share_limits). The session is
killed by cancelling its token, which ends its tasks with no final save.

Refs WI-1730

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@thedancingdeveloper
thedancingdeveloper merged commit bda6487 into main Oct 17, 2026
10 checks passed
@thedancingdeveloper
thedancingdeveloper deleted the wi-1730-engine-parity branch October 17, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant