Browse Source

Finalized initial runtime database migrations for ef core

pull/16862/head
Halil İbrahim Kalkan 3 years ago
parent
commit
9eea31b2bb
  1. 12
      framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs
  2. 11
      framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs
  3. 191
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs
  4. 84
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs

12
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; }
}

11
framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs

@ -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; }
}

191
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs

@ -14,25 +14,42 @@ using Volo.Abp.Uow;
namespace Volo.Abp.EntityFrameworkCore.Migrations; namespace Volo.Abp.EntityFrameworkCore.Migrations;
public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : ITransientDependency public abstract class DatabaseMigrationEventHandlerBase<TDbContext> :
IDistributedEventHandler<TenantCreatedEto>,
IDistributedEventHandler<TenantConnectionStringUpdatedEto>,
IDistributedEventHandler<ApplyDatabaseMigrationsEto>,
ITransientDependency
where TDbContext : DbContext, IEfCoreDbContext where TDbContext : DbContext, IEfCoreDbContext
{ {
protected const string TryCountPropertyName = "TryCount"; protected string DatabaseName { get; }
protected const int MaxEventTryCount = 3;
protected const string TryCountPropertyName = "__TryCount";
protected int MaxEventTryCount { get; set; } = 3;
/// <summary>
/// As milliseconds.
/// </summary>
protected int MinValueToWaitOnFailure { get; set; } = 5000;
/// <summary>
/// As milliseconds.
/// </summary>
protected int MaxValueToWaitOnFailure { get; set; } = 15000;
protected ICurrentTenant CurrentTenant { get; } protected ICurrentTenant CurrentTenant { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected ITenantStore TenantStore { get; } protected ITenantStore TenantStore { get; }
protected IDistributedEventBus DistributedEventBus { get; } protected IDistributedEventBus DistributedEventBus { get; }
protected ILogger<DatabaseMigrationEventHandlerBase<TDbContext>> Logger { get; } protected ILogger<DatabaseMigrationEventHandlerBase<TDbContext>> Logger { get; }
protected string DatabaseName { get; }
protected DatabaseMigrationEventHandlerBase( protected DatabaseMigrationEventHandlerBase(
ILoggerFactory loggerFactory, string databaseName,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager, IUnitOfWorkManager unitOfWorkManager,
ITenantStore tenantStore, ITenantStore tenantStore,
IDistributedEventBus distributedEventBus, IDistributedEventBus distributedEventBus,
string databaseName) ILoggerFactory loggerFactory)
{ {
CurrentTenant = currentTenant; CurrentTenant = currentTenant;
UnitOfWorkManager = unitOfWorkManager; UnitOfWorkManager = unitOfWorkManager;
@ -43,6 +60,120 @@ public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : ITransient
Logger = loggerFactory.CreateLogger<DatabaseMigrationEventHandlerBase<TDbContext>>(); Logger = loggerFactory.CreateLogger<DatabaseMigrationEventHandlerBase<TDbContext>>();
} }
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;
}
/// <summary> /// <summary>
/// Apply pending EF Core schema migrations to the database. /// Apply pending EF Core schema migrations to the database.
/// Returns true if any migration has applied. /// Returns true if any migration has applied.
@ -83,7 +214,8 @@ public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : ITransient
!tenantConfiguration.ConnectionStrings.GetOrDefault(DatabaseName).IsNullOrWhiteSpace()) !tenantConfiguration.ConnectionStrings.GetOrDefault(DatabaseName).IsNullOrWhiteSpace())
{ {
//Migrating the tenant database (only if tenant has a separate database) //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(); result = await MigrateDatabaseSchemaWithDbContextAsync();
} }
} }
@ -102,15 +234,17 @@ public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : ITransient
var tryCount = IncrementEventTryCount(eventData); var tryCount = IncrementEventTryCount(eventData);
if (tryCount <= MaxEventTryCount) 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); Logger.LogException(exception, LogLevel.Warning);
await Task.Delay(RandomHelper.GetRandom(5000, 15000)); await Task.Delay(RandomHelper.GetRandom(MinValueToWaitOnFailure, MaxValueToWaitOnFailure));
await DistributedEventBus.PublishAsync(eventData); await DistributedEventBus.PublishAsync(eventData);
} }
else 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); Logger.LogException(exception);
} }
} }
@ -122,7 +256,8 @@ public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : ITransient
var tryCount = IncrementEventTryCount(eventData); var tryCount = IncrementEventTryCount(eventData);
if (tryCount <= MaxEventTryCount) 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); Logger.LogException(exception, LogLevel.Warning);
await Task.Delay(RandomHelper.GetRandom(5000, 15000)); await Task.Delay(RandomHelper.GetRandom(5000, 15000));
@ -130,7 +265,8 @@ public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : ITransient
} }
else 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); Logger.LogException(exception);
} }
} }
@ -142,7 +278,8 @@ public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : ITransient
var tryCount = IncrementEventTryCount(eventData); var tryCount = IncrementEventTryCount(eventData);
if (tryCount <= MaxEventTryCount) 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); Logger.LogException(exception, LogLevel.Warning);
await Task.Delay(RandomHelper.GetRandom(5000, 15000)); await Task.Delay(RandomHelper.GetRandom(5000, 15000));
@ -150,34 +287,12 @@ public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : ITransient
} }
else 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); 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) private static int GetEventTryCount(EtoBase eventData)
{ {
var tryCountAsString = eventData.Properties.GetOrDefault(TryCountPropertyName); var tryCountAsString = eventData.Properties.GetOrDefault(TryCountPropertyName);
@ -200,4 +315,4 @@ public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : ITransient
SetEventTryCount(eventData, count); SetEventTryCount(eventData, count);
return count; return count;
} }
} }

84
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs → framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs

@ -4,6 +4,8 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking; using Volo.Abp.DistributedLocking;
using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
@ -11,32 +13,45 @@ using Volo.Abp.Uow;
namespace Volo.Abp.EntityFrameworkCore.Migrations; namespace Volo.Abp.EntityFrameworkCore.Migrations;
public abstract class PendingEfCoreMigrationsChecker<TDbContext> where TDbContext : DbContext, IEfCoreDbContext public abstract class EfCoreRuntimeDatabaseMigratorBase<TDbContext> : ITransientDependency
where TDbContext : DbContext, IEfCoreDbContext
{ {
protected int MinValueToWaitOnFailure { get; set; } = 5000;
protected int MaxValueToWaitOnFailure { get; set; } = 15000;
protected string DatabaseName { get; }
/// <summary>
/// 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.
/// </summary>
protected bool AlwaysSeedTenantDatabases { get; set; } = false;
protected IUnitOfWorkManager UnitOfWorkManager { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected IServiceProvider ServiceProvider { get; } protected IServiceProvider ServiceProvider { get; }
protected ICurrentTenant CurrentTenant { get; } protected ICurrentTenant CurrentTenant { get; }
protected IDistributedEventBus DistributedEventBus { get; }
protected IAbpDistributedLock DistributedLock { get; } protected IAbpDistributedLock DistributedLock { get; }
protected ILogger<PendingEfCoreMigrationsChecker<TDbContext>> Logger { get; } protected IDistributedEventBus DistributedEventBus { get; }
protected string DatabaseName { get; } protected ILogger<EfCoreRuntimeDatabaseMigratorBase<TDbContext>> Logger { get; }
protected PendingEfCoreMigrationsChecker( protected EfCoreRuntimeDatabaseMigratorBase(
ILoggerFactory loggerFactory, string databaseName,
IUnitOfWorkManager unitOfWorkManager, IUnitOfWorkManager unitOfWorkManager,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IDistributedEventBus distributedEventBus,
IAbpDistributedLock abpDistributedLock, IAbpDistributedLock abpDistributedLock,
string databaseName) IDistributedEventBus distributedEventBus,
ILoggerFactory loggerFactory)
{ {
DatabaseName = databaseName;
UnitOfWorkManager = unitOfWorkManager; UnitOfWorkManager = unitOfWorkManager;
ServiceProvider = serviceProvider; ServiceProvider = serviceProvider;
CurrentTenant = currentTenant; CurrentTenant = currentTenant;
DistributedEventBus = distributedEventBus;
DistributedLock = abpDistributedLock; DistributedLock = abpDistributedLock;
DatabaseName = databaseName; DistributedEventBus = distributedEventBus;
Logger = loggerFactory.CreateLogger<PendingEfCoreMigrationsChecker<TDbContext>>(); Logger = loggerFactory.CreateLogger<EfCoreRuntimeDatabaseMigratorBase<TDbContext>>();
} }
public virtual async Task CheckAndApplyDatabaseMigrationsAsync() public virtual async Task CheckAndApplyDatabaseMigrationsAsync()
@ -46,15 +61,19 @@ public abstract class PendingEfCoreMigrationsChecker<TDbContext> where TDbContex
protected virtual async Task LockAndApplyDatabaseMigrationsAsync() protected virtual async Task LockAndApplyDatabaseMigrationsAsync()
{ {
await using (var handle = await DistributedLock.TryAcquireAsync("Migration_" + DatabaseName)) Logger.LogInformation($"Trying to acquire the distributed lock for database migration: {DatabaseName}.");
{
Logger.LogInformation($"Lock is acquired for db migration and seeding on database named: {DatabaseName}...");
var schemaMigrated = false;
await using (var handle = await DistributedLock.TryAcquireAsync("DatabaseMigration_" + DatabaseName))
{
if (handle is null) 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; return;
} }
Logger.LogInformation($"Distributed lock is acquired for database migration: {DatabaseName}...");
using (CurrentTenant.Change(null)) using (CurrentTenant.Change(null))
{ {
@ -72,17 +91,36 @@ public abstract class PendingEfCoreMigrationsChecker<TDbContext> where TDbContex
if (pendingMigrations.Any()) if (pendingMigrations.Any())
{ {
await dbContext.Database.MigrateAsync(); await dbContext.Database.MigrateAsync();
schemaMigrated = true;
} }
await uow.CompleteAsync(); 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> task, int retryCount = 3) protected virtual Task SeedAsync()
{
return Task.CompletedTask;
}
protected virtual async Task TryAsync(Func<Task> task, int maxTryCount = 3)
{ {
try try
{ {
@ -90,18 +128,18 @@ public abstract class PendingEfCoreMigrationsChecker<TDbContext> where TDbContex
} }
catch (Exception ex) catch (Exception ex)
{ {
retryCount--; maxTryCount--;
if (retryCount <= 0) if (maxTryCount <= 0)
{ {
throw; 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);
} }
} }
} }
Loading…
Cancel
Save