fix(historian): drain gateway event streams past the cap; cancellation is not a fault (lmxopcua#746)
A capped Gateway HistoryReadEvents read broke out of the gRPC stream as soon as the page was full. Abandoning a gateway stream cancels the RPC, which the gateway treats as a connection fault: it evicts its pooled historian session, so the next read reconnects and concurrent reads on that session fail. The read now keeps consuming the stream past the cap without collecting it, bounded by ServerHistorian:Gateway:EventDrainBudget (default 10000 rows; 0 = the old abandon-at-cap). Rows of a foreign source never count toward the budget before the "more events" signal is seen, so an abandoned stream never answers a page as complete. A paged-vs-unpaged fuzz (random tie series, foreign rows, caps 1..500, budgets 0..10000) pins the page contents and the drain; it found that abandon-before-signal case. ReadEventsAsync and the native AtTime arm now rethrow a caller cancellation instead of recording it as a backend failure. 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
This commit is contained in:
@@ -106,6 +106,12 @@ public sealed class ServerHistorianOptions
|
||||
/// </summary>
|
||||
public bool GatewayAtTimeFromRaw { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// <c>Gateway</c> backend only: the <c>ServerHistorian:Gateway</c> sub-section — knobs that exist because of the
|
||||
/// HistorianGateway's wire contract (see <see cref="GatewayHistorianSettings"/>). Ignored by the other backends.
|
||||
/// </summary>
|
||||
public GatewayHistorianSettings Gateway { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in: HistoryReadEvents never reads events older than <c>now − EventReadHorizon</c> — the earlier
|
||||
/// bound of every fresh events window is raised to it (both directions, every backend; a window wholly older is
|
||||
@@ -252,6 +258,28 @@ public sealed class ServerHistorianOptions
|
||||
warnings.Add($"ServerHistorian:EventReadHorizon is {EventReadHorizon} — negative is treated as no horizon (00:00:00); HistoryReadEvents reads every window in full.");
|
||||
if (HistoryReadDeadline <= TimeSpan.Zero)
|
||||
warnings.Add($"ServerHistorian:HistoryReadDeadline is {HistoryReadDeadline} — must be > 0; HistoryRead requests run unbounded (no per-request deadline).");
|
||||
if (IsGatewayBackend && Gateway.EventDrainBudget < 0)
|
||||
warnings.Add($"ServerHistorian:Gateway:EventDrainBudget is {Gateway.EventDrainBudget} — negative is treated as 0: every capped events read abandons its gateway stream at the cap, which makes the gateway evict its historian session.");
|
||||
return warnings;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <c>ServerHistorian:Gateway</c> sub-section: Gateway-backend knobs that exist because of the HistorianGateway's
|
||||
/// wire contract. Every default is the safe posture.
|
||||
/// </summary>
|
||||
public sealed class GatewayHistorianSettings
|
||||
{
|
||||
/// <summary>The default of <see cref="EventDrainBudget"/>.</summary>
|
||||
public const int DefaultEventDrainBudget = 10_000;
|
||||
|
||||
/// <summary>
|
||||
/// How many stream rows a capped HistoryReadEvents read consumes (and discards) past its page cap before it
|
||||
/// abandons the gateway stream. The gateway's <c>ReadEvents</c> has no max-events field, so the cap is applied
|
||||
/// client-side; an abandoned stream cancels the RPC and the gateway evicts its pooled historian session (the
|
||||
/// next read of any kind reconnects, and concurrent reads on that session fail). Draining is cheap for the
|
||||
/// usual small overshoot; past the budget the stream is abandoned anyway, so a page over a far larger window
|
||||
/// does not stream the whole window. Default 10 000; 0 = abandon at the cap (the previous behaviour).
|
||||
/// </summary>
|
||||
public int EventDrainBudget { get; init; } = DefaultEventDrainBudget;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user