(bugfix): fixed fluent migrator startup order

This commit is contained in:
Maksym Sadovnychyy 2026-04-26 10:22:29 +02:00
parent d8cb164de9
commit 8a3e42a159
6 changed files with 18 additions and 17 deletions

View File

@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.3.9] - 2026-04-26
### Fixed
- **Startup / database:** FluentMigrator (`EnsureCertsEngineMigratedAsync`) now runs in `Program.cs` immediately after `WebApplication.Build()` and before `RunAsync`, so schema (including `app_runtime_leases`) exists before any `IHostedService` starts. `InitializationHostedService` only performs bootstrap lease + identity init.
## [3.3.8] - 2026-04-26 ## [3.3.8] - 2026-04-26
### Fixed ### Fixed

View File

@ -4,11 +4,11 @@ using Microsoft.Extensions.Hosting;
namespace MaksIT.CertsUI.Engine.Extensions; namespace MaksIT.CertsUI.Engine.Extensions;
/// <summary> /// <summary>
/// DB migrations are handled by FluentMigrator and optional schema sync from InitializationHostedService. /// DB migrations run in <c>Program.cs</c> via <see cref="ServiceCollectionExtensions.EnsureCertsEngineMigratedAsync"/> before <c>RunAsync</c>.
/// This method is a no-op for backward compatibility with host startup. /// This method is a no-op kept for backward compatibility with older host wiring.
/// </summary> /// </summary>
public static class ApplicationBuilderExtensions { public static class ApplicationBuilderExtensions {
public static void AddCertsEngineMigrations(this IHost host) { public static void AddCertsEngineMigrations(this IHost host) {
// No-op: migrations and schema sync run from InitializationHostedService via IRunMigrationsService and ISchemaSyncService. // No-op: see Program.cs (migrations) and InitializationHostedService (identity bootstrap under lease).
} }
} }

View File

@ -67,7 +67,7 @@ public static class ServiceCollectionExtensions {
#region Host initialization helpers #region Host initialization helpers
/// <summary> /// <summary>
/// Runs FluentMigrator then optional add-only schema sync (when <see cref="ICertsEngineConfiguration.AutoSyncSchema"/> is true). Called from host startup (e.g. InitializationHostedService). /// Runs FluentMigrator then optional add-only schema sync (when <see cref="ICertsEngineConfiguration.AutoSyncSchema"/> is true). Called from <c>Program.cs</c> before <c>RunAsync</c>.
/// </summary> /// </summary>
public static async Task EnsureCertsEngineMigratedAsync(this IServiceProvider serviceProvider) { public static async Task EnsureCertsEngineMigratedAsync(this IServiceProvider serviceProvider) {
await using var scope = serviceProvider.CreateAsyncScope(); await using var scope = serviceProvider.CreateAsyncScope();

View File

@ -1,15 +1,14 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MaksIT.CertsUI.Engine.DomainServices; using MaksIT.CertsUI.Engine.DomainServices;
using MaksIT.CertsUI.Engine.Extensions;
using MaksIT.CertsUI.Engine.Infrastructure; using MaksIT.CertsUI.Engine.Infrastructure;
using MaksIT.CertsUI.Engine.RuntimeCoordination; using MaksIT.CertsUI.Engine.RuntimeCoordination;
namespace MaksIT.CertsUI.HostedServices; namespace MaksIT.CertsUI.HostedServices;
/// <summary> /// <summary>
/// Runs startup initialization (migrations + identity bootstrap) before the API starts serving requests. /// Runs identity bootstrap before the API starts serving requests. FluentMigrator already ran in <c>Program.cs</c>
/// FluentMigrator runs first on every instance (same pattern as Vault); the bootstrap lease then ensures /// before the host starts. The bootstrap lease ensures only one replica writes against shared
/// only one replica performs identity bootstrap against shared <see cref="Configuration.CertsUIEngineConfiguration.DataFolder"/>. /// <see cref="Configuration.CertsUIEngineConfiguration.DataFolder"/>.
/// </summary> /// </summary>
public sealed class InitializationHostedService( public sealed class InitializationHostedService(
ILogger<InitializationHostedService> logger, ILogger<InitializationHostedService> logger,
@ -23,18 +22,11 @@ public sealed class InitializationHostedService(
public async Task StartAsync(CancellationToken cancellationToken) { public async Task StartAsync(CancellationToken cancellationToken) {
const int delayMilliseconds = 2000; const int delayMilliseconds = 2000;
var migrationsApplied = false;
while (!cancellationToken.IsCancellationRequested) { while (!cancellationToken.IsCancellationRequested) {
try { try {
logger.LogInformation("Running startup initialization..."); logger.LogInformation("Running startup initialization...");
// Migrations must run before lease acquisition: app_runtime_leases is created by FluentMigrator.
if (!migrationsApplied) {
await serviceProvider.EnsureCertsEngineMigratedAsync().ConfigureAwait(false);
migrationsApplied = true;
}
var holder = runtimeInstance.InstanceId; var holder = runtimeInstance.InstanceId;
var acquired = await runtimeLease.TryAcquireAsync(RuntimeLeaseNames.Bootstrap, holder, BootstrapLeaseTtl, cancellationToken).ConfigureAwait(false); var acquired = await runtimeLease.TryAcquireAsync(RuntimeLeaseNames.Bootstrap, holder, BootstrapLeaseTtl, cancellationToken).ConfigureAwait(false);
if (!acquired.IsSuccess) if (!acquired.IsSuccess)

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<Version>3.3.8</Version> <Version>3.3.9</Version>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>

View File

@ -135,6 +135,9 @@ builder.Services.AddHealthChecks()
var app = builder.Build(); var app = builder.Build();
// FluentMigrator must complete before any IHostedService starts; bootstrap lease uses app_runtime_leases.
await app.Services.EnsureCertsEngineMigratedAsync();
app.UseMiddleware<ErrorHandlingMiddleware>(); app.UseMiddleware<ErrorHandlingMiddleware>();
app.AddCertsEngineMigrations(); app.AddCertsEngineMigrations();
@ -169,4 +172,4 @@ app.MapGet("/health/ready", async (CancellationToken ct) => {
} }
}); });
app.Run(); await app.RunAsync();