Reevaluate pending migrations after each one instead of per stage

This commit is contained in:
Shadowghost
2026-07-27 12:31:36 +02:00
parent dbc796b0b0
commit f571cd5a6a
@@ -193,10 +193,15 @@ internal class JellyfinMigrationService
{
var historyRepository = dbContext.GetService<IHistoryRepository>();
var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
(string Key, IInternalMigration Migration)[] migrations = [];
var completedMigrations = 0;
string? lastMigrationKey = null;
do
{ // migrations may alter the migration state. Reevaluate the applicable migrations after every stage ran until there are no more to apply.
while (true)
{
// A single migration can change which migrations still apply: IMigrator.MigrateAsync treats its argument as the
// state to end up in, so it reverts everything applied after it, and a reverted migration can take code migrations
// with it (AddNormalizedUsername.Down drops the UpdateNormalizedUsername history row). Anything computed before
// that point is stale, so only ever run the next migration and then work out the pending set again.
var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
var pendingCodeMigrations = migrationStage
.Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
@@ -212,73 +217,86 @@ internal class JellyfinMigrationService
}
(string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDatabaseMigrations];
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
var migrationIndex = 0;
foreach (var item in migrations)
if (pendingMigrations.Length == 0)
{
// Surface generic "Running migration X of Y" progress in the always-visible startup UI header.
SetupServer.ReportActivity(StartupActivity.Migration(++migrationIndex, migrations.Length));
var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup($"{item.Key}");
try
{
migrationLogger.LogInformation("Perform migration {Name}", item.Key);
await item.Migration.PerformAsync(migrationLogger).ConfigureAwait(false);
migrationLogger.LogInformation("Migration {Name} was successfully applied", item.Key);
}
catch (Exception ex)
{
migrationLogger.LogCritical("Error: {Error}", ex.Message);
migrationLogger.LogError(ex, "Migration {Name} failed", item.Key);
break;
}
if (_backupKey != default && _backupService is not null && _jellyfinDatabaseProvider is not null)
if (completedMigrations == 0)
{
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingMigrations.Length, stage);
}
var item = pendingMigrations.OrderBy(e => e.Key, StringComparer.Ordinal).First();
if (string.Equals(item.Key, lastMigrationKey, StringComparison.Ordinal))
{
throw new InvalidOperationException($"Migration {item.Key} ran but did not record itself as applied and would repeat indefinitely.");
}
lastMigrationKey = item.Key;
// Surface generic "Running migration X of Y" progress in the always-visible startup UI header.
SetupServer.ReportActivity(StartupActivity.Migration(completedMigrations + 1, completedMigrations + pendingMigrations.Length));
var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup($"{item.Key}");
try
{
migrationLogger.LogInformation("Perform migration {Name}", item.Key);
await item.Migration.PerformAsync(migrationLogger).ConfigureAwait(false);
migrationLogger.LogInformation("Migration {Name} was successfully applied", item.Key);
}
catch (Exception ex)
{
migrationLogger.LogCritical("Error: {Error}", ex.Message);
migrationLogger.LogError(ex, "Migration {Name} failed", item.Key);
if (_backupKey != default && _backupService is not null && _jellyfinDatabaseProvider is not null)
{
if (_backupKey.LibraryDb is not null)
{
if (_backupKey.LibraryDb is not null)
migrationLogger.LogInformation("Attempt to rollback librarydb.");
try
{
migrationLogger.LogInformation("Attempt to rollback librarydb.");
try
{
var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
File.Move(_backupKey.LibraryDb, libraryDbPath, true);
}
catch (Exception inner)
{
migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.LibraryDb);
}
var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
File.Move(_backupKey.LibraryDb, libraryDbPath, true);
}
if (_backupKey.JellyfinDb is not null)
catch (Exception inner)
{
migrationLogger.LogInformation("Attempt to rollback JellyfinDb.");
try
{
await _jellyfinDatabaseProvider.RestoreBackupFast(_backupKey.JellyfinDb, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception inner)
{
migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.JellyfinDb);
}
}
if (_backupKey.FullBackup is not null)
{
migrationLogger.LogInformation("Attempt to rollback from backup.");
try
{
await _backupService.RestoreBackupAsync(_backupKey.FullBackup.Path).ConfigureAwait(false);
}
catch (Exception inner)
{
migrationLogger.LogCritical(inner, "Could not rollback from backup {Backup}. Manual intervention might be required to restore a operational state.", _backupKey.FullBackup.Path);
}
migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.LibraryDb);
}
}
throw;
if (_backupKey.JellyfinDb is not null)
{
migrationLogger.LogInformation("Attempt to rollback JellyfinDb.");
try
{
await _jellyfinDatabaseProvider.RestoreBackupFast(_backupKey.JellyfinDb, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception inner)
{
migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.JellyfinDb);
}
}
if (_backupKey.FullBackup is not null)
{
migrationLogger.LogInformation("Attempt to rollback from backup.");
try
{
await _backupService.RestoreBackupAsync(_backupKey.FullBackup.Path).ConfigureAwait(false);
}
catch (Exception inner)
{
migrationLogger.LogCritical(inner, "Could not rollback from backup {Backup}. Manual intervention might be required to restore a operational state.", _backupKey.FullBackup.Path);
}
}
}
throw;
}
} while (migrations.Length != 0);
completedMigrations++;
}
}
}