From 0077552713448cdeb464348a9ca58110f22dd637 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 20 Feb 2026 17:19:30 +0100 Subject: [PATCH] Add safe central db migration to prevent race condition false-positive errors on startup (#1758) --- apps/server/AliasVault.Admin/Program.cs | 5 +- .../Configuration/DatabaseConfiguration.cs | 52 +++++++++++++++++++ .../AliasVault.SmtpService/Program.cs | 5 +- .../Services/AliasVault.TaskRunner/Program.cs | 5 +- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/apps/server/AliasVault.Admin/Program.cs b/apps/server/AliasVault.Admin/Program.cs index 53151c8c4..1c5788c7c 100644 --- a/apps/server/AliasVault.Admin/Program.cs +++ b/apps/server/AliasVault.Admin/Program.cs @@ -163,8 +163,11 @@ app.MapRazorComponents() using (var scope = app.Services.CreateScope()) { var container = scope.ServiceProvider; + var logger = container.GetRequiredService>(); await using var db = await container.GetRequiredService().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); diff --git a/apps/server/Databases/AliasServerDb/Configuration/DatabaseConfiguration.cs b/apps/server/Databases/AliasServerDb/Configuration/DatabaseConfiguration.cs index a9e151b37..3ca68d72f 100644 --- a/apps/server/Databases/AliasServerDb/Configuration/DatabaseConfiguration.cs +++ b/apps/server/Databases/AliasServerDb/Configuration/DatabaseConfiguration.cs @@ -7,8 +7,11 @@ namespace AliasServerDb.Configuration; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Npgsql; /// /// Database configuration class. @@ -67,4 +70,53 @@ public static class DatabaseConfiguration return services; } + + /// + /// 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. + /// + /// The database context to check. + /// Optional logger for diagnostics. + /// Maximum time to wait in seconds (default: 60). + /// Interval between checks in milliseconds (default: 2000). + /// A task representing the asynchronous operation. + 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."); + } } diff --git a/apps/server/Services/AliasVault.SmtpService/Program.cs b/apps/server/Services/AliasVault.SmtpService/Program.cs index 9d65cf85e..4c5892724 100644 --- a/apps/server/Services/AliasVault.SmtpService/Program.cs +++ b/apps/server/Services/AliasVault.SmtpService/Program.cs @@ -168,8 +168,11 @@ using (var scope = host.Services.CreateScope()) { var container = scope.ServiceProvider; var factory = container.GetRequiredService(); + var logger = container.GetRequiredService>(); 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(); diff --git a/apps/server/Services/AliasVault.TaskRunner/Program.cs b/apps/server/Services/AliasVault.TaskRunner/Program.cs index e50f6fbfc..e8bda2aac 100644 --- a/apps/server/Services/AliasVault.TaskRunner/Program.cs +++ b/apps/server/Services/AliasVault.TaskRunner/Program.cs @@ -42,8 +42,11 @@ using (var scope = host.Services.CreateScope()) { var container = scope.ServiceProvider; var factory = container.GetRequiredService(); + var logger = container.GetRequiredService>(); 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();