693a78db7d
The truncation-cliff fix made alarm transitions truncation-safe but silent:
when GetXmlCurrentAlarms2 returns exactly maxAlmCnt records the worker
suppresses absence-implies-Clear inference and says so only in a rate-limited
stderr warning. No client and no operator could tell a complete active set
from a capped one.
Two additive proto3 booleans carry the verdict out:
- QueryActiveAlarmsReplyPayload.snapshot_truncated = 2 (worker IPC reply)
- ActiveAlarmSnapshot.from_truncated_snapshot = 16 (per record)
The per-record field is not an aesthetic choice. QueryActiveAlarms returns a
bare `stream ActiveAlarmSnapshot` with no envelope, header, or trailer, so a
per-record boolean is the only carrier that stays wire-compatible; an envelope
message would change every existing client's stream element type. The reply
payload states it too because a prefix filter can leave zero records and a
truncated fetch with nothing to report still has to say so. The flag means
"this set may be incomplete", never "this record is unreliable" — it is
independent of the subtag-fallback `degraded` field.
Detection is deliberately UNCHANGED: IsTruncatedFetch remains
`fetchedRecordCount >= maxAlarmsPerFetch`. The live probe (docs/AlarmProbeFindings.md,
ce5d8ae) could not verify whether ALARM_RECORDS/@COUNT reports the total active
count or only the records in the reply, so @COUNT is not parsed for detection;
switching to it stays blocked on probe evidence. The probe's comment
annotations in WnWrapAlarmConsumer.cs are preserved.
Reset semantics: not latched. WnWrapAlarmConsumer.FoldFetch replaces the
verdict on every poll under the same lock as the snapshot merge, so the first
sub-cap fetch clears it; GatewayAlarmMonitor.ClearCache drops it with the cache
generation it describes. A caveat that never turns off is one operators learn
to ignore.
Flow: WnWrapAlarmConsumer.LastSnapshotTruncated -> AlarmDispatcher (stamps every
record) / IAlarmCommandHandler (payload) -> MxAccessCommandExecutor reply ->
GatewayAlarmMonitor._snapshotTruncated -> IGatewayAlarmService.SnapshotTruncated
-> DashboardAlarmQueryResult -> AlarmsPage warning banner (render-side only; the
poll loop and DisposeAsync drain are untouched). The public QueryActiveAlarms
RPC forwards worker snapshots unmodified, so the per-record flag needed no
mapper change — a test pins that.
Parity: this describes OUR fetch mechanics — additive gateway metadata — not
MXAccess provider behavior. No event is synthesized and no MXAccess-observable
semantics change, so it is not a parity deviation.
Tests: worker LastSnapshotTruncated set/reset/consecutive-burst (windev-run);
gateway end-to-end truncated reply -> monitor -> public stream, with the
complete-reply control as the load-bearing assertion; AlarmsPage banner
present/absent. Docs: gateway.md alarm surface, docs/DesignDecisions.md entry.
132 lines
6.4 KiB
C#
132 lines
6.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
|
|
|
/// <summary>
|
|
/// Abstraction over an AVEVA alarm-consumer COM library. The production
|
|
/// implementation (<see cref="WnWrapAlarmConsumer"/>) wraps
|
|
/// <c>WNWRAPCONSUMERLib.wwAlarmConsumerClass</c> from
|
|
/// <c>C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll</c>;
|
|
/// tests substitute a fake to drive transition events without a live
|
|
/// Galaxy.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The receive surface is poll-based: the production consumer
|
|
/// periodically calls <c>GetXmlCurrentAlarms2</c>, parses the
|
|
/// returned XML payload, diffs against the previous snapshot keyed
|
|
/// by alarm GUID, and raises <see cref="AlarmTransitionEmitted"/>
|
|
/// once per state change. This bypasses the FILETIME marshaling
|
|
/// crash in <c>aaAlarmManagedClient.AlarmClient.GetHighPriAlarm</c>
|
|
/// (see <c>docs/AlarmClientDiscovery.md</c>) — XML strings carry
|
|
/// timestamps as ASCII fields, no DateTime auto-conversion happens
|
|
/// on the .NET interop boundary.
|
|
/// </para>
|
|
/// </remarks>
|
|
public interface IMxAccessAlarmConsumer : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Fires once per detected alarm-state transition (raise, acknowledge,
|
|
/// clear, or new-alarm-already-acked-on-arrival). Subscribers are
|
|
/// expected to translate the record into the proto family
|
|
/// <c>OnAlarmTransition</c> and enqueue it. Fired on the consumer's
|
|
/// polling thread (the worker's STA in production); subscribers that
|
|
/// need a different thread must marshal back themselves.
|
|
/// </summary>
|
|
event EventHandler<MxAlarmTransitionEvent>? AlarmTransitionEmitted;
|
|
|
|
/// <summary>
|
|
/// Whether the most recent fetch that reached the retained snapshot came
|
|
/// back holding the per-fetch cap. While this is <see langword="true"/>
|
|
/// the snapshot returned by <see cref="SnapshotActiveAlarms"/> is
|
|
/// authoritative about presence only: the provider may hold actives it
|
|
/// had no room to report, and the consumer has suspended the
|
|
/// absence-implies-Clear inference. Not latched — the first sub-cap fetch
|
|
/// after a run of capped ones clears it, because that fetch is complete
|
|
/// and the snapshot it produced is again authoritative about absence.
|
|
/// Consumers with no per-fetch cap (the subtag fallback, which is
|
|
/// event-driven) always report <see langword="false"/>.
|
|
/// </summary>
|
|
bool LastSnapshotTruncated { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes the AVEVA alarm-client connection, registers as a
|
|
/// consumer, and subscribes to the supplied alarm-provider expression.
|
|
/// Subscription string follows AVEVA's canonical format:
|
|
/// <c>\\<node>\Galaxy!<area></c>. The literal "Galaxy" is
|
|
/// the provider name (regardless of the configured Galaxy database
|
|
/// name). Subscribe does not start any polling of its own; the caller
|
|
/// drives polls explicitly via <see cref="PollOnce"/>.
|
|
/// </summary>
|
|
/// <param name="subscription">The subscription expression (e.g., <c>\\HOST\Galaxy!Area</c>).</param>
|
|
void Subscribe(string subscription);
|
|
|
|
/// <summary>
|
|
/// Acknowledges a single alarm with full operator-identity fidelity.
|
|
/// Reaches AVEVA's native <c>AlarmAckByGUID</c>; operator
|
|
/// user / node / domain / full-name and the comment land atomically
|
|
/// with the ack transition in the alarm-history log.
|
|
/// </summary>
|
|
/// <param name="alarmGuid">The alarm GUID.</param>
|
|
/// <param name="ackComment">The acknowledgment comment.</param>
|
|
/// <param name="ackOperatorName">The operator name.</param>
|
|
/// <param name="ackOperatorNode">The operator node.</param>
|
|
/// <param name="ackOperatorDomain">The operator domain.</param>
|
|
/// <param name="ackOperatorFullName">The operator full name.</param>
|
|
/// <returns>The AVEVA-native status code.</returns>
|
|
int AcknowledgeByGuid(
|
|
Guid alarmGuid,
|
|
string ackComment,
|
|
string ackOperatorName,
|
|
string ackOperatorNode,
|
|
string ackOperatorDomain,
|
|
string ackOperatorFullName);
|
|
|
|
/// <summary>
|
|
/// Acknowledge a single alarm by its (name, provider, group) tuple.
|
|
/// Reaches AVEVA's <c>AlarmAckByName</c> on
|
|
/// <c>wwAlarmConsumerClass</c>; same alarm-history outcome as
|
|
/// <see cref="AcknowledgeByGuid"/>, used when the caller has the
|
|
/// human-readable reference but not the canonical GUID.
|
|
/// </summary>
|
|
/// <param name="alarmName">The alarm name.</param>
|
|
/// <param name="providerName">The provider name.</param>
|
|
/// <param name="groupName">The group name.</param>
|
|
/// <param name="ackComment">The acknowledgment comment.</param>
|
|
/// <param name="ackOperatorName">The operator name.</param>
|
|
/// <param name="ackOperatorNode">The operator node.</param>
|
|
/// <param name="ackOperatorDomain">The operator domain.</param>
|
|
/// <param name="ackOperatorFullName">The operator full name.</param>
|
|
/// <returns>The AVEVA-native status code.</returns>
|
|
int AcknowledgeByName(
|
|
string alarmName,
|
|
string providerName,
|
|
string groupName,
|
|
string ackComment,
|
|
string ackOperatorName,
|
|
string ackOperatorNode,
|
|
string ackOperatorDomain,
|
|
string ackOperatorFullName);
|
|
|
|
/// <summary>
|
|
/// Returns the consumer's most recently parsed snapshot of currently
|
|
/// active alarms. Used by the gateway's QueryActiveAlarms (PR A.7)
|
|
/// ConditionRefresh path — operator clients call this after reconnect
|
|
/// to seed local Part 9 state.
|
|
/// </summary>
|
|
/// <returns>The most recently parsed snapshot of currently active alarms.</returns>
|
|
IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms();
|
|
|
|
/// <summary>
|
|
/// Drives a single synchronous poll of the underlying alarm source.
|
|
/// The production consumer owns no internal timer; the worker's STA
|
|
/// drives polls via <c>StaRuntime.InvokeAsync</c>, satisfying the
|
|
/// <c>ThreadingModel=Apartment</c> requirement of
|
|
/// <c>wwAlarmConsumerClass</c>. Fake implementations should no-op.
|
|
/// This method must be invoked on the thread that created the consumer
|
|
/// (the worker's STA in production).
|
|
/// </summary>
|
|
void PollOnce();
|
|
}
|