A server handed in through JELLYFIN_TEST_POSTGRES outlives the run, so a second
run finds the databases the first one created. Also name the failures in
Jellyfin.Database.Tests.PostgreSQL the CI step steps around.
Every PostgreSQL test carries Category=RequiresDocker and the pipeline filters
that category out, so the provider production runs on was never exercised and
two upgrade-path bugs reached it in a row. Nothing covered the whole startup
sequence either - code and schema migrations interleaved the way the migration
service orders them.
- run both database stages through JellyfinMigrationService against a seeded
library and against a fresh install
- take the server from JELLYFIN_TEST_POSTGRES when it is set, else start a
container
- add a pipeline step that runs the PostgreSQL tests on every push and pull
request, with the server inside the step: the kubernetes backend has no
docker daemon, and a service container deadlocks on the workspace volume
MigrateRatingLevels issued an ExecuteUpdate while the SELECT DISTINCT reader
was still open on the same connection. SQLite tolerates that, Npgsql does not,
so the AppInitialisation stage aborted and every PostgreSQL instance
crash-looped on startup.
- read the distinct ratings into a list before the update loop
- cover the migration against a real PostgreSQL, with NULL and empty ratings
An element the enum cannot parse throws, and the configuration manager
catches that and returns defaults, so one bad preset discarded every other
encoding setting.
- serialize EncoderPreset through a string surrogate in XML
- fall back to auto for an empty, unknown or out-of-range value
- accept only the two id shapes the uuid cast parses, so misplaced hyphens
null out instead of aborting the migration
- clear an OwnerId that already is the detached placeholder, matching the
SQLite chain, so CleanupOrphanedExtras cannot delete the item
- recreate the placeholder item before the repoint if it is missing
- compare the migrated schema against the model, constraints included
The v12.0 rebase replaced the PostgreSQL provider's initial migration instead of
adding to it, so an existing database kept the pre-12.0 schema while the code
migrations ran against it and startup aborted on a missing NormalizedUsername.
- restore 20260305010333_InitialPostgreSql as the baseline
- add 20260306000000_UpgradeToServer12Schema carrying it to the 12.0 model
- convert OwnerId and PrimaryVersionId to uuid with explicit casts, clear
unparseable ids and repoint dangling owners at the placeholder item
- drop orphaned permissions and preferences before UserId becomes non-nullable
- add 20260524120336_AddUniqueNormalizedUsernameIndex after the code migration
that fills the column in
- read an empty or unknown encoding.xml EncoderPreset as the default
- cover both paths against a real PostgreSQL and guard the migration ordering
The chart sets the lease duration but not the retention window that keeps an
orphaned session record available for takeover.
- add ha.transcodeStore.sessionRetentionSeconds and pass it to the statefulset
The branch is a fresh rebase onto the v12.0 tag, so it shares no history with
main and cannot be merged normally.
- record main as a parent without changing a single file
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
The workspace volume is 9.8G and the clone plus build output already fill 5.7G,
so a shared package cache there leaves too little free space to test against.
- restore, build and test in one step with the cache on ephemeral storage
- exclude BackupServiceTests, which requires 5GiB free on the workspace
- print workspace free space before the test run
The backup service test needs 5GiB free on the workspace and the apt mirror
occasionally serves a half-synced index, so both fail the pipeline at random.
- request and limit ephemeral storage on every step
- retry apt-get update and verify fontconfig is loadable before testing
- report workspace free space before the test run
- run the full test filter again
BackupService refuses to write a backup with less than 5GiB free, which the
pipeline workspace volume does not have, so the suite fails on disk size alone.
- exclude BackupServiceTests from the test filter
- keep restore, build and test as separate steps
- install fontconfig for the Skia tests before running them
A solution-wide test run reports one exit code, so a failing suite cannot be
identified without the pipeline logs.
- run the suites carrying HA tests as separate named steps
- run the remaining upstream suites in one step after installing fontconfig
A failure anywhere in the chain currently looks the same from outside, so there
is no way to tell a package mirror problem from a compile error.
- run the fontconfig install as the first step
- split restore, build and test into named steps
- point the package cache at the shared pipeline volume
Separate steps do not share the SDK package cache, so the build step could not
see what the restore step downloaded.
- restore, build and test in a single container
- install fontconfig after the build so a mirror failure cannot be read as a build failure
- raise the memory limit to 8Gi
A single step reports every failure the same way, so a mirror, compiler or test
failure all look identical from the outside.
- split restore, build and test into separate named steps
- probe the fontconfig package install in its own step
- share the NuGet cache through the workspace and raise the memory limit
The README and fork notes still described upstream and a pre-10.11 snapshot, so
there was nothing accurate to hand an operator setting this up.
- rewrite the README as an HA setup and configuration guide
- add architecture, contributing and transcoding design notes
- rewrite FORK-DIFF.md against the v12.0 base
Running the HA build needs Redis, PostgreSQL and shared transcode storage wired
together, which is a lot of manifests to keep in sync by hand.
- add a chart deploying the server as a StatefulSet with a PodDisruptionBudget
- ship optional Redis and PostgreSQL dependencies and a runtime config ConfigMap
- template shared media, config and transcode volume claims
- add ingress and ServiceMonitor templates
The fork is hosted on Gitea, where the upstream GitHub Actions workflows never
run and only produce noise.
- add a Woodpecker pipeline restoring, building and testing the solution
- remove the upstream .github/workflows definitions
- add Dockerfile and Dockerfile.runtime building the server image with jellyfin-web 12.0
Timer-driven library tasks fire on every replica, so a library refresh or a
database optimise runs once per pod against the same shared library.
- add IScanLeaderLease with a Redis TTL implementation and a no-op default
- skip timer-driven runs of the gated tasks on instances without the lease
- treat an unreachable Redis as holding the lease so tasks never stop running
- leave manual and API-triggered runs ungated
Live stream ownership is only tracked in process memory, so no replica can tell
which pod holds a stream open once that pod is gone.
- persist a LiveStreamSession record when the live stream mappings change
- delete the record when the stream is closed
- keep closing the stream when the store is unreachable
A transcode that is not in the store is invisible to the other replicas, so the
HLS entry points have to publish and hold the lease themselves.
- register the play session in the store when ffmpeg starts
- renew the lease on a background loop and delete the session when it ends
- shorten segments and bound the playlist window when resuming a stored session
- add RecoverySegmentLengthSeconds and RecoverySegmentBufferCount encoding options
Every replica runs the transcode cleanup task against the same shared transcode
directory, so one pod deletes segments another pod is still streaming.
- read the active sessions from ITranscodeSessionStore before deleting
- keep files matching an active session manifest path or segment prefix
- abort the sweep when the store cannot be reached
Transcode state lives only in the process that started ffmpeg, so a pod restart
drops every in-flight HLS stream with no way for a peer to pick it up.
- add ITranscodeSessionStore plus the TranscodeSession and LiveStreamSession records
- add RedisTranscodeSessionStore with TTL leases and an atomic Lua takeover script
- add NullTranscodeSessionStore for single-instance deployments
- pick the store from Jellyfin:TranscodeStore:RedisConnectionString at startup
Existing installs hold their whole library in jellyfin.db, so switching to the
PostgreSQL provider needs a one-shot data move.
- add Jellyfin.DbMigrator reading SQLite tables and bulk-writing them to PostgreSQL
- validate table names against the source schema before generating SQL
- upload a pre-migration copy of the SQLite file to S3 when S3_BACKUP_BUCKET is set
- emit a per-table row-count report and support a dry-run mode
Multi-replica deployments cannot share a SQLite file, so the database has to
move to a server engine before the rest of the HA work is usable.
- add Jellyfin.Database.Providers.PostgreSQL with an EF Core Npgsql provider
- register the provider and a pooled NpgsqlDataSource when DatabaseType is Jellyfin-PostgreSQL
- accept postgresql:// URIs and POSTGRES_CONNECTION_STRING alongside CustomProviderOptions
- add container-backed provider, CRUD, concurrency and migration tests