using System; using System.Collections.Generic; using System.Threading; using Google.Protobuf.Collections; using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Worker.MxAccess; /// /// Per-session cache of the most recent OnDataChange payload for /// each (server handle, item handle) pair. Written by the MXAccess event /// sink as new OnDataChange callbacks arrive; read by the ReadBulk command /// executor so it can satisfy a "current value" request from a tag that is /// already advised without modifying the existing subscription. /// /// /// Both writers and readers run on the worker's STA thread (COM dispatches /// events on the apartment thread; commands also execute on the STA), so /// no internal locking is required. The class is still nominally /// thread-safe via a single sync root in case tests drive it from a /// non-STA thread. /// public sealed class MxAccessValueCache { private readonly Dictionary entries = new(); private readonly object syncRoot = new(); /// Records a fresh OnDataChange payload for the given handle pair. /// MXAccess server handle. /// MXAccess item handle. /// The protobuf MxEvent created by the event mapper. public void Set( int serverHandle, int itemHandle, MxEvent mxEvent) { if (mxEvent is null) { throw new ArgumentNullException(nameof(mxEvent)); } long key = CreateItemKey(serverHandle, itemHandle); lock (syncRoot) { ulong nextVersion = entries.TryGetValue(key, out CachedValue existing) ? existing.Version + 1 : 1UL; entries[key] = new CachedValue( nextVersion, mxEvent.Value, mxEvent.Quality, mxEvent.SourceTimestamp, mxEvent.Statuses); } } /// Tries to read the most recent cached value for the handle pair. public bool TryGet( int serverHandle, int itemHandle, out CachedValue value) { long key = CreateItemKey(serverHandle, itemHandle); lock (syncRoot) { return entries.TryGetValue(key, out value); } } /// /// Removes the cache slot for a handle pair. The session calls this /// when an item is unregistered so stale values are not served to a /// subsequent ReadBulk after a tag is removed and re-added. /// public void Remove( int serverHandle, int itemHandle) { long key = CreateItemKey(serverHandle, itemHandle); lock (syncRoot) { entries.Remove(key); } } /// /// Waits until the cache entry's version exceeds /// or the deadline elapses, calling on every poll /// iteration so the worker's STA can dispatch the inbound MXAccess message. /// /// MXAccess server handle. /// MXAccess item handle. /// Version snapshot captured before the wait. /// Absolute UTC deadline. /// Action that pumps any pending Windows messages. /// How long to sleep between pump cycles. Default 5 ms. public bool TryWaitForUpdate( int serverHandle, int itemHandle, ulong sinceVersion, DateTime deadlineUtc, Action pumpStep, out CachedValue value, int pollIntervalMs = 5) { if (pumpStep is null) { throw new ArgumentNullException(nameof(pumpStep)); } while (true) { pumpStep(); if (TryGet(serverHandle, itemHandle, out value) && value.Version > sinceVersion) { return true; } if (DateTime.UtcNow >= deadlineUtc) { return false; } Thread.Sleep(pollIntervalMs); } } /// Returns the current version for a handle pair, or 0 if no entry exists. public ulong CurrentVersion( int serverHandle, int itemHandle) { return TryGet(serverHandle, itemHandle, out CachedValue existing) ? existing.Version : 0UL; } private static long CreateItemKey( int serverHandle, int itemHandle) { return ((long)serverHandle << 32) | (uint)itemHandle; } /// /// Snapshot of the most recent OnDataChange payload for a handle pair. /// increments by one on every /// call so the bulk read executor can detect "a new value arrived /// since I started waiting". /// /// /// Plain readonly struct (not a record) so this compiles under the /// worker's net48 target, which lacks IsExternalInit. /// public readonly struct CachedValue { /// Initializes a new cached value snapshot. public CachedValue( ulong version, MxValue value, int quality, Timestamp sourceTimestamp, RepeatedField statuses) { Version = version; Value = value; Quality = quality; SourceTimestamp = sourceTimestamp; Statuses = statuses; } /// Monotonic per-handle version counter. public ulong Version { get; } /// The cached MxValue payload. public MxValue Value { get; } /// Quality code from the OnDataChange event. public int Quality { get; } /// Source timestamp from the OnDataChange event. public Timestamp SourceTimestamp { get; } /// MxStatusProxy entries from the OnDataChange event. public RepeatedField Statuses { get; } } }