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
|
||||
{
|
||||
private const int AcquireHistoryRetention = 300;
|
||||
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly IDbContextTransaction _dbTx;
|
||||
private readonly ILogger _log;
|
||||
@@ -282,6 +284,8 @@ internal sealed class InventoryTransaction : IInventoryTransaction
|
||||
WriteAcquireHistory();
|
||||
await _db.SaveChangesAsync(ct);
|
||||
|
||||
await PruneAcquireHistoryAsync(ct);
|
||||
|
||||
await _dbTx.CommitAsync(ct);
|
||||
_committed = true;
|
||||
|
||||
@@ -290,6 +294,22 @@ internal sealed class InventoryTransaction : IInventoryTransaction
|
||||
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()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
@@ -241,4 +241,38 @@ public class InventoryHistoryTests
|
||||
.Where(h => h.ViewerId == viewerId).FirstAsync();
|
||||
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