fix(store-and-forward): standby applies Add/Park/Requeue as upserts so a lost Add self-heals
This commit is contained in:
@@ -203,4 +203,61 @@ public class StoreAndForwardReplicationTests : IAsyncLifetime, IDisposable
|
||||
Assert.Equal(StoreAndForwardMessageStatus.Pending, row!.Status);
|
||||
Assert.Equal(0, row.RetryCount);
|
||||
}
|
||||
|
||||
private static ReplicationService NewReplicationService() =>
|
||||
new(new StoreAndForwardOptions { ReplicationEnabled = true },
|
||||
NullLogger<ReplicationService>.Instance);
|
||||
|
||||
private static StoreAndForwardMessage NewMessage(string id) => new()
|
||||
{
|
||||
Id = id,
|
||||
Category = StoreAndForwardCategory.ExternalSystem,
|
||||
Target = "api",
|
||||
PayloadJson = "{}",
|
||||
RetryCount = 0,
|
||||
MaxRetries = 1,
|
||||
RetryIntervalMs = 0,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Status = StoreAndForwardMessageStatus.Pending,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A Park (or Requeue) whose original Add was lost — e.g. the Add's
|
||||
/// fire-and-forget replication dropped — must still materialise the row on the
|
||||
/// standby. The full message rides in the operation, so an upsert self-heals;
|
||||
/// a blind UPDATE would affect 0 rows and the row would be gone forever.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApplyReplicatedPark_WhenAddWasLost_MaterializesTheParkedRow()
|
||||
{
|
||||
var service = NewReplicationService();
|
||||
var msg = NewMessage("lost-add-1"); // never Added on this (standby) storage
|
||||
msg.Status = StoreAndForwardMessageStatus.Parked;
|
||||
|
||||
await service.ApplyReplicatedOperationAsync(
|
||||
new ReplicationOperation(ReplicationOperationType.Park, msg.Id, msg), _storage);
|
||||
|
||||
var row = await _storage.GetMessageByIdAsync("lost-add-1");
|
||||
Assert.NotNull(row); // pre-fix: blind UPDATE affected 0 rows, row is gone forever
|
||||
Assert.Equal(StoreAndForwardMessageStatus.Parked, row!.Status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A duplicate Add (e.g. after Task 21's anti-entropy resync re-issues an Add
|
||||
/// the standby already holds) must not violate the PK — the upsert applies
|
||||
/// newest-wins instead of throwing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApplyReplicatedAdd_Twice_IsIdempotent_NewestWins()
|
||||
{
|
||||
var service = NewReplicationService();
|
||||
var msg = NewMessage("dup-add");
|
||||
await service.ApplyReplicatedOperationAsync(
|
||||
new ReplicationOperation(ReplicationOperationType.Add, msg.Id, msg), _storage);
|
||||
msg.RetryCount = 3;
|
||||
await service.ApplyReplicatedOperationAsync( // pre-fix: SqliteException PK violation
|
||||
new ReplicationOperation(ReplicationOperationType.Add, msg.Id, msg), _storage);
|
||||
|
||||
Assert.Equal(3, (await _storage.GetMessageByIdAsync("dup-add"))!.RetryCount);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user