From 9eea31b2bbd3db8ab2f736c4b1aa1625d8412f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 16 Jun 2023 11:25:03 +0300 Subject: [PATCH] Finalized initial runtime database migrations for ef core --- .../Abp/Data/AppliedDatabaseMigrationsEto.cs | 12 ++ .../Data/DatabaseMigrationsAvailableEto.cs | 11 - .../DatabaseMigrationEventHandlerBase.cs | 191 ++++++++++++++---- ...s => EfCoreRuntimeDatabaseMigratorBase.cs} | 84 +++++--- 4 files changed, 226 insertions(+), 72 deletions(-) create mode 100644 framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs delete mode 100644 framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs rename framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/{PendingEfCoreMigrationsChecker.cs => EfCoreRuntimeDatabaseMigratorBase.cs} (51%) diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs new file mode 100644 index 0000000000..739571e241 --- /dev/null +++ b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs @@ -0,0 +1,12 @@ +using System; +using Volo.Abp.EventBus; + +namespace Volo.Abp.Data; + +[Serializable] +[EventName("abp.data.applied_database_migrations")] +public class AppliedDatabaseMigrationsEto +{ + public string DatabaseName { get; set; } + public Guid? TenantId { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs deleted file mode 100644 index 62eff20b6e..0000000000 --- a/framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Volo.Abp.EventBus; - -namespace Volo.Abp.Data; - -[Serializable] -[EventName("abp.data.database_migrations_available")] -public class DatabaseMigrationsAvailableEto -{ - public string DatabaseName { get; set; } -} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs index 3c196d0b6e..ce7e21dc0a 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs @@ -14,25 +14,42 @@ using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore.Migrations; -public abstract class DatabaseMigrationEventHandlerBase : ITransientDependency +public abstract class DatabaseMigrationEventHandlerBase : + IDistributedEventHandler, + IDistributedEventHandler, + IDistributedEventHandler, + ITransientDependency where TDbContext : DbContext, IEfCoreDbContext { - protected const string TryCountPropertyName = "TryCount"; - protected const int MaxEventTryCount = 3; + protected string DatabaseName { get; } + + protected const string TryCountPropertyName = "__TryCount"; + + protected int MaxEventTryCount { get; set; } = 3; + + /// + /// As milliseconds. + /// + protected int MinValueToWaitOnFailure { get; set; } = 5000; + + /// + /// As milliseconds. + /// + protected int MaxValueToWaitOnFailure { get; set; } = 15000; + protected ICurrentTenant CurrentTenant { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; } protected ITenantStore TenantStore { get; } protected IDistributedEventBus DistributedEventBus { get; } protected ILogger> Logger { get; } - protected string DatabaseName { get; } protected DatabaseMigrationEventHandlerBase( - ILoggerFactory loggerFactory, + string databaseName, ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, ITenantStore tenantStore, IDistributedEventBus distributedEventBus, - string databaseName) + ILoggerFactory loggerFactory) { CurrentTenant = currentTenant; UnitOfWorkManager = unitOfWorkManager; @@ -43,6 +60,120 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient Logger = loggerFactory.CreateLogger>(); } + public virtual async Task HandleEventAsync(ApplyDatabaseMigrationsEto eventData) + { + if (eventData.DatabaseName != DatabaseName) + { + return; + } + + var schemaMigrated = false; + try + { + schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.TenantId); + await SeedAsync(eventData.TenantId); + + if (schemaMigrated) + { + await DistributedEventBus.PublishAsync( + new AppliedDatabaseMigrationsEto + { + DatabaseName = DatabaseName, + TenantId = eventData.TenantId + } + ); + } + } + catch (Exception ex) + { + await HandleErrorOnApplyDatabaseMigrationAsync(eventData, ex); + } + + await AfterApplyDatabaseMigrations(eventData, schemaMigrated); + } + + protected virtual Task AfterApplyDatabaseMigrations(ApplyDatabaseMigrationsEto eventData, bool schemaMigrated) + { + return Task.CompletedTask; + } + + public virtual async Task HandleEventAsync(TenantCreatedEto eventData) + { + var schemaMigrated = false; + try + { + schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.Id); + await SeedAsync(eventData.Id); + + if (schemaMigrated) + { + await DistributedEventBus.PublishAsync( + new AppliedDatabaseMigrationsEto + { + DatabaseName = DatabaseName, + TenantId = eventData.Id + } + ); + } + } + catch (Exception ex) + { + await HandleErrorTenantCreatedAsync(eventData, ex); + } + + await AfterTenantCreated(eventData, schemaMigrated); + } + + protected virtual Task AfterTenantCreated(TenantCreatedEto eventData, bool schemaMigrated) + { + return Task.CompletedTask; + } + + public virtual async Task HandleEventAsync(TenantConnectionStringUpdatedEto eventData) + { + if (eventData.ConnectionStringName != DatabaseName && + eventData.ConnectionStringName != Volo.Abp.Data.ConnectionStrings.DefaultConnectionStringName || + eventData.NewValue.IsNullOrWhiteSpace()) + { + return; + } + + var schemaMigrated = false; + try + { + schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.Id); + await SeedAsync(eventData.Id); + + if (schemaMigrated) + { + await DistributedEventBus.PublishAsync( + new AppliedDatabaseMigrationsEto + { + DatabaseName = DatabaseName, + TenantId = eventData.Id + } + ); + } + } + catch (Exception ex) + { + await HandleErrorTenantConnectionStringUpdatedAsync(eventData, ex); + } + + await AfterTenantConnectionStringUpdated(eventData, schemaMigrated); + } + + protected virtual Task AfterTenantConnectionStringUpdated(TenantConnectionStringUpdatedEto eventData, + bool schemaMigrated) + { + return Task.CompletedTask; + } + + protected virtual Task SeedAsync(Guid? tenantId) + { + return Task.CompletedTask; + } + /// /// Apply pending EF Core schema migrations to the database. /// Returns true if any migration has applied. @@ -83,7 +214,8 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient !tenantConfiguration.ConnectionStrings.GetOrDefault(DatabaseName).IsNullOrWhiteSpace()) { //Migrating the tenant database (only if tenant has a separate database) - Logger.LogInformation($"Migrating separate database of tenant. Database Name = {DatabaseName}, TenantId = {tenantId}"); + Logger.LogInformation( + $"Migrating separate database of tenant. Database Name = {DatabaseName}, TenantId = {tenantId}"); result = await MigrateDatabaseSchemaWithDbContextAsync(); } } @@ -102,15 +234,17 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient var tryCount = IncrementEventTryCount(eventData); if (tryCount <= MaxEventTryCount) { - Logger.LogWarning($"Could not apply database migrations. Re-queueing the operation. TenantId = {eventData.TenantId}, Database Name = {eventData.DatabaseName}."); + Logger.LogWarning( + $"Could not apply database migrations. Re-queueing the operation. TenantId = {eventData.TenantId}, Database Name = {eventData.DatabaseName}."); Logger.LogException(exception, LogLevel.Warning); - await Task.Delay(RandomHelper.GetRandom(5000, 15000)); + await Task.Delay(RandomHelper.GetRandom(MinValueToWaitOnFailure, MaxValueToWaitOnFailure)); await DistributedEventBus.PublishAsync(eventData); } else { - Logger.LogError($"Could not apply database migrations. Canceling the operation. TenantId = {eventData.TenantId}, DatabaseName = {eventData.DatabaseName}."); + Logger.LogError( + $"Could not apply database migrations. Canceling the operation. TenantId = {eventData.TenantId}, DatabaseName = {eventData.DatabaseName}."); Logger.LogException(exception); } } @@ -122,7 +256,8 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient var tryCount = IncrementEventTryCount(eventData); if (tryCount <= MaxEventTryCount) { - Logger.LogWarning($"Could not perform tenant created event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogWarning( + $"Could not perform tenant created event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); Logger.LogException(exception, LogLevel.Warning); await Task.Delay(RandomHelper.GetRandom(5000, 15000)); @@ -130,7 +265,8 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient } else { - Logger.LogError($"Could not perform tenant created event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogError( + $"Could not perform tenant created event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); Logger.LogException(exception); } } @@ -142,7 +278,8 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient var tryCount = IncrementEventTryCount(eventData); if (tryCount <= MaxEventTryCount) { - Logger.LogWarning($"Could not perform tenant connection string updated event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogWarning( + $"Could not perform tenant connection string updated event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); Logger.LogException(exception, LogLevel.Warning); await Task.Delay(RandomHelper.GetRandom(5000, 15000)); @@ -150,34 +287,12 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient } else { - Logger.LogError($"Could not perform tenant connection string updated event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogError( + $"Could not perform tenant connection string updated event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); Logger.LogException(exception); } } - protected virtual async Task QueueTenantMigrationsAsync() - { - await DistributedEventBus.PublishAsync( - new DatabaseMigrationsAvailableEto - { - DatabaseName = DatabaseName - } - ); - /* - var tenants = await TenantStore.GetListWithSeparateConnectionStringAsync(); - foreach (var tenant in tenants) - { - await DistributedEventBus.PublishAsync( - new ApplyDatabaseMigrationsEto - { - DatabaseName = DatabaseName, - TenantId = tenant.Id - } - ); - } - */ - } - private static int GetEventTryCount(EtoBase eventData) { var tryCountAsString = eventData.Properties.GetOrDefault(TryCountPropertyName); @@ -200,4 +315,4 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient SetEventTryCount(eventData, count); return count; } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs similarity index 51% rename from framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs rename to framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs index 56680c24e4..9b9719ff1b 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; using Volo.Abp.DistributedLocking; using Volo.Abp.EventBus.Distributed; using Volo.Abp.MultiTenancy; @@ -11,32 +13,45 @@ using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore.Migrations; -public abstract class PendingEfCoreMigrationsChecker where TDbContext : DbContext, IEfCoreDbContext +public abstract class EfCoreRuntimeDatabaseMigratorBase : ITransientDependency + where TDbContext : DbContext, IEfCoreDbContext { + protected int MinValueToWaitOnFailure { get; set; } = 5000; + protected int MaxValueToWaitOnFailure { get; set; } = 15000; + + protected string DatabaseName { get; } + + /// + /// Enabling this might be inefficient if you have many tenants! + /// If disabled (default), tenant databases will be seeded only + /// if there is a schema migration applied to the host database. + /// If enabled, tenant databases will be seeded always on every service startup. + /// + protected bool AlwaysSeedTenantDatabases { get; set; } = false; + protected IUnitOfWorkManager UnitOfWorkManager { get; } protected IServiceProvider ServiceProvider { get; } protected ICurrentTenant CurrentTenant { get; } - protected IDistributedEventBus DistributedEventBus { get; } protected IAbpDistributedLock DistributedLock { get; } - protected ILogger> Logger { get; } - protected string DatabaseName { get; } + protected IDistributedEventBus DistributedEventBus { get; } + protected ILogger> Logger { get; } - protected PendingEfCoreMigrationsChecker( - ILoggerFactory loggerFactory, + protected EfCoreRuntimeDatabaseMigratorBase( + string databaseName, IUnitOfWorkManager unitOfWorkManager, IServiceProvider serviceProvider, ICurrentTenant currentTenant, - IDistributedEventBus distributedEventBus, IAbpDistributedLock abpDistributedLock, - string databaseName) + IDistributedEventBus distributedEventBus, + ILoggerFactory loggerFactory) { + DatabaseName = databaseName; UnitOfWorkManager = unitOfWorkManager; ServiceProvider = serviceProvider; CurrentTenant = currentTenant; - DistributedEventBus = distributedEventBus; DistributedLock = abpDistributedLock; - DatabaseName = databaseName; - Logger = loggerFactory.CreateLogger>(); + DistributedEventBus = distributedEventBus; + Logger = loggerFactory.CreateLogger>(); } public virtual async Task CheckAndApplyDatabaseMigrationsAsync() @@ -46,15 +61,19 @@ public abstract class PendingEfCoreMigrationsChecker where TDbContex protected virtual async Task LockAndApplyDatabaseMigrationsAsync() { - await using (var handle = await DistributedLock.TryAcquireAsync("Migration_" + DatabaseName)) - { - Logger.LogInformation($"Lock is acquired for db migration and seeding on database named: {DatabaseName}..."); + Logger.LogInformation($"Trying to acquire the distributed lock for database migration: {DatabaseName}."); + var schemaMigrated = false; + + await using (var handle = await DistributedLock.TryAcquireAsync("DatabaseMigration_" + DatabaseName)) + { if (handle is null) { - Logger.LogInformation($"Handle is null because of the locking for : {DatabaseName}"); + Logger.LogInformation($"Distributed lock could not be acquired for database migration: {DatabaseName}. Operation cancelled."); return; } + + Logger.LogInformation($"Distributed lock is acquired for database migration: {DatabaseName}..."); using (CurrentTenant.Change(null)) { @@ -72,17 +91,36 @@ public abstract class PendingEfCoreMigrationsChecker where TDbContex if (pendingMigrations.Any()) { await dbContext.Database.MigrateAsync(); + schemaMigrated = true; } await uow.CompleteAsync(); } } + + await SeedAsync(); - Logger.LogInformation($"Lock is released for db migration and seeding on database named: {DatabaseName}..."); + if (schemaMigrated || AlwaysSeedTenantDatabases) + { + await DistributedEventBus.PublishAsync( + new AppliedDatabaseMigrationsEto + { + DatabaseName = DatabaseName, + TenantId = null + } + ); + } } + + Logger.LogInformation($"Distributed lock has been released for database migration: {DatabaseName}..."); } - - public async Task TryAsync(Func task, int retryCount = 3) + + protected virtual Task SeedAsync() + { + return Task.CompletedTask; + } + + protected virtual async Task TryAsync(Func task, int maxTryCount = 3) { try { @@ -90,18 +128,18 @@ public abstract class PendingEfCoreMigrationsChecker where TDbContex } catch (Exception ex) { - retryCount--; + maxTryCount--; - if (retryCount <= 0) + if (maxTryCount <= 0) { throw; } - Logger.LogWarning($"{ex.GetType().Name} has been thrown. The operation will be tried {retryCount} times more. Exception:\n{ex.Message}"); + Logger.LogWarning($"{ex.GetType().Name} has been thrown. The operation will be tried {maxTryCount} times more. Exception:\n{ex.Message}. Stack Trace:\n{ex.StackTrace}"); - await Task.Delay(RandomHelper.GetRandom(5000, 15000)); + await Task.Delay(RandomHelper.GetRandom(MinValueToWaitOnFailure, MaxValueToWaitOnFailure)); - await TryAsync(task, retryCount); + await TryAsync(task, maxTryCount); } } } \ No newline at end of file