fix: make transcode leases ownership-checked and cleanup-aware

Cleanup never matched a live session because the controller registered empty
manifest and segment paths, renewal was a read-modify-write that could revert a
takeover, and the takeover script compared an ISO date to a number, so it errored.

- populate the session record's manifest and segment paths from the playlist path
- renew the lease via a Lua compare-and-set on the owning pod
- store the lease expiry as unix milliseconds so the scripts can compare it
- retain the session record past its lease so an orphan can still be taken over
- test the Redis store against a real Redis, including the renew-vs-takeover race
- drop the live stream record nothing ever read back
This commit is contained in:
2026-09-12 10:19:29 +10:00
parent d825f8ac81
commit baa16b6586
22 changed files with 644 additions and 1221 deletions
+11 -9
View File
@@ -32,21 +32,24 @@ auth or plugin logic is rewritten.
| File | Purpose |
|------|---------|
| `MediaBrowser.Controller/MediaEncoding/ITranscodeSessionStore.cs` | DI contract for durable transcode and live stream session tracking |
| `MediaBrowser.Controller/MediaEncoding/ITranscodeSessionStore.cs` | DI contract for durable transcode session tracking |
| `MediaBrowser.Controller/MediaEncoding/TranscodeSession.cs` | Session record: owning pod, lease expiry, manifest and segment paths, last durable segment |
| `MediaBrowser.Controller/MediaEncoding/LiveStreamSession.cs` | Record of an open live stream and the pod holding it |
| `MediaBrowser.Controller/MediaEncoding/TranscodeStoreOptions.cs` | `RedisConnectionString` and `LeaseDurationSeconds` |
| `MediaBrowser.Controller/MediaEncoding/TranscodeStoreOptions.cs` | `RedisConnectionString`, `LeaseDurationSeconds` and `SessionRetentionSeconds` |
| `MediaBrowser.Controller/MediaEncoding/NullTranscodeSessionStore.cs` | No-op store used when no Redis connection is configured |
| `Emby.Server.Implementations/MediaEncoding/RedisTranscodeSessionStore.cs` | Redis store; sessions under `jellyfin:transcode:{playSessionId}`, live streams under `jellyfin:livestream:{liveStreamId}:{sessionId}`, key TTL mirrors the lease |
| `Emby.Server.Implementations/MediaEncoding/RedisTranscodeSessionStore.cs` | Redis store; sessions under `jellyfin:transcode:{playSessionId}`, key TTL is the retention window so an orphaned session outlives its lease |
Lease takeover runs as a single Lua script, so concurrent pods cannot both claim
an expired lease:
Lease takeover and renewal each run as a single Lua script, so concurrent pods cannot both
claim an expired lease and a renewal cannot revert a takeover. The expiry is stored as unix
milliseconds so the script can compare it:
```lua
local raw = redis.call('GET', KEYS[1])
if not raw then return 0 end
local session = cjson.decode(raw)
if session['LeaseExpiresUtc'] > tonumber(ARGV[1]) then return 0 end
-- takeover: only an expired lease may be claimed
if tonumber(session['LeaseExpiresUtc']) > tonumber(ARGV[1]) then return 0 end
-- renewal: only the pod that still owns the lease may extend it
-- if session['OwnerPod'] ~= ARGV[2] then return 0 end
session['OwnerPod'] = ARGV[2]
-- update expiry and SET with PX in the same script
return 1
@@ -84,9 +87,8 @@ optionally uploading a pre-migration copy of the SQLite file to S3.
| File | Change |
|------|--------|
| `Jellyfin.Server/CoreAppHost.cs` (+46) | Registers the Redis or null transcode store and scan-leader lease from startup config |
| `Jellyfin.Api/Controllers/DynamicHlsController.cs` (+133/-9) | Registers the play session, runs lease renewal, and shortens segments when resuming a stored session |
| `Jellyfin.Api/Controllers/DynamicHlsController.cs` | Registers the play session with its manifest and segment paths, renews the lease under this pod's identity, and shortens segments when resuming a stored session |
| `Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs` (+52/-6) | Keeps files belonging to an active session in the store |
| `Emby.Server.Implementations/Session/SessionManager.cs` (+37/-4) | Persists and deletes the durable live stream record |
| `Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs` (+28/-1) | Skips timer-driven gated tasks without the leader lease |
| `Emby.Server.Implementations/ScheduledTasks/TaskManager.cs` (+12/-2) | Passes the lease and options to each worker |
| `Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs` (+53) | Registers the PostgreSQL provider and a pooled `NpgsqlDataSource` |