Skip to content

Commit

Permalink
Fix race condition in migrator (#2730)
Browse files Browse the repository at this point in the history
  • Loading branch information
leotsarev authored Aug 9, 2024
1 parent e0444f3 commit ad9aabe
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Data.Entity.Migrations;
using JoinRpg.Dal.Impl;

namespace Joinrpg.Dal.Migrate;
namespace Joinrpg.Dal.Migrate.Ef6;

internal class JoinMigrationsConfig : DbMigrationsConfiguration<MyDbContext>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Joinrpg.Dal.Migrate;
namespace Joinrpg.Dal.Migrate.Ef6;

internal class MigrateHostService(
IHostApplicationLifetime applicationLifetime,
ILogger<MigrateHostService> logger,
internal class MigrateMyDbContextService(
ILogger<MigrateMyDbContextService> logger,
IConfiguration configuration)
: OneTimeOperationHostedServiceBase(applicationLifetime, logger)
: IMigratorService
{
internal override void DoWork()
public Task MigrateAsync(CancellationToken ct)
{
logger.LogInformation("Create migration");

Expand All @@ -33,5 +31,7 @@ internal override void DoWork()
logger.LogInformation("Pending migrations {pending}", string.Join("\n", pending));
migrator.Update(); // TODO pass migration name from command line to allow reverts
logger.LogInformation("Migration completed");

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Data.Entity.Migrations.Infrastructure;
using Microsoft.Extensions.Logging;

namespace Joinrpg.Dal.Migrate;
namespace Joinrpg.Dal.Migrate.Ef6;

internal class MigrationsLoggerILoggerAdapter : MigrationsLogger
{
Expand Down
9 changes: 5 additions & 4 deletions src/Joinrpg.Dal.Migrate/EfCore/EfCoreMigrationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ internal static class EfCoreMigrationExtensions
internal static void RegisterMigrator<TContext>(this IServiceCollection services, string connectionString)
where TContext : DbContext
{
_ = services.AddHostedService<MigrateEfCoreHostService<TContext>>();
_ = services.AddScoped<IMigratorService, MigrateEfCoreHostService<TContext>>();
_ = services.AddDbContext<TContext>(options =>
{
options.UseNpgsql(connectionString);
options.EnableSensitiveDataLogging(false); // Logs of migration is publicly accessible
options.EnableDetailedErrors(true); // This will be helpful
_ = options
.UseNpgsql(connectionString)
.EnableSensitiveDataLogging(false) // Logs of migration is publicly accessible
.EnableDetailedErrors(true); // This will be helpful
});
}
}
56 changes: 18 additions & 38 deletions src/Joinrpg.Dal.Migrate/EfCore/MigrateEfCoreHostService.cs
Original file line number Diff line number Diff line change
@@ -1,71 +1,51 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Joinrpg.Dal.Migrate;

internal class MigrateEfCoreHostService<TContext>(
IServiceProvider services,
ILogger<MigrateEfCoreHostService<TContext>> logger,
IHostApplicationLifetime applicationLifetime) : BackgroundService
TContext dbContext,
ILogger<MigrateEfCoreHostService<TContext>> logger) : IMigratorService
where TContext : DbContext
{
private async Task MigrateAsync(DbContext dbContext, CancellationToken stoppingToken)
private static readonly string contextName = typeof(TContext).Name!;
public async Task MigrateAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Start migration of {contextName}", typeof(TContext).FullName);
using var logScope = logger.BeginScope("Migration of {dbContext}", contextName);

logger.LogInformation("Start migration of {dbContext}", contextName);

var lastAppliedMigration = (await dbContext.Database.GetAppliedMigrationsAsync(stoppingToken)).LastOrDefault();
if (!string.IsNullOrEmpty(lastAppliedMigration))
{
logger.LogInformation("Last applied migration: {LastAppliedMigration}", lastAppliedMigration);
logger.LogInformation("Last applied migration for {dbContext}: {LastAppliedMigration}", contextName, lastAppliedMigration);
}

if (stoppingToken.IsCancellationRequested)
{
return;
}

if (dbContext.Database.HasPendingModelChanges())
{
logger.LogError("There is pending changes in model!");
throw new InvalidOperationException("Pending changes in model");
}

var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(stoppingToken);

foreach (var pm in pendingMigrations)
{
logger.LogInformation("Pending migration: {PendingMigration}", pm);
logger.LogInformation("Pending migration for {dbContext}: {PendingMigration}", contextName, pm);
}

if (stoppingToken.IsCancellationRequested)
{
return;
}

logger.LogInformation("Applying migrations...");
logger.LogInformation("Applying migrations for {dbContext} ...", contextName);
await dbContext.Database.MigrateAsync(stoppingToken);
logger.LogInformation("Database has been successfully migrated");
}

/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Starting migrator...");
await using var scope = services.CreateAsyncScope();
try
{
await MigrateAsync(
scope.ServiceProvider.GetRequiredService<TContext>(),
stoppingToken);

if (stoppingToken.IsCancellationRequested)
{
logger.LogInformation("Terminating by cancellation token");
}
}
catch (Exception ex)
{
logger.LogError(ex, "Error executing migrator");
Environment.ExitCode = 1;
}
finally
{
applicationLifetime.StopApplication();
}
logger.LogInformation("Database {dbContext} has been successfully migrated", contextName);
}
}
5 changes: 5 additions & 0 deletions src/Joinrpg.Dal.Migrate/IMigratorService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace Joinrpg.Dal.Migrate;
internal interface IMigratorService
{
internal abstract Task MigrateAsync(CancellationToken ct);
}
25 changes: 11 additions & 14 deletions src/Joinrpg.Dal.Migrate/OneTimeOperationHostedServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,27 @@

namespace Joinrpg.Dal.Migrate;

internal abstract class OneTimeOperationHostedServiceBase : IHostedService
internal class OneTimeOperationHostedServiceBase(
IHostApplicationLifetime applicationLifetime,
ILogger<OneTimeOperationHostedServiceBase> logger,
IEnumerable<IMigratorService> migrators) : IHostedService
{
private CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource();

private Task? task;
private readonly IHostApplicationLifetime applicationLifetime;
protected readonly ILogger logger;

public OneTimeOperationHostedServiceBase(IHostApplicationLifetime applicationLifetime, ILogger<MigrateHostService> logger)
{
this.applicationLifetime = applicationLifetime;
this.logger = logger;
}

Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Starting task");
task = Task.Run(() =>
task = Task.Run(async () =>
{
try
{
DoWork();
foreach (var migrator in migrators)
{
using var scope = logger.BeginScope("Migration of {migrator}", migrator.GetType().Name);
await migrator.MigrateAsync(cancellationToken);
}
}
catch (Exception ex)
{
Expand All @@ -35,12 +34,10 @@ Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
applicationLifetime.StopApplication();
}
}, CancellationToken.None);
}, cancellationToken);
return Task.CompletedTask;
}

internal abstract void DoWork();

Task IHostedService.StopAsync(CancellationToken cancellationToken)
{
CancellationTokenSource.Cancel();
Expand Down
5 changes: 4 additions & 1 deletion src/Joinrpg.Dal.Migrate/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Joinrpg.Dal.Migrate.Ef6;
using Joinrpg.Dal.Migrate.EfCore;
using JoinRpg.Dal.JobService;
using JoinRpg.Portal.Infrastructure;
Expand All @@ -19,7 +20,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
_ = services.AddHostedService<MigrateHostService>();
_ = services.AddHostedService<OneTimeOperationHostedServiceBase>();
services.AddScoped<IMigratorService, MigrateMyDbContextService>();
services.RegisterMigrator<DataProtectionDbContext>(hostContext.Configuration.GetConnectionString("DataProtection")!);
services.RegisterMigrator<JobScheduleDataDbContext>(hostContext.Configuration.GetConnectionString("DailyJob")!);
Expand Down

0 comments on commit ad9aabe

Please sign in to comment.