A socket subscribed to Sessions is not woken between a client's reports #202

Closed
opened 2026-09-12 17:16:23 +02:00 by Claude · 2 comments
Collaborator

/Sessions over the socket is fixed only for whoever asks again. A subscriber is woken by a real report and by nothing else, so a dashboard watching a playing session sees the position it was pushed and then nothing for ten seconds — the symptom #201 described, on the path #201 did not reach.

Both sides

Ours: crates/ephyra-http/src/routes/session/socket.rs:285-291 — the Sessions feed is woken only by context.sessions.changes(), which Sessions::playing bumps (crates/ephyra-app/src/session/sessions.rs:431, changed(true)). Nothing bumps it between reports.

Upstream is not on a timer either, but the effect is one:

  • upstream/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs:119 — OnSessionManagerPlaybackProgress(…) => SendData(!e.IsAutomated)
  • upstream/Emby.Server.Implementations/Session/SessionManager.cs:953 — PlaybackProgress?.Invoke(this, eventArgs) fires on the automated path too, with IsAutomated = true
  • upstream/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs:169 — a non-forced SendData still sends to every connection where (now - state.DateLastSendUtc) >= state.IntervalMs

So upstream's per-session 1s timer (SessionInfo.cs, Timer(…, 1000, 1000)) reaches the socket as a once-a-second nudge, throttled by each subscription's own interval.

What a client sees

A dashboard subscribes SessionsStart with "0,1500". The client reports at t=0 and again at t=10.

  • upstream: ~6 payloads in between, 100000000 … 190000000
  • ours: zero. The now-playing row is frozen for the full ten seconds.

1fe062f5 fixed the REST read and SyncPlay's adoption, so anything that asks again gets the advanced value. A socket subscriber never asks again.

The open question

Whether the wake is a real cadence (something per playing session, which crates/ephyra-app/src/session/topics.rs — "Nothing is sent on a timer" — currently forbids, and which ARCHITECTURE.md forbids for admin::tasks) or whether the Sessions feed's existing periodic interval can carry it: BasePeriodicWebSocketListener is not a wall clock, but it does re-send on the subscription's interval once something marks the feed dirty. The cheap shape to try first is a nudge that costs nothing when no session is playing — if nothing is playing, upstream's timer is not running either.

Note that "nothing is sent on a timer" is a true statement about upstream's mechanism and a false one about its effect. Whatever is decided, it needs writing down: DECISIONS.md:1150-1155 is explicit that an undocumented divergence is worse than either answer.

Verifying

Two clients against a live pair, one reporting every 10s, a socket subscribed to SessionsStart on both, and a count of payloads and their PositionTicks between two reports. crates/ephyra-http/tests/sessions.rs has the Moved clock the REST tests use.

Found reviewing #201 (1fe062f5).

`/Sessions` over the socket is fixed only for whoever asks again. A subscriber is woken by a real report and by nothing else, so a dashboard watching a playing session sees the position it was pushed and then nothing for ten seconds — the symptom #201 described, on the path #201 did not reach. ## Both sides Ours: `crates/ephyra-http/src/routes/session/socket.rs:285-291` — the `Sessions` feed is woken only by `context.sessions.changes()`, which `Sessions::playing` bumps (`crates/ephyra-app/src/session/sessions.rs:431`, `changed(true)`). Nothing bumps it between reports. Upstream is not on a timer either, but the effect is one: - `upstream/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs:119` — `OnSessionManagerPlaybackProgress(…) => SendData(!e.IsAutomated)` - `upstream/Emby.Server.Implementations/Session/SessionManager.cs:953` — `PlaybackProgress?.Invoke(this, eventArgs)` fires on the automated path too, with `IsAutomated = true` - `upstream/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs:169` — a **non-forced** `SendData` still sends to every connection where `(now - state.DateLastSendUtc) >= state.IntervalMs` So upstream's per-session 1s timer (`SessionInfo.cs`, `Timer(…, 1000, 1000)`) reaches the socket as a once-a-second nudge, throttled by each subscription's own interval. ## What a client sees A dashboard subscribes `SessionsStart` with `"0,1500"`. The client reports at t=0 and again at t=10. - upstream: ~6 payloads in between, `100000000 … 190000000` - ours: **zero**. The now-playing row is frozen for the full ten seconds. `1fe062f5` fixed the REST read and SyncPlay's adoption, so anything that asks again gets the advanced value. A socket subscriber never asks again. ## The open question Whether the wake is a real cadence (something per playing session, which `crates/ephyra-app/src/session/topics.rs` — "Nothing is sent on a timer" — currently forbids, and which `ARCHITECTURE.md` forbids for `admin::tasks`) or whether the `Sessions` feed's existing periodic interval can carry it: `BasePeriodicWebSocketListener` is not a wall clock, but it does re-send on the subscription's interval once something marks the feed dirty. The cheap shape to try first is a nudge that costs nothing when no session is playing — if nothing is playing, upstream's timer is not running either. Note that "nothing is sent on a timer" is a true statement about upstream's *mechanism* and a false one about its *effect*. Whatever is decided, it needs writing down: `DECISIONS.md:1150-1155` is explicit that an undocumented divergence is worse than either answer. ## Verifying Two clients against a live pair, one reporting every 10s, a socket subscribed to `SessionsStart` on both, and a count of payloads and their `PositionTicks` between two reports. `crates/ephyra-http/tests/sessions.rs` has the `Moved` clock the REST tests use. Found reviewing #201 (`1fe062f5`).
Author
Collaborator

Measured live against 1fe062f5, so this is not only a reading of the code.

A server on :8116 over a copy of snapshots/enriched; one client (Client="player") posting /Sessions/Playing/Progress every 10s; one subscriber (Client="Jellyfin Web") on /socket sending {"MessageType":"SessionsStart","Data":"0,1500"} — the interval jellyfin-web's dashboard uses.

22 seconds, 2 Sessions payloads

  t+ 7.0s   PositionTicks 190000000
  t+17.1s   PositionTicks 290000000

Two payloads, arriving exactly on the two reports in that window. 0,1500 asks for 1.5s; upstream's automated tick would have satisfied it about fourteen times.

The REST read is right the whole time — GET /Sessions sampled once a second over the same session returned 90000000, 100000000, 110000000, 120000000, 130000000, 140000000. So the two paths now disagree with each other: a client that polls sees the position move, and a client on the socket sees it jump once per report. The dashboard is the second kind.

One incidental note for whoever picks this up: ?api_key= is refused on /socket unless enable_legacy_authorization is on (crates/ephyra-http/src/auth.rs:194). ?ApiKey= is the spelling that works with legacy auth off.

Measured live against `1fe062f5`, so this is not only a reading of the code. A server on :8116 over a copy of `snapshots/enriched`; one client (`Client="player"`) posting `/Sessions/Playing/Progress` every 10s; one subscriber (`Client="Jellyfin Web"`) on `/socket` sending `{"MessageType":"SessionsStart","Data":"0,1500"}` — the interval jellyfin-web's dashboard uses. ``` 22 seconds, 2 Sessions payloads t+ 7.0s PositionTicks 190000000 t+17.1s PositionTicks 290000000 ``` Two payloads, arriving exactly on the two reports in that window. `0,1500` asks for 1.5s; upstream's automated tick would have satisfied it about fourteen times. The REST read is right the whole time — `GET /Sessions` sampled once a second over the same session returned `90000000, 100000000, 110000000, 120000000, 130000000, 140000000`. So the two paths now disagree with each other: a client that polls sees the position move, and a client on the socket sees it jump once per report. The dashboard is the second kind. One incidental note for whoever picks this up: `?api_key=` is refused on `/socket` unless `enable_legacy_authorization` is on (`crates/ephyra-http/src/auth.rs:194`). `?ApiKey=` is the spelling that works with legacy auth off.
Author
Collaborator

Fixed in e78c7aad, landed on main at f0ffda81.

A 1 s poll in crates/ephyra-server/src/serve.rs calls Sessions::nudge_playing. While something is advancing, it wakes the session feed without forcing a send, so each subscription's own interval throttles it as upstream's BasePeriodicWebSocketListener does. D46 records the mechanism.

Measured live against upstream: a socket subscribed with SessionsStart "0,1500", one report, then 8 s of silence. Both servers pushed 2 Sessions payloads with an advancing position. Before the change, ours pushed 0.

The review found that the wake had restarted the socket's quiet timeout, which disabled the keepalive for every socket on the server while anything played. The deadline now moves only on a client message (ac89e383). Measured: a silent socket was prompted at 0 s and 45 s and dropped at 60 s, while the feed kept waking.

Fixed in `e78c7aad`, landed on `main` at `f0ffda81`. A 1 s poll in `crates/ephyra-server/src/serve.rs` calls `Sessions::nudge_playing`. While something is advancing, it wakes the session feed without forcing a send, so each subscription's own interval throttles it as upstream's `BasePeriodicWebSocketListener` does. D46 records the mechanism. Measured live against upstream: a socket subscribed with `SessionsStart "0,1500"`, one report, then 8 s of silence. Both servers pushed 2 `Sessions` payloads with an advancing position. Before the change, ours pushed 0. The review found that the wake had restarted the socket's quiet timeout, which disabled the keepalive for every socket on the server while anything played. The deadline now moves only on a client message (`ac89e383`). Measured: a silent socket was prompted at 0 s and 45 s and dropped at 60 s, while the feed kept waking.
Sign in to join this conversation.
No description provided.