mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-12 21:22:06 +00:00
fix(multiplayer): finalize empty ranked seasons
This commit is contained in:
@@ -24,6 +24,12 @@ WHERE season_id = $1 AND rolled_over_at IS NULL
|
||||
LEFT JOIN ranked_season_rollovers rr ON rr.season_id = $1 AND rr.player_id = r.player_id
|
||||
WHERE rr.player_id IS NULL)`
|
||||
|
||||
const MarkEmptySeasonsSQL = `UPDATE seasons s SET rolled_over_at = $1
|
||||
WHERE s.playlist = 'ranked' AND s.ends_at <= $1 AND s.rolled_over_at IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM ratings r
|
||||
LEFT JOIN ranked_season_rollovers rr ON rr.season_id = s.season_id AND rr.player_id = r.player_id
|
||||
WHERE rr.player_id IS NULL)`
|
||||
|
||||
type dueSeasonRollover struct {
|
||||
seasonID string
|
||||
playerID string
|
||||
@@ -36,6 +42,12 @@ func RolloverDueSeasons(ctx context.Context, db *sql.DB, now time.Time, limit in
|
||||
if db == nil || now.IsZero() || limit < 1 || limit > 1000 {
|
||||
return 0, fmt.Errorf("invalid season maintenance arguments")
|
||||
}
|
||||
// A season with no ranked profiles has no player row for DueSeasonRolloversSQL
|
||||
// to return. Mark it here so maintenance remains idempotent instead of
|
||||
// reconsidering the same empty season on every pass.
|
||||
if _, err := db.ExecContext(ctx, MarkEmptySeasonsSQL, now); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rows, err := db.QueryContext(ctx, DueSeasonRolloversSQL, now, limit)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -16,6 +16,11 @@ func TestMaintenanceSQLEnumeratesOnlyUnrolledRankedPlayers(t *testing.T) {
|
||||
t.Fatalf("mark query missing %q", fragment)
|
||||
}
|
||||
}
|
||||
for _, fragment := range []string{"s.playlist = 'ranked'", "s.ends_at <= $1", "s.rolled_over_at IS NULL", "NOT EXISTS", "ranked_season_rollovers"} {
|
||||
if !contains(MarkEmptySeasonsSQL, fragment) {
|
||||
t.Fatalf("empty-season query missing %q", fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRolloverDueSeasonsRejectsUnboundedMaintenance(t *testing.T) {
|
||||
|
||||
@@ -1255,6 +1255,26 @@ func TestPostgreSQLRankedSeasonRolloverIsExactlyOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgreSQLEmptyRankedSeasonIsMarkedRolledOver(t *testing.T) {
|
||||
db := openIntegrationPostgres(t)
|
||||
applyIntegrationMigrations(t, db)
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
ctx := context.Background()
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO seasons (season_id, playlist, starts_at, ends_at) VALUES ('empty-season', 'ranked', $1, $2)`, now.Add(-12*7*24*time.Hour), now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count, err := RolloverDueSeasons(ctx, db, now, 100); err != nil || count != 0 {
|
||||
t.Fatalf("empty-season maintenance count=%d err=%v", count, err)
|
||||
}
|
||||
var rolledAt sql.NullTime
|
||||
if err := db.QueryRowContext(ctx, `SELECT rolled_over_at FROM seasons WHERE season_id = 'empty-season'`).Scan(&rolledAt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !rolledAt.Valid {
|
||||
t.Fatal("empty ranked season was not marked rolled over")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgreSQLMigrationsAreForwardExecutable(t *testing.T) {
|
||||
db := openIntegrationPostgres(t)
|
||||
applyIntegrationMigrations(t, db)
|
||||
|
||||
Reference in New Issue
Block a user