Files
Joseph Doherty ee314d1b9e perf(historian): one QuestDB WAL wait per drain pass, with an exponential poll (lmxopcua#746)
The QuestDB live writer waited for WAL visibility after every tag group, and
the wait polled wal_tables() every 10 ms. With DrainConcurrency = 8 that was
up to eight pollers at ~100 queries/s each against a database whose WAL
apply was already behind.

New Runtime seam IHistorianPassWriter (WriteLiveValuesInPassAsync +
FlushAsync). The recorder writes every group of a pass through it and calls
FlushAsync once after all groups report, before the ack, whenever any group
wrote (an un-acked pass is redelivered and its insert-if-absent check must
see this pass's rows). A flush failure does not fail the pass. The QuestDB
writer implements it: in-pass writes only mark a flush as owed; FlushAsync
waits once. The poll now backs off 10 -> 20 -> ... -> 200 ms, never past the
timeout. Direct WriteLiveValuesAsync callers still wait per call.

Also updates the QuestDB cancelled-write integration test to the rethrow
contract from the previous commit.

Rebased from the unpushed remed/lane-H2 branch (originally authored 2026-10-22
in session_01Ub9hGmYLQvn9XETzw7CMsT) onto current master.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:40:46 -05:00

42 lines
1.7 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests;
public sealed class QuestDbWalVisibilityTests
{
[Fact]
public async Task Timeout_zero_skips_the_wait_without_touching_the_database()
{
// Port 1 refuses connections: any attempt to query would throw, so returning true proves nothing was opened.
var conn = new QuestDbConnection(new QuestDbHistorianOptions { ConnectionString = "Host=127.0.0.1;Port=1;Timeout=1" });
var wal = new QuestDbWalVisibility(conn, TimeSpan.Zero);
wal.Disabled.ShouldBeTrue();
(await wal.WaitAsync("hist_sample", CancellationToken.None)).ShouldBeTrue();
}
[Fact]
public async Task A_positive_timeout_does_touch_the_database_control()
{
var conn = new QuestDbConnection(new QuestDbHistorianOptions { ConnectionString = "Host=127.0.0.1;Port=1;Timeout=1" });
var wal = new QuestDbWalVisibility(conn, TimeSpan.FromSeconds(1));
wal.Disabled.ShouldBeFalse();
await Should.ThrowAsync<Exception>(() => wal.WaitAsync("hist_sample", CancellationToken.None));
}
[Theory]
[InlineData(5, 5, true)]
[InlineData(6, 5, true)]
[InlineData(4, 5, false)]
public void Reached_is_writerTxn_at_or_beyond_the_target(long writer, long target, bool expected) =>
QuestDbWalVisibility.Reached(writer, target).ShouldBe(expected);
[Fact]
public void The_poll_backs_off_exponentially_from_10_ms_to_a_200_ms_cap()
{
var delays = new List<TimeSpan> { QuestDbWalVisibility.FirstPollInterval };
while (delays.Count < 8) delays.Add(QuestDbWalVisibility.NextPollInterval(delays[^1]));
delays.Select(d => (int)d.TotalMilliseconds).ShouldBe([10, 20, 40, 80, 160, 200, 200, 200]);
}
}