perf(store-and-forward): epoch-ms due-check with additive column, backfill, and (status, due) index
This commit is contained in:
@@ -683,4 +683,69 @@ public class StoreAndForwardStorageTests : IAsyncLifetime, IDisposable
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Task 18: epoch-ms due-check ──
|
||||
|
||||
[Fact]
|
||||
public async Task GetMessagesForRetry_UsesEpochMs_RespectsInterval()
|
||||
{
|
||||
var msg = new StoreAndForwardMessage
|
||||
{
|
||||
Id = "due-check",
|
||||
Category = StoreAndForwardCategory.ExternalSystem,
|
||||
Target = "t",
|
||||
PayloadJson = "{}",
|
||||
RetryCount = 0,
|
||||
MaxRetries = 50,
|
||||
RetryIntervalMs = 60_000,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Status = StoreAndForwardMessageStatus.Pending,
|
||||
LastAttemptAt = DateTimeOffset.UtcNow.AddSeconds(-30), // not yet due
|
||||
};
|
||||
await _storage.EnqueueAsync(msg);
|
||||
Assert.Empty(await _storage.GetMessagesForRetryAsync());
|
||||
|
||||
msg.LastAttemptAt = DateTimeOffset.UtcNow.AddSeconds(-90); // due
|
||||
await _storage.UpdateMessageAsync(msg);
|
||||
Assert.Single(await _storage.GetMessagesForRetryAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Initialize_BackfillsEpochMs_ForLegacyRows()
|
||||
{
|
||||
// Simulate a legacy row: text timestamp present, ms column NULL.
|
||||
await ExecRawAsync(
|
||||
"INSERT INTO sf_messages (id, category, target, payload_json, created_at, last_attempt_at, status) " +
|
||||
"VALUES ('legacy', 0, 't', '{}', @c, @l, 0)",
|
||||
("@c", DateTimeOffset.UtcNow.ToString("O")),
|
||||
("@l", DateTimeOffset.UtcNow.AddHours(-1).ToString("O")));
|
||||
|
||||
await _storage.InitializeAsync(); // second init runs the one-time backfill
|
||||
|
||||
var ms = await ScalarRawAsync("SELECT last_attempt_at_ms FROM sf_messages WHERE id='legacy'");
|
||||
Assert.NotNull(ms);
|
||||
Assert.InRange(Convert.ToInt64(ms),
|
||||
DateTimeOffset.UtcNow.AddHours(-1).AddMinutes(-1).ToUnixTimeMilliseconds(),
|
||||
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||
}
|
||||
|
||||
private async Task ExecRawAsync(string sql, params (string, object)[] parameters)
|
||||
{
|
||||
await using var conn = new SqliteConnection($"Data Source={_dbName};Mode=Memory;Cache=Shared");
|
||||
await conn.OpenAsync();
|
||||
await using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
foreach (var (name, value) in parameters) cmd.Parameters.AddWithValue(name, value);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
private async Task<object?> ScalarRawAsync(string sql)
|
||||
{
|
||||
await using var conn = new SqliteConnection($"Data Source={_dbName};Mode=Memory;Cache=Shared");
|
||||
await conn.OpenAsync();
|
||||
await using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
var result = await cmd.ExecuteScalarAsync();
|
||||
return result is DBNull ? null : result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user