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;
///
/// Production over a connected
/// . Forwards SubscribeBulk / UnsubscribeBulk to the
/// gateway and streams MxEvents via the gw's bidirectional events RPC.
///
///
/// PR 6.3 wired the per-call buffered_update_interval_ms through
/// . The gw's contract is session-level
/// (SetBufferedUpdateInterval applies to all buffered subscriptions on the
/// server handle), so we cache the last-applied value and skip redundant calls.
///
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"
/// Initializes a new instance of GatewayGalaxySubscriber.
/// The Galaxy MX session to use for subscription operations.
///
/// Optional logger; surfaces a warning when SetBufferedUpdateInterval
/// soft-fails so the cadence-not-applied condition isn't silent. Null is allowed
/// for unit-test construction and falls back to .
///
public GatewayGalaxySubscriber(GalaxyMxSession session, ILogger? logger = null)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_logger = logger ?? NullLogger.Instance;
}
///
public async Task> SubscribeBulkAsync(
IReadOnlyList 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);
}
///
/// Apply the gateway's session-level SetBufferedUpdateInterval 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.
///
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;
}
}
///
/// Classifies a SetBufferedUpdateInterval reply: returns true only when
/// the requested interval was actually applied and may be cached as last-applied.
/// This is alone —
/// means the COM-side set did NOT take effect (so caching it would prevent a retry),
/// and a null or any other unexpected code is treated as not-applied.
///
/// The protocol status code from the gateway reply, or null when absent.
/// true if the interval should be recorded as applied; otherwise false.
internal static bool ClassifyIntervalReply(ProtocolStatusCode? code) => code == ProtocolStatusCode.Ok;
///
public async Task UnsubscribeBulkAsync(IReadOnlyList 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);
}
///
public IAsyncEnumerable StreamEventsAsync(CancellationToken cancellationToken)
{
var session = _session.Session
?? throw new InvalidOperationException(
"GalaxyMxSession is not connected. StreamEventsAsync called before ConnectAsync.");
return session.StreamEventsAsync(afterWorkerSequence: 0, cancellationToken);
}
}