Add safe central db migration to prevent race condition false-positive errors on startup (#1758)

This commit is contained in:
Leendert de Borst
2026-02-20 17:19:30 +01:00
committed by Leendert de Borst
parent 888214e7d0
commit 0077552713
4 changed files with 64 additions and 3 deletions

View File

@@ -163,8 +163,11 @@ app.MapRazorComponents<App>()
using (var scope = app.Services.CreateScope())
{
var container = scope.ServiceProvider;
var logger = container.GetRequiredService<ILogger<Program>>();
await using var db = await container.GetRequiredService<IAliasServerDbContextFactory>().CreateDbContextAsync();
await db.Database.MigrateAsync();
// Wait for migrations to be applied (API project runs them centrally)
await db.WaitForDatabaseReadyAsync(logger);
await StartupTasks.CreateRolesIfNotExist(scope.ServiceProvider);
await StartupTasks.SetAdminUser(scope.ServiceProvider);

View File

@@ -7,8 +7,11 @@
namespace AliasServerDb.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Npgsql;
/// <summary>
/// Database configuration class.
@@ -67,4 +70,53 @@ public static class DatabaseConfiguration
return services;
}
/// <summary>
/// Waits for the database to be ready by checking if all migrations have been applied.
/// This is useful for services that should not run migrations themselves but need to wait
/// for another service (typically the API) to complete migrations first.
/// </summary>
/// <param name="context">The database context to check.</param>
/// <param name="logger">Optional logger for diagnostics.</param>
/// <param name="timeoutSeconds">Maximum time to wait in seconds (default: 60).</param>
/// <param name="checkIntervalMs">Interval between checks in milliseconds (default: 2000).</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task WaitForDatabaseReadyAsync(this DbContext context, ILogger? logger = null, int timeoutSeconds = 60, int checkIntervalMs = 2000)
{
var timeout = DateTime.UtcNow.AddSeconds(timeoutSeconds);
var attempt = 0;
while (DateTime.UtcNow < timeout)
{
attempt++;
try
{
// Check if database is accessible and all migrations are applied
var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
if (!pendingMigrations.Any())
{
logger?.LogInformation("Database is ready. All migrations have been applied.");
return;
}
logger?.LogInformation(
"Waiting for database migrations to complete. {PendingCount} migrations pending. Attempt {Attempt}.",
pendingMigrations.Count(),
attempt);
}
catch (Exception ex)
{
logger?.LogWarning(
ex,
"Database not yet accessible. Attempt {Attempt}. Waiting {Interval}ms before retry...",
attempt,
checkIntervalMs);
}
await Task.Delay(checkIntervalMs);
}
throw new TimeoutException($"Database did not become ready within {timeoutSeconds} seconds. Migrations may not have completed.");
}
}

View File

@@ -168,8 +168,11 @@ using (var scope = host.Services.CreateScope())
{
var container = scope.ServiceProvider;
var factory = container.GetRequiredService<IAliasServerDbContextFactory>();
var logger = container.GetRequiredService<ILogger<Program>>();
await using var context = await factory.CreateDbContextAsync();
await context.Database.MigrateAsync();
// Wait for migrations to be applied (API project runs them centrally)
await context.WaitForDatabaseReadyAsync(logger);
}
await host.RunAsync();

View File

@@ -42,8 +42,11 @@ using (var scope = host.Services.CreateScope())
{
var container = scope.ServiceProvider;
var factory = container.GetRequiredService<IAliasServerDbContextFactory>();
var logger = container.GetRequiredService<ILogger<Program>>();
await using var context = await factory.CreateDbContextAsync();
await context.Database.MigrateAsync();
// Wait for migrations to be applied (API project runs them centrally)
await context.WaitForDatabaseReadyAsync(logger);
}
await host.RunAsync();