< 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
59%
Covered lines: 37
Uncovered lines: 25
Coverable lines: 62
Total lines: 163
Line coverage: 59.6%
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 5/16/2026 - 12:15:55 AM Line coverage: 0% (0/62) Total lines: 16310/29/2026 - 12:17:11 AM Line coverage: 59.6% (37/62) Total lines: 163

Coverage delta

Coverage delta 60 -60

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Initialise(...)100%210%
OnSaveChanges(...)100%11100%
OnSaveChangesAsync()100%1150%
.ctor(...)100%210%
TransactionStarting(...)100%210%
TransactionStartingAsync()100%210%
.ctor(...)100%210%
NonQueryExecuting(...)100%210%
NonQueryExecutingAsync()100%210%
ScalarExecuting(...)100%210%
ScalarExecutingAsync()100%210%
ReaderExecuting(...)100%210%
ReaderExecutingAsync()100%210%

File(s)

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

#LineLine coverage
 1#pragma warning disable CA1873
 2
 3using System;
 4using System.Data.Common;
 5using System.Linq;
 6using System.Security.Cryptography;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using Microsoft.EntityFrameworkCore;
 10using Microsoft.EntityFrameworkCore.Diagnostics;
 11using Microsoft.Extensions.Logging;
 12using Polly;
 13
 14namespace Jellyfin.Database.Implementations.Locking;
 15
 16/// <summary>
 17/// Defines a locking mechanism that will retry any write operation for a few times.
 18/// </summary>
 19public class OptimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
 20{
 21    private readonly Policy _writePolicy;
 22    private readonly AsyncPolicy _writeAsyncPolicy;
 23    private readonly ILogger<OptimisticLockBehavior> _logger;
 24
 25    /// <summary>
 26    /// Initializes a new instance of the <see cref="OptimisticLockBehavior"/> class.
 27    /// </summary>
 28    /// <param name="logger">The application logger.</param>
 29    public OptimisticLockBehavior(ILogger<OptimisticLockBehavior> logger)
 30    {
 331        TimeSpan[] sleepDurations = [
 332            TimeSpan.FromMilliseconds(50),
 333            TimeSpan.FromMilliseconds(50),
 334            TimeSpan.FromMilliseconds(50),
 335            TimeSpan.FromMilliseconds(50),
 336            TimeSpan.FromMilliseconds(250),
 337            TimeSpan.FromMilliseconds(250),
 338            TimeSpan.FromMilliseconds(250),
 339            TimeSpan.FromMilliseconds(150),
 340            TimeSpan.FromMilliseconds(150),
 341            TimeSpan.FromMilliseconds(150),
 342            TimeSpan.FromMilliseconds(500),
 343            TimeSpan.FromMilliseconds(150),
 344            TimeSpan.FromMilliseconds(500),
 345            TimeSpan.FromMilliseconds(150),
 346            TimeSpan.FromSeconds(3)
 347        ];
 48
 349        Func<int, Context, TimeSpan> backoffProvider = (retryNo, context) =>
 350        {
 351            var backoff = sleepDurations[retryNo - 1];
 352            return backoff + TimeSpan.FromMilliseconds(RandomNumberGenerator.GetInt32(0, (int)(backoff.TotalMilliseconds
 353        };
 54
 355        _logger = logger;
 356        _writePolicy = Policy
 357            .HandleInner<Exception>(e =>
 358                e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
 359                e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
 360            .WaitAndRetry(sleepDurations.Length, backoffProvider, RetryHandle);
 361        _writeAsyncPolicy = Policy
 362            .HandleInner<Exception>(e =>
 363                e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
 364                e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
 365            .WaitAndRetryAsync(sleepDurations.Length, backoffProvider, RetryHandle);
 66
 67        void RetryHandle(Exception exception, TimeSpan timespan, int retryNo, Context context)
 68        {
 69            if (retryNo < sleepDurations.Length)
 70            {
 71                _logger.LogWarning("Operation failed retry {RetryNo}", retryNo);
 72            }
 73            else
 74            {
 75                _logger.LogError(exception, "Operation failed retry {RetryNo}", retryNo);
 76            }
 77        }
 378    }
 79
 80    /// <inheritdoc/>
 81    public void Initialise(DbContextOptionsBuilder optionsBuilder)
 82    {
 083        _logger.LogInformation("The database locking mode has been set to: Optimistic.");
 084        optionsBuilder.AddInterceptors(new RetryInterceptor(_writeAsyncPolicy, _writePolicy));
 085        optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_writeAsyncPolicy, _writePolicy));
 086    }
 87
 88    /// <inheritdoc/>
 89    public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
 90    {
 291        _writePolicy.Execute(saveChanges);
 192    }
 93
 94    /// <inheritdoc/>
 95    public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
 96    {
 197        await _writeAsyncPolicy.ExecuteAsync(saveChanges).ConfigureAwait(false);
 098    }
 99
 100    private sealed class TransactionLockingInterceptor : DbTransactionInterceptor
 101    {
 102        private readonly AsyncPolicy _asyncRetryPolicy;
 103        private readonly Policy _retryPolicy;
 104
 0105        public TransactionLockingInterceptor(AsyncPolicy asyncRetryPolicy, Policy retryPolicy)
 106        {
 0107            _asyncRetryPolicy = asyncRetryPolicy;
 0108            _retryPolicy = retryPolicy;
 0109        }
 110
 111        public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStarti
 112        {
 0113            return InterceptionResult<DbTransaction>.SuppressWithResult(_retryPolicy.Execute(() => connection.BeginTrans
 114        }
 115
 116        public override async ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connect
 117        {
 0118            return InterceptionResult<DbTransaction>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () =>
 0119        }
 120    }
 121
 122    private sealed class RetryInterceptor : DbCommandInterceptor
 123    {
 124        private readonly AsyncPolicy _asyncRetryPolicy;
 125        private readonly Policy _retryPolicy;
 126
 0127        public RetryInterceptor(AsyncPolicy asyncRetryPolicy, Policy retryPolicy)
 128        {
 0129            _asyncRetryPolicy = asyncRetryPolicy;
 0130            _retryPolicy = retryPolicy;
 0131        }
 132
 133        public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, Interce
 134        {
 0135            return InterceptionResult<int>.SuppressWithResult(_retryPolicy.Execute(command.ExecuteNonQuery));
 136        }
 137
 138        public override async ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventD
 139        {
 0140            return InterceptionResult<int>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => await com
 0141        }
 142
 143        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, Interc
 144        {
 0145            return InterceptionResult<object>.SuppressWithResult(_retryPolicy.Execute(() => command.ExecuteScalar()!));
 146        }
 147
 148        public override async ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEvent
 149        {
 0150            return InterceptionResult<object>.SuppressWithResult((await _asyncRetryPolicy.ExecuteAsync(async () => await
 0151        }
 152
 153        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, 
 154        {
 0155            return InterceptionResult<DbDataReader>.SuppressWithResult(_retryPolicy.Execute(command.ExecuteReader));
 156        }
 157
 158        public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, Comman
 159        {
 0160            return InterceptionResult<DbDataReader>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => 
 0161        }
 162    }
 163}