backend debounce on disk refresh
This commit is contained in:
@@ -26,6 +26,11 @@ _sem_lock = threading.Lock()
|
|||||||
_runner_lock = threading.Lock()
|
_runner_lock = threading.Lock()
|
||||||
_watchdog_started = False
|
_watchdog_started = False
|
||||||
_watchdog_lock = threading.Lock()
|
_watchdog_lock = threading.Lock()
|
||||||
|
_disk_refresh_delays = (30, 90)
|
||||||
|
_disk_refresh_min_immediate_seconds = 5
|
||||||
|
_disk_refresh_lock = threading.Lock()
|
||||||
|
_disk_refresh_timers: dict[tuple[int, int], threading.Timer] = {}
|
||||||
|
_disk_refresh_last_immediate: dict[int, float] = {}
|
||||||
|
|
||||||
|
|
||||||
def set_socketio(socketio):
|
def set_socketio(socketio):
|
||||||
@@ -232,17 +237,67 @@ def _remove_job_deletes_data(action_name: str, payload: dict, result: dict | Non
|
|||||||
return bool(ctx.get("remove_data") or (result or {}).get("remove_data"))
|
return bool(ctx.get("remove_data") or (result or {}).get("remove_data"))
|
||||||
|
|
||||||
|
|
||||||
def _emit_disk_refresh_requested(profile_id: int, action_name: str, payload: dict, result: dict | None = None) -> None:
|
def _clear_disk_refresh_cache(profile_id: int) -> None:
|
||||||
if not _remove_job_deletes_data(action_name, payload, result):
|
try:
|
||||||
return
|
# Note: Remove-with-data jobs invalidate disk cache before notifying browsers, otherwise /api/system/disk may return stale values.
|
||||||
|
rtorrent.clear_profile_runtime_caches(int(profile_id))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_profile_disk_refresh(profile_id: int, reason: str, hash_count: int = 0, delay_seconds: int = 0) -> None:
|
||||||
|
_clear_disk_refresh_cache(profile_id)
|
||||||
# Note: The browser performs the fresh /api/system/disk read so user-specific disk monitor preferences stay respected.
|
# Note: The browser performs the fresh /api/system/disk read so user-specific disk monitor preferences stay respected.
|
||||||
_emit("disk_refresh_requested", {
|
_emit("disk_refresh_requested", {
|
||||||
"profile_id": int(profile_id),
|
"profile_id": int(profile_id),
|
||||||
"action": str(action_name or ""),
|
"hash_count": int(hash_count or 0),
|
||||||
"hash_count": len((payload or {}).get("hashes") or []),
|
"reason": reason,
|
||||||
"reason": "remove_data_done",
|
"delay_seconds": int(delay_seconds or 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_delayed_disk_refresh(profile_id: int, delay_seconds: int) -> None:
|
||||||
|
key = (int(profile_id), int(delay_seconds))
|
||||||
|
try:
|
||||||
|
_emit_profile_disk_refresh(profile_id, "remove_data_settled", delay_seconds=delay_seconds)
|
||||||
|
finally:
|
||||||
|
with _disk_refresh_lock:
|
||||||
|
current = _disk_refresh_timers.get(key)
|
||||||
|
if current is threading.current_thread():
|
||||||
|
_disk_refresh_timers.pop(key, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _schedule_profile_disk_refresh(profile_id: int, hash_count: int = 0) -> None:
|
||||||
|
profile_id = int(profile_id)
|
||||||
|
now = time.monotonic()
|
||||||
|
emit_immediately = False
|
||||||
|
timers_to_start: list[threading.Timer] = []
|
||||||
|
with _disk_refresh_lock:
|
||||||
|
last_immediate = float(_disk_refresh_last_immediate.get(profile_id) or 0)
|
||||||
|
if now - last_immediate >= _disk_refresh_min_immediate_seconds:
|
||||||
|
_disk_refresh_last_immediate[profile_id] = now
|
||||||
|
emit_immediately = True
|
||||||
|
for delay_seconds in _disk_refresh_delays:
|
||||||
|
key = (profile_id, int(delay_seconds))
|
||||||
|
old_timer = _disk_refresh_timers.get(key)
|
||||||
|
if old_timer:
|
||||||
|
old_timer.cancel()
|
||||||
|
# Note: Repeated delete jobs share one delayed refresh per profile and delay, preventing timer storms during bulk cleanup.
|
||||||
|
timer = threading.Timer(float(delay_seconds), _run_delayed_disk_refresh, args=(profile_id, int(delay_seconds)))
|
||||||
|
timer.daemon = True
|
||||||
|
_disk_refresh_timers[key] = timer
|
||||||
|
timers_to_start.append(timer)
|
||||||
|
if emit_immediately:
|
||||||
|
_emit_profile_disk_refresh(profile_id, "remove_data_done", hash_count=hash_count, delay_seconds=0)
|
||||||
|
for timer in timers_to_start:
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_disk_refresh_requested(profile_id: int, action_name: str, payload: dict, result: dict | None = None) -> None:
|
||||||
|
if not _remove_job_deletes_data(action_name, payload, result):
|
||||||
|
return
|
||||||
|
_schedule_profile_disk_refresh(int(profile_id), len((payload or {}).get("hashes") or []))
|
||||||
|
|
||||||
def _execute(profile: dict, action_name: str, payload: dict, user_id: int | None = None):
|
def _execute(profile: dict, action_name: str, payload: dict, user_id: int | None = None):
|
||||||
if action_name == "smart_queue_check":
|
if action_name == "smart_queue_check":
|
||||||
from . import smart_queue
|
from . import smart_queue
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const systemStatsSocketSource = " socket.on('system_stats',s=>{\n if(!isActiveProfilePayload(s)) return;\n const usageAvailable=s.usage_available!==false && s.cpu!==undefined && s.ram!==undefined;\n $('statCpuBox')?.classList.toggle('d-none',!usageAvailable);\n $('statRamBox')?.classList.toggle('d-none',!usageAvailable);\n $('systemChart')?.classList.toggle('d-none',!usageAvailable);\n if(usageAvailable){\n $('statCpu').textContent=s.cpu??'-';\n $('statRam').textContent=s.ram??'-';\n drawSystemUsage(s.cpu,s.ram);\n }\n $('statVersion').textContent=s.version||'-';\n applyLiveSpeedStats(s);\n lastLimits={down:Number(s.down_limit||0),up:Number(s.up_limit||0)};\n $('statDlLimit').textContent=s.down_limit_h||'∞';\n $('statUlLimit').textContent=s.up_limit_h||'∞';\n $('statTotalDl').textContent=compactTransferText(s.total_down_h);\n $('statTotalUl').textContent=compactTransferText(s.total_up_h);\n updateSpeedPeaks(s.speed_peaks||{});\n drawTraffic(s.down_rate,s.up_rate);\n if(diskMonitorMode==='default'){\n drawDiskUsage(s.disk);\n }else{\n refreshUserDiskUsage(false);\n }\n updateRtorrentFooterStats(s, false);\n saveFooterStatusCache(s);\n if(s.poller) fillPoller(null,s.poller);\n applyFooterPreferences();\n });\n\n socket.on('disk_refresh_requested',msg=>{\n if(!isActiveProfilePayload(msg)) return;\n // Note: Remove-with-data jobs can free space between normal poller ticks, so refresh the footer immediately and once more after filesystem metadata settles.\n refreshUserDiskUsage(true);\n setTimeout(()=>refreshUserDiskUsage(true), 1500);\n });\n";
|
export const systemStatsSocketSource = " socket.on('system_stats',s=>{\n if(!isActiveProfilePayload(s)) return;\n const usageAvailable=s.usage_available!==false && s.cpu!==undefined && s.ram!==undefined;\n $('statCpuBox')?.classList.toggle('d-none',!usageAvailable);\n $('statRamBox')?.classList.toggle('d-none',!usageAvailable);\n $('systemChart')?.classList.toggle('d-none',!usageAvailable);\n if(usageAvailable){\n $('statCpu').textContent=s.cpu??'-';\n $('statRam').textContent=s.ram??'-';\n drawSystemUsage(s.cpu,s.ram);\n }\n $('statVersion').textContent=s.version||'-';\n applyLiveSpeedStats(s);\n lastLimits={down:Number(s.down_limit||0),up:Number(s.up_limit||0)};\n $('statDlLimit').textContent=s.down_limit_h||'∞';\n $('statUlLimit').textContent=s.up_limit_h||'∞';\n $('statTotalDl').textContent=compactTransferText(s.total_down_h);\n $('statTotalUl').textContent=compactTransferText(s.total_up_h);\n updateSpeedPeaks(s.speed_peaks||{});\n drawTraffic(s.down_rate,s.up_rate);\n if(diskMonitorMode==='default'){\n drawDiskUsage(s.disk);\n }else{\n refreshUserDiskUsage(false);\n }\n updateRtorrentFooterStats(s, false);\n saveFooterStatusCache(s);\n if(s.poller) fillPoller(null,s.poller);\n applyFooterPreferences();\n });\n\n socket.on('disk_refresh_requested',msg=>{\n if(!isActiveProfilePayload(msg)) return;\n // Note: Backend coalesces delayed remove-data refreshes, so the browser only reacts to received events.\n refreshUserDiskUsage(true);\n });\n";
|
||||||
|
|||||||
Reference in New Issue
Block a user