Compare commits

..

2 Commits

Author SHA1 Message Date
Josh Hawkins
7a222488a0 add maintainer test 2026-04-17 16:51:30 -05:00
Josh Hawkins
da8579040d drop cache segments past retain cutoff regardless of retention mode 2026-04-17 16:51:19 -05:00
2 changed files with 47 additions and 4 deletions

View File

@ -464,10 +464,12 @@ class RecordingMaintainer(threading.Thread):
self.drop_segment(cache_path) self.drop_segment(cache_path)
return None return None
# if it doesn't overlap with an review item, go ahead and drop the segment # if it doesn't overlap with a review item, drop the segment once it
# if it ends more than the configured pre_capture for the camera # ends more than event_pre_capture before the most recently processed
# BUT only if continuous/motion is NOT enabled (otherwise wait for processing) # frame. at this point we've already decided not to keep it for
elif highest is None: # continuous/motion retention (either disabled or segment_stats said
# discard), so waiting longer just fills the cache.
else:
camera_info = self.object_recordings_info[camera] camera_info = self.object_recordings_info[camera]
most_recently_processed_frame_time = ( most_recently_processed_frame_time = (
camera_info[-1][0] if len(camera_info) > 0 else 0 camera_info[-1][0] if len(camera_info) > 0 else 0

View File

@ -1,3 +1,4 @@
import datetime
import sys import sys
import unittest import unittest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -74,6 +75,46 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase):
f"Expected a single warning for unexpected files, got {len(matching)}", f"Expected a single warning for unexpected files, got {len(matching)}",
) )
async def test_drops_quiet_segment_when_only_motion_retention(self):
# Regression: when motion retention is enabled but a segment has no
# motion and no review overlaps it, the segment must still be dropped.
# Otherwise it sits in cache forever, accumulates, and triggers the
# "Unable to keep up with recording segments in cache" warning every
# ~10s as the overflow trim in move_files discards the oldest one.
config = MagicMock(spec=FrigateConfig)
camera_config = MagicMock()
camera_config.record.enabled = True
camera_config.record.continuous.days = 0
camera_config.record.motion.days = 1
camera_config.record.event_pre_capture = 5
config.cameras = {"test_cam": camera_config}
stop_event = MagicMock()
maintainer = RecordingMaintainer(config, stop_event)
now = datetime.datetime.now(datetime.timezone.utc)
start_time = now - datetime.timedelta(seconds=20)
end_time = now - datetime.timedelta(seconds=10)
cache_path = "/tmp/cache/test_cam@20260417150000+0000.mp4"
maintainer.end_time_cache = {cache_path: (end_time, 10.0)}
# Single processed frame well past end_time with no motion/objects.
maintainer.object_recordings_info["test_cam"] = [(now.timestamp(), [], [], [])]
maintainer.audio_recordings_info["test_cam"] = []
maintainer.drop_segment = MagicMock()
maintainer.recordings_publisher = MagicMock()
result = await maintainer.validate_and_move_segment(
"test_cam",
reviews=[],
recording={"start_time": start_time, "cache_path": cache_path},
)
self.assertIsNone(result)
maintainer.drop_segment.assert_called_once_with(cache_path)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()