mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-19 05:02:49 +03:00
An exception from any dispatcher handler escaped InterProcessCommunicator.read, which only caught ZMQError. The REP thread died owing a reply, so every InterProcessRequestor blocked forever. read now logs the error and replies with an empty response, and Dispatcher.publish and publish_local catch per communicator so one failing communicator can't take down the rest.
77 lines
2.5 KiB
Python
77 lines
2.5 KiB
Python
"""Tests that IPC handler errors cannot stall inter-process communication."""
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import zmq
|
|
|
|
from frigate.comms.dispatcher import Dispatcher
|
|
from frigate.comms.inter_process import InterProcessCommunicator
|
|
|
|
|
|
class TestInterProcessHandlerErrors(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.address = f"ipc://{os.path.join(self.tmp.name, 'comms')}"
|
|
|
|
with patch("frigate.comms.inter_process.SOCKET_REP_REQ", self.address):
|
|
self.communicator = InterProcessCommunicator()
|
|
|
|
self.context = zmq.Context()
|
|
self.requestor = self.context.socket(zmq.REQ)
|
|
self.requestor.setsockopt(zmq.RCVTIMEO, 5000)
|
|
self.requestor.connect(self.address)
|
|
|
|
def tearDown(self):
|
|
self.requestor.close(linger=0)
|
|
self.context.destroy(linger=0)
|
|
self.communicator.stop()
|
|
self.tmp.cleanup()
|
|
|
|
def test_handler_exception_still_replies_and_keeps_reading(self):
|
|
def dispatcher(topic, value):
|
|
if topic == "boom":
|
|
raise ValueError("handler failed")
|
|
return value
|
|
|
|
self.communicator.subscribe(dispatcher)
|
|
self.communicator.start()
|
|
|
|
with self.assertLogs("frigate.comms.inter_process", level="ERROR"):
|
|
self.requestor.send_json(("boom", None))
|
|
self.assertEqual(self.requestor.recv_json(), [])
|
|
|
|
self.requestor.send_json(("echo", "ok"))
|
|
self.assertEqual(self.requestor.recv_json(), "ok")
|
|
self.assertTrue(self.communicator.reader_thread.is_alive())
|
|
|
|
|
|
class TestDispatcherPublishErrors(unittest.TestCase):
|
|
def test_failing_communicator_does_not_block_the_others(self):
|
|
failing = MagicMock()
|
|
failing.publish.side_effect = ValueError("publish failed")
|
|
healthy = MagicMock()
|
|
|
|
with (
|
|
patch("frigate.comms.dispatcher.CameraActivityManager"),
|
|
patch("frigate.comms.dispatcher.AudioActivityManager"),
|
|
):
|
|
dispatcher = Dispatcher(
|
|
MagicMock(), MagicMock(), MagicMock(), {}, [failing, healthy]
|
|
)
|
|
|
|
healthy.publish.reset_mock()
|
|
|
|
with self.assertLogs("frigate.comms.dispatcher", level="ERROR"):
|
|
dispatcher.publish("reviews", "{}")
|
|
dispatcher.publish_local("notices", "[]")
|
|
|
|
healthy.publish.assert_any_call("reviews", "{}", False)
|
|
healthy.publish.assert_any_call("notices", "[]", False)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|