storage: percentage / free-space based retention (min_free_space) - #1429
Open
fluffyspace wants to merge 1 commit into
Open
storage: percentage / free-space based retention (min_free_space)#1429fluffyspace wants to merge 1 commit into
fluffyspace wants to merge 1 commit into
Conversation
Retention limits can currently only be expressed as absolute bytes per camera
per tier (max_size). On a shared disk that composes badly: the real ceiling is
per-camera cap x number of cameras, and a byte cap cannot express "always
leave headroom on the volume".
Add an optional `min_free_space: {percent: N | gb: N | mb: N}` key to each tier
(recorder continuous/events blocks and snapshot/timelapse tiers). When set, the
oldest files on that tier -- across ALL cameras on the filesystem -- are
evicted whenever live free space (shutil.disk_usage) drops below the floor. It
layers on top of max_age/max_size (evict when ANY rule fires) and is fully
backward compatible: unset => floor 0 => previous behaviour exactly. No DB
migration.
Free space is a filesystem-global property while the existing tier check is
per-camera, so this is a dedicated all-cameras eviction pass:
- const/config: `min_free_space` schema (percent in (0,100], gb/mb), attached
to TIER_SCHEMA_BASE so it applies to continuous/events/snapshots/timelapse.
- util.calculate_free_space_floor: resolves percent-of-total + absolute gb/mb
to a byte floor (max of them); filesystem total read via disk_usage.
- check_tier.get_files_to_delete_for_free_space: pure-numpy oldest-first
selection covering a deficit, with a write-protection window guarding freshly
written segments (active HLS).
- check_tier.load_free_space_candidates: loads a tier's files across all
cameras for one subcategory.
- check_tier.Worker.free_space_evict: piggybacked on each camera's check_tier
but serialized + throttled per filesystem so N concurrent camera checks cause
one eviction pass, not N. Reuses the existing delete_file path.
- DataItem carries min_free_bytes + tier_fs_path (defaults keep it inert).
- tier handlers resolve the floor once at init and plumb it through.
Deletion (never move) is used because only deletion returns space to a disk;
the feature is meant for the terminal tier of a filesystem. Selection is a
single size-summed batch (no per-row disk_usage re-poll) so a filesystem whose
accounting lags behind unlinks (e.g. btrfs) is not over-evicted.
Tests: free-space compute selection (oldest-first, boundaries, min-age
protection), calculate_free_space_floor (percent/absolute/max/stat-failure),
and schema accept/reject for min_free_space.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ Deploy Preview for viseron canceled.
|
Owner
|
Thank you, this seems like a great addition. I am preparing for a new release atm, will look into this when that is done |
roflcoopter
reviewed
Aug 13, 2026
| DESC_CHECK_INTERVAL_SECONDS = "Seconds between checks for files to move/delete." | ||
| DESC_MIN_SIZE = "Minimum size of files to keep in this tier." | ||
| DESC_MAX_SIZE = "Maximum size of files to keep in this tier." | ||
| DESC_MIN_FREE_SPACE = ( |
Owner
There was a problem hiding this comment.
This description is a little too chatty, try to shorten it if possible
| ) | ||
|
|
||
|
|
||
| def load_free_space_candidates( |
Owner
There was a problem hiding this comment.
Loading all files for a tier could potentially cause memory issues, maybe this one would suite better as purely an SQL query instead of loading and using numpy.
What do you think about that?
The reason why the normal eviction uses loading + numpy is because a similar SQL query became extremely complicated and slow, using numpy was the better choice there to get better control of the CPU usage using throttling etc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional
min_free_spaceretention rule to thestoragecomponent so a tier can be told to "keep at least X% (or X GB) of the filesystem free", evaluated live, instead of (or in addition to) the existing absolute per-cameramax_sizebyte cap.Motivation
Today a tier's size limit is only expressible as absolute bytes, enforced per camera per tier (
max_size: { gb, mb }). That has two sharp edges on real deployments:max_sizeas a total, but the true worst case isper-camera cap × number of cameras. On a shared disk it's easy to under-provision and fill the volume.What this does
When set, the oldest files on that tier across all cameras on the filesystem are evicted whenever live free space (
shutil.disk_usage) drops below the floor. It layers on top ofmax_age/max_size— a file is removed when any configured rule fires. Accepted on the recordercontinuous/eventsblocks and on snapshot/timelapse tiers.Fully backward compatible: unset ⇒ floor
0⇒ the previous size/age-only behaviour, exactly. No DB/schema migration.Implementation
Free space is a filesystem-global property while the existing tier check is per-camera, so this is a dedicated all-cameras eviction pass rather than a change to the per-camera path:
util.calculate_free_space_floor— resolves percent-of-total + absolute gb/mb to a byte floor (the largest wins); total read once viadisk_usage.check_tier.get_files_to_delete_for_free_space— pure-numpy oldest-first selection covering the deficit, with a write-protection window guarding freshly written segments (active HLS).check_tier.load_free_space_candidates— loads a tier's files across all cameras for one subcategory.check_tier.Worker.free_space_evict— piggybacked on each camera'scheck_tier, but serialized + throttled per filesystem so N concurrent camera checks cause one eviction pass, not N. Reuses the existingdelete_filepath.DataItemcarriesmin_free_bytes+tier_fs_path(defaults keep it inert); tier handlers resolve the floor once at init and plumb it through.Design notes
disk_usagere-poll), so a filesystem whose accounting lags behind unlinks isn't over-evicted.Tests
test_free_space.py— compute selection (oldest-first, exact boundaries, min-age protection, empty/no-deficit).test_util.py—calculate_free_space_floor(percent / absolute / max-of-several / stat-failure / unset).test_config.py— schema accept/reject formin_free_space(percent(0,100]) + defaults presence on continuous/events/snapshots.