using System; using System.Collections.Generic; using System.Threading; using Google.Protobuf.Collections; using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Worker.MxAccess; /// /// Per-session cache of the most recent OnWriteComplete status rows /// for each (server handle, item handle) pair. Written by the MXAccess /// event sink as completion callbacks arrive; read by the write command /// executor so a WriteSecured/WriteSecured2 reply can carry the correlated /// MXAccess outcome instead of proving command acceptance only. /// /// /// Same threading posture as : 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. A single sync root keeps it nominally thread-safe /// for tests that drive it from a non-STA thread. /// public sealed class MxAccessWriteCompletionCache { private readonly Dictionary entries = new(); private readonly object syncRoot = new(); /// Records the status rows of a fresh OnWriteComplete callback for the given handle pair. /// MXAccess server handle. /// MXAccess item handle. /// Status rows from the mapped OnWriteComplete event; cloned before storing. public void Record( int serverHandle, int itemHandle, RepeatedField statuses) { if (statuses is null) { throw new ArgumentNullException(nameof(statuses)); } lock (syncRoot) { long key = CreateItemKey(serverHandle, itemHandle); ulong version = entries.TryGetValue(key, out CompletionEntry existing) ? existing.Version + 1 : 1UL; entries[key] = new CompletionEntry(version, statuses.Clone()); } } /// Returns the current completion version for a handle pair, or 0 if none was recorded. /// MXAccess server handle. /// MXAccess item handle. /// The current completion version, or 0 if no completion was recorded. public ulong CurrentVersion( int serverHandle, int itemHandle) { lock (syncRoot) { return entries.TryGetValue(CreateItemKey(serverHandle, itemHandle), out CompletionEntry existing) ? existing.Version : 0UL; } } /// /// Polls for a completion newer than until it /// arrives or the deadline elapses, calling on every /// poll iteration so the worker's STA can dispatch the inbound MXAccess /// OnWriteComplete message. Same loop shape as /// . /// /// MXAccess server handle. /// MXAccess item handle. /// Version snapshot captured before the write COM call. /// Absolute UTC deadline. /// Action that pumps any pending Windows messages. /// The recorded status rows if a completion arrived before the deadline; empty otherwise. /// How long to sleep between pump cycles. Default 5 ms. /// if a completion newer than arrived before the deadline; otherwise . public bool TryWaitForCompletion( int serverHandle, int itemHandle, ulong sinceVersion, DateTime deadlineUtc, Action pumpStep, out RepeatedField statuses, int pollIntervalMs = 5) { if (pumpStep is null) { throw new ArgumentNullException(nameof(pumpStep)); } while (true) { pumpStep(); lock (syncRoot) { if (entries.TryGetValue(CreateItemKey(serverHandle, itemHandle), out CompletionEntry entry) && entry.Version > sinceVersion) { statuses = entry.Statuses; return true; } } if (DateTime.UtcNow >= deadlineUtc) { statuses = new RepeatedField(); return false; } Thread.Sleep(pollIntervalMs); } } private static long CreateItemKey( int serverHandle, int itemHandle) { return ((long)serverHandle << 32) | (uint)itemHandle; } /// /// Snapshot of the most recent OnWriteComplete status rows for a handle /// pair. increments by one on every /// call so the write executor can detect "a new /// completion arrived since I captured my baseline". /// /// /// Plain readonly struct (not a record) so this compiles under the /// worker's net48 target, which lacks IsExternalInit. /// private readonly struct CompletionEntry { public CompletionEntry( ulong version, RepeatedField statuses) { Version = version; Statuses = statuses; } public ulong Version { get; } public RepeatedField Statuses { get; } } }