The six plain-bool lock flags (Plex, Trakt, Emby/Jellyfin/Plex collections, troubleshooting playback) used a non-atomic check-then-set, so two concurrent Lock* callers could both win. Convert them to int flags mutated only via Interlocked.CompareExchange, so the caller that wins the 0->1 transition is the sole owner and the only one that fires the change event. The three ConcurrentDictionary-backed kinds (Library/Playout/RemoteMediaSource) were already atomic; drop their redundant ContainsKey pre-checks. Define the ownership contract (tokenless single-owner discipline, no interface change) on IEntityLocker and in docs/decisions.md: a true from Lock* confers ownership of exactly one release; Unlock* on an unlocked slot returns false, fires no event, and logs a warning (the double-release / non-owner tripwire). Adds EntityLockerTests (real locker, parallel-caller races) proving exactly one winner per kind, one-releaser-per-slot, and event-fires-once-per-transition. Ref #231. Scan-lifecycle call-site fixes that consume this contract land in the same PR (#232); the BuildPlayout/subtitle finally-gating is #234. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.5 KiB
C#
55 lines
2.5 KiB
C#
namespace ErsatzTV.Core.Interfaces.Locking;
|
|
|
|
/// <summary>
|
|
/// Advisory "an operation on entity X is in progress" mutex layer, registered as a process-wide singleton.
|
|
/// Each lock kind is a single exclusive slot (not reentrant or counted): a second acquire while held returns false.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Ownership contract (see docs/decisions.md 2026-07-11, issue #231): every <c>Lock*</c> call returns
|
|
/// <c>true</c> iff it transitioned the slot to locked. A <c>true</c> confers ownership of exactly one release —
|
|
/// performed either in the acquiring scope (release in a <c>finally</c> gated on the captured return value) or by
|
|
/// the single designated releaser it hands off to (the consumer of a message enqueued while holding the lock, with
|
|
/// a compensating unlock if the enqueue itself throws). Callers must never release a lock they did not acquire.
|
|
/// <c>Unlock*</c> on an already-unlocked slot is a no-op that returns <c>false</c>, raises no event, and logs a
|
|
/// warning — the tripwire for double-release / non-owner-release bugs; it deliberately does not throw, so the
|
|
/// primitive stays safe to call from <c>finally</c> paths.
|
|
/// </remarks>
|
|
public interface IEntityLocker
|
|
{
|
|
event EventHandler OnLibraryChanged;
|
|
event EventHandler OnPlexChanged;
|
|
event EventHandler<Type> OnRemoteMediaSourceChanged;
|
|
event EventHandler OnTraktChanged;
|
|
event EventHandler OnEmbyCollectionsChanged;
|
|
event EventHandler OnJellyfinCollectionsChanged;
|
|
event EventHandler OnPlexCollectionsChanged;
|
|
event EventHandler OnTroubleshootingPlaybackChanged;
|
|
bool LockLibrary(int libraryId);
|
|
bool UnlockLibrary(int libraryId);
|
|
bool IsLibraryLocked(int libraryId);
|
|
bool LockPlex();
|
|
bool UnlockPlex();
|
|
bool IsPlexLocked();
|
|
bool IsRemoteMediaSourceLocked<TMediaSource>();
|
|
bool LockRemoteMediaSource<TMediaSource>();
|
|
bool UnlockRemoteMediaSource<TMediaSource>();
|
|
bool IsTraktLocked();
|
|
bool LockTrakt();
|
|
bool UnlockTrakt();
|
|
bool LockEmbyCollections();
|
|
bool UnlockEmbyCollections();
|
|
bool AreEmbyCollectionsLocked();
|
|
bool LockJellyfinCollections();
|
|
bool UnlockJellyfinCollections();
|
|
bool AreJellyfinCollectionsLocked();
|
|
bool LockPlexCollections();
|
|
bool UnlockPlexCollections();
|
|
bool ArePlexCollectionsLocked();
|
|
Task<bool> LockPlayout(int playoutId);
|
|
Task<bool> UnlockPlayout(int playoutId);
|
|
bool IsPlayoutLocked(int playoutId);
|
|
bool LockTroubleshootingPlayback();
|
|
bool UnlockTroubleshootingPlayback();
|
|
bool IsTroubleshootingPlaybackLocked();
|
|
}
|