diff options
Diffstat (limited to 'src/Jellyfin.Database')
3 files changed, 16 insertions, 4 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs index 76ffa5a9ea..29a073ff74 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs @@ -88,13 +88,13 @@ public class OptimisticLockBehavior : IEntityFrameworkCoreLockingBehavior /// <inheritdoc/> public void OnSaveChanges(JellyfinDbContext context, Action saveChanges) { - _writePolicy.ExecuteAndCapture(saveChanges); + _writePolicy.Execute(saveChanges); } /// <inheritdoc/> public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges) { - await _writeAsyncPolicy.ExecuteAndCaptureAsync(saveChanges).ConfigureAwait(false); + await _writeAsyncPolicy.ExecuteAsync(saveChanges).ConfigureAwait(false); } private sealed class TransactionLockingInterceptor : DbTransactionInterceptor diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs index 404292e8eb..e7a7d5a53f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs @@ -17,6 +17,13 @@ namespace Jellyfin.Database.Implementations.Locking; /// <summary> /// A locking behavior that will always block any operation while a write is requested. Mimicks the old SqliteRepository behavior. /// </summary> +/// <remarks> +/// Unsafe with asynchronous transactions; because <see cref="ReaderWriterLockSlim"/> is +/// thread-affine, holding it from <c>TransactionStarting</c> to <c>TransactionCommitted</c> +/// works only while continuations resume inline. A genuinely-async continuation inside a +/// transaction releases on another thread, throwing +/// <see cref="SynchronizationLockException"/> or deadlocking a later write. +/// </remarks> public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior { private readonly ILogger<PessimisticLockBehavior> _logger; @@ -47,7 +54,8 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior /// <inheritdoc/> public void Initialise(DbContextOptionsBuilder optionsBuilder) { - _logger.LogInformation("The database locking mode has been set to: Pessimistic."); + _logger.LogWarning( + "The database locking mode has been set to: Pessimistic. This mode is not safe with asynchronous transactions and can deadlock."); optionsBuilder.AddInterceptors(new CommandLockingInterceptor(_loggerFactory.CreateLogger<CommandLockingInterceptor>())); optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_loggerFactory.CreateLogger<TransactionLockingInterceptor>())); } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index 044fd0131f..8020fe1f93 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -63,7 +63,11 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider var sqliteConnectionBuilder = new SqliteConnectionStringBuilder { DataSource = GetOption(customOptions, "path", e => e, () => Path.Combine(_applicationPaths.DataPath, "jellyfin.db")), - Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Default), + // Private, not Default: sqlite3_enable_shared_cache is process-global, so a plugin + // enabling it makes these connections share a cache too. Contention then surfaces as + // SQLITE_LOCKED ("database table is locked"), which the busy handler does not cover, + // so busy_timeout is skipped and the command fails at CommandTimeout instead. + Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Private), Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true), DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 60) }; |
