Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a07afef85 |
@@ -67,6 +67,19 @@ The fixed source blacklist is:
|
||||
|
||||
Client error reports never modify this list.
|
||||
|
||||
## Cache resizing
|
||||
|
||||
`CACHE_MAX_BYTES` is the worker's configurable LRU limit. After changing it and
|
||||
restarting the container:
|
||||
|
||||
- A smaller value evicts the oldest unlocked entries until usage fits.
|
||||
- A larger value immediately makes the additional capacity available for
|
||||
background population, up to the size of the mounted cache filesystem.
|
||||
|
||||
The host filesystem is deliberately separate from application logic. Enlarge or
|
||||
shrink its preallocated image during infrastructure maintenance, then set the
|
||||
matching `CACHE_MAX_BYTES` value in Coolify.
|
||||
|
||||
## Playback locking
|
||||
|
||||
- An active movie locks that movie.
|
||||
|
||||
+31
-6
@@ -31,6 +31,7 @@ JELLYFIN_LIBRARY_ROOT = os.environ.get("JELLYFIN_LIBRARY_ROOT", "/data/library")
|
||||
JELLYFIN_ORIGINAL_ROOT = os.environ.get("JELLYFIN_ORIGINAL_ROOT", "/data/original").rstrip("/")
|
||||
JELLYFIN_CACHE_ROOT = os.environ.get("JELLYFIN_CACHE_ROOT", "/data/cache").rstrip("/")
|
||||
JELLYFIN_API_KEY = os.environ.get("JELLYFIN_API_KEY", "")
|
||||
CACHE_MAX_BYTES = env_int("CACHE_MAX_BYTES", 1_000_000_000_000)
|
||||
SERIES_COOLDOWN_SECONDS = env_int("SERIES_COOLDOWN_SECONDS", 3600)
|
||||
POLL_SECONDS = env_int("POLL_SECONDS", 5)
|
||||
RESCAN_SECONDS = env_int("RESCAN_SECONDS", 300)
|
||||
@@ -501,9 +502,33 @@ class Manager:
|
||||
manifest.unlink(missing_ok=True)
|
||||
return True
|
||||
|
||||
def cache_usage(self):
|
||||
return sum(
|
||||
path.stat().st_size
|
||||
for path in (CACHE_ROOT / "files").iterdir()
|
||||
if path.is_file()
|
||||
)
|
||||
|
||||
def cache_available(self):
|
||||
stat = os.statvfs(CACHE_ROOT)
|
||||
filesystem_free = stat.f_bavail * stat.f_frsize
|
||||
configured_free = max(0, CACHE_MAX_BYTES - self.cache_usage())
|
||||
return min(filesystem_free, configured_free)
|
||||
|
||||
def enforce_cache_limit(self):
|
||||
usage = self.cache_usage()
|
||||
for _, relative, output, manifest in self.cache_entries():
|
||||
if usage <= CACHE_MAX_BYTES:
|
||||
break
|
||||
if self.is_locked(relative):
|
||||
continue
|
||||
size = output.stat().st_size + manifest.stat().st_size
|
||||
log(f"Evicting {relative} after cache resize")
|
||||
if self.remove_cache(relative):
|
||||
usage -= size
|
||||
|
||||
def ensure_space(self, required, may_evict):
|
||||
free = os.statvfs(CACHE_ROOT).f_bavail * os.statvfs(CACHE_ROOT).f_frsize
|
||||
if free >= required:
|
||||
if self.cache_available() >= required:
|
||||
return True
|
||||
if not may_evict:
|
||||
return False
|
||||
@@ -512,8 +537,7 @@ class Manager:
|
||||
continue
|
||||
log(f"Evicting {relative}")
|
||||
self.remove_cache(relative)
|
||||
free = os.statvfs(CACHE_ROOT).f_bavail * os.statvfs(CACHE_ROOT).f_frsize
|
||||
if free >= required:
|
||||
if self.cache_available() >= required:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -625,6 +649,7 @@ class Manager:
|
||||
if not JELLYFIN_API_KEY:
|
||||
raise RuntimeError("JELLYFIN_API_KEY is required")
|
||||
self.read_sessions()
|
||||
self.enforce_cache_limit()
|
||||
self.scan_inventory()
|
||||
self.sync_view()
|
||||
self.save()
|
||||
@@ -632,6 +657,7 @@ class Manager:
|
||||
if not self.read_sessions():
|
||||
time.sleep(POLL_SECONDS)
|
||||
continue
|
||||
self.enforce_cache_limit()
|
||||
now = time.time()
|
||||
if now - self.last_scan >= RESCAN_SECONDS:
|
||||
changed = self.scan_inventory()
|
||||
@@ -651,8 +677,7 @@ class Manager:
|
||||
for rank, _, relative in candidates:
|
||||
source = SOURCE_ROOT / relative
|
||||
estimate = min(max(int(source.stat().st_size * 2.2), 1_000_000_000), 100_000_000_000)
|
||||
free = os.statvfs(CACHE_ROOT).f_bavail * os.statvfs(CACHE_ROOT).f_frsize
|
||||
if rank == 0 or free >= estimate:
|
||||
if rank == 0 or self.cache_available() >= estimate:
|
||||
selected = (rank == 0, relative)
|
||||
break
|
||||
if not selected:
|
||||
|
||||
@@ -64,6 +64,26 @@ class CoreTests(unittest.TestCase):
|
||||
source.write_bytes(b"changed")
|
||||
self.assertFalse(manager.cache_valid(relative))
|
||||
|
||||
def test_smaller_cache_limit_evicts_oldest_content(self):
|
||||
m = self.module
|
||||
manager = m.Manager()
|
||||
relative = "movies/Resize/Resize.mkv"
|
||||
source = self.root / "source" / relative
|
||||
source.parent.mkdir(parents=True, exist_ok=True)
|
||||
source.write_bytes(b"original")
|
||||
output, manifest = manager.cache_paths(relative)
|
||||
output.write_bytes(b"cached rendition")
|
||||
manifest.write_text(json.dumps({"relative": relative, "source_fingerprint": m.fingerprint(source)}))
|
||||
manager.ensure_link(relative, m.cache_target(relative))
|
||||
old_limit = m.CACHE_MAX_BYTES
|
||||
try:
|
||||
m.CACHE_MAX_BYTES = 1
|
||||
manager.enforce_cache_limit()
|
||||
finally:
|
||||
m.CACHE_MAX_BYTES = old_limit
|
||||
self.assertFalse(output.exists())
|
||||
self.assertEqual(os.readlink(self.root / "view" / relative), m.source_target(relative))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user