| | 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 | | } |