diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlDatabaseMigrator.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlDatabaseMigrator.cs new file mode 100644 index 0000000000..b0ea89fa1b --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlDatabaseMigrator.cs @@ -0,0 +1,23 @@ +namespace ServiceControl.Persistence.EFCore.PostgreSql; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using ServiceControl.Persistence.EFCore.Abstractions; +using ServiceControl.Persistence.EFCore.DbContexts; + +class PostgreSqlDatabaseMigrator(ServiceControlDbContext dbContext, ILogger logger) : IDatabaseMigrator +{ + public async Task ApplyMigrations(CancellationToken cancellationToken = default) + { + logger.LogInformation("Starting PostgreSQL database migration"); + + var previousTimeout = dbContext.Database.GetCommandTimeout(); + dbContext.Database.SetCommandTimeout(EFPersisterSettings.MigrationCommandTimeout); + + await dbContext.Database.MigrateAsync(cancellationToken); + + dbContext.Database.SetCommandTimeout(previousTimeout); + + logger.LogInformation("PostgreSQL database migration completed"); + } +} diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlPersistence.cs b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlPersistence.cs index 0f3f01ad9a..bba7a9506a 100644 --- a/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlPersistence.cs +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/PostgreSqlPersistence.cs @@ -3,6 +3,7 @@ namespace ServiceControl.Persistence.EFCore.PostgreSql; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using ServiceControl.Persistence.EFCore.Abstractions; +using ServiceControl.Persistence.EFCore.DbContexts; class PostgreSqlPersistence(PostgreSqlPersisterSettings settings) : BasePersistence, IPersistence { @@ -17,6 +18,8 @@ public void AddInstaller(IServiceCollection services) { RegisterSettings(services); ConfigureDbContext(services); + + services.AddScoped(); } void RegisterSettings(IServiceCollection services) @@ -26,7 +29,28 @@ void RegisterSettings(IServiceCollection services) services.AddSingleton(settings); } - void ConfigureDbContext(IServiceCollection services) => - services.AddPooledDbContextFactory(options => - options.UseNpgsql(settings.ConnectionString, npgsql => npgsql.CommandTimeout(settings.CommandTimeout))); + void ConfigureDbContext(IServiceCollection services) + { + services.AddDbContext((serviceProvider, options) => + { + options.UseNpgsql(settings.ConnectionString, npgsqlOptions => + { + npgsqlOptions.CommandTimeout(settings.CommandTimeout); + if (settings.EnableRetryOnFailure) + { + npgsqlOptions.EnableRetryOnFailure( + maxRetryCount: settings.MaxRetryCount, + maxRetryDelay: TimeSpan.FromSeconds(settings.MaxRetryDelayInSeconds), + errorCodesToAdd: null); + } + }); + + if (settings.EnableSensitiveDataLogging) + { + options.EnableSensitiveDataLogging(); + } + }, ServiceLifetime.Scoped); + + services.AddScoped(provider => provider.GetRequiredService()); + } } diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerDatabaseMigrator.cs b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerDatabaseMigrator.cs new file mode 100644 index 0000000000..dfa0f8e4b8 --- /dev/null +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerDatabaseMigrator.cs @@ -0,0 +1,23 @@ +namespace ServiceControl.Persistence.EFCore.SqlServer; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using ServiceControl.Persistence.EFCore.Abstractions; +using ServiceControl.Persistence.EFCore.DbContexts; + +class SqlServerDatabaseMigrator(ServiceControlDbContext dbContext, ILogger logger) : IDatabaseMigrator +{ + public async Task ApplyMigrations(CancellationToken cancellationToken = default) + { + logger.LogInformation("Starting SQL Server database migration"); + + var previousTimeout = dbContext.Database.GetCommandTimeout(); + dbContext.Database.SetCommandTimeout(EFPersisterSettings.MigrationCommandTimeout); + + await dbContext.Database.MigrateAsync(cancellationToken); + + dbContext.Database.SetCommandTimeout(previousTimeout); + + logger.LogInformation("SQL Server database migration completed"); + } +} diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerPersistence.cs b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerPersistence.cs index afbf49c124..171f08a837 100644 --- a/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerPersistence.cs +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/SqlServerPersistence.cs @@ -3,6 +3,7 @@ namespace ServiceControl.Persistence.EFCore.SqlServer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using ServiceControl.Persistence.EFCore.Abstractions; +using ServiceControl.Persistence.EFCore.DbContexts; class SqlServerPersistence(SqlServerPersisterSettings settings) : BasePersistence, IPersistence { @@ -17,6 +18,8 @@ public void AddInstaller(IServiceCollection services) { RegisterSettings(services); ConfigureDbContext(services); + + services.AddScoped(); } void RegisterSettings(IServiceCollection services) @@ -26,7 +29,28 @@ void RegisterSettings(IServiceCollection services) services.AddSingleton(settings); } - void ConfigureDbContext(IServiceCollection services) => - services.AddPooledDbContextFactory(options => - options.UseSqlServer(settings.ConnectionString, sqlServer => sqlServer.CommandTimeout(settings.CommandTimeout))); + void ConfigureDbContext(IServiceCollection services) + { + services.AddDbContext((serviceProvider, options) => + { + options.UseSqlServer(settings.ConnectionString, sqlOptions => + { + sqlOptions.CommandTimeout(settings.CommandTimeout); + if (settings.EnableRetryOnFailure) + { + sqlOptions.EnableRetryOnFailure( + maxRetryCount: settings.MaxRetryCount, + maxRetryDelay: TimeSpan.FromSeconds(settings.MaxRetryDelayInSeconds), + errorNumbersToAdd: null); + } + }); + + if (settings.EnableSensitiveDataLogging) + { + options.EnableSensitiveDataLogging(); + } + }, ServiceLifetime.Scoped); + + services.AddScoped(provider => provider.GetRequiredService()); + } } diff --git a/src/ServiceControl.Persistence.EFCore/Abstractions/EFPersisterSettings.cs b/src/ServiceControl.Persistence.EFCore/Abstractions/EFPersisterSettings.cs index 6789116fb7..d6a5d6ca38 100644 --- a/src/ServiceControl.Persistence.EFCore/Abstractions/EFPersisterSettings.cs +++ b/src/ServiceControl.Persistence.EFCore/Abstractions/EFPersisterSettings.cs @@ -2,9 +2,15 @@ namespace ServiceControl.Persistence.EFCore.Abstractions; public abstract class EFPersisterSettings : PersistenceSettings { + public static readonly TimeSpan MigrationCommandTimeout = TimeSpan.FromMinutes(40); + public required string ConnectionString { get; set; } public int CommandTimeout { get; set; } = 30; public TimeSpan ErrorRetentionPeriod { get; set; } public string? MessageBodyStoragePath { get; set; } public int MinBodySizeForCompression { get; set; } = 4096; + public int MaxRetryCount { get; set; } = 5; + public int MaxRetryDelayInSeconds { get; set; } = 30; + public bool EnableSensitiveDataLogging { get; set; } + public bool EnableRetryOnFailure { get; set; } = true; } diff --git a/src/ServiceControl.Persistence/IDatabaseMigrator.cs b/src/ServiceControl.Persistence/IDatabaseMigrator.cs new file mode 100644 index 0000000000..d5836fe503 --- /dev/null +++ b/src/ServiceControl.Persistence/IDatabaseMigrator.cs @@ -0,0 +1,9 @@ +namespace ServiceControl.Persistence; + +using System.Threading; +using System.Threading.Tasks; + +public interface IDatabaseMigrator +{ + Task ApplyMigrations(CancellationToken cancellationToken = default); +} diff --git a/src/ServiceControl/Hosting/Commands/SetupCommand.cs b/src/ServiceControl/Hosting/Commands/SetupCommand.cs index 5c6170da21..8cfd79d05e 100644 --- a/src/ServiceControl/Hosting/Commands/SetupCommand.cs +++ b/src/ServiceControl/Hosting/Commands/SetupCommand.cs @@ -2,6 +2,7 @@ { using System.Runtime.InteropServices; using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Particular.ServiceControl; @@ -9,6 +10,7 @@ using ServiceBus.Management.Infrastructure.Installers; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl.Infrastructure; + using ServiceControl.Persistence; using Transports; class SetupCommand : AbstractCommand @@ -47,6 +49,14 @@ public override async Task Execute(HostArguments args, Settings settings) await transportCustomization.ProvisionQueues(transportSettings, componentSetupContext.Queues); } + await using (var scope = host.Services.CreateAsyncScope()) + { + if (scope.ServiceProvider.GetService() is { } databaseMigrator) + { + await databaseMigrator.ApplyMigrations(); + } + } + await host.StopAsync(); } }