The six maintenance jobs: nothing sweeps the transcodes, the cache or the logs, and nothing optimizes the database #103
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Six of upstream's tasks are housekeeping, and none of them exists here. Together they are what keeps a long-running install from filling its disk.
DeleteTranscodeFiles(startup + 24 h) — sweeps the transcoding directory. Without it, every abandoned HLS session's segments stay on disk forever. This is the one that bites first on a real server.DeleteCacheFiles(24 h) — sweeps the cache directory. Our subtitle conversions are cached by a key that names every argument (M7), and nothing ever removes one.CleanLogFiles(24 h) — trims the log directory against the configured retention.CleanActivityLog— trims the activity table against its retention. Nothing here writes a row yet either.CleanupUserDataTask— dropsUserDatarows whose item is gone. Ours accumulate the same way upstream's would.OptimizeDatabaseTask(6 h) —VACUUM/ANALYZEon the SQLite file. Relevant to us specifically:report.md's read-path numbers come partly from mapping the database, and a file that is never analysed gives the planner stale statistics.All six are small, and all six are pure server behaviour with no upstream wire contract to diff — which is worth saying out loud, because it means the harness cannot check them and a test has to.
Where to start
Six small ones, all in
Emby.Server.Implementations/ScheduledTasks/Tasks/:DeleteTranscodeFileTask,DeleteCacheFileTask,DeleteLogFileTask,CleanActivityLogTask,CleanupUserDataTask,OptimizeDatabaseTask. Read eachfor its retention rule and its trigger —
DeleteTranscodeFilesruns at startupand every 24 h, which is deliberate: a server that was killed mid-transcode
leaves segments nobody will ever ask for.
Ours: the directories come from
[paths]in the configuration(
crates/jellyfin-server/src/config.rs), the transcode directory is whatcrates/jellyfin-app/src/streaming/transcode.rsandcrates/jellyfin-app/src/streaming/encoder/write into, and thesubtitle conversion cache is the one M7 keyed by every argument and never
sweeps.
OptimizeDatabaseTaskis worth doing early for a reason of ours rather thanupstream's:
report.md's read-path numbers come partly from mapping thedatabase, and a file that is never
ANALYZEd hands the planner stale statistics.How to know it is done
These have no wire contract, so the harness cannot check them and a test has
to. Each sweep gets a test that puts files in a temporary directory, runs the
task against a clock the test controls, and asserts what is left.
VACUUMgetsone that shows the file shrank after deletions.
House rules
CLAUDE.mdis the short version and these are the parts that bite: tests rununder
cargo nextest; upstream's source is the checkout inupstream/at thepin (
v12.0-rc5,6d501ba4) and not the clone beside the repository;generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and
Dnreferences stay out of code and comments.ARCHITECTURE.mddecides which crate a thing goes in and which dependency edgesare illegal,
DECISIONS.mdsays why a rule exists.The reference pair is
cargo xtask reference up(andreference provision,which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,with
diff-dbfor the two databases.crates/jellyfin-harness/README.mdsayswhat a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.
Done, on
main(d28d318). All six, at upstream's own keys, categories, descriptions and triggers.Measured against the reference, not the C#
Each of the six was diffed by id against the pinned image with the pair up —
/ScheduledTasks/{id}for all six, 0 differences: the key, the name, the category, the description and the triggers agree byte for byte. The six ids are intasks.jsonnow, so they go on being diffed.That caught the one description carrying a setting:
CleanLogFilesreads "Deletes log files that are more than 3 days old", formatted from the retention.Kind::descriptiontakes it rather than returning a constant, so an operator who moves the retention is told the number they set.DeleteTranscodeFilesDeleteCacheFilescache/tempat oneCleanLogFilesCleanActivityLogCleanupUserDataTaskOptimizeDatabaseTaskwal_checkpoint,optimize,VACUUM,wal_checkpointHow it is checked
No wire contract, so the tests are it. The three file sweeps get a directory the test fills and a clock it controls — a clock set days ahead is what ages a file written a moment ago, which is cheaper and steadier than setting mtimes. The two database ones live in
jellyfin-db, where the code does: the vacuum is asserted on the file actually shrinking, and the user-data sweep builds its fixture by deleting an item rather than hand-writing the detached row, so the detaching half and the sweeping half are held to agreeing about the placeholder id and the retention column.Three decisions worth reading
cache/tempis swept at a day, not thirty. Upstream's second pass targets the OS temp directory, which this server does not use;cache/tempis where it puts a concat list and a probe's scratch file, so that is the directory the one-day rule is applied to.log_*because serilog trims those itself;tracing-appendertrims nothing, so excluding them would make the task sweep nothing at all on a real install. A day is the floor under the retention, which is what keeps the file being written to.OptimizeDatabaseTaskstands down while a scan runs, which is upstream's ownIsScanRunningcheck — a vacuum wants an exclusive lock and would stall the pass.ActivityLogRetentionDaysandLogFileRetentionDaysare[maintenance]in the config and are reported and taken on/System/Configuration. Everything else — a day for the transcodes, thirty for the cache, ninety for the detached playback state — is upstream's own constant, named beside the sweep that reads it.cargo nextest run --workspace: 2373 passed, 0 skipped.The review round (
0ff1992)One finding here, and it was a real one: a vacuum that failed was reporting the task as failed. Upstream's
OptimizeDatabaseTask.ExecuteAsynccatches the exception and only logs it, so it never reports a failure — and now thatActivityLogsis diffed, reporting one would be aTaskFailedrow upstream does not write, on top of aLastExecutionResultthat disagrees. It answers zero and logs instead.The same finding's other half: upstream runs the task and returns early when a scan is in progress, recording a completed run, where this refused to start it at all and left the previous result standing. Since
/ScheduledTasks/{id}is diffed for all six now, that was a difference waiting to happen. The stand-down is a run that happened and did nothing; the people refresh keeps its refusal, which is ours and is about rows a pass is mid-write on.Verified against the reference
/ScheduledTasks/{id}for all six: 0 differences — key, name, category, description and triggers agree byte for bytecargo nextest run --workspace: 2376 passed, 0 skippedf3b9015gives the identical countsClosing.
CleanActivityLoghas rows to sweep because #104 landed beside it; the umbrella is #97, which stays open on the two trickplay rows in batch D.