< 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: 48
Coverable lines: 48
Total lines: 157
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

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 => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnor
 056            .WaitAndRetry(sleepDurations.Length, backoffProvider, RetryHandle);
 057        _writeAsyncPolicy = Policy
 058            .HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnor
 059            .WaitAndRetryAsync(sleepDurations.Length, backoffProvider, RetryHandle);
 60
 61        void RetryHandle(Exception exception, TimeSpan timespan, int retryNo, Context context)
 62        {
 63            if (retryNo < sleepDurations.Length)
 64            {
 65                _logger.LogWarning("Operation failed retry {RetryNo}", retryNo);
 66            }
 67            else
 68            {
 69                _logger.LogError(exception, "Operation failed retry {RetryNo}", retryNo);
 70            }
 71        }
 072    }
 73
 74    /// <inheritdoc/>
 75    public void Initialise(DbContextOptionsBuilder optionsBuilder)
 76    {
 077        _logger.LogInformation("The database locking mode has been set to: Optimistic.");
 078        optionsBuilder.AddInterceptors(new RetryInterceptor(_writeAsyncPolicy, _writePolicy));
 079        optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_writeAsyncPolicy, _writePolicy));
 080    }
 81
 82    /// <inheritdoc/>
 83    public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
 84    {
 085        _writePolicy.ExecuteAndCapture(saveChanges);
 086    }
 87
 88    /// <inheritdoc/>
 89    public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
 90    {
 91        await _writeAsyncPolicy.ExecuteAndCaptureAsync(saveChanges).ConfigureAwait(false);
 92    }
 93
 94    private sealed class TransactionLockingInterceptor : DbTransactionInterceptor
 95    {
 96        private readonly AsyncPolicy _asyncRetryPolicy;
 97        private readonly Policy _retryPolicy;
 98
 099        public TransactionLockingInterceptor(AsyncPolicy asyncRetryPolicy, Policy retryPolicy)
 100        {
 0101            _asyncRetryPolicy = asyncRetryPolicy;
 0102            _retryPolicy = retryPolicy;
 0103        }
 104
 105        public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStarti
 106        {
 0107            return InterceptionResult<DbTransaction>.SuppressWithResult(_retryPolicy.Execute(() => connection.BeginTrans
 108        }
 109
 110        public override async ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connect
 111        {
 112            return InterceptionResult<DbTransaction>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () =>
 113        }
 114    }
 115
 116    private sealed class RetryInterceptor : DbCommandInterceptor
 117    {
 118        private readonly AsyncPolicy _asyncRetryPolicy;
 119        private readonly Policy _retryPolicy;
 120
 0121        public RetryInterceptor(AsyncPolicy asyncRetryPolicy, Policy retryPolicy)
 122        {
 0123            _asyncRetryPolicy = asyncRetryPolicy;
 0124            _retryPolicy = retryPolicy;
 0125        }
 126
 127        public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, Interce
 128        {
 0129            return InterceptionResult<int>.SuppressWithResult(_retryPolicy.Execute(command.ExecuteNonQuery));
 130        }
 131
 132        public override async ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventD
 133        {
 134            return InterceptionResult<int>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => await com
 135        }
 136
 137        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, Interc
 138        {
 0139            return InterceptionResult<object>.SuppressWithResult(_retryPolicy.Execute(() => command.ExecuteScalar()!));
 140        }
 141
 142        public override async ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEvent
 143        {
 144            return InterceptionResult<object>.SuppressWithResult((await _asyncRetryPolicy.ExecuteAsync(async () => await
 145        }
 146
 147        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, 
 148        {
 0149            return InterceptionResult<DbDataReader>.SuppressWithResult(_retryPolicy.Execute(command.ExecuteReader));
 150        }
 151
 152        public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, Comman
 153        {
 154            return InterceptionResult<DbDataReader>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => 
 155        }
 156    }
 157}