feat(inventory): prune acquire history above 300-row cap
Adds PruneAcquireHistoryAsync to InventoryTransaction.CommitAsync; runs inside the open DB transaction after history rows are flushed, keeping at most 300 rows per viewer (oldest discarded). Adds a covering test that verifies the cap and per-viewer isolation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ namespace SVSim.Database.Services.Inventory;
|
|||||||
|
|
||||||
internal sealed class InventoryTransaction : IInventoryTransaction
|
internal sealed class InventoryTransaction : IInventoryTransaction
|
||||||
{
|
{
|
||||||
|
private const int AcquireHistoryRetention = 300;
|
||||||
|
|
||||||
private readonly SVSimDbContext _db;
|
private readonly SVSimDbContext _db;
|
||||||
private readonly IDbContextTransaction _dbTx;
|
private readonly IDbContextTransaction _dbTx;
|
||||||
private readonly ILogger _log;
|
private readonly ILogger _log;
|
||||||
@@ -282,6 +284,8 @@ internal sealed class InventoryTransaction : IInventoryTransaction
|
|||||||
WriteAcquireHistory();
|
WriteAcquireHistory();
|
||||||
await _db.SaveChangesAsync(ct);
|
await _db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
await PruneAcquireHistoryAsync(ct);
|
||||||
|
|
||||||
await _dbTx.CommitAsync(ct);
|
await _dbTx.CommitAsync(ct);
|
||||||
_committed = true;
|
_committed = true;
|
||||||
|
|
||||||
@@ -290,6 +294,22 @@ internal sealed class InventoryTransaction : IInventoryTransaction
|
|||||||
return new InventoryCommitResult(rewardList, deltas);
|
return new InventoryCommitResult(rewardList, deltas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task PruneAcquireHistoryAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var overflowIds = await _db.ViewerAcquireHistory
|
||||||
|
.Where(h => h.ViewerId == Viewer.Id)
|
||||||
|
.OrderByDescending(h => h.AcquireTime).ThenByDescending(h => h.Id)
|
||||||
|
.Skip(AcquireHistoryRetention)
|
||||||
|
.Select(h => h.Id)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
if (overflowIds.Count == 0) return;
|
||||||
|
|
||||||
|
await _db.ViewerAcquireHistory
|
||||||
|
.Where(h => overflowIds.Contains(h.Id))
|
||||||
|
.ExecuteDeleteAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
private void WriteAcquireHistory()
|
private void WriteAcquireHistory()
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -241,4 +241,38 @@ public class InventoryHistoryTests
|
|||||||
.Where(h => h.ViewerId == viewerId).FirstAsync();
|
.Where(h => h.ViewerId == viewerId).FirstAsync();
|
||||||
Assert.That(row.RewardDetailId, Is.EqualTo(0));
|
Assert.That(row.RewardDetailId, Is.EqualTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Commit_prunes_history_above_retention_cap_per_viewer()
|
||||||
|
{
|
||||||
|
using var factory = new SVSim.UnitTests.Infrastructure.SVSimTestFactory();
|
||||||
|
long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_001UL);
|
||||||
|
long otherViewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_002UL);
|
||||||
|
using var scope = factory.Services.CreateScope();
|
||||||
|
var inv = scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.Inventory.IInventoryService>();
|
||||||
|
|
||||||
|
// Pre-seed 305 rows for the primary viewer via 305 single-grant commits.
|
||||||
|
for (int i = 0; i < 305; i++)
|
||||||
|
{
|
||||||
|
await using var tx = await inv.BeginAsync(viewerId, configure: c => c.Source = GrantSource.DailyBonus);
|
||||||
|
await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.Rupy, 0, 1);
|
||||||
|
await tx.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed 50 rows for an unrelated viewer to verify pruning is per-viewer.
|
||||||
|
for (int i = 0; i < 50; i++)
|
||||||
|
{
|
||||||
|
await using var tx = await inv.BeginAsync(otherViewerId, configure: c => c.Source = GrantSource.DailyBonus);
|
||||||
|
await tx.GrantAsync(SVSim.Database.Enums.UserGoodsType.Rupy, 0, 1);
|
||||||
|
await tx.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var verifyScope = factory.Services.CreateScope();
|
||||||
|
var ctx = verifyScope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||||
|
var primaryCount = await ctx.ViewerAcquireHistory.CountAsync(h => h.ViewerId == viewerId);
|
||||||
|
var otherCount = await ctx.ViewerAcquireHistory.CountAsync(h => h.ViewerId == otherViewerId);
|
||||||
|
|
||||||
|
Assert.That(primaryCount, Is.EqualTo(300), "primary viewer pruned to cap");
|
||||||
|
Assert.That(otherCount, Is.EqualTo(50), "other viewer untouched by primary's prune");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user