Address review: clear daily limits via 0; document soft-cap semantics

- Settings.js: send 0 (not null) when a daily-limit field is cleared, so the
  settings API actually clears a previously-saved limit instead of dropping the
  null and keeping the old value.
- downloader.py: document that the daily limit is a best-effort soft cap; minor
  bounded overshoot is possible when create_downloads' background dispatch overlaps
  the perpetual cycle, and self-corrects the next cycle. A strict cross-process cap
  is intentionally not implemented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Roland 2026-06-28 17:11:45 -06:00
parent d95249218b
commit 706e22bd8f
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 14 additions and 3 deletions

View file

@ -367,11 +367,13 @@ export function SettingsPage() {
setPendingSave(true);
newSettings.download_timeout = parseInt(newSettings.download_timeout);
newSettings.download_wait = parseInt(newSettings.download_wait);
// Empty daily-limit fields become null (no change / unlimited); a number is sent as-is.
// Empty daily-limit fields become 0 (unlimited); a number is sent as-is. Sending 0 rather than
// null matters because the settings API drops null fields, which would leave a previously-saved
// limit active when the user clears the field.
newSettings.download_daily_limit_per_domain = newSettings.download_daily_limit_per_domain === ''
? null : parseInt(newSettings.download_daily_limit_per_domain);
? 0 : parseInt(newSettings.download_daily_limit_per_domain);
newSettings.download_daily_limit_global = newSettings.download_daily_limit_global === ''
? null : parseInt(newSettings.download_daily_limit_global);
? 0 : parseInt(newSettings.download_daily_limit_global);
newSettings.log_level = toApiLogLevel(newSettings.log_level);
try {
await saveSettings(newSettings);

View file

@ -1076,6 +1076,15 @@ class DownloadManager:
# Daily download limits. A value of None/0 means unlimited. Recurring downloads (Channel/RSS
# catalog updates) are never gated and never counted; only their non-recurring children and
# one-off downloads count.
#
# This is a best-effort soft cap, not a hard quota. The count is read from the DB and the slot is
# claimed (last_download_attempt set) inside this dispatch transaction. The steady-state dispatcher
# (perpetual_download_worker) is single-process and self-serializing, so it never races itself; but
# create_downloads fires a background dispatch_downloads() to "start ASAP", which can overlap this
# cycle in another worker and read the same pre-commit count. At a limit boundary that can leak a
# small, bounded overshoot (paced at one download per domain per cycle); it self-corrects the next
# cycle once last_download_attempt is committed and visible. Enforcing a strict cap would require
# coordinating the count in shared_ctx with cross-process locking, which is not warranted here.
limit_per_domain = get_wrolpi_config().download_daily_limit_per_domain or 0
limit_global = get_wrolpi_config().download_daily_limit_global or 0
global_count, domain_counts = 0, {}