mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 00:14:00 +00:00
fix(multiplayer): use locked rating for season rollover
This commit is contained in:
@@ -1218,7 +1218,7 @@ func TestPostgreSQLRankedSeasonRolloverIsExactlyOnce(t *testing.T) {
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO identities (player_id, steam_id) VALUES ('season-player', 'season-steam')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO ratings (player_id, rating, deviation, volatility, ranked_games) VALUES ('season-player', 1900, 100, 0.12, 25)`); err != nil {
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO ratings (player_id, rating, deviation, volatility, ranked_games) VALUES ('season-player', 2000, 100, 0.12, 25)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO seasons (season_id, playlist, starts_at, ends_at) VALUES ('season-1', 'ranked', $1, $2)`, now.Add(-12*7*24*time.Hour), now); err != nil {
|
||||
@@ -1229,7 +1229,7 @@ func TestPostgreSQLRankedSeasonRolloverIsExactlyOnce(t *testing.T) {
|
||||
if err != nil || !applied {
|
||||
t.Fatalf("first season rollover = %+v applied=%v err=%v", updated, applied, err)
|
||||
}
|
||||
if updated.Value != 1800 || updated.RD != 200 {
|
||||
if updated.Value != 1875 || updated.RD != 200 {
|
||||
t.Fatalf("unexpected rolled rating: %+v", updated)
|
||||
}
|
||||
var rating float64
|
||||
@@ -1240,17 +1240,17 @@ func TestPostgreSQLRankedSeasonRolloverIsExactlyOnce(t *testing.T) {
|
||||
if err := db.QueryRow(`SELECT count(*) FROM ranked_season_rollovers WHERE player_id = 'season-player' AND season_id = 'season-1'`).Scan(&markers); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rating != 1800 || markers != 1 {
|
||||
if rating != 1875 || markers != 1 {
|
||||
t.Fatalf("durable rollover state rating=%v markers=%d", rating, markers)
|
||||
}
|
||||
_, applied, err = ApplyRankedSeasonRollover(ctx, db, "season-player", "season-1", profile, now.Add(time.Second))
|
||||
if err != nil || applied {
|
||||
t.Fatalf("duplicate season rollover applied=%v err=%v", applied, err)
|
||||
duplicate, applied, err := ApplyRankedSeasonRollover(ctx, db, "season-player", "season-1", profile, now.Add(time.Second))
|
||||
if err != nil || applied || duplicate.Value != 1875 {
|
||||
t.Fatalf("duplicate rollover = %+v applied=%v err=%v", duplicate, applied, err)
|
||||
}
|
||||
if err := db.QueryRow(`SELECT rating FROM ratings WHERE player_id = 'season-player'`).Scan(&rating); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rating != 1800 {
|
||||
if rating != 1875 {
|
||||
t.Fatalf("duplicate rollover changed rating to %v", rating)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,16 +28,24 @@ WHERE player_id = $1`
|
||||
// retry after a worker failure cannot apply compression twice or leave a
|
||||
// marker without its corresponding rating snapshot.
|
||||
func ApplyRankedSeasonRollover(ctx context.Context, db *sql.DB, playerID, seasonID string, profile domain.RankedProfile, now time.Time) (domain.RankedProfile, bool, error) {
|
||||
if playerID == "" {
|
||||
return domain.RankedProfile{}, false, fmt.Errorf("player ID is required")
|
||||
}
|
||||
updated, _, err := domain.ApplySeasonRollover(profile, seasonID)
|
||||
if err != nil {
|
||||
return domain.RankedProfile{}, false, err
|
||||
if db == nil || playerID == "" || seasonID == "" || now.IsZero() {
|
||||
return domain.RankedProfile{}, false, fmt.Errorf("invalid season rollover arguments")
|
||||
}
|
||||
// The caller's profile is only a validation-compatible hint. The durable
|
||||
// row is authoritative because a result update may have committed after the
|
||||
// caller read its snapshot but before this transaction acquired the lock.
|
||||
_ = profile
|
||||
var updated domain.RankedProfile
|
||||
applied := false
|
||||
err = RunSerializable(ctx, db, DefaultSerializableAttempts, func(ctx context.Context, tx *sql.Tx) error {
|
||||
if _, err := tx.ExecContext(ctx, SeasonRatingLockSQL, playerID); err != nil {
|
||||
err := RunSerializable(ctx, db, DefaultSerializableAttempts, func(ctx context.Context, tx *sql.Tx) error {
|
||||
var locked domain.RankedProfile
|
||||
var revision int64
|
||||
if err := tx.QueryRowContext(ctx, SeasonRatingLockSQL, playerID).Scan(&locked.Value, &locked.RD, &locked.Volatility, &locked.RankedGames, &revision); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
updated, _, err = domain.ApplySeasonRollover(locked, seasonID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, SeasonRolloverInsertSQL, playerID, seasonID, updated.Value, updated.RD, updated.Volatility, updated.RankedGames, now)
|
||||
@@ -49,6 +57,7 @@ func ApplyRankedSeasonRollover(ctx context.Context, db *sql.DB, playerID, season
|
||||
return err
|
||||
}
|
||||
if changed == 0 {
|
||||
updated = locked
|
||||
return nil
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, SeasonRatingUpdateSQL, playerID, updated.Value, updated.RD, updated.Volatility, now); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user