Skip time-based recording expiry in continuous_rollover mode

In rollover mode, RecordingCleanup no longer deletes recordings based
on continuous.days / motion.days. Instead, StorageMaintainer handles
overflow by deleting oldest recordings when disk fills up. Deleted
cameras have their recordings removed immediately rather than waiting
for time-based expiry. Review segment expiry still runs normally.
This commit is contained in:
jon 2026-03-01 12:19:33 -06:00
parent aed3793ce1
commit c9b208a255

View File

@ -10,7 +10,7 @@ from pathlib import Path
from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqlite_ext import SqliteExtDatabase
from frigate.config import CameraConfig, FrigateConfig, RetainModeEnum from frigate.config import CameraConfig, FrigateConfig, RetainModeEnum, RetainPolicyEnum
from frigate.const import CACHE_DIR, CLIPS_DIR, MAX_WAL_SIZE, RECORD_DIR from frigate.const import CACHE_DIR, CLIPS_DIR, MAX_WAL_SIZE, RECORD_DIR
from frigate.models import Previews, Recordings, ReviewSegment, UserReviewStatus from frigate.models import Previews, Recordings, ReviewSegment, UserReviewStatus
from frigate.util.builtin import clear_and_unlink from frigate.util.builtin import clear_and_unlink
@ -281,9 +281,27 @@ class RecordingCleanup(threading.Thread):
def expire_recordings(self) -> set[Path]: def expire_recordings(self) -> set[Path]:
"""Delete recordings based on retention config.""" """Delete recordings based on retention config."""
logger.debug("Start expire recordings.") logger.debug("Start expire recordings.")
logger.debug("Start deleted cameras.")
is_rollover = (
self.config.record.retain_policy == RetainPolicyEnum.continuous_rollover
)
# Handle deleted cameras # Handle deleted cameras
logger.debug("Start deleted cameras.")
if is_rollover:
# In rollover mode, delete recordings from removed cameras immediately
no_camera_recordings: Recordings = (
Recordings.select(
Recordings.id,
Recordings.path,
)
.where(
Recordings.camera.not_in(list(self.config.cameras.keys())),
)
.namedtuples()
.iterator()
)
else:
expire_days = max( expire_days = max(
self.config.record.continuous.days, self.config.record.motion.days self.config.record.continuous.days, self.config.record.motion.days
) )
@ -313,7 +331,6 @@ class RecordingCleanup(threading.Thread):
maybe_empty_dirs.add(recording_path.parent) maybe_empty_dirs.add(recording_path.parent)
logger.debug(f"Expiring {len(deleted_recordings)} recordings") logger.debug(f"Expiring {len(deleted_recordings)} recordings")
# delete up to 100,000 at a time
max_deletes = 100000 max_deletes = 100000
deleted_recordings_list = list(deleted_recordings) deleted_recordings_list = list(deleted_recordings)
for i in range(0, len(deleted_recordings_list), max_deletes): for i in range(0, len(deleted_recordings_list), max_deletes):
@ -327,7 +344,11 @@ class RecordingCleanup(threading.Thread):
logger.debug(f"Start camera: {camera}.") logger.debug(f"Start camera: {camera}.")
now = datetime.datetime.now() now = datetime.datetime.now()
# Always expire review segments (alerts/detections) regardless of policy
maybe_empty_dirs |= self.expire_review_segments(config, now) maybe_empty_dirs |= self.expire_review_segments(config, now)
# Skip continuous/motion time-based expiry in rollover mode
if not is_rollover:
continuous_expire_date = ( continuous_expire_date = (
now - datetime.timedelta(days=config.record.continuous.days) now - datetime.timedelta(days=config.record.continuous.days)
).timestamp() ).timestamp()
@ -336,11 +357,10 @@ class RecordingCleanup(threading.Thread):
- datetime.timedelta( - datetime.timedelta(
days=max( days=max(
config.record.motion.days, config.record.continuous.days config.record.motion.days, config.record.continuous.days
) # can't keep motion for less than continuous )
) )
).timestamp() ).timestamp()
# Get all the reviews to check against
reviews: ReviewSegment = ( reviews: ReviewSegment = (
ReviewSegment.select( ReviewSegment.select(
ReviewSegment.start_time, ReviewSegment.start_time,
@ -349,8 +369,6 @@ class RecordingCleanup(threading.Thread):
) )
.where( .where(
ReviewSegment.camera == camera, ReviewSegment.camera == camera,
# need to ensure segments for all reviews starting
# before the expire date are included
ReviewSegment.start_time < motion_expire_date, ReviewSegment.start_time < motion_expire_date,
) )
.order_by(ReviewSegment.start_time) .order_by(ReviewSegment.start_time)
@ -360,6 +378,7 @@ class RecordingCleanup(threading.Thread):
maybe_empty_dirs |= self.expire_existing_camera_recordings( maybe_empty_dirs |= self.expire_existing_camera_recordings(
continuous_expire_date, motion_expire_date, config, reviews continuous_expire_date, motion_expire_date, config, reviews
) )
logger.debug(f"End camera: {camera}.") logger.debug(f"End camera: {camera}.")
logger.debug("End all cameras.") logger.debug("End all cameras.")