8d82d049e9
- I-1: QuestDbConnection caps Npgsql Maximum Pool Size at 24 (DefaultMaxPoolSize) when the connection string does not set it; an explicit value is kept. The effective value is logged once at Information. - M-1: QuestDbTagCatalog.EnsureAsync waits for the tag table's sequenced commits and re-checks before inserting, so it no longer overwrites a provisioner row (unit/description) that is committed but not yet applied. Integration test reproduces the clobber against real QuestDB (falsified). - M-3: the pre-1970 drop Warning is rate-limited per tag (once per 10 min); DroppedPre1970 still counts every sample. - M-5: QuestDbHistorianProvider is IAsyncDisposable/IDisposable and disposes the shared Npgsql pool; Program.cs registers the provider through a factory and every seam resolves it from the container, so DI disposes it at shutdown. - M-8, M-9: docs fixes (HistoryUpdate transaction wording; folded duplicate HistoryUpdate paragraph). Pool-size docs updated for the new default cap. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
72 lines
3.2 KiB
C#
72 lines
3.2 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests;
|
|
|
|
/// <summary>
|
|
/// The pre-1970 drop Warning is rate-limited per tag (final review M-3) while the counter still counts every dropped
|
|
/// sample. All-pre-1970 batches return before any QuestDB call, so the connection points at a closed port.
|
|
/// </summary>
|
|
public sealed class QuestDbHistorianValueWriterTests
|
|
{
|
|
private static readonly DateTime FileTimeZero = new(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
|
|
private static (QuestDbHistorianValueWriter W, ListLogger Log, Action<TimeSpan> Advance) Make()
|
|
{
|
|
var opts = new QuestDbHistorianOptions { ConnectionString = "Host=127.0.0.1;Port=1;Timeout=1" };
|
|
var conn = new QuestDbConnection(opts);
|
|
var boot = new QuestDbSchemaBootstrapper(conn, opts);
|
|
var wal = new QuestDbWalVisibility(conn, opts.VisibilityTimeout);
|
|
var now = new DateTime(2026, 10, 11, 12, 0, 0, DateTimeKind.Utc);
|
|
var log = new ListLogger();
|
|
var w = new QuestDbHistorianValueWriter(conn, new QuestDbTagCatalog(conn, boot, wal), wal, log, () => now);
|
|
return (w, log, d => now += d);
|
|
}
|
|
|
|
private static HistorizationValue[] Pre1970(int n) =>
|
|
Enumerable.Range(0, n).Select(i => new HistorizationValue(FileTimeZero.AddSeconds(i), HistorianValue.FromDouble(i), 0)).ToArray();
|
|
|
|
[Fact]
|
|
public async Task The_pre_1970_warning_is_logged_once_per_tag_per_interval_but_every_drop_is_counted()
|
|
{
|
|
var (w, log, advance) = Make();
|
|
|
|
(await w.WriteLiveValuesAsync("a", Pre1970(2), CancellationToken.None)).ShouldBeTrue();
|
|
advance(TimeSpan.FromSeconds(2));
|
|
(await w.WriteLiveValuesAsync("a", Pre1970(3), CancellationToken.None)).ShouldBeTrue();
|
|
advance(QuestDbHistorianValueWriter.Pre1970WarningInterval - TimeSpan.FromSeconds(3));
|
|
(await w.WriteLiveValuesAsync("a", Pre1970(1), CancellationToken.None)).ShouldBeTrue();
|
|
|
|
log.Warnings.ShouldBe(1);
|
|
w.DroppedPre1970.ShouldBe(6);
|
|
|
|
// A different tag has its own budget.
|
|
(await w.WriteLiveValuesAsync("b", Pre1970(1), CancellationToken.None)).ShouldBeTrue();
|
|
log.Warnings.ShouldBe(2);
|
|
|
|
// Once the interval has passed since the last Warning for "a", it warns again.
|
|
advance(TimeSpan.FromSeconds(1));
|
|
(await w.WriteLiveValuesAsync("a", Pre1970(1), CancellationToken.None)).ShouldBeTrue();
|
|
log.Warnings.ShouldBe(3);
|
|
w.DroppedPre1970.ShouldBe(8);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_interval_is_ten_minutes() =>
|
|
QuestDbHistorianValueWriter.Pre1970WarningInterval.ShouldBe(TimeSpan.FromMinutes(10));
|
|
|
|
private sealed class ListLogger : ILogger
|
|
{
|
|
private int _warnings;
|
|
public int Warnings => Volatile.Read(ref _warnings);
|
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
|
public bool IsEnabled(LogLevel logLevel) => true;
|
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
|
{
|
|
if (logLevel == LogLevel.Warning) Interlocked.Increment(ref _warnings);
|
|
}
|
|
}
|
|
}
|