< Summary - Jellyfin

Information
Class: Jellyfin.Database.Implementations.Locking.PessimisticLockBehavior
Assembly: Jellyfin.Database.Implementations
File(s): /srv/git/jellyfin/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 109
Coverable lines: 109
Total lines: 305
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 16
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/6/2026 - 12:15:23 AM Line coverage: 0% (0/108) Branch coverage: 0% (0/16) Total lines: 2978/9/2026 - 12:16:58 AM Line coverage: 0% (0/109) Branch coverage: 0% (0/16) Total lines: 305 5/6/2026 - 12:15:23 AM Line coverage: 0% (0/108) Branch coverage: 0% (0/16) Total lines: 2978/9/2026 - 12:16:58 AM Line coverage: 0% (0/109) Branch coverage: 0% (0/16) Total lines: 305

Coverage delta

Coverage delta 1 -1

Metrics

File(s)

/srv/git/jellyfin/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs

#LineLine coverage
 1#pragma warning disable MT1013 // Releasing lock without guarantee of execution
 2#pragma warning disable MT1012 // Acquiring lock without guarantee of releasing
 3#pragma warning disable CA1873
 4
 5using System;
 6using System.Data;
 7using System.Data.Common;
 8using System.Runtime.CompilerServices;
 9using System.Threading;
 10using System.Threading.Tasks;
 11using Microsoft.EntityFrameworkCore;
 12using Microsoft.EntityFrameworkCore.Diagnostics;
 13using Microsoft.Extensions.Logging;
 14
 15namespace Jellyfin.Database.Implementations.Locking;
 16
 17/// <summary>
 18/// A locking behavior that will always block any operation while a write is requested. Mimicks the old SqliteRepository
 19/// </summary>
 20/// <remarks>
 21/// Unsafe with asynchronous transactions; because <see cref="ReaderWriterLockSlim"/> is
 22/// thread-affine, holding it from <c>TransactionStarting</c> to <c>TransactionCommitted</c>
 23/// works only while continuations resume inline. A genuinely-async continuation inside a
 24/// transaction releases on another thread, throwing
 25/// <see cref="SynchronizationLockException"/> or deadlocking a later write.
 26/// </remarks>
 27public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
 28{
 29    private readonly ILogger<PessimisticLockBehavior> _logger;
 30    private readonly ILoggerFactory _loggerFactory;
 31
 32    /// <summary>
 33    /// Initializes a new instance of the <see cref="PessimisticLockBehavior"/> class.
 34    /// </summary>
 35    /// <param name="logger">The application logger.</param>
 36    /// <param name="loggerFactory">The logger factory.</param>
 37    public PessimisticLockBehavior(ILogger<PessimisticLockBehavior> logger, ILoggerFactory loggerFactory)
 38    {
 039        _logger = logger;
 040        _loggerFactory = loggerFactory;
 041    }
 42
 043    private static ReaderWriterLockSlim DatabaseLock { get; } = new(LockRecursionPolicy.SupportsRecursion);
 44
 45    /// <inheritdoc/>
 46    public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
 47    {
 048        using (DbLock.EnterWrite(_logger))
 49        {
 050            saveChanges();
 051        }
 052    }
 53
 54    /// <inheritdoc/>
 55    public void Initialise(DbContextOptionsBuilder optionsBuilder)
 56    {
 057        _logger.LogWarning(
 058                   "The database locking mode has been set to: Pessimistic. This mode is not safe with asynchronous tran
 059        optionsBuilder.AddInterceptors(new CommandLockingInterceptor(_loggerFactory.CreateLogger<CommandLockingIntercept
 060        optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_loggerFactory.CreateLogger<TransactionLockingI
 061    }
 62
 63    /// <inheritdoc/>
 64    public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
 65    {
 066        using (DbLock.EnterWrite(_logger))
 67        {
 068            await saveChanges().ConfigureAwait(false);
 069        }
 070    }
 71
 72    private sealed class TransactionLockingInterceptor : DbTransactionInterceptor
 73    {
 74        private readonly ILogger _logger;
 75
 076        public TransactionLockingInterceptor(ILogger logger)
 77        {
 078            _logger = logger;
 079        }
 80
 81        public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStarti
 82        {
 083            DbLock.BeginWriteLock(_logger);
 84
 085            return base.TransactionStarting(connection, eventData, result);
 86        }
 87
 88        public override ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connection, T
 89        {
 090            DbLock.BeginWriteLock(_logger);
 91
 092            return base.TransactionStartingAsync(connection, eventData, result, cancellationToken);
 93        }
 94
 95        public override void TransactionCommitted(DbTransaction transaction, TransactionEndEventData eventData)
 96        {
 097            DbLock.EndWriteLock(_logger);
 98
 099            base.TransactionCommitted(transaction, eventData);
 0100        }
 101
 102        public override Task TransactionCommittedAsync(DbTransaction transaction, TransactionEndEventData eventData, Can
 103        {
 0104            DbLock.EndWriteLock(_logger);
 105
 0106            return base.TransactionCommittedAsync(transaction, eventData, cancellationToken);
 107        }
 108
 109        public override void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData)
 110        {
 0111            DbLock.EndWriteLock(_logger);
 112
 0113            base.TransactionFailed(transaction, eventData);
 0114        }
 115
 116        public override Task TransactionFailedAsync(DbTransaction transaction, TransactionErrorEventData eventData, Canc
 117        {
 0118            DbLock.EndWriteLock(_logger);
 119
 0120            return base.TransactionFailedAsync(transaction, eventData, cancellationToken);
 121        }
 122
 123        public override void TransactionRolledBack(DbTransaction transaction, TransactionEndEventData eventData)
 124        {
 0125            DbLock.EndWriteLock(_logger);
 126
 0127            base.TransactionRolledBack(transaction, eventData);
 0128        }
 129
 130        public override Task TransactionRolledBackAsync(DbTransaction transaction, TransactionEndEventData eventData, Ca
 131        {
 0132            DbLock.EndWriteLock(_logger);
 133
 0134            return base.TransactionRolledBackAsync(transaction, eventData, cancellationToken);
 135        }
 136    }
 137
 138    /// <summary>
 139    /// Adds strict read/write locking.
 140    /// </summary>
 141    private sealed class CommandLockingInterceptor : DbCommandInterceptor
 142    {
 143        private readonly ILogger _logger;
 144
 0145        public CommandLockingInterceptor(ILogger logger)
 146        {
 0147            _logger = logger;
 0148        }
 149
 150        public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, Interce
 151        {
 0152            using (DbLock.EnterWrite(_logger, command))
 153            {
 0154                return InterceptionResult<int>.SuppressWithResult(command.ExecuteNonQuery());
 155            }
 0156        }
 157
 158        public override async ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventD
 159        {
 0160            using (DbLock.EnterWrite(_logger, command))
 161            {
 0162                return InterceptionResult<int>.SuppressWithResult(await command.ExecuteNonQueryAsync(cancellationToken).
 163            }
 0164        }
 165
 166        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, Interc
 167        {
 0168            using (DbLock.EnterRead(_logger))
 169            {
 0170                return InterceptionResult<object>.SuppressWithResult(command.ExecuteScalar()!);
 171            }
 0172        }
 173
 174        public override async ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEvent
 175        {
 0176            using (DbLock.EnterRead(_logger))
 177            {
 0178                return InterceptionResult<object>.SuppressWithResult((await command.ExecuteScalarAsync(cancellationToken
 179            }
 0180        }
 181
 182        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, 
 183        {
 0184            using (DbLock.EnterRead(_logger))
 185            {
 0186                return InterceptionResult<DbDataReader>.SuppressWithResult(command.ExecuteReader()!);
 187            }
 0188        }
 189
 190        public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, Comman
 191        {
 0192            using (DbLock.EnterRead(_logger))
 193            {
 0194                return InterceptionResult<DbDataReader>.SuppressWithResult(await command.ExecuteReaderAsync(cancellation
 195            }
 0196        }
 197    }
 198
 199    private sealed class DbLock : IDisposable
 200    {
 201        private readonly Action? _action;
 202        private bool _disposed;
 203
 0204        private static readonly IDisposable _noLock = new DbLock(null) { _disposed = true };
 205        private static (string Command, Guid Id, DateTimeOffset QueryDate, bool Printed) _blockQuery;
 206
 207        public DbLock(Action? action = null)
 208        {
 0209            _action = action;
 0210        }
 211
 212#pragma warning disable IDISP015 // Member should not return created and cached instance
 213        public static IDisposable EnterWrite(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? call
 214#pragma warning restore IDISP015 // Member should not return created and cached instance
 215        {
 0216            logger.LogTrace("Enter Write for {Caller}:{Line}", callerMemberName, callerNo);
 0217            if (DatabaseLock.IsWriteLockHeld)
 218            {
 0219                logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
 0220                return _noLock;
 221            }
 222
 0223            BeginWriteLock(logger, command, callerMemberName, callerNo);
 0224            return new DbLock(() =>
 0225            {
 0226                EndWriteLock(logger, callerMemberName, callerNo);
 0227            });
 228        }
 229
 230#pragma warning disable IDISP015 // Member should not return created and cached instance
 231        public static IDisposable EnterRead(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerL
 232#pragma warning restore IDISP015 // Member should not return created and cached instance
 233        {
 0234            logger.LogTrace("Enter Read {Caller}:{Line}", callerMemberName, callerNo);
 0235            if (DatabaseLock.IsWriteLockHeld)
 236            {
 0237                logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
 0238                return _noLock;
 239            }
 240
 0241            BeginReadLock(logger, callerMemberName, callerNo);
 0242            return new DbLock(() =>
 0243            {
 0244                ExitReadLock(logger, callerMemberName, callerNo);
 0245            });
 246        }
 247
 248        public static void BeginWriteLock(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? callerM
 249        {
 0250            logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo);
 0251            if (!DatabaseLock.TryEnterWriteLock(TimeSpan.FromMilliseconds(1000)))
 252            {
 0253                var blockingQuery = _blockQuery;
 0254                if (!blockingQuery.Printed)
 255                {
 0256                    _blockQuery = (blockingQuery.Command, blockingQuery.Id, blockingQuery.QueryDate, true);
 0257                    logger.LogInformation("QueryLock: {Id} --- {Query}", blockingQuery.Id, blockingQuery.Command);
 258                }
 259
 0260                logger.LogInformation("Query congestion detected: '{Id}' since '{Date}'", blockingQuery.Id, blockingQuer
 261
 0262                DatabaseLock.EnterWriteLock();
 263
 0264                logger.LogInformation("Query congestion cleared: '{Id}' for '{Date}'", blockingQuery.Id, DateTimeOffset.
 265            }
 266
 0267            _blockQuery = (command?.CommandText ?? "Transaction", Guid.NewGuid(), DateTimeOffset.Now, false);
 268
 0269            logger.LogTrace("Write Aquired {Caller}:{Line}", callerMemberName, callerNo);
 0270        }
 271
 272        public static void BeginReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLine
 273        {
 0274            logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo);
 0275            DatabaseLock.EnterReadLock();
 0276            logger.LogTrace("Read Aquired {Caller}:{Line}", callerMemberName, callerNo);
 0277        }
 278
 279        public static void EndWriteLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineN
 280        {
 0281            logger.LogTrace("Release Write {Caller}:{Line}", callerMemberName, callerNo);
 0282            DatabaseLock.ExitWriteLock();
 0283        }
 284
 285        public static void ExitReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineN
 286        {
 0287            logger.LogTrace("Release Read {Caller}:{Line}", callerMemberName, callerNo);
 0288            DatabaseLock.ExitReadLock();
 0289        }
 290
 291        public void Dispose()
 292        {
 0293            if (_disposed)
 294            {
 0295                return;
 296            }
 297
 0298            _disposed = true;
 0299            if (_action is not null)
 300            {
 0301                _action();
 302            }
 0303        }
 304    }
 305}

Methods/Properties

.ctor(Microsoft.Extensions.Logging.ILogger`1<Jellyfin.Database.Implementations.Locking.PessimisticLockBehavior>,Microsoft.Extensions.Logging.ILoggerFactory)
.cctor()
OnSaveChanges(Jellyfin.Database.Implementations.JellyfinDbContext,System.Action)
Initialise(Microsoft.EntityFrameworkCore.DbContextOptionsBuilder)
OnSaveChangesAsync()
.ctor(Microsoft.Extensions.Logging.ILogger)
TransactionStarting(System.Data.Common.DbConnection,Microsoft.EntityFrameworkCore.Diagnostics.TransactionStartingEventData,Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult`1<System.Data.Common.DbTransaction>)
TransactionStartingAsync(System.Data.Common.DbConnection,Microsoft.EntityFrameworkCore.Diagnostics.TransactionStartingEventData,Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult`1<System.Data.Common.DbTransaction>,System.Threading.CancellationToken)
TransactionCommitted(System.Data.Common.DbTransaction,Microsoft.EntityFrameworkCore.Diagnostics.TransactionEndEventData)
TransactionCommittedAsync(System.Data.Common.DbTransaction,Microsoft.EntityFrameworkCore.Diagnostics.TransactionEndEventData,System.Threading.CancellationToken)
TransactionFailed(System.Data.Common.DbTransaction,Microsoft.EntityFrameworkCore.Diagnostics.TransactionErrorEventData)
TransactionFailedAsync(System.Data.Common.DbTransaction,Microsoft.EntityFrameworkCore.Diagnostics.TransactionErrorEventData,System.Threading.CancellationToken)
TransactionRolledBack(System.Data.Common.DbTransaction,Microsoft.EntityFrameworkCore.Diagnostics.TransactionEndEventData)
TransactionRolledBackAsync(System.Data.Common.DbTransaction,Microsoft.EntityFrameworkCore.Diagnostics.TransactionEndEventData,System.Threading.CancellationToken)
.ctor(Microsoft.Extensions.Logging.ILogger)
NonQueryExecuting(System.Data.Common.DbCommand,Microsoft.EntityFrameworkCore.Diagnostics.CommandEventData,Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult`1<System.Int32>)
NonQueryExecutingAsync()
ScalarExecuting(System.Data.Common.DbCommand,Microsoft.EntityFrameworkCore.Diagnostics.CommandEventData,Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult`1<System.Object>)
ScalarExecutingAsync()
ReaderExecuting(System.Data.Common.DbCommand,Microsoft.EntityFrameworkCore.Diagnostics.CommandEventData,Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult`1<System.Data.Common.DbDataReader>)
ReaderExecutingAsync()
.cctor()
.ctor(System.Action)
EnterWrite(Microsoft.Extensions.Logging.ILogger,System.Data.IDbCommand,System.String,System.Nullable`1<System.Int32>)
EnterRead(Microsoft.Extensions.Logging.ILogger,System.String,System.Nullable`1<System.Int32>)
BeginWriteLock(Microsoft.Extensions.Logging.ILogger,System.Data.IDbCommand,System.String,System.Nullable`1<System.Int32>)
BeginReadLock(Microsoft.Extensions.Logging.ILogger,System.String,System.Nullable`1<System.Int32>)
EndWriteLock(Microsoft.Extensions.Logging.ILogger,System.String,System.Nullable`1<System.Int32>)
ExitReadLock(Microsoft.Extensions.Logging.ILogger,System.String,System.Nullable`1<System.Int32>)
Dispose()