Tighten up teardown logic

- Stop active DB update work before tearing down executor pools.
- Short-circuit scan completion callbacks during shutdown so in-flight timer ticks don’t queue follow-up work.
- Prevent the download monitor from draining deferred/completed tasks after shutdown starts.
- Make listening stats startup stop-aware so it exits cleanly if teardown begins during warmup.
- If a metadata update is already running, it can now observe should_stop and exit cleanly instead of continuing after SIGTERM.
This commit is contained in:
Antti Kettunen 2026-04-12 17:36:15 +03:00
parent aec3047216
commit 29d964e8b0
5 changed files with 42 additions and 11 deletions

View file

@ -90,15 +90,20 @@ class ListeningStatsWorker:
logger.info("Listening stats worker thread started")
# Build cache from existing data immediately (before first poll)
interruptible_sleep(self._stop_event, 5)
if interruptible_sleep(self._stop_event, 5):
return
try:
self._build_stats_cache()
logger.info("Initial stats cache built from existing data")
except Exception as e:
logger.debug(f"Initial cache build skipped: {e}")
if self.should_stop:
return
# Wait before first poll
interruptible_sleep(self._stop_event, 10)
if interruptible_sleep(self._stop_event, 10):
return
while not self.should_stop:
try:

View file

@ -256,6 +256,8 @@ class MediaScanManager:
if is_scanning:
# Still scanning - trigger database update and continue periodic updates
logger.info(f"{server_type.upper()} still scanning - triggering database update")
if self._shutting_down:
return
self._call_completion_callbacks()
# Schedule next periodic update
@ -268,6 +270,8 @@ class MediaScanManager:
else:
# Scanning stopped - final update and cleanup
logger.info(f"{server_type.upper()} scanning completed - doing final database update")
if self._shutting_down:
return
self._call_completion_callbacks()
self._stop_periodic_updates()

View file

@ -174,6 +174,8 @@ class PlexScanManager:
if is_scanning:
# Still scanning - trigger database update and continue periodic updates
logger.info("Plex still scanning - triggering database update")
if self._shutting_down:
return
self._call_completion_callbacks()
# Schedule next periodic update
@ -186,6 +188,8 @@ class PlexScanManager:
else:
# Scanning stopped - final update and cleanup
logger.info("Plex scanning completed - doing final database update")
if self._shutting_down:
return
self._call_completion_callbacks()
self._stop_periodic_updates()

View file

@ -290,12 +290,19 @@ class WebScanManager:
def _handle_scan_completion(self):
"""Handle scan completion and trigger callbacks"""
logger.info(f"Web {self._current_server_type.upper()} library scan completed")
# Call completion callbacks
callbacks_to_call = []
with self._lock:
if self._shutting_down:
return
server_type = self._current_server_type
callbacks_to_call = self._scan_completion_callbacks.copy()
downloads_during_scan = self._downloads_during_scan
if not server_type:
logger.debug("Skipping web scan completion: no active server type")
self._reset_scan_state()
return
logger.info(f"Web {server_type.upper()} library scan completed")
for callback in callbacks_to_call:
try:
@ -308,10 +315,9 @@ class WebScanManager:
self._reset_scan_state()
# Check if we need another scan due to downloads during this scan
with self._lock:
if self._downloads_during_scan:
logger.info("Web scan follow-up needed for downloads during scan")
self.request_scan("Follow-up scan for downloads during previous scan")
if downloads_during_scan:
logger.info("Web scan follow-up needed for downloads during scan")
self.request_scan("Follow-up scan for downloads during previous scan")
def _reset_scan_state(self):
"""Reset internal scan state"""

View file

@ -2638,6 +2638,8 @@ class WebUIDownloadMonitor:
completed_tasks.append((batch_id, task_id))
# ---- All work below runs WITHOUT tasks_lock held ----
if globals().get('IS_SHUTTING_DOWN', False) or not self.monitoring:
return
# Execute deferred operations from _should_retry_task (network calls, nested locks)
for op in deferred_ops:
@ -3460,6 +3462,11 @@ def _shutdown_runtime_components():
except Exception as e:
print(f"Error saving API call history: {e}")
# Stop the active DB update worker before tearing down the executor it runs on.
# This lets an in-flight update observe should_stop and exit cleanly.
_stop_component(db_update_worker, "db update worker")
_stop_component(metadata_update_runtime_worker, "metadata update worker")
# Stop long-lived worker components in parallel so shutdown waits for the
# slowest worker instead of serially burning the timeout for each one.
_stop_components_parallel([
@ -40789,6 +40796,7 @@ metadata_update_state = {
}
metadata_update_worker = None
metadata_update_runtime_worker = None
metadata_update_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="metadata_update")
# ===============================
@ -44903,7 +44911,7 @@ def _old_get_listenbrainz_playlist_tracks_DEPRECATED(playlist_mbid):
@app.route('/api/metadata/start', methods=['POST'])
def start_metadata_update():
"""Start the metadata update process - EXACT copy of dashboard.py logic"""
global metadata_update_worker, metadata_update_state
global metadata_update_worker, metadata_update_runtime_worker, metadata_update_state
try:
# Check if already running
@ -44971,6 +44979,7 @@ def start_metadata_update():
# Start the metadata update worker - EXACTLY like dashboard.py
def run_metadata_update():
global metadata_update_runtime_worker
try:
metadata_worker = WebMetadataUpdateWorker(
None, # Artists will be loaded in the worker thread - EXACTLY like dashboard.py
@ -44979,12 +44988,15 @@ def start_metadata_update():
active_server,
refresh_interval_days
)
metadata_update_runtime_worker = metadata_worker
metadata_worker.run()
except Exception as e:
print(f"Error in metadata update worker: {e}")
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = str(e)
add_activity_item("", "Metadata Error", str(e), "Now")
finally:
metadata_update_runtime_worker = None
metadata_update_worker = metadata_update_executor.submit(run_metadata_update)