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
5.9 KiB
Fork Diff: unkin/jellyfin-ha-src vs jellyfin/jellyfin
Base:
v12.0(6c073e19ddf604b2369c638716164fdab4c952dc) Head:origin/mainSummary: 56 files added · 16 upstream files modified · 15 upstream files deleted
What changed and why
This fork adds a high-availability layer so Jellyfin can run as more than one replica against shared storage. The design principle: extend through dependency injection, touch as little upstream code as possible. No core media, library, auth or plugin logic is rewritten.
| Bucket | Files | Lines added |
|---|---|---|
HA contracts and models (MediaBrowser.Controller) |
8 | ~330 |
Redis implementations (Emby.Server.Implementations) |
2 | ~350 |
| PostgreSQL database provider | 6 | ~4,980 |
| SQLite to PostgreSQL migration tool | 6 | ~720 |
| Modified upstream files | 16 | ~420 |
| Tests | 14 | ~2,790 |
| Helm chart | 17 | ~1,370 |
| CI and container build | 3 | ~175 |
New components
Transcode session store
| File | Purpose |
|---|---|
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/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}, key TTL is the retention window so an orphaned session outlives its 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:
local raw = redis.call('GET', KEYS[1])
if not raw then return 0 end
local session = cjson.decode(raw)
-- 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
Scan-leader lease
| File | Purpose |
|---|---|
MediaBrowser.Controller/ScheduledTasks/IScanLeaderLease.cs |
DI contract for the leader lease |
MediaBrowser.Controller/ScheduledTasks/ScanLeaderOptions.cs |
Enabled, LeaseDurationSeconds, GatedTaskKeys |
MediaBrowser.Controller/ScheduledTasks/NullScanLeaderLease.cs |
Always-leader default, preserving single-instance behaviour |
Emby.Server.Implementations/ScheduledTasks/RedisScanLeaderLease.cs |
Redis TTL lease; an unreachable Redis is treated as holding the lease |
Gated by default: RefreshLibrary, RefreshPeople, RefreshChapterImages,
AudioNormalization, TaskExtractMediaSegments, KeyframeExtraction,
CleanupUserDataTask, OptimizeDatabaseTask. Only timer-driven runs are gated;
manual and API-triggered runs always execute locally.
PostgreSQL provider
src/Jellyfin.Database/Jellyfin.Database.Providers.PostgreSQL/ is an EF Core
provider parallel to the SQLite one, selected with
DatabaseType: Jellyfin-PostgreSQL. It carries a single initial migration
generated against the v12.0 model. SQLite remains the default, so existing
deployments are unaffected.
tools/Jellyfin.DbMigrator/ moves an existing jellyfin.db into PostgreSQL,
optionally uploading a pre-migration copy of the SQLite file to S3.
Modified upstream files
| 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 |
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/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 |
Jellyfin.Server/Program.cs (+5) |
Exposes IServerConfigurationManager to the startup migration container |
MediaBrowser.Model/Configuration/EncodingOptions.cs (+16) |
Adds RecoverySegmentLengthSeconds and RecoverySegmentBufferCount |
Directory.Packages.props, Jellyfin.sln, four .csproj files |
New packages, projects and references |
tests/.../SessionManagerTests.cs, tests/.../IdlePlaybackTests.cs |
Pass the new constructor argument |
Deleted upstream files
.github/workflows/* (15 files). The fork is hosted on Gitea and builds on
Woodpecker via .woodpecker/ci.yaml.
Rebasing onto a newer upstream release
git remote add upstream https://github.com/jellyfin/jellyfin.git
git fetch upstream --tags
# files this fork owns outright
git diff v12.0...HEAD --name-only --diff-filter=A
# files that need re-porting onto the new release
git diff v12.0...HEAD --name-only --diff-filter=M
# upstream churn on a touched file since this base
git log v12.0..<new-tag> --oneline -- <path>
Re-apply the modified-file changes onto the new upstream code rather than reverting upstream's changes to make a patch apply.