Compare commits

..
Author SHA1 Message Date
Josh Hawkins 17331d0c71 disable open_file_cache for review thumbnails
Review thumbnails are rewritten in place while a review is active, and nginx's open_file_cache reused the stale file size for up to 2 minutes. A larger rewrite was served truncated with a 200, and a smaller one aborted the response mid-transfer, which broke notification images. The review thumbnail location now skips the cache.
2026-10-19 13:00:28 -05:00
4 changed files with 9 additions and 69 deletions
@@ -176,6 +176,12 @@ http {
add_header Cache-Control "public";
autoindex on;
root /media/frigate;
# review thumbnails are rewritten in place while a review is active,
# so a cached file size truncates the response
location /clips/review/ {
open_file_cache off;
}
}
location /cache/ {
-8
View File
@@ -224,14 +224,6 @@ class RecordConfig(FrigateBaseModel):
self.detections.pre_capture,
)
@property
def event_post_capture(self) -> int:
"""Largest post-capture window used to fetch candidate reviews."""
return max(
self.alerts.post_capture,
self.detections.post_capture,
)
def get_review_pre_capture(self, severity: SeverityEnum) -> int:
if severity == SeverityEnum.alert:
return self.alerts.pre_capture
+3 -11
View File
@@ -471,14 +471,9 @@ class RecordingMaintainer(threading.Thread):
for key, recordings in grouped_recordings.items():
camera, stream_type = key
# get all reviews whose post capture reaches the oldest cache file
# or with end_time None; shared across stream types
# get all reviews with the end time after the start of the oldest
# cache file or with end_time None; shared across stream types
if camera not in reviews_by_camera:
post_capture = (
self.config.cameras[camera].record.event_post_capture
if camera in self.config.cameras
else 0
)
reviews_by_camera[camera] = (
ReviewSegment.select(
ReviewSegment.start_time,
@@ -489,10 +484,7 @@ class RecordingMaintainer(threading.Thread):
.where(
ReviewSegment.camera == camera,
(ReviewSegment.end_time == None)
| (
ReviewSegment.end_time
>= min_start_per_camera[camera] - post_capture
),
| (ReviewSegment.end_time >= min_start_per_camera[camera]),
)
.order_by(ReviewSegment.start_time)
)
-50
View File
@@ -6,8 +6,6 @@ import unittest
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
from peewee import SqliteDatabase
# Mock complex imports before importing maintainer, saving originals so we can
# restore them after import and avoid polluting sys.modules for other tests.
_MOCKED_MODULES = [
@@ -22,7 +20,6 @@ for name in _MOCKED_MODULES:
# Now import the class under test
from frigate.config import FrigateConfig, RetainModeEnum # noqa: E402
from frigate.models import ReviewSegment # noqa: E402
from frigate.record.maintainer import RecordingMaintainer, SegmentInfo # noqa: E402
# Restore original modules (or remove mock if there was no original)
@@ -84,53 +81,6 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase):
f"Expected a single warning for unexpected files, got {len(matching)}",
)
async def test_ended_review_stays_eligible_through_post_capture(self):
config = MagicMock(spec=FrigateConfig)
camera_config = MagicMock()
camera_config.record.sub.enabled = False
camera_config.record.event_post_capture = 60
config.cameras = {"test_cam": camera_config}
maintainer = RecordingMaintainer(config, MagicMock())
maintainer.requestor = MagicMock()
maintainer.recordings_publisher = MagicMock()
maintainer.validate_and_move_segment = AsyncMock(return_value=None)
db = SqliteDatabase(":memory:")
with db.bind_ctx([ReviewSegment]):
db.create_tables([ReviewSegment])
# oldest cached segment starts 40s after the review ended
segment_start = datetime.datetime(
2026, 10, 19, 13, 1, 40, tzinfo=datetime.UTC
)
ReviewSegment.create(
id="review",
camera="test_cam",
start_time=segment_start.timestamp() - 50,
end_time=segment_start.timestamp() - 40,
severity="alert",
thumb_path="",
data={},
)
with tempfile.TemporaryDirectory() as cache_dir:
name = segment_start.strftime("%Y%m%d%H%M%S%z")
Path(cache_dir, f"test_cam@{name}.mp4").touch()
with (
patch("frigate.record.maintainer.CACHE_DIR", cache_dir),
patch(
"frigate.record.maintainer.psutil.process_iter", return_value=[]
),
):
await maintainer.move_files()
reviews = maintainer.validate_and_move_segment.call_args.args[1]
self.assertEqual(
[segment_start.timestamp() - 40], [r.end_time for r in reviews]
)
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.