From 7a8cc281af22e88a5405a363590338a6de5ed063 Mon Sep 17 00:00:00 2001 From: meysamhadeli Date: Sun, 22 Jan 2023 19:11:34 +0330 Subject: [PATCH] fix: Fix DbUpdateConcurrencyException in PersistMessageDbContext --- src/BuildingBlocks/EFCore/AppDbContextBase.cs | 55 +------------- src/BuildingBlocks/EFCore/Extensions.cs | 3 + ...{DatabaseOptions.cs => PostgresOptions.cs} | 0 .../PersistMessageConfiguration.cs | 3 + ....cs => 20230122153121_initial.Designer.cs} | 7 +- ...4_initial.cs => 20230122153121_initial.cs} | 3 +- .../PersistMessageDbContextModelSnapshot.cs | 5 ++ .../Data/PersistMessageDbContext.cs | 73 +++++++++++++++++-- .../IPersistMessageDbContext.cs | 10 ++- .../PersistMessageProcessor/PersistMessage.cs | 5 +- .../InfrastructureExtensions.cs | 2 +- 11 files changed, 99 insertions(+), 67 deletions(-) rename src/BuildingBlocks/EFCore/{DatabaseOptions.cs => PostgresOptions.cs} (100%) rename src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/{20230120222214_initial.Designer.cs => 20230122153121_initial.Designer.cs} (91%) rename src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/{20230120222214_initial.cs => 20230122153121_initial.cs} (94%) diff --git a/src/BuildingBlocks/EFCore/AppDbContextBase.cs b/src/BuildingBlocks/EFCore/AppDbContextBase.cs index 7ae1f1d5..437a589b 100644 --- a/src/BuildingBlocks/EFCore/AppDbContextBase.cs +++ b/src/BuildingBlocks/EFCore/AppDbContextBase.cs @@ -1,18 +1,13 @@ +namespace BuildingBlocks.EFCore; + using System.Collections.Immutable; using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; - -namespace BuildingBlocks.EFCore; - using System.Data; -using System.Net; using System.Security.Claims; -using global::Polly; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Exception = System.Exception; public abstract class AppDbContextBase : DbContext, IDbContext @@ -76,51 +71,7 @@ public async Task RollbackTransactionAsync(CancellationToken cancellationToken = public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { OnBeforeSaving(); - try - { - return await base.SaveChangesAsync(cancellationToken); - } - //ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api#resolving-concurrency-conflicts - catch (DbUpdateConcurrencyException ex) - { - throw new DbUpdateConcurrencyException("try for get unhandled exception with DbUpdateConcurrencyException", ex); - var logger = _httpContextAccessor?.HttpContext?.RequestServices - .GetRequiredService>(); - - var entry = ex.Entries.SingleOrDefault(); - - if (entry == null) - { - return 0; - } - - var currentValue = entry.CurrentValues; - var databaseValue = await entry.GetDatabaseValuesAsync(cancellationToken); - - logger?.LogInformation("The entity being updated is already use by another Thread!" + - " database value is: {DatabaseValue} and current value is: {CurrentValue}", - databaseValue, currentValue); - - var policy = Policy.Handle() - .WaitAndRetryAsync(retryCount: 3, - sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(1), - onRetry: (exception, timeSpan, retryCount, context) => - { - if (exception != null) - { - logger?.LogError(exception, - "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}.", - HttpStatusCode.Conflict, - timeSpan, - retryCount); - } - }); - return await policy.ExecuteAsync(async () => await base.SaveChangesAsync(cancellationToken)); - } - catch (Exception ex) - { - throw new Exception("try for get unhandled exception bt default", ex); - } + return await base.SaveChangesAsync(cancellationToken); } public IReadOnlyList GetDomainEvents() diff --git a/src/BuildingBlocks/EFCore/Extensions.cs b/src/BuildingBlocks/EFCore/Extensions.cs index 7237edbb..45d76f3f 100644 --- a/src/BuildingBlocks/EFCore/Extensions.cs +++ b/src/BuildingBlocks/EFCore/Extensions.cs @@ -11,6 +11,7 @@ namespace BuildingBlocks.EFCore; +using Ardalis.GuardClauses; using Humanizer; using Microsoft.EntityFrameworkCore.Metadata; @@ -28,6 +29,8 @@ public static IServiceCollection AddCustomDbContext( { var postgresOptions = sp.GetRequiredService(); + Guard.Against.Null(options, nameof(postgresOptions)); + options.UseNpgsql(postgresOptions?.ConnectionString, dbOptions => { diff --git a/src/BuildingBlocks/EFCore/DatabaseOptions.cs b/src/BuildingBlocks/EFCore/PostgresOptions.cs similarity index 100% rename from src/BuildingBlocks/EFCore/DatabaseOptions.cs rename to src/BuildingBlocks/EFCore/PostgresOptions.cs diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/Configurations/PersistMessageConfiguration.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/Configurations/PersistMessageConfiguration.cs index 5d753285..3142840e 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/Configurations/PersistMessageConfiguration.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/Configurations/PersistMessageConfiguration.cs @@ -14,6 +14,9 @@ public void Configure(EntityTypeBuilder builder) builder.Property(r => r.Id) .IsRequired().ValueGeneratedNever(); + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + builder.Property(x => x.DeliveryType) .HasDefaultValue(MessageDeliveryType.Outbox) .HasConversion( diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.Designer.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.Designer.cs similarity index 91% rename from src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.Designer.cs rename to src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.Designer.cs index 3265c90b..55c6a0ae 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.Designer.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.Designer.cs @@ -12,7 +12,7 @@ namespace BuildingBlocks.PersistMessageProcessor.Data.Migrations { [DbContext(typeof(PersistMessageDbContext))] - [Migration("20230120222214_initial")] + [Migration("20230122153121_initial")] partial class initial { /// @@ -61,6 +61,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("retry_count"); + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + b.HasKey("Id") .HasName("pk_persist_message"); diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.cs similarity index 94% rename from src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.cs rename to src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.cs index d69e646c..47847cf6 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.cs @@ -21,7 +21,8 @@ protected override void Up(MigrationBuilder migrationBuilder) created = table.Column(type: "timestamp with time zone", nullable: false), retrycount = table.Column(name: "retry_count", type: "integer", nullable: false), messagestatus = table.Column(name: "message_status", type: "text", nullable: false, defaultValue: "InProgress"), - deliverytype = table.Column(name: "delivery_type", type: "text", nullable: false, defaultValue: "Outbox") + deliverytype = table.Column(name: "delivery_type", type: "text", nullable: false, defaultValue: "Outbox"), + version = table.Column(type: "bigint", nullable: false) }, constraints: table => { diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/PersistMessageDbContextModelSnapshot.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/PersistMessageDbContextModelSnapshot.cs index 5b82f171..2f55c12f 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/PersistMessageDbContextModelSnapshot.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/PersistMessageDbContextModelSnapshot.cs @@ -58,6 +58,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("retry_count"); + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + b.HasKey("Id") .HasName("pk_persist_message"); diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs index 295868b6..b0ecdaa5 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs @@ -1,22 +1,85 @@ using BuildingBlocks.EFCore; -using BuildingBlocks.PersistMessageProcessor.Data.Configurations; using Microsoft.EntityFrameworkCore; namespace BuildingBlocks.PersistMessageProcessor.Data; -using Microsoft.AspNetCore.Http; +using System.Net; +using Configurations; +using global::Polly; +using Microsoft.Extensions.Logging; -public class PersistMessageDbContext : AppDbContextBase, IPersistMessageDbContext +public class PersistMessageDbContext : DbContext, IPersistMessageDbContext { - public PersistMessageDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor = default) - : base(options, httpContextAccessor) + public PersistMessageDbContext(DbContextOptions options) + : base(options) { } + public DbSet PersistMessages { get; set; } + protected override void OnModelCreating(ModelBuilder builder) { builder.ApplyConfiguration(new PersistMessageConfiguration()); base.OnModelCreating(builder); builder.ToSnakeCaseTables(); } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + OnBeforeSaving(); + + var policy = Policy.Handle() + .WaitAndRetryAsync(retryCount: 3, + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(1), + onRetry: (exception, timeSpan, retryCount, context) => + { + if (exception != null) + { + var factory = LoggerFactory.Create(b => b.AddConsole()); + var logger = factory.CreateLogger(); + + logger.LogError(exception, + "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}.", + HttpStatusCode.Conflict, + timeSpan, + retryCount); + } + }); + try + { + await policy.ExecuteAsync(async () => await base.SaveChangesAsync(cancellationToken)); + } + catch (DbUpdateConcurrencyException ex) + { + foreach (var entry in ex.Entries) + { + var currentEntity = entry.Entity; + var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken); + + if (databaseValues != null) + entry.OriginalValues.SetValues(databaseValues); + } + + return await base.SaveChangesAsync(cancellationToken); + } + + return 0; + } + + private void OnBeforeSaving() + { + foreach (var entry in ChangeTracker.Entries()) + { + switch (entry.State) + { + case EntityState.Modified: + entry.Entity.Version++; + break; + + case EntityState.Deleted: + entry.Entity.Version++; + break; + } + } + } } diff --git a/src/BuildingBlocks/PersistMessageProcessor/IPersistMessageDbContext.cs b/src/BuildingBlocks/PersistMessageProcessor/IPersistMessageDbContext.cs index 25521264..0eee375b 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/IPersistMessageDbContext.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/IPersistMessageDbContext.cs @@ -1,9 +1,11 @@ -using BuildingBlocks.EFCore; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace BuildingBlocks.PersistMessageProcessor; -public interface IPersistMessageDbContext : IDbContext +using EFCore; + +public interface IPersistMessageDbContext { - DbSet PersistMessages => Set(); + DbSet PersistMessages { get; set; } + Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/src/BuildingBlocks/PersistMessageProcessor/PersistMessage.cs b/src/BuildingBlocks/PersistMessageProcessor/PersistMessage.cs index 4335eb2f..77526586 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/PersistMessage.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/PersistMessage.cs @@ -1,6 +1,4 @@ -using System.Reflection; - -namespace BuildingBlocks.PersistMessageProcessor; +namespace BuildingBlocks.PersistMessageProcessor; public class PersistMessage { @@ -22,6 +20,7 @@ public PersistMessage(long id, string dataType, string data, MessageDeliveryType public int RetryCount { get; private set; } public MessageStatus MessageStatus { get; private set; } public MessageDeliveryType DeliveryType { get; private set; } + public long Version { get; set; } public void ChangeState(MessageStatus messageStatus) { diff --git a/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs b/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs index 00cdc5f4..0fc0be65 100644 --- a/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs +++ b/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs @@ -100,7 +100,7 @@ public static WebApplication UseInfrastructure(this WebApplication app) }); app.UseCorrelationId(); app.UseHttpMetrics(); - app.UseMigration(env); + // app.UseMigration(env); app.UseCustomHealthCheck(); app.MapMetrics(); app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));