fix(data): lock racing portfolio publications

This commit is contained in:
2026-06-02 07:10:37 +03:00
parent d762ecc377
commit 1d62f69ff0
5 changed files with 45 additions and 18 deletions
@@ -497,9 +497,12 @@ public sealed class PortfolioMigrationPostgresTests(PortfolioMigrationPostgresFi
var database = await fixture.CreateMigratedDatabaseAsync();
await using var publishConnection = await database.OpenConnectionAsync();
await using var rescheduleConnection = await database.OpenConnectionAsync();
await using var observerConnection = await database.OpenConnectionAsync();
var seed = await SeedCardAsync(publishConnection, isPublic: false);
await using var publishTransaction = await publishConnection.BeginTransactionAsync();
await using var rescheduleTransaction = await rescheduleConnection.BeginTransactionAsync();
var publishPid = await GetBackendPidAsync(publishConnection, publishTransaction);
var reschedulePid = await GetBackendPidAsync(rescheduleConnection, rescheduleTransaction);
await ExecuteNonQueryAsync(
publishConnection,
@@ -518,14 +521,15 @@ public sealed class PortfolioMigrationPostgresTests(PortfolioMigrationPostgresFi
rescheduleTransaction,
new NpgsqlParameter("sessionId", seed.SessionIds[0]));
var commitStates = await Task.WhenAll(
CommitAndCaptureSqlStateAsync(publishTransaction),
CommitAndCaptureSqlStateAsync(rescheduleTransaction)).WaitAsync(CommandTimeout);
var forceRescheduleTriggerTask = ExecuteNonQueryAsync(
rescheduleConnection,
"SET CONSTRAINTS trg_sessions_unpublish_public_portfolio_games_for_future_reschedule IMMEDIATE",
rescheduleTransaction);
await WaitUntilBlockedByAsync(observerConnection, reschedulePid, publishPid);
Assert.True(
commitStates[0] is null or PostgresErrorCodes.CheckViolation,
$"Unexpected publish SQLSTATE: {commitStates[0] ?? "<none>"}.");
Assert.Null(commitStates[1]);
Assert.Null(await CommitAndCaptureSqlStateAsync(publishTransaction).WaitAsync(CommandTimeout));
await forceRescheduleTriggerTask.WaitAsync(CommandTimeout);
await rescheduleTransaction.CommitAsync().WaitAsync(CommandTimeout);
await using var verificationConnection = await database.OpenConnectionAsync();
Assert.False(await ExecuteScalarAsync<bool>(
@@ -48,6 +48,7 @@ public sealed class PortfolioMigrationTests
Assert.Contains("CREATE FUNCTION unpublish_public_portfolio_games_for_future_session() RETURNS TRIGGER LANGUAGE plpgsql", normalizedMigration, StringComparison.Ordinal);
Assert.Contains("SELECT s.scheduled_at INTO final_scheduled_at FROM sessions s WHERE s.id = NEW.id;", normalizedMigration, StringComparison.Ordinal);
Assert.Contains("IF final_scheduled_at >= now() THEN", normalizedMigration, StringComparison.Ordinal);
Assert.Contains("PERFORM pg.id FROM portfolio_games pg WHERE EXISTS", normalizedMigration, StringComparison.Ordinal);
Assert.Contains("ORDER BY pg.id FOR UPDATE OF pg;", normalizedMigration, StringComparison.Ordinal);
Assert.Contains("UPDATE portfolio_games pg SET is_public = false, updated_at = now() WHERE pg.is_public = true AND EXISTS (SELECT 1 FROM portfolio_game_sessions pgs JOIN sessions s ON s.id = pgs.session_id WHERE pgs.portfolio_game_id = pg.id AND s.scheduled_at >= now());", normalizedMigration, StringComparison.Ordinal);
Assert.Contains("CREATE CONSTRAINT TRIGGER trg_sessions_unpublish_public_portfolio_games_for_future_reschedule AFTER UPDATE OF scheduled_at ON sessions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION unpublish_public_portfolio_games_for_future_session();", normalizedMigration, StringComparison.Ordinal);