From 155fac059a32b0503f80f8ec42eb403158467ae3 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Wed, 3 Dec 2025 05:48:01 +0000 Subject: [PATCH] all dbs now set as simple mode --- .../DbContexts/AuthenticationDbContext.cs | 59 ++++++++-- .../HostedServices/DatabaseInitializer.cs | 110 ++++++++++++++++++ SecurityService/Startup.cs | 58 ++++----- 3 files changed, 192 insertions(+), 35 deletions(-) create mode 100644 SecurityService/HostedServices/DatabaseInitializer.cs diff --git a/SecurityService.Database/DbContexts/AuthenticationDbContext.cs b/SecurityService.Database/DbContexts/AuthenticationDbContext.cs index 4e45d42..cda4d88 100644 --- a/SecurityService.Database/DbContexts/AuthenticationDbContext.cs +++ b/SecurityService.Database/DbContexts/AuthenticationDbContext.cs @@ -1,28 +1,69 @@ -using SecurityService.BusinessLogic; +using Microsoft.Data.SqlClient; +using SecurityService.BusinessLogic; namespace SecurityService.Database.DbContexts { - using System; using Duende.IdentityServer.EntityFramework.DbContexts; using Duende.IdentityServer.EntityFramework.Options; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Shared.General; + using System; + using System.Threading; + using System.Threading.Tasks; - public class AuthenticationDbContext : IdentityDbContext - { - public AuthenticationDbContext(DbContextOptions options) - : base(options) - { + public class AuthenticationDbContext : IdentityDbContext { + public AuthenticationDbContext(DbContextOptions options) : base(options) { } - protected override void OnModelCreating(ModelBuilder builder) - { + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // Customize the ASP.NET Identity model and override the defaults if needed. // For example, you can rename the ASP.NET Identity table names and more. // Add your customizations after calling base.OnModelCreating(builder); } } + public static class Extensions + { + + public static async Task SetDbInSimpleMode(this DbContext context, CancellationToken cancellationToken) + { + var dbName = context.Database.GetDbConnection().Database; + + var connection = context.Database.GetDbConnection(); + if (connection.State != System.Data.ConnectionState.Open) + await connection.OpenAsync(cancellationToken); + + // 1. Check current recovery model + await using var checkCommand = connection.CreateCommand(); + checkCommand.CommandText = @" +SELECT recovery_model_desc +FROM sys.databases +WHERE name = @dbName; +"; + var param = checkCommand.CreateParameter(); + param.ParameterName = "@dbName"; + param.Value = dbName; + checkCommand.Parameters.Add(param); + + var result = await checkCommand.ExecuteScalarAsync(cancellationToken); + var currentRecoveryModel = result?.ToString(); + + if (currentRecoveryModel != "SIMPLE") + { + // 2. Alter database outside transaction + await using var alterCommand = connection.CreateCommand(); + alterCommand.CommandText = $@" +ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; +ALTER DATABASE [{dbName}] SET RECOVERY SIMPLE; +ALTER DATABASE [{dbName}] SET MULTI_USER; +"; + // Execute outside EF transaction + await alterCommand.ExecuteNonQueryAsync(cancellationToken); + } + } + } + + } \ No newline at end of file diff --git a/SecurityService/HostedServices/DatabaseInitializer.cs b/SecurityService/HostedServices/DatabaseInitializer.cs new file mode 100644 index 0000000..112259d --- /dev/null +++ b/SecurityService/HostedServices/DatabaseInitializer.cs @@ -0,0 +1,110 @@ +using System; +using Duende.IdentityServer.EntityFramework.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using SecurityService.Database.DbContexts; +using Microsoft.Extensions.Logging; +using Shared.Logger; + +public class DatabaseInitializer : IHostedService, IDisposable +{ + private readonly IServiceProvider Services; + private readonly IHostApplicationLifetime Lifetime; + private readonly CancellationTokenSource InternalCts = new(); + private Task? StartupTask; + + public DatabaseInitializer(IServiceProvider services, IHostApplicationLifetime lifetime) { + Services = services; + this.Lifetime = lifetime; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + // start migrations/seeding on the threadpool so StartAsync returns quickly if you want. + StartupTask = Task.Run(() => RunMigrationsAsync(InternalCts.Token), CancellationToken.None); + return Task.CompletedTask; + } + + private async Task RunMigrationsAsync(CancellationToken token) + { + try { + + Logger.LogWarning("About to start DatabaseMigrations"); + + using IServiceScope scope = Services.CreateScope(); + PersistedGrantDbContext persistedGrant = scope.ServiceProvider.GetRequiredService(); + ConfigurationDbContext config = scope.ServiceProvider.GetRequiredService(); + AuthenticationDbContext auth = scope.ServiceProvider.GetRequiredService(); + + if (persistedGrant.Database.IsRelational()) { + await persistedGrant.Database.MigrateAsync(token); + await persistedGrant.SetDbInSimpleMode(token); + } + + if (config.Database.IsRelational()){ + await config.Database.MigrateAsync(token); + await config.SetDbInSimpleMode(token); + } + + if (auth.Database.IsRelational()) { + await auth.Database.MigrateAsync(token); + await auth.SetDbInSimpleMode(token); + } + + Logger.LogWarning("Database Migrations Successful"); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + Logger.LogInformation("DatabaseInitializer migration canceled."); + } + catch (Exception ex) + { + Logger.LogError("DatabaseInitializer migration failed.", ex); + // Request host shutdown: + try + { + Environment.ExitCode = 1; + Lifetime.StopApplication(); + } + catch + { + // ignore any exception from StopApplication, we are already failing + } + + // Rethrow to ensure host doesn't continue starting successfully. + throw; + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + // per-service shutdown window + TimeSpan perServiceTimeout = TimeSpan.FromSeconds(20); + + // request cancel of internal work + await this.InternalCts.CancelAsync(); + + if (StartupTask == null) + return; + + using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + Task timeout = Task.Delay(perServiceTimeout, linked.Token); + Task completed = await Task.WhenAny(StartupTask, timeout); + + if (completed == timeout) + { + Logger.LogWarning($"DatabaseInitializer did not finish within {perServiceTimeout.TotalSeconds}s; continuing shutdown."); + // host will continue shutdown; hosted service StopAsync returned after per-service timeout. + } + else + { + await linked.CancelAsync(); // cancel the delay + await StartupTask; // propagate exceptions if any + } + } + + public void Dispose() => InternalCts.Dispose(); +} \ No newline at end of file diff --git a/SecurityService/Startup.cs b/SecurityService/Startup.cs index 000c667..9d3cd5d 100644 --- a/SecurityService/Startup.cs +++ b/SecurityService/Startup.cs @@ -1,4 +1,6 @@ -using SecurityService.Endpoints; +using System.Threading; +using System.Threading.Tasks; +using SecurityService.Endpoints; namespace SecurityService { @@ -156,7 +158,7 @@ public void Configure(IApplicationBuilder app, app.UseSwaggerUI(); // this will do the initial DB population - this.InitializeDatabase(app); + //this.InitializeDatabase(app); } /// @@ -172,6 +174,9 @@ public void ConfigureContainer(ServiceRegistry services) services.IncludeRegistry(); services.IncludeRegistry(); + // Register the hosted service via the ServiceRegistry (Lamar) + services.AddHostedService(); + Startup.Container = new Container(services); } @@ -179,30 +184,31 @@ public void ConfigureContainer(ServiceRegistry services) /// Initializes the database. /// /// The application. - private void InitializeDatabase(IApplicationBuilder app) - { - using(IServiceScope serviceScope = app.ApplicationServices.GetService().CreateScope()) - { - PersistedGrantDbContext persistedGrantDbContext = serviceScope.ServiceProvider.GetRequiredService(); - ConfigurationDbContext configurationDbContext = serviceScope.ServiceProvider.GetRequiredService(); - AuthenticationDbContext authenticationContext = serviceScope.ServiceProvider.GetRequiredService(); - - if (persistedGrantDbContext != null && persistedGrantDbContext.Database.IsRelational()) - { - persistedGrantDbContext.Database.Migrate(); - } - - if (configurationDbContext != null && configurationDbContext.Database.IsRelational()) - { - configurationDbContext.Database.Migrate(); - } - - if (authenticationContext != null && authenticationContext.Database.IsRelational()) - { - authenticationContext.Database.Migrate(); - } - } - } + //private void InitializeDatabase(IApplicationBuilder app) + //{ + // using(IServiceScope serviceScope = app.ApplicationServices.GetService().CreateScope()) + // { + // PersistedGrantDbContext persistedGrantDbContext = serviceScope.ServiceProvider.GetRequiredService(); + // ConfigurationDbContext configurationDbContext = serviceScope.ServiceProvider.GetRequiredService(); + // AuthenticationDbContext authenticationContext = serviceScope.ServiceProvider.GetRequiredService(); + + // if (persistedGrantDbContext != null && persistedGrantDbContext.Database.IsRelational()) + // { + // persistedGrantDbContext.Database.Migrate(); + // //_ = persistedGrantDbContext.SetDbInSimpleMode(CancellationToken.None); + // } + + // if (configurationDbContext != null && configurationDbContext.Database.IsRelational()) + // { + // configurationDbContext.Database.Migrate(); + // } + + // if (authenticationContext != null && authenticationContext.Database.IsRelational()) + // { + // authenticationContext.Database.Migrate(); + // } + // } + //} #endregion }