Merge PR #1035: release per-download status_queue proxy on close to fix fd leak (#485)

Wraps Download.close() in try/finally and nulls self.status_queue so the
per-download manager.Queue() proxy is released once the completed Download is
retained in the done list. Previously every finished download permanently
pinned one Manager-process connection, accumulating file descriptors until
the instance hit 'too many open files' and self-terminated (#485, #980).

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
This commit is contained in:
Alex Shnitman
2026-07-24 11:44:47 +03:00
2 changed files with 39 additions and 2 deletions
+34
View File
@@ -898,6 +898,40 @@ def _make_download(dq_env, *, download_type="video", status="downloading", filen
) )
def test_download_close_releases_status_queue(dq_env):
download = _make_download(dq_env)
status_queue = MagicMock()
proc = MagicMock()
download.status_queue = status_queue
download.proc = proc
download.close()
proc.close.assert_called_once()
assert download.status_queue is None
def test_download_close_releases_status_queue_without_process(dq_env):
download = _make_download(dq_env)
download.status_queue = MagicMock()
download.close()
assert download.status_queue is None
def test_download_close_releases_status_queue_when_process_close_fails(dq_env):
download = _make_download(dq_env)
download.status_queue = MagicMock()
download.proc = MagicMock()
download.proc.close.side_effect = RuntimeError('close failed')
with pytest.raises(RuntimeError, match='close failed'):
download.close()
assert download.status_queue is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_post_download_cleanup_clears_filename_on_error(dq_env): async def test_post_download_cleanup_clears_filename_on_error(dq_env):
notifier = AsyncMock() notifier = AsyncMock()
+5 -2
View File
@@ -787,8 +787,11 @@ class Download:
def close(self): def close(self):
log.info(f"Closing download process for: {self.info.title}") log.info(f"Closing download process for: {self.info.title}")
if self.started(): try:
self.proc.close() if self.started():
self.proc.close()
finally:
self.status_queue = None
def running(self): def running(self):
try: try: