A playing session's position stands still between reports, where upstream's advances #201
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?
Upstream advances a playing session's position on a timer and this server
does not, so
/Sessionsdisagrees about where a client is within a second ofit reporting.
SessionInfo.StartAutomaticProgressadds a second toPlayState.PositionTicksevery second while a session is playing and not paused, between the reports a
client actually sends. Measured side by side on the same anchor copy, after
one report of
90000000:A paused session agrees on both.
Why it is worth filing rather than shrugging at
It was found while diffing SyncPlay's group adoption, and it is what made that
diff disagree: upstream's group adopts the position its timer has advanced to,
and then seeks every member back to it. Controlling for the drift, adoption
agrees exactly — so this is the session registry's difference and not
SyncPlay's, and it will keep surfacing as one wherever a position is read
between two reports.
What reads a position between reports:
/Sessions(the dashboard's now-playingrow), the resume point a second client is offered, and now a group's adopted
position.
What it is not
Not the write path. What a client reports is filed identically on both sides —
crates/ephyra-app/src/session/sessions.rsrecords the report and movesLastPlaybackCheckIn, which is upstream's own behaviour. The difference isonly in what is reported back between two reports.
Worth settling before it is written
Whether the advance is computed on read —
position + (now - last report)foran unpaused session, which needs no timer and no task — or written by a ticker
as upstream's is. The first is cheaper and has no state to leak; the second is
what upstream does and is what a reader comparing the two servers'
/Sessionsrow by row would expect to find in the column.
Landed on
mainas78be052e(the advance) and1fe062f5(the guards the review found untested, and the ignore-list entry a replay now needs).The shape: computed on read, not a ticker
Live::position_ticks_at(now, run_time_ticks)incrates/ephyra-app/src/session/sessions.rs. No timer, no task, no second stored position.Upstream does run one —
SessionInfo._progressTimer,Timer(…, 1000, 1000), armed byOnPlaybackStart(SessionManager.cs:784) and re-armed by every non-automatedOnPlaybackProgress(:959), stopped onOnPlaybackStopped(:1061). The callback adds exactly one second and re-entersOnPlaybackProgress(isAutomated: true), which writesPlayState.PositionTicksandLastActivityDatebut notLastPlaybackCheckInand not user data (:925, "only update saved user data on actual check-ins").Because the timer mutates
_lastProgressInfo.PositionTicksin place and only a real report replaces that object, upstream's iterative sum is exactlyreported + floor(elapsed since the last real report). That equivalence is what makes the closed form safe — it was checked against a simulation of the timer over 14 inputs (advancing, paused, null, negative, exact-second clamp, fractional runtime, position past the end, no runtime), agreeing in every case. Its two gates carry over:if (progressInfo.IsPaused) return;andif (newPositionTicks >= runtimeTicks) return;, the second of which stops the position at the last whole second strictly inside the item.Measured against upstream v12.0.0
After one report of
900000000:Paused stands still on both. Resume and seek both restart the advance from the new report. Reported 4.3s from the end: both climb to runtime − 3s and stay. Reported exactly 4s from the end: both stop at runtime − 1s, three steps and not four. Stopped: both
null.SyncPlay was visibly wrong and is the reason this was worth doing beyond the dashboard — a group opened 6s after a report adopted
StartPositionTicks=900000000where upstream adopted960000000.The review
Twelve mutations, nine killed. The three survivors were all guards with no test, and one of them was a panic: without
.max(0)on the room left in the item, a client reporting a position past the runtime makesi64::clamppanic (min = 0, max = -11) — andplaystate.rs:174rejects negatives only on/Sessions/Playing/Stopped, so that is reachable input.1fe062f5adds the three tests; each now fails on exactly its own mutation and on nothing else. The code was already right — what was missing was anything that would notice if it stopped being.3249 tests, clippy silent, fmt clean.
Split out rather than buried
Sessionsis not woken between reports, so a dashboard on the websocket is still frozen. This is the one piece of the symptom described here that the fix does not reach.LastActivityDatenot advancing, which changes theactiveWithinSecondsfilter; and an emptyItemIdwhich upstream does advance).Closing this one: the position a reader is told is now upstream's, on every path that asks.