Add concurrency token to UserData and enhance retry logic for DbUpdateConcurrencyException handling
This commit is contained in:
@@ -7,11 +7,13 @@
|
||||
namespace Jellyfin.Database.Implementations.Entities
|
||||
{
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Jellyfin.Database.Implementations.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Provides <see cref="BaseItemEntity"/> and <see cref="User"/> related data.
|
||||
/// </summary>
|
||||
public class UserData
|
||||
public class UserData : IHasConcurrencyToken
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the custom data key.
|
||||
@@ -99,5 +101,19 @@ namespace Jellyfin.Database.Implementations.Entities
|
||||
/// Gets or Sets the User.
|
||||
/// </summary>
|
||||
public required User? User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the concurrency token used for optimistic concurrency control.
|
||||
/// </summary>
|
||||
[ConcurrencyCheck]
|
||||
public uint RowVersion { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the concurrency token before saving changes.
|
||||
/// </summary>
|
||||
public void OnSavingChanges()
|
||||
{
|
||||
this.RowVersion++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
namespace Jellyfin.Database.Implementations;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
@@ -363,14 +364,63 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
|
||||
{
|
||||
try
|
||||
{
|
||||
// Process each conflicted entry
|
||||
var entriesToRemove = new List<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry>();
|
||||
|
||||
foreach (var entry in ex.Entries)
|
||||
{
|
||||
// Reload the entity from the database to get the latest version
|
||||
await entry.ReloadAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Attempt to reload the entity from the database to get the latest version
|
||||
// This will fail silently for deleted entities
|
||||
await entry.ReloadAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check if the entity still exists in the database
|
||||
// If it was deleted, it will have a null entity state after reload
|
||||
if (entry.State == EntityState.Detached)
|
||||
{
|
||||
logger.LogInformation("Entity {EntityType} was deleted by another operation, skipping update.", entry.Entity.GetType().Name);
|
||||
entriesToRemove.Add(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Re-mark as Modified since reload resets the state
|
||||
entry.State = EntityState.Modified;
|
||||
|
||||
// Re-apply concurrency token update for entities that still exist
|
||||
if (entry.Entity is IHasConcurrencyToken concurrencyEntity)
|
||||
{
|
||||
concurrencyEntity.OnSavingChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception reloadEx)
|
||||
{
|
||||
logger.LogWarning(reloadEx, "Failed to reload entity {EntityType}, treating as deleted.", entry.Entity.GetType().Name);
|
||||
entriesToRemove.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Detach entries that were deleted so they're not included in the retry save
|
||||
foreach (var entryToRemove in entriesToRemove)
|
||||
{
|
||||
this.Entry(entryToRemove.Entity).State = EntityState.Detached;
|
||||
}
|
||||
|
||||
// If all entries were deleted, no point in retrying
|
||||
if (entriesToRemove.Count == ex.Entries.Count && ex.Entries.Count > 0)
|
||||
{
|
||||
logger.LogInformation("All {Count} conflicted entities were deleted by other operations. Continuing without them.", ex.Entries.Count);
|
||||
return 0; // Return 0 to indicate no rows were affected
|
||||
}
|
||||
|
||||
// Retry saving changes after resolving conflicts
|
||||
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false);
|
||||
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false);
|
||||
if (result >= 0)
|
||||
{
|
||||
logger.LogInformation("Concurrency retry {RetryCount} succeeded, saved {RowsAffected} row(s).", i + 1, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (DbUpdateConcurrencyException retryEx) when (i < maxRetries - 1)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user