Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Historian/ContinuousHistorizationOptionsTests.cs
T
Joseph Doherty a5557375ee perf(historian): recorder appends off its mailbox; Periodic commit default (lmxopcua#746)
The continuous-historization recorder awaited every outbox append (an fsync
under PerEntry) on its mailbox, so ingestion was capped at 1/fsync_latency
bursts per second while the mux kept telling into an unbounded, unmetered
mailbox. GetStatus also suspended the mailbox on an unbounded outbox count.

- Gated ingest bursts go to a bounded channel (64 bursts) drained by one
  ordered append loop, which commits every burst waiting behind a slow
  append in a single AppendBatchAsync (FIFO kept). Only a full queue holds
  the mailbox (backpressure, bounded memory). Counters are Interlocked; the
  loop nudges the drain. PostStop flushes queued bursts within the shared
  5 s stop budget.
- GetStatus reads counters on the actor thread and pipes a count bounded by
  a 10 s timeout (-1 when it does not answer); it no longer suspends the
  mailbox. RecorderStatus gains PendingAppends, MailboxDepth and
  AppendBackpressureWaits; two gauges publish pending appends and mailbox
  depth.
- ContinuousHistorization:CommitMode defaults to Periodic (100 ms). The
  Host's appsettings.json, its unparseable-value fallback and docker-dev
  still say PerEntry: filed as a Host follow-up (outside this lane).

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

72 lines
3.1 KiB
C#

using Xunit;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Historian;
/// <summary>
/// Verifies <see cref="ContinuousHistorizationOptions.Validate"/> self-gates on <c>Enabled</c> and
/// warns (warn-only, never blocks startup) on the two gated misconfigurations: a blank
/// <c>OutboxPath</c> while enabled, and a non-positive <c>CommitIntervalMs</c> under
/// <c>Periodic</c> commit mode. No warning text carries a secret value.
/// </summary>
public sealed class ContinuousHistorizationOptionsTests
{
/// <summary>A disabled recorder yields no warnings regardless of the other knobs.</summary>
[Fact]
public void Disabled_no_warnings()
=> Assert.Empty(new ContinuousHistorizationOptions { Enabled = false }.Validate());
/// <summary>An enabled recorder with a blank outbox directory warns about <c>OutboxPath</c>.</summary>
[Fact]
public void Enabled_requires_outbox_path()
=> Assert.Contains(
new ContinuousHistorizationOptions { Enabled = true, OutboxPath = "" }.Validate(),
m => m.Contains("OutboxPath"));
/// <summary>Periodic commit mode with a non-positive interval warns about <c>CommitIntervalMs</c>.</summary>
[Fact]
public void Periodic_requires_positive_interval()
=> Assert.Contains(
new ContinuousHistorizationOptions
{
Enabled = true,
OutboxPath = "x",
CommitMode = "Periodic",
CommitIntervalMs = 0,
}.Validate(),
m => m.Contains("CommitIntervalMs"));
/// <summary>
/// Pins the shipped defaults, which are a tuned set rather than arbitrary numbers: the drain is
/// bounded-parallel over an in-memory peek (so a wide <c>DrainBatchSize</c> is cheap and a narrow
/// one lets the outbox fill during a gateway hiccup), <c>Capacity</c> is the #538 disk backstop,
/// and <c>MaxWriteAttempts</c> is the #538 poison-tag budget. The shipped
/// <c>appsettings.json</c> restates these values, so a silent drift between the two is exactly
/// what this test exists to catch.
/// </summary>
[Fact]
public void Defaults_are_the_documented_batch_era_values()
{
var options = new ContinuousHistorizationOptions();
Assert.Equal("Periodic", options.CommitMode); // 100 ms group commits — PerEntry capped ingest at 1 / fsync latency
Assert.Equal(100, options.CommitIntervalMs);
Assert.Equal(1024, options.DrainBatchSize);
Assert.Equal(8, options.DrainConcurrency);
Assert.Equal(500_000, options.Capacity);
Assert.Equal(5, options.MaxWriteAttempts);
Assert.True(options.CaptureWhilePrimaryUnreachable); // Gitea #672
}
/// <summary>A fully-configured enabled recorder produces no warnings.</summary>
[Fact]
public void Valid_config_is_clean()
=> Assert.Empty(
new ContinuousHistorizationOptions
{
Enabled = true,
OutboxPath = "/var/lib/otopcua/historization",
CommitMode = "PerEntry",
}.Validate());
}