fix async handling in SerieScanner and add image_downloader cleanup
- SerieScanner.scan() now detects running event loop and uses create_task() when already in async context, avoids RuntimeError - NFOService.close() now also closes image_downloader to prevent resource leaks - Add integration tests for TMDBClient lifecycle management Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -44,8 +44,13 @@ class SerieScanner:
|
|||||||
in keyDict and can be retrieved after scanning.
|
in keyDict and can be retrieved after scanning.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
# Synchronous context (CLI):
|
||||||
scanner = SerieScanner("/path/to/anime", loader)
|
scanner = SerieScanner("/path/to/anime", loader)
|
||||||
scanner.scan()
|
scanner.scan() # asyncio.run() used internally when no event loop
|
||||||
|
|
||||||
|
# Asynchronous context (server/scheduler):
|
||||||
|
# scan() detects running event loop and uses create_task()
|
||||||
|
# internally, so no special handling needed by caller.
|
||||||
# Results are in scanner.keyDict
|
# Results are in scanner.keyDict
|
||||||
|
|
||||||
# With DB lookup fallback:
|
# With DB lookup fallback:
|
||||||
@@ -432,7 +437,14 @@ class SerieScanner:
|
|||||||
|
|
||||||
# Persist to database (async)
|
# Persist to database (async)
|
||||||
try:
|
try:
|
||||||
asyncio.run(self._persist_serie_to_db(serie))
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
# No running loop — safe to use asyncio.run()
|
||||||
|
asyncio.run(self._persist_serie_to_db(serie))
|
||||||
|
else:
|
||||||
|
# Already in async context — schedule as task
|
||||||
|
asyncio.create_task(self._persist_serie_to_db(serie))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"DB persistence failed for '%s', "
|
"DB persistence failed for '%s', "
|
||||||
|
|||||||
@@ -784,6 +784,7 @@ class NFOService:
|
|||||||
async def close(self):
|
async def close(self):
|
||||||
"""Clean up resources."""
|
"""Clean up resources."""
|
||||||
await self.tmdb_client.close()
|
await self.tmdb_client.close()
|
||||||
|
await self.image_downloader.close()
|
||||||
|
|
||||||
async def create_minimal_nfo(
|
async def create_minimal_nfo(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -444,6 +444,77 @@ class TestTMDBClientSessionLeak:
|
|||||||
"Unexpected warning about unclosed session"
|
"Unexpected warning about unclosed session"
|
||||||
|
|
||||||
|
|
||||||
|
class TestTMDBClientLifecycleIntegration:
|
||||||
|
"""Integration tests for TMDBClient lifecycle management."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_context_manager_no_resource_warning(self, caplog):
|
||||||
|
"""Test async with TMDBClient produces no ResourceWarning."""
|
||||||
|
import logging
|
||||||
|
import warnings
|
||||||
|
caplog.set_level(logging.WARNING)
|
||||||
|
|
||||||
|
# Use context manager properly - should not leak
|
||||||
|
async with TMDBClient(api_key="test_key") as client:
|
||||||
|
await client._ensure_session()
|
||||||
|
assert client.session is not None
|
||||||
|
|
||||||
|
# Session should be closed after context exit
|
||||||
|
assert client.session is None or client.session.closed
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_exception_safety_during_api_call(self, caplog):
|
||||||
|
"""Test session is closed even when exception raised during API call."""
|
||||||
|
import logging
|
||||||
|
caplog.set_level(logging.WARNING)
|
||||||
|
|
||||||
|
close_called = False
|
||||||
|
|
||||||
|
class TrackingSession:
|
||||||
|
def __init__(self):
|
||||||
|
self.closed = False
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
nonlocal close_called
|
||||||
|
close_called = True
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
async def get(self, url, **kwargs):
|
||||||
|
raise TMDBAPIError("Simulated API failure")
|
||||||
|
|
||||||
|
client = TMDBClient(api_key="test_key")
|
||||||
|
client.session = TrackingSession()
|
||||||
|
|
||||||
|
# Exception during context should still close session
|
||||||
|
with pytest.raises(TMDBAPIError):
|
||||||
|
async with client:
|
||||||
|
raise TMDBAPIError("Simulated API failure")
|
||||||
|
|
||||||
|
assert close_called, "Session was not closed after API exception"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reuse_session_across_multiple_requests(self, caplog):
|
||||||
|
"""Test session is reused across multiple requests without leaks."""
|
||||||
|
import logging
|
||||||
|
caplog.set_level(logging.WARNING)
|
||||||
|
|
||||||
|
client = TMDBClient(api_key="test_key")
|
||||||
|
|
||||||
|
async with client as c:
|
||||||
|
# First request
|
||||||
|
await c._ensure_session()
|
||||||
|
session1 = c.session
|
||||||
|
|
||||||
|
# Second request should reuse same session
|
||||||
|
await c._ensure_session()
|
||||||
|
session2 = c.session
|
||||||
|
|
||||||
|
assert session1 is session2, "Session should be reused"
|
||||||
|
|
||||||
|
# After context exit, session should be closed
|
||||||
|
assert client.session is None or client.session.closed
|
||||||
|
|
||||||
|
|
||||||
class TestTMDBClientConnectorClosed:
|
class TestTMDBClientConnectorClosed:
|
||||||
"""Test handling of 'Connector is closed' errors."""
|
"""Test handling of 'Connector is closed' errors."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user