review(bp): move SaveChanges into repo with race protection; JST constant

GetOrCreateProgressAsync now persists the new row itself and catches
DbUpdateException on unique-constraint violations — concurrent /info
calls no longer throw 500s. BattlePassService no longer calls
SaveChangesAsync after the get-or-create. FormatWireDate uses a named
JstOffset constant instead of an inline TimeSpan.FromHours(9).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-26 23:22:48 -04:00
parent 8a35f8c40b
commit d877febcb8
2 changed files with 16 additions and 4 deletions

View File

@@ -26,7 +26,18 @@ public sealed class ViewerBattlePassRepository : IViewerBattlePassRepository
WeeklyPeriodStart = null,
};
_db.ViewerBattlePassProgress.Add(entry);
return entry;
try
{
await _db.SaveChangesAsync(ct);
return entry;
}
catch (DbUpdateException)
{
// Concurrent /info call won the race; re-read the row the other thread persisted.
_db.Entry(entry).State = EntityState.Detached;
return await _db.ViewerBattlePassProgress
.FirstAsync(p => p.ViewerId == viewerId && p.SeasonId == seasonId, ct);
}
}
public Task<List<ViewerBattlePassClaimEntry>> GetClaimsAsync(long viewerId, int seasonId, CancellationToken ct) =>

View File

@@ -13,6 +13,9 @@ public sealed class BattlePassService : IBattlePassService
// Default cap mirrors the captured /battle_pass/info.gauge_info.weekly_limit_point.
public const int WeeklyLimitPointDefault = 3000;
/// <summary>JST = UTC+9. Capture format ("2026-04-01 02:00:00") is implicit JST.</summary>
private static readonly TimeSpan JstOffset = TimeSpan.FromHours(9);
private readonly IBattlePassRepository _bp;
private readonly IViewerBattlePassRepository _viewerBp;
private readonly TimeProvider _time;
@@ -46,8 +49,6 @@ public sealed class BattlePassService : IBattlePassService
if (season is null) return null;
var progress = await _viewerBp.GetOrCreateProgressAsync(viewerId, season.Id, ct);
// Persist the lazy-created row so concurrent /info calls don't try to create twice.
await _db.SaveChangesAsync(ct);
var rewards = await _bp.GetSeasonRewardsAsync(season.Id, ct);
var claims = await _viewerBp.GetClaimsAsync(viewerId, season.Id, ct);
@@ -123,7 +124,7 @@ public sealed class BattlePassService : IBattlePassService
private static string FormatWireDate(DateTimeOffset dt) =>
// Capture format is "2026-04-01 02:00:00" (JST, space-separated). Emit in same shape
// in JST so the client gets back what it gave.
dt.ToOffset(TimeSpan.FromHours(9))
dt.ToOffset(JstOffset)
.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
private static string Inv(long v) => v.ToString(CultureInfo.InvariantCulture);