| | | 1 | | #pragma warning disable MT1013 // Releasing lock without guarantee of execution |
| | | 2 | | #pragma warning disable MT1012 // Acquiring lock without guarantee of releasing |
| | | 3 | | |
| | | 4 | | using System; |
| | | 5 | | using System.Data; |
| | | 6 | | using System.Data.Common; |
| | | 7 | | using System.Runtime.CompilerServices; |
| | | 8 | | using System.Threading; |
| | | 9 | | using System.Threading.Tasks; |
| | | 10 | | using Microsoft.EntityFrameworkCore; |
| | | 11 | | using Microsoft.EntityFrameworkCore.Diagnostics; |
| | | 12 | | using Microsoft.Extensions.Logging; |
| | | 13 | | |
| | | 14 | | namespace 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> |
| | | 19 | | public 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 | | { |
| | 0 | 31 | | _logger = logger; |
| | 0 | 32 | | _loggerFactory = loggerFactory; |
| | 0 | 33 | | } |
| | | 34 | | |
| | 0 | 35 | | private static ReaderWriterLockSlim DatabaseLock { get; } = new(LockRecursionPolicy.SupportsRecursion); |
| | | 36 | | |
| | | 37 | | /// <inheritdoc/> |
| | | 38 | | public void OnSaveChanges(JellyfinDbContext context, Action saveChanges) |
| | | 39 | | { |
| | 0 | 40 | | using (DbLock.EnterWrite(_logger)) |
| | | 41 | | { |
| | 0 | 42 | | saveChanges(); |
| | 0 | 43 | | } |
| | 0 | 44 | | } |
| | | 45 | | |
| | | 46 | | /// <inheritdoc/> |
| | | 47 | | public void Initialise(DbContextOptionsBuilder optionsBuilder) |
| | | 48 | | { |
| | 0 | 49 | | _logger.LogInformation("The database locking mode has been set to: Pessimistic."); |
| | 0 | 50 | | optionsBuilder.AddInterceptors(new CommandLockingInterceptor(_loggerFactory.CreateLogger<CommandLockingIntercept |
| | 0 | 51 | | optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_loggerFactory.CreateLogger<TransactionLockingI |
| | 0 | 52 | | } |
| | | 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 | | |
| | 0 | 67 | | public TransactionLockingInterceptor(ILogger logger) |
| | | 68 | | { |
| | 0 | 69 | | _logger = logger; |
| | 0 | 70 | | } |
| | | 71 | | |
| | | 72 | | public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStarti |
| | | 73 | | { |
| | 0 | 74 | | DbLock.BeginWriteLock(_logger); |
| | | 75 | | |
| | 0 | 76 | | return base.TransactionStarting(connection, eventData, result); |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | public override ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connection, T |
| | | 80 | | { |
| | 0 | 81 | | DbLock.BeginWriteLock(_logger); |
| | | 82 | | |
| | 0 | 83 | | return base.TransactionStartingAsync(connection, eventData, result, cancellationToken); |
| | | 84 | | } |
| | | 85 | | |
| | | 86 | | public override void TransactionCommitted(DbTransaction transaction, TransactionEndEventData eventData) |
| | | 87 | | { |
| | 0 | 88 | | DbLock.EndWriteLock(_logger); |
| | | 89 | | |
| | 0 | 90 | | base.TransactionCommitted(transaction, eventData); |
| | 0 | 91 | | } |
| | | 92 | | |
| | | 93 | | public override Task TransactionCommittedAsync(DbTransaction transaction, TransactionEndEventData eventData, Can |
| | | 94 | | { |
| | 0 | 95 | | DbLock.EndWriteLock(_logger); |
| | | 96 | | |
| | 0 | 97 | | return base.TransactionCommittedAsync(transaction, eventData, cancellationToken); |
| | | 98 | | } |
| | | 99 | | |
| | | 100 | | public override void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData) |
| | | 101 | | { |
| | 0 | 102 | | DbLock.EndWriteLock(_logger); |
| | | 103 | | |
| | 0 | 104 | | base.TransactionFailed(transaction, eventData); |
| | 0 | 105 | | } |
| | | 106 | | |
| | | 107 | | public override Task TransactionFailedAsync(DbTransaction transaction, TransactionErrorEventData eventData, Canc |
| | | 108 | | { |
| | 0 | 109 | | DbLock.EndWriteLock(_logger); |
| | | 110 | | |
| | 0 | 111 | | return base.TransactionFailedAsync(transaction, eventData, cancellationToken); |
| | | 112 | | } |
| | | 113 | | |
| | | 114 | | public override void TransactionRolledBack(DbTransaction transaction, TransactionEndEventData eventData) |
| | | 115 | | { |
| | 0 | 116 | | DbLock.EndWriteLock(_logger); |
| | | 117 | | |
| | 0 | 118 | | base.TransactionRolledBack(transaction, eventData); |
| | 0 | 119 | | } |
| | | 120 | | |
| | | 121 | | public override Task TransactionRolledBackAsync(DbTransaction transaction, TransactionEndEventData eventData, Ca |
| | | 122 | | { |
| | 0 | 123 | | DbLock.EndWriteLock(_logger); |
| | | 124 | | |
| | 0 | 125 | | 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 | | |
| | 0 | 136 | | public CommandLockingInterceptor(ILogger logger) |
| | | 137 | | { |
| | 0 | 138 | | _logger = logger; |
| | 0 | 139 | | } |
| | | 140 | | |
| | | 141 | | public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, Interce |
| | | 142 | | { |
| | 0 | 143 | | using (DbLock.EnterWrite(_logger, command)) |
| | | 144 | | { |
| | 0 | 145 | | return InterceptionResult<int>.SuppressWithResult(command.ExecuteNonQuery()); |
| | | 146 | | } |
| | 0 | 147 | | } |
| | | 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 | | { |
| | 0 | 159 | | using (DbLock.EnterRead(_logger)) |
| | | 160 | | { |
| | 0 | 161 | | return InterceptionResult<object>.SuppressWithResult(command.ExecuteScalar()!); |
| | | 162 | | } |
| | 0 | 163 | | } |
| | | 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 | | { |
| | 0 | 175 | | using (DbLock.EnterRead(_logger)) |
| | | 176 | | { |
| | 0 | 177 | | return InterceptionResult<DbDataReader>.SuppressWithResult(command.ExecuteReader()!); |
| | | 178 | | } |
| | 0 | 179 | | } |
| | | 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 | | |
| | 0 | 195 | | 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 | | { |
| | 0 | 200 | | _action = action; |
| | 0 | 201 | | } |
| | | 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 | | { |
| | 0 | 207 | | logger.LogTrace("Enter Write for {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 208 | | if (DatabaseLock.IsWriteLockHeld) |
| | | 209 | | { |
| | 0 | 210 | | logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 211 | | return _noLock; |
| | | 212 | | } |
| | | 213 | | |
| | 0 | 214 | | BeginWriteLock(logger, command, callerMemberName, callerNo); |
| | 0 | 215 | | return new DbLock(() => |
| | 0 | 216 | | { |
| | 0 | 217 | | EndWriteLock(logger, callerMemberName, callerNo); |
| | 0 | 218 | | }); |
| | | 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 | | { |
| | 0 | 225 | | logger.LogTrace("Enter Read {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 226 | | if (DatabaseLock.IsWriteLockHeld) |
| | | 227 | | { |
| | 0 | 228 | | logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 229 | | return _noLock; |
| | | 230 | | } |
| | | 231 | | |
| | 0 | 232 | | BeginReadLock(logger, callerMemberName, callerNo); |
| | 0 | 233 | | return new DbLock(() => |
| | 0 | 234 | | { |
| | 0 | 235 | | ExitReadLock(logger, callerMemberName, callerNo); |
| | 0 | 236 | | }); |
| | | 237 | | } |
| | | 238 | | |
| | | 239 | | public static void BeginWriteLock(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? callerM |
| | | 240 | | { |
| | 0 | 241 | | logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 242 | | if (!DatabaseLock.TryEnterWriteLock(TimeSpan.FromMilliseconds(1000))) |
| | | 243 | | { |
| | 0 | 244 | | var blockingQuery = _blockQuery; |
| | 0 | 245 | | if (!blockingQuery.Printed) |
| | | 246 | | { |
| | 0 | 247 | | _blockQuery = (blockingQuery.Command, blockingQuery.Id, blockingQuery.QueryDate, true); |
| | 0 | 248 | | logger.LogInformation("QueryLock: {Id} --- {Query}", blockingQuery.Id, blockingQuery.Command); |
| | | 249 | | } |
| | | 250 | | |
| | 0 | 251 | | logger.LogInformation("Query congestion detected: '{Id}' since '{Date}'", blockingQuery.Id, blockingQuer |
| | | 252 | | |
| | 0 | 253 | | DatabaseLock.EnterWriteLock(); |
| | | 254 | | |
| | 0 | 255 | | logger.LogInformation("Query congestion cleared: '{Id}' for '{Date}'", blockingQuery.Id, DateTimeOffset. |
| | | 256 | | } |
| | | 257 | | |
| | 0 | 258 | | _blockQuery = (command?.CommandText ?? "Transaction", Guid.NewGuid(), DateTimeOffset.Now, false); |
| | | 259 | | |
| | 0 | 260 | | logger.LogTrace("Write Aquired {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 261 | | } |
| | | 262 | | |
| | | 263 | | public static void BeginReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLine |
| | | 264 | | { |
| | 0 | 265 | | logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 266 | | DatabaseLock.EnterReadLock(); |
| | 0 | 267 | | logger.LogTrace("Read Aquired {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 268 | | } |
| | | 269 | | |
| | | 270 | | public static void EndWriteLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineN |
| | | 271 | | { |
| | 0 | 272 | | logger.LogTrace("Release Write {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 273 | | DatabaseLock.ExitWriteLock(); |
| | 0 | 274 | | } |
| | | 275 | | |
| | | 276 | | public static void ExitReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineN |
| | | 277 | | { |
| | 0 | 278 | | logger.LogTrace("Release Read {Caller}:{Line}", callerMemberName, callerNo); |
| | 0 | 279 | | DatabaseLock.ExitReadLock(); |
| | 0 | 280 | | } |
| | | 281 | | |
| | | 282 | | public void Dispose() |
| | | 283 | | { |
| | 0 | 284 | | if (_disposed) |
| | | 285 | | { |
| | 0 | 286 | | return; |
| | | 287 | | } |
| | | 288 | | |
| | 0 | 289 | | _disposed = true; |
| | 0 | 290 | | if (_action is not null) |
| | | 291 | | { |
| | 0 | 292 | | _action(); |
| | | 293 | | } |
| | 0 | 294 | | } |
| | | 295 | | } |
| | | 296 | | } |