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,
|
WeeklyPeriodStart = null,
|
||||||
};
|
};
|
||||||
_db.ViewerBattlePassProgress.Add(entry);
|
_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) =>
|
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.
|
// Default cap mirrors the captured /battle_pass/info.gauge_info.weekly_limit_point.
|
||||||
public const int WeeklyLimitPointDefault = 3000;
|
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 IBattlePassRepository _bp;
|
||||||
private readonly IViewerBattlePassRepository _viewerBp;
|
private readonly IViewerBattlePassRepository _viewerBp;
|
||||||
private readonly TimeProvider _time;
|
private readonly TimeProvider _time;
|
||||||
@@ -46,8 +49,6 @@ public sealed class BattlePassService : IBattlePassService
|
|||||||
if (season is null) return null;
|
if (season is null) return null;
|
||||||
|
|
||||||
var progress = await _viewerBp.GetOrCreateProgressAsync(viewerId, season.Id, ct);
|
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 rewards = await _bp.GetSeasonRewardsAsync(season.Id, ct);
|
||||||
var claims = await _viewerBp.GetClaimsAsync(viewerId, 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) =>
|
private static string FormatWireDate(DateTimeOffset dt) =>
|
||||||
// Capture format is "2026-04-01 02:00:00" (JST, space-separated). Emit in same shape
|
// 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.
|
// 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);
|
.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
private static string Inv(long v) => v.ToString(CultureInfo.InvariantCulture);
|
private static string Inv(long v) => v.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|||||||
Reference in New Issue
Block a user