queue lock stalled

This commit is contained in:
Mateusz Gruszczyński
2026-05-08 21:32:08 +02:00
parent f2eed0f7fe
commit 8a33a8af24
5 changed files with 29 additions and 25 deletions

View File

@@ -39,6 +39,7 @@ def _default_settings(user_id: int, profile_id: int) -> dict[str, Any]:
'min_seeds': 1,
'min_peers': 0,
'ignore_seed_peer': 0,
'ignore_speed': 0,
'manage_stopped': 1,
'updated_at': utcnow(),
}
@@ -65,16 +66,18 @@ def save_settings(profile_id: int, data: dict[str, Any], user_id: int | None = N
'min_seeds': _int_setting(data, current, 'min_seeds', 0, 0),
# Note: Min peers is optional; when set, stalled detection requires low speed, low seeds and low peers.
'min_peers': _int_setting(data, current, 'min_peers', 0, 0),
# Note: Ignore seed/peer lets long-stalled torrents wait by timer only, useful when sources appear rarely.
# Note: Ignore seed/peer removes source counts from stalled detection, useful when sources appear rarely.
'ignore_seed_peer': 1 if data.get('ignore_seed_peer', current.get('ignore_seed_peer')) else 0,
# Note: Ignore speed removes low transfer rate from stalled detection; with both ignores enabled only stalled_seconds matters.
'ignore_speed': 1 if data.get('ignore_speed', current.get('ignore_speed')) else 0,
# Note: Compatibility field retained; enabled Smart Queue always manages stopped torrents and never manages user-paused torrents.
'manage_stopped': 1,
}
now = utcnow()
with connect() as conn:
conn.execute(
'''INSERT INTO smart_queue_settings(user_id,profile_id,enabled,max_active_downloads,stalled_seconds,min_speed_bytes,min_seeds,min_peers,ignore_seed_peer,manage_stopped,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?)
'''INSERT INTO smart_queue_settings(user_id,profile_id,enabled,max_active_downloads,stalled_seconds,min_speed_bytes,min_seeds,min_peers,ignore_seed_peer,ignore_speed,manage_stopped,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(user_id, profile_id) DO UPDATE SET
enabled=excluded.enabled,
max_active_downloads=excluded.max_active_downloads,
@@ -83,9 +86,10 @@ def save_settings(profile_id: int, data: dict[str, Any], user_id: int | None = N
min_seeds=excluded.min_seeds,
min_peers=excluded.min_peers,
ignore_seed_peer=excluded.ignore_seed_peer,
ignore_speed=excluded.ignore_speed,
manage_stopped=excluded.manage_stopped,
updated_at=excluded.updated_at''',
(user_id, profile_id, settings['enabled'], settings['max_active_downloads'], settings['stalled_seconds'], settings['min_speed_bytes'], settings['min_seeds'], settings['min_peers'], settings['ignore_seed_peer'], settings['manage_stopped'], now),
(user_id, profile_id, settings['enabled'], settings['max_active_downloads'], settings['stalled_seconds'], settings['min_speed_bytes'], settings['min_seeds'], settings['min_peers'], settings['ignore_seed_peer'], settings['ignore_speed'], settings['manage_stopped'], now),
)
return get_settings(profile_id, user_id)
@@ -459,24 +463,20 @@ def _is_running_download_slot(t: dict[str, Any]) -> bool:
return _is_started_download_slot(t)
def _is_stalled_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int, ignore_seed_peer: bool) -> bool:
def _is_stalled_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int, ignore_seed_peer: bool, ignore_speed: bool) -> bool:
"""Return True when a started torrent should begin or continue the stalled timer."""
low_speed = int(t.get('down_rate') or 0) <= max(0, int(min_speed or 0))
if ignore_seed_peer:
# Note: Optional seed/peer ignore keeps no-source torrents waiting until stalled_seconds expires.
return low_speed
return low_speed and int(t.get('seeds') or 0) <= max(0, int(min_seeds or 0)) and (min_peers <= 0 or int(t.get('peers') or 0) <= min_peers)
# Note: Each ignore switch disables one stalled criterion; when both are enabled, only stalled_seconds matters.
speed_ok = True if ignore_speed else int(t.get('down_rate') or 0) <= max(0, int(min_speed or 0))
source_ok = True if ignore_seed_peer else int(t.get('seeds') or 0) <= max(0, int(min_seeds or 0)) and (min_peers <= 0 or int(t.get('peers') or 0) <= min_peers)
return speed_ok and source_ok
def _is_low_activity_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int, ignore_seed_peer: bool = False) -> bool:
def _is_low_activity_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int, ignore_seed_peer: bool = False, ignore_speed: bool = False) -> bool:
"""Return True when a started torrent is weak and should be stopped first."""
# Note: These settings define stop priority; the hard queue cap still applies to the full started queue.
low_speed = int(t.get('down_rate') or 0) <= max(0, int(min_speed or 0))
if ignore_seed_peer:
# Note: With seed/peer ignore enabled, source counts never make a torrent stop earlier than the stalled timer.
return low_speed
low_seeds = int(t.get('seeds') or 0) <= max(0, int(min_seeds or 0))
low_peers = int(t.get('peers') or 0) <= max(0, int(min_peers or 0)) if min_peers > 0 else False
# Note: Stop priority uses only criteria that are not ignored, so disabled criteria cannot stop torrents earlier.
low_speed = False if ignore_speed else int(t.get('down_rate') or 0) <= max(0, int(min_speed or 0))
low_seeds = False if ignore_seed_peer else int(t.get('seeds') or 0) <= max(0, int(min_seeds or 0))
low_peers = False if ignore_seed_peer or min_peers <= 0 else int(t.get('peers') or 0) <= max(0, int(min_peers or 0))
return low_speed or low_seeds or low_peers
@@ -544,6 +544,7 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
min_seeds = int(settings.get('min_seeds') or 0)
min_peers = int(settings.get('min_peers') or 0)
ignore_seed_peer = bool(int(settings.get('ignore_seed_peer') or 0))
ignore_speed = bool(int(settings.get('ignore_speed') or 0))
stalled_seconds = int(settings.get('stalled_seconds') or 300)
now = utcnow()
now_ts = datetime.now(timezone.utc).timestamp()
@@ -552,10 +553,10 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
with connect() as conn:
for t in downloading:
# Note: Stalled detection can ignore seed/peer counts and rely only on low speed plus the configured timer.
is_stalled = _is_stalled_download(t, min_speed, min_seeds, min_peers, ignore_seed_peer)
# Note: Hard-limit enforcement respects the same seed/peer ignore option before choosing weak items.
if _is_low_activity_download(t, min_speed, min_seeds, min_peers, ignore_seed_peer):
# Note: Stalled detection respects seed/peer and speed ignore switches before starting the timer.
is_stalled = _is_stalled_download(t, min_speed, min_seeds, min_peers, ignore_seed_peer, ignore_speed)
# Note: Hard-limit enforcement respects the same ignore switches before choosing weak items.
if _is_low_activity_download(t, min_speed, min_seeds, min_peers, ignore_seed_peer, ignore_speed):
stop_eligible.append(t)
h = t.get('hash')
if not h: