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:
@@ -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) =>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user