diff --git a/src/ScadaLink.ConfigurationDatabase/MigrationHelper.cs b/src/ScadaLink.ConfigurationDatabase/MigrationHelper.cs index 4acec5e..b38c4bd 100644 --- a/src/ScadaLink.ConfigurationDatabase/MigrationHelper.cs +++ b/src/ScadaLink.ConfigurationDatabase/MigrationHelper.cs @@ -16,12 +16,21 @@ public static class MigrationHelper /// /// The database context to migrate or validate. /// When true, auto-applies migrations. When false, validates schema version matches. + /// Optional logger for readiness-wait diagnostics. /// Cancellation token. public static async Task ApplyOrValidateMigrationsAsync( ScadaLinkDbContext dbContext, bool isDevelopment, + ILogger? logger = null, CancellationToken cancellationToken = default) { + // Wait for the target database to accept connections before invoking MigrateAsync. + // On a fresh MSSQL container, user databases recover asynchronously after the server + // starts accepting connections — DB_ID(@dbName) returns null until recovery completes. + // Without this wait, MigrateAsync sees the database as missing and falls through to + // CREATE DATABASE, which fails for non-privileged app logins. + await WaitForDatabaseReadyAsync(dbContext, logger, cancellationToken); + if (isDevelopment) { await dbContext.Database.MigrateAsync(cancellationToken); @@ -38,4 +47,48 @@ public static class MigrationHelper } } } + + private static async Task WaitForDatabaseReadyAsync( + ScadaLinkDbContext dbContext, + ILogger? logger, + CancellationToken cancellationToken) + { + var timeout = TimeSpan.FromSeconds(60); + var pollInterval = TimeSpan.FromSeconds(2); + var deadline = DateTimeOffset.UtcNow + timeout; + var attempt = 0; + Exception? lastException = null; + + while (DateTimeOffset.UtcNow < deadline) + { + attempt++; + try + { + if (await dbContext.Database.CanConnectAsync(cancellationToken)) + { + if (attempt > 1) + { + logger?.LogInformation( + "Configuration database ready after {Attempt} attempt(s).", attempt); + } + return; + } + logger?.LogDebug( + "Configuration database not yet reachable (attempt {Attempt}).", attempt); + } + catch (Exception ex) + { + lastException = ex; + logger?.LogDebug(ex, + "Configuration database not yet reachable (attempt {Attempt}).", attempt); + } + + await Task.Delay(pollInterval, cancellationToken); + } + + throw new InvalidOperationException( + $"Configuration database not ready after {timeout.TotalSeconds:N0}s ({attempt} attempts). " + + "Verify SQL Server is running and the configuration database exists and is attached.", + lastException); + } } diff --git a/src/ScadaLink.Host/Program.cs b/src/ScadaLink.Host/Program.cs index 29116f8..637fdba 100644 --- a/src/ScadaLink.Host/Program.cs +++ b/src/ScadaLink.Host/Program.cs @@ -109,7 +109,10 @@ try using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); - await MigrationHelper.ApplyOrValidateMigrationsAsync(dbContext, isDevelopment); + var migrationLogger = scope.ServiceProvider + .GetRequiredService() + .CreateLogger(typeof(MigrationHelper).FullName!); + await MigrationHelper.ApplyOrValidateMigrationsAsync(dbContext, isDevelopment, migrationLogger); } }