Replace asyncio.to_thread with ThreadPoolExecutor.run_in_executor

- Add ThreadPoolExecutor with 3 max workers to SeriesApp
- Replace all asyncio.to_thread calls with loop.run_in_executor
- Add shutdown() method to properly cleanup executor
- Integrate SeriesApp.shutdown() into FastAPI shutdown sequence
- Ensures proper resource cleanup on Ctrl+C (SIGINT/SIGTERM)
This commit is contained in:
2026-01-03 21:04:52 +01:00
parent b1726968e5
commit ab7d78261e
6 changed files with 102 additions and 329 deletions

View File

@@ -12,6 +12,7 @@ Note:
import asyncio
import logging
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Dict, List, Optional
from events import Events
@@ -148,6 +149,9 @@ class SeriesApp:
self.directory_to_search = directory_to_search
# Initialize thread pool executor
self.executor = ThreadPoolExecutor(max_workers=3)
# Initialize events
self._events = Events()
self._events.download_status = None
@@ -229,7 +233,9 @@ class SeriesApp:
async def _init_list(self) -> None:
"""Initialize the series list with missing episodes (async)."""
self.series_list = await asyncio.to_thread(
loop = asyncio.get_running_loop()
self.series_list = await loop.run_in_executor(
self.executor,
self.list.GetMissingEpisode
)
logger.debug(
@@ -251,7 +257,12 @@ class SeriesApp:
RuntimeError: If search fails
"""
logger.info("Searching for: %s", words)
results = await asyncio.to_thread(self.loader.search, words)
loop = asyncio.get_running_loop()
results = await loop.run_in_executor(
self.executor,
self.loader.search,
words
)
logger.info("Found %d results", len(results))
return results
@@ -348,7 +359,9 @@ class SeriesApp:
try:
# Perform download in thread to avoid blocking event loop
download_success = await asyncio.to_thread(
loop = asyncio.get_running_loop()
download_success = await loop.run_in_executor(
self.executor,
self.loader.download,
self.directory_to_search,
serie_folder,
@@ -481,7 +494,9 @@ class SeriesApp:
try:
# Get total items to scan
logger.info("Getting total items to scan...")
total_to_scan = await asyncio.to_thread(
loop = asyncio.get_running_loop()
total_to_scan = await loop.run_in_executor(
self.executor,
self.serie_scanner.get_total_to_scan
)
logger.info("Total folders to scan: %d", total_to_scan)
@@ -503,7 +518,10 @@ class SeriesApp:
)
# Reinitialize scanner
await asyncio.to_thread(self.serie_scanner.reinit)
await loop.run_in_executor(
self.executor,
self.serie_scanner.reinit
)
def scan_progress_handler(progress_data):
"""Handle scan progress events from scanner."""
@@ -528,7 +546,10 @@ class SeriesApp:
try:
# Perform scan (file-based, returns results in scanner.keyDict)
await asyncio.to_thread(self.serie_scanner.scan)
await loop.run_in_executor(
self.executor,
self.serie_scanner.scan
)
finally:
# Always unsubscribe after scan completes or fails
self.serie_scanner.unsubscribe_on_progress(
@@ -685,3 +706,14 @@ class SeriesApp:
)
return all_series
def shutdown(self) -> None:
"""
Shutdown the thread pool executor.
Should be called when the SeriesApp instance is no longer needed
to properly clean up resources.
"""
if hasattr(self, 'executor'):
self.executor.shutdown(wait=True)
logger.info("ThreadPoolExecutor shut down successfully")