UserDataManager cache causes lost updates across replicas #13
Reference in New Issue
Block a user
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?
Context: multi-replica stateless deployment (two 2-replica apps,
cheeztvandfafflix, each one PostgreSQL plus valkey, no sticky sessions). Not a 12.0 regression — live in the deployed build. Tracking: #12.Problem
Emby.Server.Implementations/Library/UserDataManager.cs:30holds an LRU cache of user data. Reads at:189return the cached entry with no freshness comparison against the database; writes go through:82.With two replicas and no sticky sessions, pod B serves a stale resume position out of its own cache and then writes it back over pod A's newer value. Resume positions, played/unplayed state, favourites and ratings are silently lost. This is user-visible data loss, happening in production now.
Proposal
Cache-aside with a real invalidation signal: either a
RowVersion/concurrency token compared on read, or valkey pub/sub fan-out onSaveUserData.Alternatively drop the cache entirely, as was done for
DeviceManager, where the measured cost was one indexed single-row SELECT per request. The same may well be acceptable here, but this path is hotter (a SELECT per playback progress tick), so the trade-off deserves measuring before the cache is removed.LibraryManager's item cache (#19) needs the same invalidation mechanism — likely one piece of work with this.Acceptance
UserDataManagerinstances against real PostgreSQL: A writes a new resume position; B, which has already cached the old value, reads the new one; a subsequent write from B does not revert A's value.PostgreSqlTestServerintests/Jellyfin.Server.Testsand runs in thepostgres-migration-chainWoodpecker step (JELLYFIN_TEST_POSTGRES). In-memory providers have hidden different semantics and are how three bugs reached production, so an in-memory-only test does not close this.