< 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: 163
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 10/25/2025 - 12:09:58 AM Line coverage: 0% (0/48) Total lines: 15711/4/2025 - 12:11:59 AM Line coverage: 0% (0/52) Total lines: 1611/19/2026 - 12:13:54 AM Line coverage: 0% (0/52) Total lines: 163

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
 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    {
 031        TimeSpan[] sleepDurations = [
 032            TimeSpan.FromMilliseconds(50),
 033            TimeSpan.FromMilliseconds(50),
 034            TimeSpan.FromMilliseconds(50),
 035            TimeSpan.FromMilliseconds(50),
 036            TimeSpan.FromMilliseconds(250),
 037            TimeSpan.FromMilliseconds(250),
 038            TimeSpan.FromMilliseconds(250),
 039            TimeSpan.FromMilliseconds(150),
 040            TimeSpan.FromMilliseconds(150),
 041            TimeSpan.FromMilliseconds(150),
 042            TimeSpan.FromMilliseconds(500),
 043            TimeSpan.FromMilliseconds(150),
 044            TimeSpan.FromMilliseconds(500),
 045            TimeSpan.FromMilliseconds(150),
 046            TimeSpan.FromSeconds(3)
 047        ];
 48
 049        Func<int, Context, TimeSpan> backoffProvider = (index, context) =>
 050        {
 051            var backoff = sleepDurations[index];
 052            return backoff + TimeSpan.FromMilliseconds(RandomNumberGenerator.GetInt32(0, (int)(backoff.TotalMilliseconds
 053        };
 54
 055        _logger = logger;
 056        _writePolicy = Policy
 057            .HandleInner<Exception>(e =>
 058                e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
 059                e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
 060            .WaitAndRetry(sleepDurations.Length, backoffProvider, RetryHandle);
 061        _writeAsyncPolicy = Policy
 062            .HandleInner<Exception>(e =>
 063                e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) ||
 064                e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase))
 065            .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        }
 078    }
 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    {
 091        _writePolicy.ExecuteAndCapture(saveChanges);
 092    }
 93
 94    /// <inheritdoc/>
 95    public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
 96    {
 97        await _writeAsyncPolicy.ExecuteAndCaptureAsync(saveChanges).ConfigureAwait(false);
 98    }
 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        {
 118            return InterceptionResult<DbTransaction>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () =>
 119        }
 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        {
 140            return InterceptionResult<int>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => await com
 141        }
 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        {
 150            return InterceptionResult<object>.SuppressWithResult((await _asyncRetryPolicy.ExecuteAsync(async () => await
 151        }
 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        {
 160            return InterceptionResult<DbDataReader>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => 
 161        }
 162    }
 163}