From 63325b9cadf79df9a53ec4cab00ab7a167853c96 Mon Sep 17 00:00:00 2001 From: Matthew Alvernaz Date: Sat, 6 Jun 2026 22:23:12 -0700 Subject: [PATCH] Skip EF migration lock when no migrations are pending (#1729) DbContexts.GetContext() runs ApplyMigrations on every context creation, calling Database.Migrate(). Migrate() acquires the EF migration lock before checking whether anything is pending. For SQLite that lock is a persisted row in __EFMigrationsLock with no timeout (SQLite has no connection-scoped lock that frees on disconnect). A process killed after acquiring the lock but before releasing it - even during an otherwise no-op Migrate() - orphans the row, and every later Migrate() then spins forever in SqliteHistoryRepository.AcquireDatabaseLock(). The row lives in the db file, so restarts never clear it, matching the "scan hangs, reboot doesn't help" reports in #1729. Guard Migrate()/MigrateAsync() behind GetPendingMigrations(), which only reads __EFMigrationsHistory and never acquires the lock. The steady-state path (schema already current) no longer touches the lock at all; first run and real migrations are unchanged. --- Source/DataLayer/LibationContextFactory.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Source/DataLayer/LibationContextFactory.cs b/Source/DataLayer/LibationContextFactory.cs index 451caea7..a43fd169 100644 --- a/Source/DataLayer/LibationContextFactory.cs +++ b/Source/DataLayer/LibationContextFactory.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -49,7 +50,15 @@ public class LibationContextFactory { try { - context.Database.Migrate(); + // Migrate() acquires the EF migration lock before checking whether anything is pending. + // For SQLite that lock is a row in __EFMigrationsLock with no timeout (SQLite has no + // connection-scoped lock that frees on disconnect). A process killed after acquiring the + // lock but before releasing it - even during an otherwise no-op Migrate() - orphans the row, + // and every later Migrate() then spins forever in SqliteHistoryRepository.AcquireDatabaseLock(). + // The row is persisted in the db file, so restarting does not clear it. Skipping Migrate() + // when nothing is pending keeps the steady-state path off the lock entirely. See #1729. + if (context.Database.GetPendingMigrations().Any()) + context.Database.Migrate(); } // SQLITE_READONLY == 8 (https://www.sqlite.org/rescode.html) catch (SqliteException ex) when (ex.SqliteErrorCode == 8 && sqliteDatabaseFilePath is not null) @@ -63,7 +72,9 @@ public class LibationContextFactory { try { - await context.Database.MigrateAsync(cancellationToken); + // See ApplyMigrations for why pending migrations are checked before acquiring the lock (#1729). + if ((await context.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) + await context.Database.MigrateAsync(cancellationToken); } // SQLITE_READONLY == 8 (https://www.sqlite.org/rescode.html) catch (SqliteException ex) when (ex.SqliteErrorCode == 8 && sqliteDatabaseFilePath is not null)