< 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: 296
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

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
 4using System;
 5using System.Data;
 6using System.Data.Common;
 7using System.Runtime.CompilerServices;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Microsoft.EntityFrameworkCore;
 11using Microsoft.EntityFrameworkCore.Diagnostics;
 12using Microsoft.Extensions.Logging;
 13
 14namespace Jellyfin.Database.Implementations.Locking;
 15
 16/// <summary>
 17/// A locking behavior that will always block any operation while a write is requested. Mimicks the old SqliteRepository
 18/// </summary>
 19public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
 20{
 21    private readonly ILogger<PessimisticLockBehavior> _logger;
 22    private readonly ILoggerFactory _loggerFactory;
 23
 24    /// <summary>
 25    /// Initializes a new instance of the <see cref="PessimisticLockBehavior"/> class.
 26    /// </summary>
 27    /// <param name="logger">The application logger.</param>
 28    /// <param name="loggerFactory">The logger factory.</param>
 29    public PessimisticLockBehavior(ILogger<PessimisticLockBehavior> logger, ILoggerFactory loggerFactory)
 30    {
 031        _logger = logger;
 032        _loggerFactory = loggerFactory;
 033    }
 34
 035    private static ReaderWriterLockSlim DatabaseLock { get; } = new(LockRecursionPolicy.SupportsRecursion);
 36
 37    /// <inheritdoc/>
 38    public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
 39    {
 040        using (DbLock.EnterWrite(_logger))
 41        {
 042            saveChanges();
 043        }
 044    }
 45
 46    /// <inheritdoc/>
 47    public void Initialise(DbContextOptionsBuilder optionsBuilder)
 48    {
 049        _logger.LogInformation("The database locking mode has been set to: Pessimistic.");
 050        optionsBuilder.AddInterceptors(new CommandLockingInterceptor(_loggerFactory.CreateLogger<CommandLockingIntercept
 051        optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_loggerFactory.CreateLogger<TransactionLockingI
 052    }
 53
 54    /// <inheritdoc/>
 55    public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
 56    {
 57        using (DbLock.EnterWrite(_logger))
 58        {
 59            await saveChanges().ConfigureAwait(false);
 60        }
 61    }
 62
 63    private sealed class TransactionLockingInterceptor : DbTransactionInterceptor
 64    {
 65        private readonly ILogger _logger;
 66
 067        public TransactionLockingInterceptor(ILogger logger)
 68        {
 069            _logger = logger;
 070        }
 71
 72        public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStarti
 73        {
 074            DbLock.BeginWriteLock(_logger);
 75
 076            return base.TransactionStarting(connection, eventData, result);
 77        }
 78
 79        public override ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connection, T
 80        {
 081            DbLock.BeginWriteLock(_logger);
 82
 083            return base.TransactionStartingAsync(connection, eventData, result, cancellationToken);
 84        }
 85
 86        public override void TransactionCommitted(DbTransaction transaction, TransactionEndEventData eventData)
 87        {
 088            DbLock.EndWriteLock(_logger);
 89
 090            base.TransactionCommitted(transaction, eventData);
 091        }
 92
 93        public override Task TransactionCommittedAsync(DbTransaction transaction, TransactionEndEventData eventData, Can
 94        {
 095            DbLock.EndWriteLock(_logger);
 96
 097            return base.TransactionCommittedAsync(transaction, eventData, cancellationToken);
 98        }
 99
 100        public override void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData)
 101        {
 0102            DbLock.EndWriteLock(_logger);
 103
 0104            base.TransactionFailed(transaction, eventData);
 0105        }
 106
 107        public override Task TransactionFailedAsync(DbTransaction transaction, TransactionErrorEventData eventData, Canc
 108        {
 0109            DbLock.EndWriteLock(_logger);
 110
 0111            return base.TransactionFailedAsync(transaction, eventData, cancellationToken);
 112        }
 113
 114        public override void TransactionRolledBack(DbTransaction transaction, TransactionEndEventData eventData)
 115        {
 0116            DbLock.EndWriteLock(_logger);
 117
 0118            base.TransactionRolledBack(transaction, eventData);
 0119        }
 120
 121        public override Task TransactionRolledBackAsync(DbTransaction transaction, TransactionEndEventData eventData, Ca
 122        {
 0123            DbLock.EndWriteLock(_logger);
 124
 0125            return base.TransactionRolledBackAsync(transaction, eventData, cancellationToken);
 126        }
 127    }
 128
 129    /// <summary>
 130    /// Adds strict read/write locking.
 131    /// </summary>
 132    private sealed class CommandLockingInterceptor : DbCommandInterceptor
 133    {
 134        private readonly ILogger _logger;
 135
 0136        public CommandLockingInterceptor(ILogger logger)
 137        {
 0138            _logger = logger;
 0139        }
 140
 141        public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, Interce
 142        {
 0143            using (DbLock.EnterWrite(_logger, command))
 144            {
 0145                return InterceptionResult<int>.SuppressWithResult(command.ExecuteNonQuery());
 146            }
 0147        }
 148
 149        public override async ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventD
 150        {
 151            using (DbLock.EnterWrite(_logger, command))
 152            {
 153                return InterceptionResult<int>.SuppressWithResult(await command.ExecuteNonQueryAsync(cancellationToken).
 154            }
 155        }
 156
 157        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, Interc
 158        {
 0159            using (DbLock.EnterRead(_logger))
 160            {
 0161                return InterceptionResult<object>.SuppressWithResult(command.ExecuteScalar()!);
 162            }
 0163        }
 164
 165        public override async ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEvent
 166        {
 167            using (DbLock.EnterRead(_logger))
 168            {
 169                return InterceptionResult<object>.SuppressWithResult((await command.ExecuteScalarAsync(cancellationToken
 170            }
 171        }
 172
 173        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, 
 174        {
 0175            using (DbLock.EnterRead(_logger))
 176            {
 0177                return InterceptionResult<DbDataReader>.SuppressWithResult(command.ExecuteReader()!);
 178            }
 0179        }
 180
 181        public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, Comman
 182        {
 183            using (DbLock.EnterRead(_logger))
 184            {
 185                return InterceptionResult<DbDataReader>.SuppressWithResult(await command.ExecuteReaderAsync(cancellation
 186            }
 187        }
 188    }
 189
 190    private sealed class DbLock : IDisposable
 191    {
 192        private readonly Action? _action;
 193        private bool _disposed;
 194
 0195        private static readonly IDisposable _noLock = new DbLock(null) { _disposed = true };
 196        private static (string Command, Guid Id, DateTimeOffset QueryDate, bool Printed) _blockQuery;
 197
 198        public DbLock(Action? action = null)
 199        {
 0200            _action = action;
 0201        }
 202
 203#pragma warning disable IDISP015 // Member should not return created and cached instance
 204        public static IDisposable EnterWrite(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? call
 205#pragma warning restore IDISP015 // Member should not return created and cached instance
 206        {
 0207            logger.LogTrace("Enter Write for {Caller}:{Line}", callerMemberName, callerNo);
 0208            if (DatabaseLock.IsWriteLockHeld)
 209            {
 0210                logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
 0211                return _noLock;
 212            }
 213
 0214            BeginWriteLock(logger, command, callerMemberName, callerNo);
 0215            return new DbLock(() =>
 0216            {
 0217                EndWriteLock(logger, callerMemberName, callerNo);
 0218            });
 219        }
 220
 221#pragma warning disable IDISP015 // Member should not return created and cached instance
 222        public static IDisposable EnterRead(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerL
 223#pragma warning restore IDISP015 // Member should not return created and cached instance
 224        {
 0225            logger.LogTrace("Enter Read {Caller}:{Line}", callerMemberName, callerNo);
 0226            if (DatabaseLock.IsWriteLockHeld)
 227            {
 0228                logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
 0229                return _noLock;
 230            }
 231
 0232            BeginReadLock(logger, callerMemberName, callerNo);
 0233            return new DbLock(() =>
 0234            {
 0235                ExitReadLock(logger, callerMemberName, callerNo);
 0236            });
 237        }
 238
 239        public static void BeginWriteLock(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? callerM
 240        {
 0241            logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo);
 0242            if (!DatabaseLock.TryEnterWriteLock(TimeSpan.FromMilliseconds(1000)))
 243            {
 0244                var blockingQuery = _blockQuery;
 0245                if (!blockingQuery.Printed)
 246                {
 0247                    _blockQuery = (blockingQuery.Command, blockingQuery.Id, blockingQuery.QueryDate, true);
 0248                    logger.LogInformation("QueryLock: {Id} --- {Query}", blockingQuery.Id, blockingQuery.Command);
 249                }
 250
 0251                logger.LogInformation("Query congestion detected: '{Id}' since '{Date}'", blockingQuery.Id, blockingQuer
 252
 0253                DatabaseLock.EnterWriteLock();
 254
 0255                logger.LogInformation("Query congestion cleared: '{Id}' for '{Date}'", blockingQuery.Id, DateTimeOffset.
 256            }
 257
 0258            _blockQuery = (command?.CommandText ?? "Transaction", Guid.NewGuid(), DateTimeOffset.Now, false);
 259
 0260            logger.LogTrace("Write Aquired {Caller}:{Line}", callerMemberName, callerNo);
 0261        }
 262
 263        public static void BeginReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLine
 264        {
 0265            logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo);
 0266            DatabaseLock.EnterReadLock();
 0267            logger.LogTrace("Read Aquired {Caller}:{Line}", callerMemberName, callerNo);
 0268        }
 269
 270        public static void EndWriteLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineN
 271        {
 0272            logger.LogTrace("Release Write {Caller}:{Line}", callerMemberName, callerNo);
 0273            DatabaseLock.ExitWriteLock();
 0274        }
 275
 276        public static void ExitReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineN
 277        {
 0278            logger.LogTrace("Release Read {Caller}:{Line}", callerMemberName, callerNo);
 0279            DatabaseLock.ExitReadLock();
 0280        }
 281
 282        public void Dispose()
 283        {
 0284            if (_disposed)
 285            {
 0286                return;
 287            }
 288
 0289            _disposed = true;
 0290            if (_action is not null)
 291            {
 0292                _action();
 293            }
 0294        }
 295    }
 296}

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()