feat(r2-04): meter unconfirmed + advise-failed Galaxy writes

This commit is contained in:
Joseph Doherty
2026-07-13 10:53:17 -04:00
parent 610306734c
commit 084c6e3cee
3 changed files with 114 additions and 1 deletions
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Client;
@@ -37,6 +38,18 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
private readonly ConcurrentDictionary<int, byte> _supervisedHandles = new();
private int _addItemCallCount;
// Write failure-visibility meters (archreview 06/S-1). Reuse the pump's meter name so the single host
// listener catches both. advise_failed = writes short-circuited to Bad because AdviseSupervisory failed
// (the value could not have committed); unconfirmed = writes reported Good off an EMPTY gateway statuses
// array (command accepted, COM commit unconfirmed pending the gateway WriteComplete correlation follow-up).
private static readonly Meter WriterMeter = new(EventPump.MeterName);
private static readonly Counter<long> WriteAdviseFailed =
WriterMeter.CreateCounter<long>("galaxy.writes.advise_failed", unit: "{write}",
description: "Writes short-circuited to Bad because AdviseSupervisory failed (value could not have committed).");
private static readonly Counter<long> WriteUnconfirmed =
WriterMeter.CreateCounter<long>("galaxy.writes.unconfirmed", unit: "{write}",
description: "Writes reported Good off an EMPTY gateway statuses array (command accepted; commit unconfirmed).");
/// <summary>Initializes a new Galaxy data writer.</summary>
/// <param name="session">The MXAccess gateway session.</param>
/// <param name="writeUserId">The user ID for write operations.</param>
@@ -186,6 +199,7 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
// the galaxy.writes.* meters are the honest signal.
if (!await EnsureSupervisoryAdvisedAsync(ops, itemHandle, ct).ConfigureAwait(false))
{
WriteAdviseFailed.Add(1);
return new WriteResult(StatusCodeMap.BadCommunicationError);
}
reply = await ops.WriteRawAsync(itemHandle, mxValue, _writeUserId, ct)
@@ -310,6 +324,11 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
/// Translate a gateway <see cref="MxCommandReply"/> into an OPC UA
/// <see cref="WriteResult"/>. Honours the protocol-level Status field first
/// (transport / dispatch failures), then the first MXAccess status row.
/// <para>An EMPTY statuses array is treated as <c>Good</c> but is PROVISIONAL: the reply proves
/// worker-side command acceptance, not the COM-side commit (the gateway does not yet correlate a
/// WriteComplete row onto the unary reply — mxaccessgw backlog). Each such reply increments
/// <c>galaxy.writes.unconfirmed</c> so the unconfirmed-write rate is operator-visible today
/// (archreview 06/S-1).</para>
/// </summary>
private WriteResult TranslateReply(MxCommandReply reply, string fullRef)
{
@@ -331,6 +350,9 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
return new WriteResult(StatusCodeMap.FromMxStatus(status, _logger));
}
// Empty statuses ⇒ provisional Good. Meter it so the unconfirmed-write rate is visible until the
// gateway-side WriteComplete correlation lands (archreview 06/S-1 cross-repo follow-up).
WriteUnconfirmed.Add(1);
return new WriteResult(StatusCodeMap.Good);
}
}