@@ -118,4 +118,59 @@ public static async Task<IApplicationBuilder> ApplyPendingOpenShockMigrations(th
118118
119119 return app ;
120120 }
121+
122+ /// <summary>
123+ /// Blocks startup until the database schema is up to date (no pending migrations), for hosts that do
124+ /// <em>not</em> own migrations (e.g. the Cron worker - the API is the sole migrator). This is not just
125+ /// tidiness: <see cref="OpenShockContext"/> binds Postgres enum types (<c>email_status</c>,
126+ /// <c>email_type</c>, ...) to CLR enums <em>by name</em>, and Npgsql resolves those names against the
127+ /// type catalog it loads on the data source's first connection and caches for the life of the process.
128+ /// If that first connection happens before the migrator has created a newly-added enum, every query
129+ /// using it fails permanently ("data type name '...' could not be found") until the process restarts.
130+ /// Waiting here guarantees the schema - and its enum types - exists before anything opens the pooled
131+ /// context, closing the deploy-time race without this host performing any schema writes of its own.
132+ /// </summary>
133+ public static async Task < IApplicationBuilder > WaitForOpenShockSchemaReady ( this IApplicationBuilder app ,
134+ DatabaseOptions options , TimeSpan ? timeout = null )
135+ {
136+ using var scope = app . ApplicationServices . CreateScope ( ) ;
137+ var loggerFactory = scope . ServiceProvider . GetRequiredService < ILoggerFactory > ( ) ;
138+ var logger = loggerFactory . CreateLogger ( "SchemaReadyGate" ) ;
139+
140+ logger . LogInformation ( "Waiting for database schema to be up to date (migrations owned by another host)..." ) ;
141+
142+ var deadline = DateTime . UtcNow + ( timeout ?? TimeSpan . FromMinutes ( 5 ) ) ;
143+ var delay = TimeSpan . FromSeconds ( 1 ) ;
144+ var maxDelay = TimeSpan . FromSeconds ( 15 ) ;
145+
146+ while ( true )
147+ {
148+ try
149+ {
150+ await using var migrationContext = new MigrationOpenShockContext ( options . Conn , options . Debug , loggerFactory ) ;
151+ var pending = ( await migrationContext . Database . GetPendingMigrationsAsync ( ) ) . ToArray ( ) ;
152+ if ( pending . Length == 0 )
153+ {
154+ logger . LogInformation ( "Database schema is up to date; proceeding with startup" ) ;
155+ return app ;
156+ }
157+
158+ logger . LogWarning ( "Schema not ready: {Count} pending migration(s) [{Migrations}] not yet applied by the migrator" ,
159+ pending . Length , string . Join ( ", " , pending ) ) ;
160+ }
161+ catch ( Exception ex )
162+ {
163+ // Database not reachable yet, or the migrator is mid-run. Keep waiting rather than crash-looping.
164+ logger . LogWarning ( ex , "Could not determine migration state; will retry" ) ;
165+ }
166+
167+ if ( DateTime . UtcNow >= deadline )
168+ throw new TimeoutException (
169+ "Timed out waiting for the database schema to be brought up to date by the migrator. " +
170+ "Ensure the API host (the sole migrator) is running and reachable." ) ;
171+
172+ await Task . Delay ( delay ) ;
173+ delay = TimeSpan . FromSeconds ( Math . Min ( maxDelay . TotalSeconds , delay . TotalSeconds * 2 ) ) ;
174+ }
175+ }
121176}
0 commit comments