fix(alarms): fetch/poll ceilings; truncation-semantics docs; log-format conformance
This commit is contained in:
@@ -35,6 +35,15 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
|
||||
/// </summary>
|
||||
internal static readonly TimeSpan MinimumAlarmPollInterval = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <summary>
|
||||
/// Ceiling on the resolved alarm poll cadence (one hour). Mirrors the
|
||||
/// gateway-side MxGateway:Alarms:PollIntervalMilliseconds maximum.
|
||||
/// Without it, int.MaxValue milliseconds — which passed startup
|
||||
/// validation before this existed — silently disables alarm polling
|
||||
/// for ~24 days rather than reporting a misconfiguration.
|
||||
/// </summary>
|
||||
internal static readonly TimeSpan MaximumAlarmPollInterval = TimeSpan.FromMilliseconds(3_600_000);
|
||||
|
||||
/// <summary>Default alarm poll cadence when the environment says nothing usable.</summary>
|
||||
internal static readonly TimeSpan DefaultAlarmPollInterval = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
@@ -235,7 +244,9 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
|
||||
}
|
||||
|
||||
TimeSpan resolved = TimeSpan.FromMilliseconds(milliseconds);
|
||||
return resolved < MinimumAlarmPollInterval ? DefaultAlarmPollInterval : resolved;
|
||||
return resolved < MinimumAlarmPollInterval || resolved > MaximumAlarmPollInterval
|
||||
? DefaultAlarmPollInterval
|
||||
: resolved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -58,10 +58,20 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
/// </summary>
|
||||
internal const int MinimumMaxAlarmsPerFetch = 64;
|
||||
|
||||
/// <summary>
|
||||
/// Ceiling on the resolved per-fetch cap. Mirrors the gateway-side
|
||||
/// <c>MxGateway:Alarms:MaxAlarmsPerFetch</c> maximum. The worker is a
|
||||
/// 32-bit process: every poll materializes the whole reply as one BSTR
|
||||
/// and then a full <see cref="XmlDocument"/> over it, so an unbounded
|
||||
/// cap (<see cref="int.MaxValue"/> passed startup validation before
|
||||
/// this existed) is an out-of-memory fault on the STA, not a slow poll.
|
||||
/// </summary>
|
||||
internal const int MaximumMaxAlarmsPerFetch = 65_536;
|
||||
|
||||
/// <summary>
|
||||
/// Environment variable the gateway's <c>WorkerProcessLauncher</c>
|
||||
/// sets from <c>MxGateway:Alarms:MaxAlarmsPerFetch</c>. A missing,
|
||||
/// unparseable, or below-floor value falls back to
|
||||
/// unparseable, or out-of-range value falls back to
|
||||
/// <see cref="DefaultMaxAlarmsPerFetch"/> — a bad environment value
|
||||
/// must never keep the alarm consumer from starting.
|
||||
/// </summary>
|
||||
@@ -117,10 +127,13 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the per-fetch cap from the launcher-provided environment
|
||||
/// variable. A missing, unparseable, or below-floor value falls back
|
||||
/// variable. A missing, unparseable, or out-of-range value falls back
|
||||
/// to <see cref="DefaultMaxAlarmsPerFetch"/> rather than throwing:
|
||||
/// polling at the default cap is always safe, and a session that
|
||||
/// refuses to subscribe over a mistyped environment variable is not.
|
||||
/// The ceiling is enforced here as well as in the gateway validator so
|
||||
/// a hand-set environment (or a future launcher bug) cannot hand the
|
||||
/// x86 worker a cap that faults it on the first poll.
|
||||
/// </summary>
|
||||
/// <returns>The cap passed to <c>GetXmlCurrentAlarms2</c>.</returns>
|
||||
internal static int ResolveMaxAlarmsPerFetch()
|
||||
@@ -132,6 +145,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
CultureInfo.InvariantCulture,
|
||||
out int cap)
|
||||
&& cap >= MinimumMaxAlarmsPerFetch
|
||||
&& cap <= MaximumMaxAlarmsPerFetch
|
||||
? cap
|
||||
: DefaultMaxAlarmsPerFetch;
|
||||
}
|
||||
@@ -498,7 +512,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
{
|
||||
total = ++truncatedFetchCount;
|
||||
elapsed = truncationWarningClock.ElapsedMilliseconds;
|
||||
if (elapsed - lastTruncationWarningMilliseconds < TruncationWarningIntervalMilliseconds)
|
||||
if (!ShouldWarnTruncation(elapsed, lastTruncationWarningMilliseconds))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -506,13 +520,24 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
lastTruncationWarningMilliseconds = elapsed;
|
||||
}
|
||||
|
||||
// Format matches WorkerConsoleLogger's "level=<L> event=<E> k=v k=v"
|
||||
// line shape so the worker's stderr parses uniformly. Deviation, on
|
||||
// purpose: IWorkerLogger exposes only Information and Error, so
|
||||
// "Warning" is a third level string no other worker line emits. A
|
||||
// truncated snapshot is not an error (the poll succeeded and the
|
||||
// snapshot is safe) but it is not routine either, so downgrading it to
|
||||
// Information would bury it. Revisit if the truncation signal becomes
|
||||
// structural — see docs/DesignDecisions.md.
|
||||
//
|
||||
// Identifiers and counts only — no tag names, values, limits, or
|
||||
// comments, per the gateway's "don't log tag values by default" rule.
|
||||
// The trailing note stays a single bare k=v token (no spaces, no
|
||||
// semicolons); remediation prose lives in docs/GatewayConfiguration.md.
|
||||
string message = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"level=Warning event=AlarmSnapshotTruncated maxAlarmsPerFetch={0} fetchedRecords={1} "
|
||||
+ "parsedRecords={2} retainedSnapshotSize={3} truncatedFetchesSinceStart={4} "
|
||||
+ "note=absence-implies-clear suppressed for this poll; raise MxGateway:Alarms:MaxAlarmsPerFetch",
|
||||
+ "note=truncated-snapshot-retained",
|
||||
maxAlarmsPerFetch,
|
||||
fetchedRecordCount,
|
||||
parsedRecordCount,
|
||||
@@ -523,6 +548,25 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
sink(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate-limit decision for the truncated-fetch warning: emit only when
|
||||
/// a full <see cref="TruncationWarningIntervalMilliseconds"/> has
|
||||
/// elapsed since the last one. A galaxy parked above the cap truncates
|
||||
/// on every poll, so consecutive truncated polls inside one interval
|
||||
/// must produce exactly one line. Exposed as <c>internal static</c>
|
||||
/// so the throttle is unit-testable without the wnwrapConsumer COM
|
||||
/// object.
|
||||
/// </summary>
|
||||
/// <param name="elapsedMilliseconds">Consumer-lifetime clock reading for this poll.</param>
|
||||
/// <param name="lastWarningMilliseconds">Clock reading when the last warning was emitted.</param>
|
||||
/// <returns><see langword="true"/> when this poll may emit a warning.</returns>
|
||||
internal static bool ShouldWarnTruncation(
|
||||
long elapsedMilliseconds,
|
||||
long lastWarningMilliseconds)
|
||||
{
|
||||
return elapsedMilliseconds - lastWarningMilliseconds >= TruncationWarningIntervalMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure snapshot-to-transitions diff. Compares the previous polled
|
||||
/// snapshot to the next snapshot and produces one
|
||||
@@ -641,6 +685,13 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
// first-match-wins (null means "not seen yet"), and a single
|
||||
// coalesce to string.Empty at the end — which keeps the absent and
|
||||
// empty cases indistinguishable exactly as before.
|
||||
//
|
||||
// One theoretical divergence: XmlNode.Name is the QName, so a child
|
||||
// carrying its own default-namespace declaration (<GUID xmlns="...">)
|
||||
// matches here where the unprefixed XPath name test would not. It is
|
||||
// theoretical — wnwrap emits no namespaces, and a namespaced ALARM
|
||||
// wrapper makes the outer SelectNodes return nothing either way — and
|
||||
// it errs permissive (field populated rather than silently dropped).
|
||||
string? guidHex = null;
|
||||
string? xmlDate = null;
|
||||
string? xmlTime = null;
|
||||
|
||||
Reference in New Issue
Block a user