9cad9ed0fc
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
164 lines
7.8 KiB
C#
164 lines
7.8 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ZB.MOM.WW.MxGateway.Client;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
// Use the generated nested status enum for the SetBufferedUpdateInterval reply check.
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
/// <summary>
|
|
/// Production <see cref="IGalaxySubscriber"/> over a connected
|
|
/// <see cref="GalaxyMxSession"/>. Forwards SubscribeBulk / UnsubscribeBulk to the
|
|
/// gateway and streams MxEvents via the gw's bidirectional events RPC.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// PR 6.3 wired the per-call <c>buffered_update_interval_ms</c> through
|
|
/// <see cref="SubscribeBulkAsync"/>. The gw's contract is session-level
|
|
/// (<c>SetBufferedUpdateInterval</c> applies to all buffered subscriptions on the
|
|
/// server handle), so we cache the last-applied value and skip redundant calls.
|
|
/// </remarks>
|
|
public sealed class GatewayGalaxySubscriber : IGalaxySubscriber
|
|
{
|
|
private readonly GalaxyMxSession _session;
|
|
private readonly ILogger _logger;
|
|
private readonly Lock _intervalLock = new();
|
|
private int _lastAppliedIntervalMs = -1; // -1 = never applied; 0 = explicit "use gw default"
|
|
|
|
/// <summary>Initializes a new instance of GatewayGalaxySubscriber.</summary>
|
|
/// <param name="session">The Galaxy MX session to use for subscription operations.</param>
|
|
/// <param name="logger">
|
|
/// Optional logger; surfaces a warning when <c>SetBufferedUpdateInterval</c>
|
|
/// soft-fails so the cadence-not-applied condition isn't silent. Null is allowed
|
|
/// for unit-test construction and falls back to <see cref="NullLogger.Instance"/>.
|
|
/// </param>
|
|
public GatewayGalaxySubscriber(GalaxyMxSession session, ILogger? logger = null)
|
|
{
|
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
|
_logger = logger ?? NullLogger.Instance;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
|
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
|
{
|
|
var session = _session.Session
|
|
?? throw new InvalidOperationException(
|
|
"GalaxyMxSession is not connected. Call ConnectAsync before subscribing.");
|
|
var serverHandle = _session.ServerHandle;
|
|
|
|
// The gw's SubscribeBulk RPC doesn't carry a per-call interval — buffered cadence
|
|
// is session-level, set via SetBufferedUpdateInterval. Apply it before the
|
|
// SubscribeBulk so the very first events on the new handles publish at the
|
|
// requested cadence. Skip when the last-applied value already matches.
|
|
if (bufferedUpdateIntervalMs > 0)
|
|
{
|
|
await EnsureSessionIntervalAsync(session, serverHandle, bufferedUpdateIntervalMs, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
return await session.SubscribeBulkAsync(serverHandle, fullReferences, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply the gateway's session-level <c>SetBufferedUpdateInterval</c> command. The
|
|
/// gw's contract is "for this server handle, every buffered subscription publishes
|
|
/// at this cadence" — there's no per-handle granularity, so we cache the last
|
|
/// applied value and skip redundant calls.
|
|
/// </summary>
|
|
private async Task EnsureSessionIntervalAsync(
|
|
ZB.MOM.WW.MxGateway.Client.MxGatewaySession session, int serverHandle, int intervalMs, CancellationToken cancellationToken)
|
|
{
|
|
lock (_intervalLock)
|
|
{
|
|
if (_lastAppliedIntervalMs == intervalMs) return;
|
|
}
|
|
|
|
var reply = await session.InvokeAsync(
|
|
new MxCommandRequest
|
|
{
|
|
SessionId = session.SessionId,
|
|
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
|
Command = new MxCommand
|
|
{
|
|
Kind = MxCommandKind.SetBufferedUpdateInterval,
|
|
SetBufferedUpdateInterval = new SetBufferedUpdateIntervalCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
UpdateIntervalMilliseconds = intervalMs,
|
|
},
|
|
},
|
|
},
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
var code = reply.ProtocolStatus?.Code;
|
|
|
|
// MxaccessFailure means the COM-side SetBufferedUpdateInterval did NOT apply, so
|
|
// we must NOT cache the requested value — caching it would suppress the retry on
|
|
// the next subscribe at this interval. Only Ok records the value as applied.
|
|
if (!ClassifyIntervalReply(code))
|
|
{
|
|
// Don't throw on a soft failure — the SubscribeBulk will still succeed at the
|
|
// gw's default cadence, which is functional just not the requested cadence.
|
|
// The trace span (PR 6.1) plus this warning gives ops the signal, and leaving
|
|
// _lastAppliedIntervalMs unchanged lets the next subscribe re-attempt the set.
|
|
if (code == ProtocolStatusCode.MxaccessFailure)
|
|
{
|
|
_logger.LogWarning(
|
|
"Galaxy SetBufferedUpdateInterval({IntervalMs}ms) soft-failed (MxaccessFailure); " +
|
|
"buffered subscriptions on server handle {ServerHandle} will publish at the gateway's " +
|
|
"default cadence. The requested interval was not cached, so a later subscribe will retry it.",
|
|
intervalMs, serverHandle);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning(
|
|
"Galaxy SetBufferedUpdateInterval({IntervalMs}ms) returned an unexpected protocol status " +
|
|
"{Code} on server handle {ServerHandle}; treating it as not-applied and leaving the " +
|
|
"requested interval uncached so a later subscribe retries it.",
|
|
intervalMs, code, serverHandle);
|
|
}
|
|
return;
|
|
}
|
|
|
|
lock (_intervalLock)
|
|
{
|
|
_lastAppliedIntervalMs = intervalMs;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Classifies a <c>SetBufferedUpdateInterval</c> reply: returns <c>true</c> only when
|
|
/// the requested interval was actually applied and may be cached as last-applied.
|
|
/// This is <see cref="ProtocolStatusCode.Ok"/> alone — <see cref="ProtocolStatusCode.MxaccessFailure"/>
|
|
/// means the COM-side set did NOT take effect (so caching it would prevent a retry),
|
|
/// and a <c>null</c> or any other unexpected code is treated as not-applied.
|
|
/// </summary>
|
|
/// <param name="code">The protocol status code from the gateway reply, or <c>null</c> when absent.</param>
|
|
/// <returns><c>true</c> if the interval should be recorded as applied; otherwise <c>false</c>.</returns>
|
|
internal static bool ClassifyIntervalReply(ProtocolStatusCode? code) => code == ProtocolStatusCode.Ok;
|
|
|
|
/// <inheritdoc />
|
|
public async Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
|
{
|
|
if (itemHandles.Count == 0) return;
|
|
|
|
var session = _session.Session
|
|
?? throw new InvalidOperationException(
|
|
"GalaxyMxSession is not connected. UnsubscribeBulk called after disconnect.");
|
|
var serverHandle = _session.ServerHandle;
|
|
|
|
await session.UnsubscribeBulkAsync(serverHandle, itemHandles, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
|
{
|
|
var session = _session.Session
|
|
?? throw new InvalidOperationException(
|
|
"GalaxyMxSession is not connected. StreamEventsAsync called before ConnectAsync.");
|
|
return session.StreamEventsAsync(afterWorkerSequence: 0, cancellationToken);
|
|
}
|
|
}
|