fix(store-and-forward): resolve StoreAndForward-004,005,010,013 — accurate handler-contract doc, conditional sweep writes, reset LastAttemptAt on parked retry, test coverage
This commit is contained in:
@@ -177,6 +177,49 @@ public class StoreAndForwardServiceTests : IAsyncLifetime, IDisposable
|
||||
Assert.Equal(1, msg.RetryCount); // one sweep retry recorded
|
||||
}
|
||||
|
||||
// ── StoreAndForward-005: sweep-vs-management race hardening ──
|
||||
|
||||
[Fact]
|
||||
public async Task RetryMessageAsync_StatusChangedDuringDelivery_SweepParkWriteIsSkipped()
|
||||
{
|
||||
// StoreAndForward-005: the retry sweep's state-changing writes must be
|
||||
// conditional on the status it observed, so a concurrent operator action that
|
||||
// moved the row out of Pending (e.g. between the sweep's snapshot load and its
|
||||
// park write) is not silently overwritten by the sweep's stale view.
|
||||
var result = await _service.EnqueueAsync(
|
||||
StoreAndForwardCategory.ExternalSystem, "api", """{}""",
|
||||
attemptImmediateDelivery: false, maxRetries: 1);
|
||||
|
||||
_service.RegisterDeliveryHandler(StoreAndForwardCategory.ExternalSystem,
|
||||
async msg =>
|
||||
{
|
||||
// Simulate an operator action winning the race: the row leaves Pending
|
||||
// (here: parked) while the sweep is still mid-delivery. The sweep would
|
||||
// otherwise unconditionally re-write this row from its stale snapshot.
|
||||
var parkedOutFromUnderTheSweep = new StoreAndForwardMessage
|
||||
{
|
||||
Id = msg.Id, Category = msg.Category, Target = msg.Target,
|
||||
PayloadJson = msg.PayloadJson, RetryCount = 7,
|
||||
MaxRetries = msg.MaxRetries, RetryIntervalMs = msg.RetryIntervalMs,
|
||||
CreatedAt = msg.CreatedAt, LastAttemptAt = DateTimeOffset.UtcNow,
|
||||
Status = StoreAndForwardMessageStatus.Parked,
|
||||
LastError = "operator/other writer"
|
||||
};
|
||||
await _storage.UpdateMessageAsync(parkedOutFromUnderTheSweep);
|
||||
throw new HttpRequestException("transient — sweep will try to park");
|
||||
});
|
||||
|
||||
await _service.RetryPendingMessagesAsync();
|
||||
|
||||
// The sweep observed Pending; the row is now Parked with the other writer's
|
||||
// RetryCount (7), not the sweep's (1). The sweep's conditional write was skipped.
|
||||
var msg = await _storage.GetMessageByIdAsync(result.MessageId);
|
||||
Assert.NotNull(msg);
|
||||
Assert.Equal(StoreAndForwardMessageStatus.Parked, msg!.Status);
|
||||
Assert.Equal(7, msg.RetryCount);
|
||||
Assert.Equal("operator/other writer", msg.LastError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RetryPendingMessagesAsync_PermanentFailureOnRetry_ParksMessage()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user