Files
DodoSSH/src/DodoSSH.Api/Setup/StartupLog.cs
T
jaap-janandClaude Opus 5 093f3904c1
ci / build and test (push) Failing after 1m48s
ci / api image (push) Skipped
ci / android head (push) Failing after 5s
Apply pending migrations at startup instead of asking for a second command
The API deliberately never migrated: it failed readiness while a migration was pending and
named it, and a separate step applied them. That is the right split for a deployment with a
release pipeline and the wrong one for a self-hosted server, where it means an image that
boots, refuses traffic, and waits for somebody to know that dotnet ef exists. The schema and
the code that expects it ship in the same image, so the image is where the two are
reconciled now.

Before RunAsync rather than in the background. A migration racing the first requests would
let them through against a half-applied schema, and the first authenticated request is the
one that provisions accounts. Failing to migrate therefore fails to start, which is the
loudest signal available and the one an orchestrator already acts on.

Concurrent starts take a Postgres advisory lock first. Without it two replicas rolled out
together read the same empty history table, both apply the same migration, and the second
dies on an object that already exists — a crash loop on the day of a schema change, which
is the worst day to have one. The lock is held on a connection of its own because EF opens
and closes one per command, and a session lock belongs to the connection that took it.

The exception is a database that does not exist yet: there is nothing to hold a lock in, so
that path migrates without one and says so. Two instances creating it at once still
converges — one wins, the other restarts into the ordinary locked path — and refusing to
start would leave a fresh deployment stuck on the step this removes.

Database:AutoMigrate turns it off for the deployments that own their schema: a migrator job,
a rollout where new code must run against the old schema first, or a database user denied
DDL. With it off the behaviour is exactly what it was, and the health check now explains
which of the two situations a pending migration means.

Verified against a throwaway PostgreSQL container: an empty database gets all seven
migrations applied before the port opens, the tables land in the dodo schema, and a second
start logs the schema up to date and serves. The API suite passes, which exercises the
startup path once per assembly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 21:12:22 +02:00

75 lines
3.1 KiB
C#

namespace DodoSSH.Api.Setup;
/// <summary>
/// Source-generated log messages for startup and health.
/// </summary>
/// <remarks>
/// <c>[LoggerMessage]</c> rather than <c>ILogger</c> extension calls: allocation-free, and the
/// event ids and message templates become a stable, greppable inventory instead of string literals
/// scattered through wiring code.
/// </remarks>
internal static partial class StartupLog
{
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Warning,
Message = "Oidc:RequireHttpsMetadata is false outside Development. Token signing keys are "
+ "being fetched over plaintext HTTP, so anyone on the network path can serve their "
+ "own keys and mint tokens this server will accept.")]
internal static partial void InsecureMetadataOutsideDevelopment(ILogger logger);
[LoggerMessage(
EventId = 1002,
Level = LogLevel.Warning,
Message = "Oidc:AllowEmailLinking is enabled. A token from any configured provider "
+ "carrying a victim's email address will be linked to that victim's existing account. "
+ "Only enable this when every configured provider verifies email ownership.")]
internal static partial void EmailLinkingEnabled(ILogger logger);
[LoggerMessage(
EventId = 1010,
Level = LogLevel.Critical,
Message = "Database schema is out of date: {PendingCount} migration(s) pending "
+ "({PendingMigrations}). Readiness will fail until they are applied.")]
internal static partial void PendingMigrations(
ILogger logger,
int pendingCount,
string pendingMigrations);
[LoggerMessage(
EventId = 1011,
Level = LogLevel.Information,
Message = "Database schema is up to date.")]
internal static partial void SchemaUpToDate(ILogger logger);
[LoggerMessage(
EventId = 1012,
Level = LogLevel.Information,
Message = "Applying {PendingCount} pending migration(s): {PendingMigrations}.")]
internal static partial void ApplyingMigrations(
ILogger logger,
int pendingCount,
string pendingMigrations);
[LoggerMessage(
EventId = 1013,
Level = LogLevel.Information,
Message = "Applied {AppliedCount} migration(s). The schema is now up to date.")]
internal static partial void MigrationsApplied(ILogger logger, int appliedCount);
[LoggerMessage(
EventId = 1014,
Level = LogLevel.Information,
Message = "Database:AutoMigrate is false. This instance will not touch the schema, and will "
+ "fail readiness for as long as a migration is pending.")]
internal static partial void AutoMigrateDisabled(ILogger logger);
[LoggerMessage(
EventId = 1015,
Level = LogLevel.Warning,
Message = "The database does not exist yet, so migrations run without the advisory lock that "
+ "normally serialises replicas. Two instances creating it at once will have one of them "
+ "fail and restart into the ordinary path.")]
internal static partial void MigratingUnlockedDatabase(ILogger logger);
}