< 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: 95
Coverable lines: 95
Total lines: 297
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 10/25/2025 - 12:09:58 AM Line coverage: 0% (0/95) Branch coverage: 0% (0/16) Total lines: 2961/19/2026 - 12:13:54 AM Line coverage: 0% (0/95) Branch coverage: 0% (0/16) Total lines: 297 10/25/2025 - 12:09:58 AM Line coverage: 0% (0/95) Branch coverage: 0% (0/16) Total lines: 2961/19/2026 - 12:13:54 AM Line coverage: 0% (0/95) Branch coverage: 0% (0/16) Total lines: 297

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>
 20public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
 21{
 22    private readonly ILogger<PessimisticLockBehavior> _logger;
 23    private readonly ILoggerFactory _loggerFactory;
 24
 25    /// <summary>
 26    /// Initializes a new instance of the <see cref="PessimisticLockBehavior"/> class.
 27    /// </summary>
 28    /// <param name="logger">The application logger.</param>
 29    /// <param name="loggerFactory">The logger factory.</param>
 30    public PessimisticLockBehavior(ILogger<PessimisticLockBehavior> logger, ILoggerFactory loggerFactory)
 31    {
 032        _logger = logger;
 033        _loggerFactory = loggerFactory;
 034    }
 35
 036    private static ReaderWriterLockSlim DatabaseLock { get; } = new(LockRecursionPolicy.SupportsRecursion);
 37
 38    /// <inheritdoc/>
 39    public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
 40    {
 041        using (DbLock.EnterWrite(_logger))
 42        {
 043            saveChanges();
 044        }
 045    }
 46
 47    /// <inheritdoc/>
 48    public void Initialise(DbContextOptionsBuilder optionsBuilder)
 49    {
 050        _logger.LogInformation("The database locking mode has been set to: Pessimistic.");
 051        optionsBuilder.AddInterceptors(new CommandLockingInterceptor(_loggerFactory.CreateLogger<CommandLockingIntercept
 052        optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_loggerFactory.CreateLogger<TransactionLockingI
 053    }
 54
 55    /// <inheritdoc/>
 56    public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
 57    {
 58        using (DbLock.EnterWrite(_logger))
 59        {
 60            await saveChanges().ConfigureAwait(false);
 61        }
 62    }
 63
 64    private sealed class TransactionLockingInterceptor : DbTransactionInterceptor
 65    {
 66        private readonly ILogger _logger;
 67
 068        public TransactionLockingInterceptor(ILogger logger)
 69        {
 070            _logger = logger;
 071        }
 72
 73        public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStarti
 74        {
 075            DbLock.BeginWriteLock(_logger);
 76
 077            return base.TransactionStarting(connection, eventData, result);
 78        }
 79
 80        public override ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connection, T
 81        {
 082            DbLock.BeginWriteLock(_logger);
 83
 084            return base.TransactionStartingAsync(connection, eventData, result, cancellationToken);
 85        }
 86
 87        public override void TransactionCommitted(DbTransaction transaction, TransactionEndEventData eventData)
 88        {
 089            DbLock.EndWriteLock(_logger);
 90
 091            base.TransactionCommitted(transaction, eventData);
 092        }
 93
 94        public override Task TransactionCommittedAsync(DbTransaction transaction, TransactionEndEventData eventData, Can
 95        {
 096            DbLock.EndWriteLock(_logger);
 97
 098            return base.TransactionCommittedAsync(transaction, eventData, cancellationToken);
 99        }
 100
 101        public override void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData)
 102        {
 0103            DbLock.EndWriteLock(_logger);
 104
 0105            base.TransactionFailed(transaction, eventData);
 0106        }
 107
 108        public override Task TransactionFailedAsync(DbTransaction transaction, TransactionErrorEventData eventData, Canc
 109        {
 0110            DbLock.EndWriteLock(_logger);
 111
 0112            return base.TransactionFailedAsync(transaction, eventData, cancellationToken);
 113        }
 114
 115        public override void TransactionRolledBack(DbTransaction transaction, TransactionEndEventData eventData)
 116        {
 0117            DbLock.EndWriteLock(_logger);
 118
 0119            base.TransactionRolledBack(transaction, eventData);
 0120        }
 121
 122        public override Task TransactionRolledBackAsync(DbTransaction transaction, TransactionEndEventData eventData, Ca
 123        {
 0124            DbLock.EndWriteLock(_logger);
 125
 0126            return base.TransactionRolledBackAsync(transaction, eventData, cancellationToken);
 127        }
 128    }
 129
 130    /// <summary>
 131    /// Adds strict read/write locking.
 132    /// </summary>
 133    private sealed class CommandLockingInterceptor : DbCommandInterceptor
 134    {
 135        private readonly ILogger _logger;
 136
 0137        public CommandLockingInterceptor(ILogger logger)
 138        {
 0139            _logger = logger;
 0140        }
 141
 142        public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, Interce
 143        {
 0144            using (DbLock.EnterWrite(_logger, command))
 145            {
 0146                return InterceptionResult<int>.SuppressWithResult(command.ExecuteNonQuery());
 147            }
 0148        }
 149
 150        public override async ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventD
 151        {
 152            using (DbLock.EnterWrite(_logger, command))
 153            {
 154                return InterceptionResult<int>.SuppressWithResult(await command.ExecuteNonQueryAsync(cancellationToken).
 155            }
 156        }
 157
 158        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, Interc
 159        {
 0160            using (DbLock.EnterRead(_logger))
 161            {
 0162                return InterceptionResult<object>.SuppressWithResult(command.ExecuteScalar()!);
 163            }
 0164        }
 165
 166        public override async ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEvent
 167        {
 168            using (DbLock.EnterRead(_logger))
 169            {
 170                return InterceptionResult<object>.SuppressWithResult((await command.ExecuteScalarAsync(cancellationToken
 171            }
 172        }
 173
 174        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, 
 175        {
 0176            using (DbLock.EnterRead(_logger))
 177            {
 0178                return InterceptionResult<DbDataReader>.SuppressWithResult(command.ExecuteReader()!);
 179            }
 0180        }
 181
 182        public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, Comman
 183        {
 184            using (DbLock.EnterRead(_logger))
 185            {
 186                return InterceptionResult<DbDataReader>.SuppressWithResult(await command.ExecuteReaderAsync(cancellation
 187            }
 188        }
 189    }
 190
 191    private sealed class DbLock : IDisposable
 192    {
 193        private readonly Action? _action;
 194        private bool _disposed;
 195
 0196        private static readonly IDisposable _noLock = new DbLock(null) { _disposed = true };
 197        private static (string Command, Guid Id, DateTimeOffset QueryDate, bool Printed) _blockQuery;
 198
 199        public DbLock(Action? action = null)
 200        {
 0201            _action = action;
 0202        }
 203
 204#pragma warning disable IDISP015 // Member should not return created and cached instance
 205        public static IDisposable EnterWrite(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? call
 206#pragma warning restore IDISP015 // Member should not return created and cached instance
 207        {
 0208            logger.LogTrace("Enter Write for {Caller}:{Line}", callerMemberName, callerNo);
 0209            if (DatabaseLock.IsWriteLockHeld)
 210            {
 0211                logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
 0212                return _noLock;
 213            }
 214
 0215            BeginWriteLock(logger, command, callerMemberName, callerNo);
 0216            return new DbLock(() =>
 0217            {
 0218                EndWriteLock(logger, callerMemberName, callerNo);
 0219            });
 220        }
 221
 222#pragma warning disable IDISP015 // Member should not return created and cached instance
 223        public static IDisposable EnterRead(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerL
 224#pragma warning restore IDISP015 // Member should not return created and cached instance
 225        {
 0226            logger.LogTrace("Enter Read {Caller}:{Line}", callerMemberName, callerNo);
 0227            if (DatabaseLock.IsWriteLockHeld)
 228            {
 0229                logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
 0230                return _noLock;
 231            }
 232
 0233            BeginReadLock(logger, callerMemberName, callerNo);
 0234            return new DbLock(() =>
 0235            {
 0236                ExitReadLock(logger, callerMemberName, callerNo);
 0237            });
 238        }
 239
 240        public static void BeginWriteLock(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? callerM
 241        {
 0242            logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo);
 0243            if (!DatabaseLock.TryEnterWriteLock(TimeSpan.FromMilliseconds(1000)))
 244            {
 0245                var blockingQuery = _blockQuery;
 0246                if (!blockingQuery.Printed)
 247                {
 0248                    _blockQuery = (blockingQuery.Command, blockingQuery.Id, blockingQuery.QueryDate, true);
 0249                    logger.LogInformation("QueryLock: {Id} --- {Query}", blockingQuery.Id, blockingQuery.Command);
 250                }
 251
 0252                logger.LogInformation("Query congestion detected: '{Id}' since '{Date}'", blockingQuery.Id, blockingQuer
 253
 0254                DatabaseLock.EnterWriteLock();
 255
 0256                logger.LogInformation("Query congestion cleared: '{Id}' for '{Date}'", blockingQuery.Id, DateTimeOffset.
 257            }
 258
 0259            _blockQuery = (command?.CommandText ?? "Transaction", Guid.NewGuid(), DateTimeOffset.Now, false);
 260
 0261            logger.LogTrace("Write Aquired {Caller}:{Line}", callerMemberName, callerNo);
 0262        }
 263
 264        public static void BeginReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLine
 265        {
 0266            logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo);
 0267            DatabaseLock.EnterReadLock();
 0268            logger.LogTrace("Read Aquired {Caller}:{Line}", callerMemberName, callerNo);
 0269        }
 270
 271        public static void EndWriteLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineN
 272        {
 0273            logger.LogTrace("Release Write {Caller}:{Line}", callerMemberName, callerNo);
 0274            DatabaseLock.ExitWriteLock();
 0275        }
 276
 277        public static void ExitReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineN
 278        {
 0279            logger.LogTrace("Release Read {Caller}:{Line}", callerMemberName, callerNo);
 0280            DatabaseLock.ExitReadLock();
 0281        }
 282
 283        public void Dispose()
 284        {
 0285            if (_disposed)
 286            {
 0287                return;
 288            }
 289
 0290            _disposed = true;
 0291            if (_action is not null)
 292            {
 0293                _action();
 294            }
 0295        }
 296    }
 297}

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)
.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>)
ScalarExecuting(System.Data.Common.DbCommand,Microsoft.EntityFrameworkCore.Diagnostics.CommandEventData,Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult`1<System.Object>)
ReaderExecuting(System.Data.Common.DbCommand,Microsoft.EntityFrameworkCore.Diagnostics.CommandEventData,Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult`1<System.Data.Common.DbDataReader>)
.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()