feat(runtime): ServerHistorian HistoryRead parallelism/limiter/deadline knobs + node-manager wiring (03/S3)
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
{ "id": "R2-08-T5", "subject": "Directory-outage circuit: fail-fast deny after N consecutive system-side failures, half-open after cooldown", "status": "completed", "blockedBy": ["R2-08-T3", "R2-08-T4"] },
|
||||
{ "id": "R2-08-T6", "subject": "Outage-sim integration test (real library, unroutable host, via internal HandleImpersonation) + SDK m_eventLock contract doc + docs/security.md", "status": "completed", "blockedBy": ["R2-08-T4"] },
|
||||
{ "id": "R2-08-T7", "subject": "S3 sequential-serving repro test (RED): MaxObservedConcurrency >= 2 on an 8-node Raw batch — fails on current code", "status": "completed", "blockedBy": [] },
|
||||
{ "id": "R2-08-T8", "subject": "ServerHistorianOptions: HistoryReadBatchParallelism/MaxConcurrentHistoryReadBatches/HistoryReadDeadline + Validate warnings + node-manager properties + hosted-service wiring", "status": "pending", "blockedBy": [] },
|
||||
{ "id": "R2-08-T8", "subject": "ServerHistorianOptions: HistoryReadBatchParallelism/MaxConcurrentHistoryReadBatches/HistoryReadDeadline + Validate warnings + node-manager properties + hosted-service wiring", "status": "completed", "blockedBy": [] },
|
||||
{ "id": "R2-08-T9", "subject": "Async-ify ServeNode + Processed/AtTime arms (ct threading, OCE=>BadTimeout, behavior-neutral)", "status": "pending", "blockedBy": ["R2-08-T7", "R2-08-T8"] },
|
||||
{ "id": "R2-08-T10", "subject": "Async-ify ServeRawPaged (page read + tie-cluster over-fetch) + extract ServeEventsAsync", "status": "pending", "blockedBy": ["R2-08-T9"] },
|
||||
{ "id": "R2-08-T11", "subject": "RunBounded fan-out in all four HistoryRead arms, single bridge per arm (turns T7 green) + upper-bound + isolation tests", "status": "pending", "blockedBy": ["R2-08-T9", "R2-08-T10"] },
|
||||
|
||||
@@ -222,6 +222,13 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
|
||||
// section is absent; NodeManager is non-null here (guarded above).
|
||||
_server.NodeManager.MaxTieClusterOverfetch = _serverHistorianOptions.MaxTieClusterOverfetch;
|
||||
|
||||
// Propagate the S3 HistoryRead concurrency bounds (arch-review 03/S3): bounded per-node fan-out
|
||||
// within a batch, the process-wide concurrent-batch limiter, and the per-request deadline. Defaults
|
||||
// (4 / 16 / 60 s) survive when the ServerHistorian section is absent.
|
||||
_server.NodeManager.HistoryReadBatchParallelism = _serverHistorianOptions.HistoryReadBatchParallelism;
|
||||
_server.NodeManager.MaxConcurrentHistoryReadBatches = _serverHistorianOptions.MaxConcurrentHistoryReadBatches;
|
||||
_server.NodeManager.HistoryReadDeadline = _serverHistorianOptions.HistoryReadDeadline;
|
||||
|
||||
// ServiceLevel publisher needs IServerInternal — only available after Start.
|
||||
if (_server.CurrentInstance is { } serverInternal)
|
||||
{
|
||||
|
||||
@@ -197,6 +197,30 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// </summary>
|
||||
public int MaxTieClusterOverfetch { get; set; } = 65536;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum historized nodes served concurrently within one HistoryRead batch (arch-review 03/S3).
|
||||
/// Mirrors <c>ServerHistorianOptions.HistoryReadBatchParallelism</c>; the Host sets it at
|
||||
/// <c>StartAsync</c>. <c>≤ 1</c> falls back to sequential serving. The default (4) survives when the
|
||||
/// ServerHistorian section is absent.
|
||||
/// </summary>
|
||||
public int HistoryReadBatchParallelism { get; set; } = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide cap on concurrently-served HistoryRead batches (arch-review 03/S3). A batch that
|
||||
/// cannot acquire a slot within its (bounded) wait budget fast-fails every handle with
|
||||
/// <c>BadTooManyOperations</c>. Mirrors <c>ServerHistorianOptions.MaxConcurrentHistoryReadBatches</c>;
|
||||
/// the Host sets it at <c>StartAsync</c>. The default (16) survives when the section is absent.
|
||||
/// </summary>
|
||||
public int MaxConcurrentHistoryReadBatches { get; set; } = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Per-request wall-clock deadline for a HistoryRead batch (arch-review 03/S3). A node still in
|
||||
/// flight at the deadline surfaces <c>BadTimeout</c>. Non-positive = unbounded. Mirrors
|
||||
/// <c>ServerHistorianOptions.HistoryReadDeadline</c>; the Host sets it at <c>StartAsync</c>. The
|
||||
/// default (60 s) survives when the section is absent.
|
||||
/// </summary>
|
||||
public TimeSpan HistoryReadDeadline { get; set; } = TimeSpan.FromSeconds(60);
|
||||
|
||||
private volatile IHistoryContinuationStore _historyContinuationStore = new SessionHistoryContinuationStore();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -68,6 +68,32 @@ public sealed class ServerHistorianOptions
|
||||
/// </summary>
|
||||
public int MaxTieClusterOverfetch { get; init; } = 65536;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of historized nodes served CONCURRENTLY within one HistoryRead batch (arch-review
|
||||
/// 03/S3). The SDK's <c>HistoryRead*</c> override surface is synchronous, so each arm block-bridges
|
||||
/// once at its boundary; this bound turns the per-node fan-out inside that boundary from sequential
|
||||
/// (worst case <c>N × CallTimeout</c>) into <c>⌈N / P⌉ × CallTimeout</c>. It also caps the gateway-load
|
||||
/// multiplication a single client can command. Default 4.
|
||||
/// </summary>
|
||||
public int HistoryReadBatchParallelism { get; init; } = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide limit on the number of HistoryRead batches served CONCURRENTLY (arch-review 03/S3).
|
||||
/// A flood of history clients degrades to <c>BadTooManyOperations</c> rather than exhausting the SDK
|
||||
/// request-thread pool. Together with <see cref="HistoryReadBatchParallelism"/> this caps total
|
||||
/// in-flight gateway reads at <c>MaxConcurrentHistoryReadBatches × HistoryReadBatchParallelism</c>
|
||||
/// (default 16 × 4 = 64). Default 16.
|
||||
/// </summary>
|
||||
public int MaxConcurrentHistoryReadBatches { get; init; } = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Per-request wall-clock deadline for a HistoryRead batch (arch-review 03/S3). A single request can
|
||||
/// no longer hold an SDK request thread longer than this regardless of node count; a node still
|
||||
/// in flight at the deadline surfaces <c>BadTimeout</c>. Non-positive = unbounded (a
|
||||
/// <see cref="Validate"/> warning). Default 60 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan HistoryReadDeadline { get; init; } = TimeSpan.FromSeconds(60);
|
||||
|
||||
/// <summary>Returns operator-facing misconfiguration warnings for an <c>Enabled</c> historian
|
||||
/// (empty when disabled or correctly configured). Pure — the registration logs each entry.
|
||||
/// <para>
|
||||
@@ -100,6 +126,14 @@ public sealed class ServerHistorianOptions
|
||||
// so a zero/negative value is harmless (and noise-free) when the historian is disabled.
|
||||
if (MaxTieClusterOverfetch <= 0)
|
||||
warnings.Add($"ServerHistorian:MaxTieClusterOverfetch is {MaxTieClusterOverfetch} — must be > 0; HistoryRead-Raw cannot page within an oversized tie cluster and will surface BadHistoryOperationUnsupported for those reads.");
|
||||
// S3 HistoryRead concurrency knobs — a non-positive value disables the corresponding bound, so warn
|
||||
// (these gate the per-node fan-out / batch limiter that only run when a real data source is wired).
|
||||
if (HistoryReadBatchParallelism <= 0)
|
||||
warnings.Add($"ServerHistorian:HistoryReadBatchParallelism is {HistoryReadBatchParallelism} — must be > 0; HistoryRead falls back to serving each node sequentially.");
|
||||
if (MaxConcurrentHistoryReadBatches <= 0)
|
||||
warnings.Add($"ServerHistorian:MaxConcurrentHistoryReadBatches is {MaxConcurrentHistoryReadBatches} — must be > 0; the process-wide HistoryRead batch limiter is disabled.");
|
||||
if (HistoryReadDeadline <= TimeSpan.Zero)
|
||||
warnings.Add($"ServerHistorian:HistoryReadDeadline is {HistoryReadDeadline} — must be > 0; HistoryRead requests run unbounded (no per-request deadline).");
|
||||
return warnings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,4 +50,55 @@ public sealed class ServerHistorianOptionsTests
|
||||
.Validate();
|
||||
Assert.Contains(w, m => m.Contains("MaxTieClusterOverfetch"));
|
||||
}
|
||||
|
||||
// ── S3 HistoryRead concurrency knobs (arch-review 03/S3) ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HistoryRead_knob_defaults()
|
||||
{
|
||||
var o = new ServerHistorianOptions();
|
||||
Assert.Equal(4, o.HistoryReadBatchParallelism);
|
||||
Assert.Equal(16, o.MaxConcurrentHistoryReadBatches);
|
||||
Assert.Equal(TimeSpan.FromSeconds(60), o.HistoryReadDeadline);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enabled_with_nonpositive_HistoryReadBatchParallelism_warns()
|
||||
{
|
||||
var w = new ServerHistorianOptions
|
||||
{ Enabled = true, Endpoint = "https://h:5222", ApiKey = "histgw_x_y", HistoryReadBatchParallelism = 0 }
|
||||
.Validate();
|
||||
Assert.Contains(w, m => m.Contains("HistoryReadBatchParallelism"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enabled_with_nonpositive_MaxConcurrentHistoryReadBatches_warns()
|
||||
{
|
||||
var w = new ServerHistorianOptions
|
||||
{ Enabled = true, Endpoint = "https://h:5222", ApiKey = "histgw_x_y", MaxConcurrentHistoryReadBatches = 0 }
|
||||
.Validate();
|
||||
Assert.Contains(w, m => m.Contains("MaxConcurrentHistoryReadBatches"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enabled_with_nonpositive_HistoryReadDeadline_warns()
|
||||
{
|
||||
var w = new ServerHistorianOptions
|
||||
{ Enabled = true, Endpoint = "https://h:5222", ApiKey = "histgw_x_y", HistoryReadDeadline = TimeSpan.Zero }
|
||||
.Validate();
|
||||
Assert.Contains(w, m => m.Contains("HistoryReadDeadline"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Disabled_with_nonpositive_HistoryRead_knobs_is_silent()
|
||||
{
|
||||
var w = new ServerHistorianOptions
|
||||
{
|
||||
Enabled = false,
|
||||
HistoryReadBatchParallelism = 0,
|
||||
MaxConcurrentHistoryReadBatches = 0,
|
||||
HistoryReadDeadline = TimeSpan.Zero,
|
||||
}.Validate();
|
||||
Assert.Empty(w);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user