Every stream request (a segment, a key, a playlist) read and decoded the
whole bouquets map at start-up; only auth, probe and rtmp read it. On a
panel of 50 bouquets and 5,000 streams that is 3 MB and about 16 ms a
request. The three scripts load it themselves.
FileCache::get() keeps what a process last read of a file while it is still
the same file (inode, time, size): ServerRepository::getAll() has 106 call
sites, 49 of them `getAll()[$id]`, some once a table row, and each read and
decoded the servers' cache again. A value is kept only once its file's
second is over, so a file written twice within a second is never taken for
the one read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trailing whitespace in CacheEngineCronJob and FileCacheTest's member
spacing (phpcbf). The md5() writing the ffprobe cache names a file, as
every reader of that key does; it is marked as such for semgrep's
weak-crypto rule, like the repo's other non-security md5 names.
Readers (stream auth, player API, Ministra, Enigma2) read cache files with
no lock, while cron:cache_engine rewrote them in place with
file_put_contents(), and kills its own previous run with kill -9. A
request could read a truncated stream_/line_i_ entry; a killed worker left
one behind.
- FileCache::writeAtomic(): temp dotfile + rename(); set() uses it.
The temp name no longer matches the stream_*/line_i_* entry globs.
cleanStaleTemps() sweeps temps a killed writer left.
- cache_engine, StreamCacheBuilder, UserRepository's DB-fallback line
cache, the balancer and ffprobe caches all write atomically.
- A failed DB read no longer counts as "deleted": streams_update /
lines_update used to unlink the entries of every requested id when
the query failed, and a failed full-run page let the sweep delete
live entries. Workers now flag the failure and the sweep is skipped.
(Database::query() leaves $db->result set on a failed execute, so the
query's return value is checked.)
- Full-run paging is in id order (streams, lines before the MAG join,
episodes tie-broken by stream_id): unordered LIMIT pages could skip
rows, which the sweep then deleted.
- lines_update drops the old line_c_/line_t_ lookup files when a line's
credentials or token change or the line is deleted.
- Incremental mode removes series_/episodes_ files of deleted series and
of series left with no episodes (they used to keep stale lists).
- Change detection uses <= so an edit in the same second as the last
write is not missed; the kill of older runs matches the PID exactly.
- The empty-series run's temp files are merged and removed.
- CacheCronJob's startup wipe no longer unlink()s '.' and '..'.
Fourth coverage batch — file-backed cache and i18n, each driven against a
throwaway temp directory:
- FileCacheTest: set/get roundtrip through serialization, miss = false, has(),
idempotent delete, flush, maxAge expiry (backdated mtime), and the path/age
accessors.
- TranslatorTest: English default, cookie language detection with fallback,
available() listing, {token} substitution, guarded setLanguage(), and the
hardened backfill — a key missing from the active language is written back
with the English value, or with itself as a visible placeholder when English
has none either.
+15 tests. Suite: 661 tests, 0 errors.