From 421a8cc0928538ee1040ed402176f134abf2ccf8 Mon Sep 17 00:00:00 2001 From: Wendell Jones Date: Tue, 7 Jul 2026 11:31:47 -0400 Subject: [PATCH] fix: implement exponential backoff strategy for concurrency exceptions in SaveChangesAsync --- .../JellyfinDbContext.cs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs index 97b9442e..7c650f41 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs @@ -352,18 +352,36 @@ public class JellyfinDbContext(DbContextOptions options, ILog } catch (DbUpdateConcurrencyException ex) { - // When marking items as played in quick succession, concurrency conflicts can occur. - // To resolve this, we'll reload the conflicting entities from the database and retry. - logger.LogWarning(ex, "Concurrency exception, retrying operation."); + // Concurrency conflicts can occur when marking items as played in quick succession. + // To resolve this, we'll retry the operation with an exponential backoff strategy. + logger.LogWarning(ex, "Concurrency exception, retrying operation with exponential backoff."); - foreach (var entry in ex.Entries) + var maxRetries = 3; + var delay = TimeSpan.FromMilliseconds(100); + + for (var i = 0; i < maxRetries; i++) { - // Reload the entity from the database to get the latest version - await entry.ReloadAsync(cancellationToken).ConfigureAwait(false); + try + { + foreach (var entry in ex.Entries) + { + // Reload the entity from the database to get the latest version + await entry.ReloadAsync(cancellationToken).ConfigureAwait(false); + } + + // Retry saving changes after resolving conflicts + return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false); + } + catch (DbUpdateConcurrencyException retryEx) when (i < maxRetries - 1) + { + logger.LogWarning(retryEx, "Concurrency exception on retry {RetryCount}, delaying for {Delay}ms.", i + 1, delay.TotalMilliseconds); + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + delay *= 2; // Exponential backoff + } } - // Retry saving changes after resolving conflicts - return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false); + // If all retries fail, rethrow the last exception + throw; } catch (DbUpdateException ex) {