Nothing runs on a schedule: the triggers a dashboard renders are decoration #95
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?
crates/jellyfin-app/src/tasks.rssays it plainly in its own module docs — nothing here is a timer. A task runs when a request asks for it, and the intervals reported beside it are what upstream would have run at. The only periodic thing in the process is the encoder supervisor (crates/jellyfin-server/src/main.rs:108).So a server installed and left alone never scans again after its first start, never tidies its credits, and never does any of the maintenance filed under #97. An operator watching the dashboard sees the next-run time move and nothing happen.
Two more halves of the same gap, both named at M7 and still open:
DELETE /ScheduledTasks/Running/{taskId}answers and the scan runs to its end.POST /ScheduledTasks/{taskId}/Triggersis accepted and dropped. The triggers a client posts are not stored, so an operator who moves the nightly scan to 04:00 is told it worked.What upstream has is
ScheduledTaskWorkerinEmby.Server.Implementations/ScheduledTasks/, with four trigger kinds — interval, daily, weekly, and startup — and the triggers persisted per task. The last-run outcome we already record is the other half of what that worker keeps.Worth settling at the same time: a scheduled scan is what makes #92's forty-two seconds a recurring cost rather than a thing somebody presses.
Where to start
crates/jellyfin-app/src/tasks.rsholdsKind,StateandTasks; the routesare
crates/jellyfin-http/src/routes/tasks.rs. The polling belongs in thecomposition root beside the encoder supervisor (
crates/jellyfin-server/src/main.rs:108),which is the shape M5 chose for the same reason — a loop the root drives rather
than a timer per job.
Upstream:
Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.csand
TaskManager.cs, with the trigger kinds inMediaBrowser.Model/Tasks/.There are four — interval, daily, weekly and startup — and the worker persists
each task's triggers rather than keeping the defaults. Read what the reference
writes on disk before designing where ours go: a client posts triggers and
expects them back after a restart, and that file is a contract with the
dashboard rather than an implementation detail.
Do not skip the two write paths while you are in here:
DELETE /ScheduledTasks/Running/{taskId}has to actually stop a scan (which means the scan needs a cancellation token
threaded from
crates/jellyfin-app/src/scan/run.rs), andPOST /ScheduledTasks/{taskId}/Triggershas to store what it is given.How to know it is done
/ScheduledTasksand/ScheduledTasks/{id}diffs cleanagainst the reference — trigger shapes included, which is the part that is
currently invented.
a log line with a timestamp is enough.
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— eleven commits,28fed85through46a26db. All three parts, and a reviewer re-ran every claim.It fires
A 30-second interval trigger posted at
23:03:07fired at23:04:42, which is upstream'sIntervalTrigger.cs:51—max(last_end, armed + 1 min) + interval— landing on the next 5-second poll tick after being missed by 3 ms. A trigger survives a restart and fires at start + 1 min + 30 s where the default is twelve hours, so only the stored trigger can produce that. A startup trigger fires at boot with no request.Where the triggers live was measured before it was designed, which the issue asked for:
<data>/config/ScheduledTasks/<hyphenated-guid>.jsfor the triggers,<data>/data/ScheduledTasks/<guid>.jsfor the last run, upstream's own paths fromGetConfigurationFilePath. Ours writes the same fields in the same order with the same tick encoding and the same nulls omitted.The task ids are upstream's.
Kind::id()now hashes the C# class name (ScheduledTaskWorker.cs:219,GetType().FullName.GetMD5()), soRefreshLibraryis7738148ffcd07979c7ceb148e06b3aedandRefreshPeopleis866456ed0d44e15468124ce33d85961eon both servers, and the dashboard addressed ours at the reference's URL. This is wire-breaking for a client holding an id derived from the old key hash;fabba23says so.Cancel — and what the first pass got wrong
The first pass reported a cancel stopping a scan 2.1 s in. The reviewer reproduced it on a library with a middle and got 2 minutes 39 seconds: the token reached the per-library and per-item checks, and between them sat the walk and the resolve, uninterruptible. The scan then discarded everything those minutes had computed.
Profiling the stages found where the time actually is — 129 s of a 139 s rescan inside
library_tree— so the check now runs per directory walked and per directory resolved, plus the existing per-batch one, and the people refresh checks between each of its five phases. Re-measured on the same 4,200-film tree:That stage profile is the sharpest thing this milestone produced and it belongs to #92, where it is now recorded: the walk and the database are innocent and the resolve is the whole cost.
Cancelling
refresh_peopleused to reportCompleted— the state flipped toCancelling, the dashboard said "Stopping", and the run finished and recorded success. A wrong outcome rather than a late one, which is the defect this issue was filed about surviving in one of the two tasks. It now returnsScanErrorrather than flattening into a string.Three more the review found
Statushad three variants where upstream has four.ScheduledTaskWorker.Dispose:601writesAbortedwhenever the server shuts down with a task running — a normal event — and reading one of those files failed the whole record, which also droppedlastand armed the interval an hour out instead of twelve. There is now a fourth variant and a deserializer that maps any unrecognised name to it, with a test that round-trips an upstream-shaped file byte-identically.poll()re-armed every trigger, not the one that fired. Given a one-hour and a twenty-four-hour interval on one task, the long one was pushed forward every hour and never fired. Fixed, and the test was confirmed to fail against the old behaviour.TaskManager.QueueScheduledTask:171). Kept, and now written down in the code, the module doc andARCHITECTURE.mdrather than left to be discovered: a scan is the whole library either way, so a queued one finds what the running one filed, and a queue turns a library slower than its interval into a server that never stops scanning.Harness
scenarios/tasks.json: 11 responses, 0 differences, 10 suppressed by one new entry — value-scoped, covering the twoLastExecutionResulttimestamps only, with a reason that names what stays diffed (Status,Name,Key,Id, and whether a result exists), so a cancelled run reportingCompletedis still a difference. Re-running gives the same numbers, which is the scenario being idempotent: the cancel is written start → stop → start → read, so the run it diffs is one both servers completed.The README no longer claims the scenario puts the reference back as it found it. It says what a run actually leaves: a trigger file where a pristine server had none, holding what it would have defaulted to, and a
LastExecutionResultonRefreshPeoplethat nothing removes./ScheduledTasks— the listing — cannot diff clean while this server declares two of upstream's twenty, and no entry was added to pretend otherwise. The scenario reads each task by id instead and the README says why.Numbers
JELLYFIN_RS_REQUIRE_FIXTURES=1 cargo nextest run --workspaceonmain: 2151 passed, 0 skipped. Clippy clean.Browser: the dashboard lists both tasks with their triggers, Add Trigger → Daily → 4:00 AM writes
{"Type":"DailyTrigger","TimeOfDayTicks":144000000000}and survives a hard reload, and ■ flips the row to "Stopping" and ends the run asCancelled. Console clean; the only non-2xx are the known pre-auth/socket403s before the 101 upgrade.MILESTONES.md's M7 entry gained a*Corrected at M8.*paragraph in the M4 style — the record is not rewritten.Three defects this turned up are filed: #125, #126, #127. The reference was left at its defaults, verified: 20 tasks, all Idle, every trigger where upstream puts it.
One claim is the implementer's rather than independently checked. The reviewer was under orders not to write to the reference, and
tasks.jsonposts triggers — so the replay numbers above were not reproduced by a second pass. The ignore entry was checked statically, the suppression arithmetic verified, and four of the eight new refusal cases hand-checked read-only.