fix(site-event-log): queries on own read connection off the actor thread, bounded keyword window; cap trim counts/vacuums rarely (review 2026-10-21 02/P1, 02/P4)

This commit is contained in:
Joseph Doherty
2026-10-22 08:30:35 -04:00
parent f7c4adbaed
commit 3169429c3f
9 changed files with 194 additions and 35 deletions
@@ -124,6 +124,61 @@ public class EventLogQueryServiceTests : IDisposable
Assert.All(response.Entries, e => Assert.Equal("inst-1", e.InstanceId));
}
private void InsertRawEvent(DateTimeOffset ts, string message) =>
_eventLogger.WithConnection(c =>
{
using var cmd = c.CreateCommand();
cmd.CommandText = @"INSERT INTO site_events (id, timestamp, event_type, severity, instance_id, source, message, details)
VALUES ($id, $ts, 'script', 'Info', NULL, 'Src', $msg, NULL)";
cmd.Parameters.AddWithValue("$id", Guid.NewGuid().ToString("N"));
cmd.Parameters.AddWithValue("$ts", ts.ToUniversalTime().ToString("o"));
cmd.Parameters.AddWithValue("$msg", message);
cmd.ExecuteNonQuery();
});
/// <summary>
/// Review 2026-10-21 02/P1: a keyword search with no window is clamped to
/// <c>KeywordSearchMaxWindow</c> (default 7 days) instead of full-scanning the log.
/// </summary>
[Fact]
public void Query_KeywordSearch_WithoutWindow_IsClampedToMaxWindow()
{
InsertRawEvent(DateTimeOffset.UtcNow.AddDays(-30), "ancient needle");
InsertRawEvent(DateTimeOffset.UtcNow.AddDays(-1), "recent needle");
var keyword = _queryService.ExecuteQuery(MakeRequest(keyword: "needle"));
Assert.True(keyword.Success);
Assert.Single(keyword.Entries);
Assert.Equal("recent needle", keyword.Entries[0].Message);
// Non-keyword queries are unaffected.
var all = _queryService.ExecuteQuery(MakeRequest());
Assert.Equal(2, all.Entries.Count);
}
/// <summary>Review 2026-10-21 02/P1: a query must complete while another thread holds the writer lock.</summary>
[Fact]
public async Task Query_DoesNotBlockOnTheWriteLock()
{
await SeedEvents();
var held = new ManualResetEventSlim();
var release = new ManualResetEventSlim();
var holder = Task.Run(() => _eventLogger.WithConnection(_ =>
{
held.Set();
release.Wait(TimeSpan.FromSeconds(30)); // blocks the holder thread only
}));
try
{
Assert.True(await Task.Run(() => held.Wait(TimeSpan.FromSeconds(5))));
var q = Task.Run(() => _queryService.ExecuteQuery(MakeRequest()));
var done = await Task.WhenAny(q, Task.Delay(TimeSpan.FromSeconds(10)));
Assert.Same(q, done);
Assert.Equal(5, (await q).Entries.Count);
}
finally { release.Set(); await holder; }
}
/// <summary>Verifies that a keyword search matches the event message.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]