using System;
using ArchestrA.MxAccess;
using Proto = ZB.MOM.WW.MxGateway.Contracts.Proto;
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
/// Sink for MXAccess COM events that converts them to protobuf format.
public sealed class MxAccessBaseEventSink : IMxAccessEventSink
{
private readonly MxAccessEventMapper eventMapper;
private readonly MxAccessEventQueue eventQueue;
private readonly MxAccessValueCache valueCache;
private LMXProxyServerClass? server;
private string sessionId = string.Empty;
/// Initializes a new instance of the MxAccessBaseEventSink class with a default queue.
public MxAccessBaseEventSink()
: this(new MxAccessEventQueue())
{
}
/// Initializes a new instance of the MxAccessBaseEventSink class with a provided queue.
/// Queue for buffering converted MXAccess events.
public MxAccessBaseEventSink(MxAccessEventQueue eventQueue)
: this(eventQueue, new MxAccessEventMapper(), new MxAccessValueCache())
{
}
/// Initializes a new instance of the MxAccessBaseEventSink class with provided queue and mapper.
/// Queue for buffering converted MXAccess events.
/// Converter for MXAccess events to protobuf format.
public MxAccessBaseEventSink(
MxAccessEventQueue eventQueue,
MxAccessEventMapper eventMapper)
: this(eventQueue, eventMapper, new MxAccessValueCache())
{
}
///
/// Initializes a new instance of the MxAccessBaseEventSink class with
/// provided queue, mapper, and a shared value cache. The cache is
/// populated from every successful OnDataChange dispatch so the
/// worker's ReadBulk executor can satisfy a "current value" request
/// from an already-advised tag without touching the subscription.
///
/// Queue for buffering converted MXAccess events.
/// Converter for MXAccess events to protobuf format.
/// Per-session last-value cache shared with the MxAccessSession.
public MxAccessBaseEventSink(
MxAccessEventQueue eventQueue,
MxAccessEventMapper eventMapper,
MxAccessValueCache valueCache)
{
this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue));
this.eventMapper = eventMapper ?? throw new ArgumentNullException(nameof(eventMapper));
this.valueCache = valueCache ?? throw new ArgumentNullException(nameof(valueCache));
}
///
/// The last-value cache populated by this sink. Exposed so the
/// MxAccessSession can share the same instance for ReadBulk lookups.
///
public MxAccessValueCache ValueCache => valueCache;
///
public void Attach(
object mxAccessComObject,
string sessionId)
{
this.sessionId = sessionId ?? string.Empty;
server = (LMXProxyServerClass)mxAccessComObject;
server.OnDataChange += OnDataChange;
server.OnWriteComplete += OnWriteComplete;
server.OperationComplete += OperationComplete;
server.OnBufferedDataChange += OnBufferedDataChange;
}
///
public void Detach()
{
if (server is null)
{
return;
}
server.OnDataChange -= OnDataChange;
server.OnWriteComplete -= OnWriteComplete;
server.OperationComplete -= OperationComplete;
server.OnBufferedDataChange -= OnBufferedDataChange;
server = null;
sessionId = string.Empty;
}
///
/// Handles the MXAccess OnDataChange COM event: converts the
/// event arguments to a protobuf and enqueues
/// it. Subscribed to the COM object's event in .
/// Exposed internal so unit tests can drive the integrated
/// sink → mapper → queue path without a live MXAccess COM event source.
///
/// The MXAccess server handle.
/// The MXAccess item handle.
/// The new item value.
/// The item quality.
/// The item timestamp.
/// Status array from MXAccess event.
internal void OnDataChange(
int hLMXServerHandle,
int phItemHandle,
object pvItemValue,
int pwItemQuality,
object pftItemTimeStamp,
ref MXSTATUS_PROXY[] pVars)
{
MXSTATUS_PROXY[] statuses = pVars;
// Build the protobuf event once, enqueue it for the outbound stream, and
// also publish it into the per-session value cache so ReadBulk can serve
// it as a "current value" without re-advising. The cache update is the
// ONLY new side effect — fail-fast on conversion still drops the event
// through the same EnqueueEvent path as before.
EnqueueEvent(
() => eventMapper.CreateOnDataChange(
sessionId,
hLMXServerHandle,
phItemHandle,
pvItemValue,
pwItemQuality,
pftItemTimeStamp,
statuses),
mxEvent => valueCache.Set(hLMXServerHandle, phItemHandle, mxEvent));
}
///
/// Handles the MXAccess OnWriteComplete COM event. Exposed
/// internal as a unit-test seam; see .
///
/// The MXAccess server handle.
/// The MXAccess item handle.
/// Status array from MXAccess event.
internal void OnWriteComplete(
int hLMXServerHandle,
int phItemHandle,
ref MXSTATUS_PROXY[] pVars)
{
MXSTATUS_PROXY[] statuses = pVars;
EnqueueEvent(() => eventMapper.CreateOnWriteComplete(
sessionId,
hLMXServerHandle,
phItemHandle,
statuses));
}
///
/// Handles the MXAccess OperationComplete COM event. Exposed
/// internal as a unit-test seam; see .
///
/// The MXAccess server handle.
/// The MXAccess item handle.
/// Status array from MXAccess event.
internal void OperationComplete(
int hLMXServerHandle,
int phItemHandle,
ref MXSTATUS_PROXY[] pVars)
{
MXSTATUS_PROXY[] statuses = pVars;
EnqueueEvent(() => eventMapper.CreateOperationComplete(
sessionId,
hLMXServerHandle,
phItemHandle,
statuses));
}
///
/// Handles the MXAccess OnBufferedDataChange COM event. Exposed
/// internal as a unit-test seam; see .
///
/// The MXAccess server handle.
/// The MXAccess item handle.
/// The data type of the buffered value.
/// The new item value.
/// The item quality.
/// The item timestamp.
/// Status array from MXAccess event.
internal void OnBufferedDataChange(
int hLMXServerHandle,
int phItemHandle,
MxDataType dtDataType,
object pvItemValue,
object pwItemQuality,
object pftItemTimeStamp,
ref MXSTATUS_PROXY[] pVars)
{
MXSTATUS_PROXY[] statuses = pVars;
EnqueueEvent(() => eventMapper.CreateOnBufferedDataChange(
sessionId,
hLMXServerHandle,
phItemHandle,
(int)dtDataType,
pvItemValue,
pwItemQuality,
pftItemTimeStamp,
statuses));
}
private void EnqueueEvent(Func createEvent)
{
EnqueueEvent(createEvent, postPublish: null);
}
private void EnqueueEvent(Func createEvent, Action? postPublish)
{
Proto.MxEvent mxEvent;
try
{
mxEvent = createEvent();
}
catch (Exception exception)
{
eventQueue.RecordFault(CreateEventConversionFault(exception));
return;
}
try
{
eventQueue.Enqueue(mxEvent);
}
catch (Exception exception)
{
// Two distinct failures land here, both intentionally fail-fast:
// - A conversion failure from createEvent() — recorded here as an
// MxaccessEventConversionFailed fault.
// - An MxAccessEventQueueOverflowException from Enqueue when the
// queue is at capacity. Per the fail-fast backpressure design
// (docs/DesignDecisions.md) the event is dropped and the queue
// has *already* self-recorded a QueueOverflow fault. Because
// MxAccessEventQueue.RecordFault keeps only the first fault,
// this catch's RecordFault call is then a deliberate near
// no-op rather than a second, conflicting fault.
eventQueue.RecordFault(CreateEventConversionFault(exception));
return;
}
// Only publish to caches/observers after the event has cleared the
// queue, so a queue overflow does not leak a "fresher" cached value
// than what was actually shipped to the gateway.
if (postPublish is not null)
{
try
{
postPublish(mxEvent);
}
catch (Exception exception)
{
eventQueue.RecordFault(CreateEventConversionFault(exception));
}
}
}
private Proto.WorkerFault CreateEventConversionFault(Exception exception)
{
return new Proto.WorkerFault
{
Category = Proto.WorkerFaultCategory.MxaccessEventConversionFailed,
ExceptionType = exception.GetType().FullName ?? string.Empty,
DiagnosticMessage = $"{exception.GetType().FullName}: HRESULT 0x{unchecked((uint)exception.HResult):X8}",
ProtocolStatus = new Proto.ProtocolStatus
{
Code = Proto.ProtocolStatusCode.MxaccessFailure,
Message = "MXAccess event conversion failed.",
},
};
}
}