fix(notification-outbox): per-attempt timestamps, Pending/Retrying split claim + sweep budget, SMTP MaxConcurrentConnections clamp (review 2026-10-21 06/COR-1, 06/PERF-5, 06/IMPL-2)
This commit is contained in:
@@ -567,6 +567,71 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
Assert.Equal(retryingDue.NotificationId, capped[1].NotificationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a retrying backlog older than every fresh Pending row cannot monopolise
|
||||
/// the claim: the batch is split between the two statuses, each oldest-first
|
||||
/// (review 2026-10-21 06/PERF-5).
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetDueAsync_RetryingBacklog_SharesBatchWithPending()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 19, 12, 0, 0, TimeSpan.Zero);
|
||||
var retrying = Enumerable.Range(0, 4)
|
||||
.Select(i => MakeNotification(Guid.NewGuid().ToString(), NotificationStatus.Retrying,
|
||||
createdAt: now.AddHours(-10).AddMinutes(i), nextAttemptAt: now.AddMinutes(-1)))
|
||||
.ToList();
|
||||
var pending = Enumerable.Range(0, 4)
|
||||
.Select(i => MakeNotification(Guid.NewGuid().ToString(), NotificationStatus.Pending,
|
||||
createdAt: now.AddMinutes(-10 + i)))
|
||||
.ToList();
|
||||
_context.Notifications.AddRange(retrying);
|
||||
_context.Notifications.AddRange(pending);
|
||||
await _context.SaveChangesAsync();
|
||||
_context.ChangeTracker.Clear();
|
||||
|
||||
var due = await _repository.GetDueAsync(now, batchSize: 4);
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
retrying[0].NotificationId, retrying[1].NotificationId,
|
||||
pending[0].NotificationId, pending[1].NotificationId,
|
||||
},
|
||||
due.Select(n => n.NotificationId).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that an unused share of the batch is back-filled from the other status, so a
|
||||
/// sweep with a short Retrying (or Pending) set still claims a full batch.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task GetDueAsync_ShortRetryingSet_BackfillsFromPending()
|
||||
{
|
||||
var now = new DateTimeOffset(2026, 5, 19, 12, 0, 0, TimeSpan.Zero);
|
||||
var retrying = MakeNotification(Guid.NewGuid().ToString(), NotificationStatus.Retrying,
|
||||
createdAt: now.AddHours(-10), nextAttemptAt: now.AddMinutes(-1));
|
||||
var pending = Enumerable.Range(0, 5)
|
||||
.Select(i => MakeNotification(Guid.NewGuid().ToString(), NotificationStatus.Pending,
|
||||
createdAt: now.AddMinutes(-10 + i)))
|
||||
.ToList();
|
||||
_context.Notifications.Add(retrying);
|
||||
_context.Notifications.AddRange(pending);
|
||||
await _context.SaveChangesAsync();
|
||||
_context.ChangeTracker.Clear();
|
||||
|
||||
var due = await _repository.GetDueAsync(now, batchSize: 4);
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
retrying.NotificationId,
|
||||
pending[0].NotificationId, pending[1].NotificationId, pending[2].NotificationId,
|
||||
},
|
||||
due.Select(n => n.NotificationId).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that get by id returns the row when present and null otherwise.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user