mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-25 20:18:59 +03:00
close onvif sessions on shutdown
`OnvifController.close()` only stopped its event loop, so the aiohttp sessions each `ONVIFCamera` holds and the `_poll_config_updates` task were left to be garbage collected during interpreter shutdown, when their warnings can no longer be logged. Every restart ended with a run of `Unclosed client session` and `Task was destroyed but it is pending!` logging errors, which only became visible once restart started exiting the process itself under non-root. `close()` now closes each camera's client and cancels the tasks on the loop before stopping it.
This commit is contained in:
@@ -1120,6 +1120,18 @@ class OnvifController:
|
||||
f"Camera {camera_name} is still in ONVIF 'MOVING' status."
|
||||
)
|
||||
|
||||
async def _shutdown(self) -> None:
|
||||
"""Close the camera sessions and cancel the tasks running on the loop."""
|
||||
for cam_name in list(self.cams):
|
||||
await self._close_camera(cam_name)
|
||||
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Gracefully shut down the ONVIF controller."""
|
||||
if not hasattr(self, "loop") or self.loop.is_closed():
|
||||
@@ -1127,6 +1139,16 @@ class OnvifController:
|
||||
return
|
||||
|
||||
logger.info("Exiting ONVIF controller...")
|
||||
|
||||
# anything left open here is garbage collected during interpreter
|
||||
# shutdown, where its warnings can no longer be logged cleanly
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(self._shutdown(), self.loop).result(
|
||||
timeout=5
|
||||
)
|
||||
except TimeoutError:
|
||||
logger.debug("Timed out closing ONVIF sessions")
|
||||
|
||||
self.config_subscriber.stop()
|
||||
|
||||
def stop_and_cleanup():
|
||||
|
||||
@@ -15,6 +15,8 @@ Also covers the inverse direction: the ptz movement timestamps must not be writt
|
||||
for a camera that has autotracking off, because nothing clears them back out.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
@@ -234,5 +236,40 @@ class TestManualRelativeMoveMetrics(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestOnvifClose(unittest.TestCase):
|
||||
"""close() must release everything on the loop, since whatever it leaves is
|
||||
garbage collected during interpreter shutdown, where the resulting warnings
|
||||
fail to log and fill the shutdown output with logging errors."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.controller = _make_controller(autotracking_enabled=False)
|
||||
self.onvif = self.controller.cams[CAMERA]["onvif"]
|
||||
self.onvif.close = AsyncMock()
|
||||
self.controller.config_subscriber = MagicMock()
|
||||
self.controller.loop = asyncio.new_event_loop()
|
||||
self.controller.loop_thread = threading.Thread(
|
||||
target=self.controller._run_event_loop, daemon=True
|
||||
)
|
||||
self.controller.loop_thread.start()
|
||||
self.addCleanup(self.controller.loop.close)
|
||||
|
||||
def test_close_closes_camera_sessions(self) -> None:
|
||||
self.controller.close()
|
||||
|
||||
self.onvif.close.assert_awaited_once()
|
||||
|
||||
def test_close_cancels_tasks_left_on_the_loop(self) -> None:
|
||||
async def forever() -> None:
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
poll = asyncio.run_coroutine_threadsafe(forever(), self.controller.loop)
|
||||
|
||||
self.controller.close()
|
||||
|
||||
self.assertTrue(poll.cancelled())
|
||||
self.assertFalse(self.controller.loop_thread.is_alive())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user