< Summary - Jellyfin

Information
Class: Jellyfin.Database.Implementations.Locking.OptimisticLockBehavior
Assembly: Jellyfin.Database.Implementations
File(s): /srv/git/jellyfin/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 52
Coverable lines: 52
Total lines: 161
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/19/2025 - 12:10:00 AM Line coverage: 0% (0/31) Total lines: 1379/13/2025 - 12:11:04 AM Line coverage: 0% (0/48) Total lines: 15711/4/2025 - 12:11:59 AM Line coverage: 0% (0/52) Total lines: 161

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
Initialise(...)100%210%
OnSaveChanges(...)100%210%
.ctor(...)100%210%
TransactionStarting(...)100%210%
.ctor(...)100%210%
NonQueryExecuting(...)100%210%
ScalarExecuting(...)100%210%
ReaderExecuting(...)100%210%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Data.Common;
 3using System.Linq;
 4using System.Security.Cryptography;
 5using System.Threading;
 6using System.Threading.Tasks;
 7using Microsoft.EntityFrameworkCore;
 8using Microsoft.EntityFrameworkCore.Diagnostics;
 9using Microsoft.Extensions.Logging;
 10using Polly;
 11
 12namespace Jellyfin.Database.Implementations.Locking;
 13
 14/// <summary>
 15/// Defines a locking mechanism that will retry any write operation for a few times.
 16/// </summary>
 17public class OptimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
 18{
 19    private readonly Policy _writePolicy;
 20    private readonly AsyncPolicy _writeAsyncPolicy;
 21    private readonly ILogger<OptimisticLockBehavior> _logger;
 22
 23    /// <summary>
 24    /// Initializes a new instance of the <see cref="OptimisticLockBehavior"/> class.
 25    /// </summary>
 26    /// <param name="logger">The application logger.</param>
 27    public OptimisticLockBehavior(ILogger<OptimisticLockBehavior> logger)
 28    {
 029        TimeSpan[] sleepDurations = [
 030            TimeSpan.FromMilliseconds(50),
 031            TimeSpan.FromMilliseconds(50),
 032            TimeSpan.FromMilliseconds(50),
 033            TimeSpan.FromMilliseconds(50),
 034            TimeSpan.FromMilliseconds(250),
 035            TimeSpan.FromMilliseconds(250),
 036            TimeSpan.FromMilliseconds(250),
 037            TimeSpan.FromMilliseconds(150),
 038            TimeSpan.FromMilliseconds(150),
 039            TimeSpan.FromMilliseconds(150),
 040            TimeSpan.FromMilliseconds(500),
 041            TimeSpan.FromMilliseconds(150),
 042            TimeSpan.FromMilliseconds(500),
 043            TimeSpan.FromMilliseconds(150),
 044            TimeSpan.FromSeconds(3)
 045        ];
 46
 047        Func<int, Context, TimeSpan> backoffProvider = (index, context) =>
 048        {
 049            var backoff = sleepDurations[index];
 050            return backoff + TimeSpan.FromMilliseconds(RandomNumberGenerator.GetInt32(0, (int)(backoff.TotalMilliseconds
 051        };
 52
 053        _logger = logger;
 054        _writePolicy = Policy
 055            .HandleInner<Exception>(e =>
 056                e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
 057                e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
 058            .WaitAndRetry(sleepDurations.Length, backoffProvider, RetryHandle);
 059        _writeAsyncPolicy = Policy
 060            .HandleInner<Exception>(e =>
 061                e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
 062                e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
 063            .WaitAndRetryAsync(sleepDurations.Length, backoffProvider, RetryHandle);
 64
 65        void RetryHandle(Exception exception, TimeSpan timespan, int retryNo, Context context)
 66        {
 67            if (retryNo < sleepDurations.Length)
 68            {
 69                _logger.LogWarning("Operation failed retry {RetryNo}", retryNo);
 70            }
 71            else
 72            {
 73                _logger.LogError(exception, "Operation failed retry {RetryNo}", retryNo);
 74            }
 75        }
 076    }
 77
 78    /// <inheritdoc/>
 79    public void Initialise(DbContextOptionsBuilder optionsBuilder)
 80    {
 081        _logger.LogInformation("The database locking mode has been set to: Optimistic.");
 082        optionsBuilder.AddInterceptors(new RetryInterceptor(_writeAsyncPolicy, _writePolicy));
 083        optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_writeAsyncPolicy, _writePolicy));
 084    }
 85
 86    /// <inheritdoc/>
 87    public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
 88    {
 089        _writePolicy.ExecuteAndCapture(saveChanges);
 090    }
 91
 92    /// <inheritdoc/>
 93    public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
 94    {
 95        await _writeAsyncPolicy.ExecuteAndCaptureAsync(saveChanges).ConfigureAwait(false);
 96    }
 97
 98    private sealed class TransactionLockingInterceptor : DbTransactionInterceptor
 99    {
 100        private readonly AsyncPolicy _asyncRetryPolicy;
 101        private readonly Policy _retryPolicy;
 102
 0103        public TransactionLockingInterceptor(AsyncPolicy asyncRetryPolicy, Policy retryPolicy)
 104        {
 0105            _asyncRetryPolicy = asyncRetryPolicy;
 0106            _retryPolicy = retryPolicy;
 0107        }
 108
 109        public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStarti
 110        {
 0111            return InterceptionResult<DbTransaction>.SuppressWithResult(_retryPolicy.Execute(() => connection.BeginTrans
 112        }
 113
 114        public override async ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connect
 115        {
 116            return InterceptionResult<DbTransaction>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () =>
 117        }
 118    }
 119
 120    private sealed class RetryInterceptor : DbCommandInterceptor
 121    {
 122        private readonly AsyncPolicy _asyncRetryPolicy;
 123        private readonly Policy _retryPolicy;
 124
 0125        public RetryInterceptor(AsyncPolicy asyncRetryPolicy, Policy retryPolicy)
 126        {
 0127            _asyncRetryPolicy = asyncRetryPolicy;
 0128            _retryPolicy = retryPolicy;
 0129        }
 130
 131        public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, Interce
 132        {
 0133            return InterceptionResult<int>.SuppressWithResult(_retryPolicy.Execute(command.ExecuteNonQuery));
 134        }
 135
 136        public override async ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventD
 137        {
 138            return InterceptionResult<int>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => await com
 139        }
 140
 141        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, Interc
 142        {
 0143            return InterceptionResult<object>.SuppressWithResult(_retryPolicy.Execute(() => command.ExecuteScalar()!));
 144        }
 145
 146        public override async ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEvent
 147        {
 148            return InterceptionResult<object>.SuppressWithResult((await _asyncRetryPolicy.ExecuteAsync(async () => await
 149        }
 150
 151        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, 
 152        {
 0153            return InterceptionResult<DbDataReader>.SuppressWithResult(_retryPolicy.Execute(command.ExecuteReader));
 154        }
 155
 156        public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, Comman
 157        {
 158            return InterceptionResult<DbDataReader>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => 
 159        }
 160    }
 161}