perf(store-and-forward): epoch-ms due-check with additive column, backfill, and (status, due) index

This commit is contained in:
Joseph Doherty
2026-07-08 21:14:26 -04:00
parent 992f343f11
commit 5fe1b9747b
2 changed files with 113 additions and 7 deletions
@@ -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;
}
}