chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Bookkeeping for live subscriptions. Maps each driver-issued <c>SubscriptionId</c> to the
|
||||
/// set of (full-reference, gw item-handle) pairs the gateway returned, and maintains the
|
||||
/// reverse map (item-handle → set of driver subscriptions) so the
|
||||
/// <see cref="EventPump"/> can fan out a single OnDataChange event to every driver
|
||||
/// subscription that includes the changed tag.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A tag may legitimately appear in multiple driver subscriptions (separate clients or
|
||||
/// OPC UA monitored items observing the same Galaxy attribute). Using a single shared
|
||||
/// gw subscription per session and fanning out on the driver side keeps the gateway's
|
||||
/// work bounded; the reverse map is the fan-out index.
|
||||
/// </remarks>
|
||||
internal sealed class SubscriptionRegistry
|
||||
{
|
||||
private readonly ConcurrentDictionary<long, SubscriptionEntry> _bySubscriptionId = new();
|
||||
private readonly ConcurrentDictionary<int, ConcurrentBag<long>> _subscribersByItemHandle = new();
|
||||
private long _nextSubscriptionId;
|
||||
|
||||
public int TrackedSubscriptionCount => _bySubscriptionId.Count;
|
||||
public int TrackedItemHandleCount => _subscribersByItemHandle.Count;
|
||||
|
||||
/// <summary>Allocate a fresh subscription id. Monotonic; unique per registry lifetime.</summary>
|
||||
public long NextSubscriptionId() => Interlocked.Increment(ref _nextSubscriptionId);
|
||||
|
||||
/// <summary>
|
||||
/// Register a subscription and the per-tag item handles the gateway returned for it.
|
||||
/// Failed tags (item handle = 0 or negative) are stored anyway so unsubscribe can
|
||||
/// emit per-tag UnsubscribeBulk for the ones that did succeed.
|
||||
/// </summary>
|
||||
public void Register(long subscriptionId, IReadOnlyList<TagBinding> bindings)
|
||||
{
|
||||
var entry = new SubscriptionEntry(subscriptionId, bindings);
|
||||
_bySubscriptionId[subscriptionId] = entry;
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (binding.ItemHandle <= 0) continue; // failed gw subscribe — no events expected
|
||||
_subscribersByItemHandle.AddOrUpdate(
|
||||
binding.ItemHandle,
|
||||
_ => [subscriptionId],
|
||||
(_, bag) => { bag.Add(subscriptionId); return bag; });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a subscription. Returns the bindings the caller should pass to
|
||||
/// <c>UnsubscribeBulkAsync</c>; null when the id was never registered.
|
||||
/// </summary>
|
||||
public IReadOnlyList<TagBinding>? Remove(long subscriptionId)
|
||||
{
|
||||
if (!_bySubscriptionId.TryRemove(subscriptionId, out var entry)) return null;
|
||||
|
||||
foreach (var binding in entry.Bindings)
|
||||
{
|
||||
if (binding.ItemHandle <= 0) continue;
|
||||
if (!_subscribersByItemHandle.TryGetValue(binding.ItemHandle, out var bag)) continue;
|
||||
|
||||
// Filter the bag to drop this subscription id. ConcurrentBag has no Remove —
|
||||
// rebuild it from the remaining entries. The contention here is bounded by
|
||||
// the number of tags in the dropped subscription.
|
||||
var remaining = new ConcurrentBag<long>(bag.Where(id => id != subscriptionId));
|
||||
if (remaining.IsEmpty) _subscribersByItemHandle.TryRemove(binding.ItemHandle, out _);
|
||||
else _subscribersByItemHandle[binding.ItemHandle] = remaining;
|
||||
}
|
||||
|
||||
return entry.Bindings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Look up the (subscription id, full reference) pairs that should receive an
|
||||
/// OnDataChange for the given gw item handle. Returns empty when nobody subscribes.
|
||||
/// </summary>
|
||||
public IReadOnlyList<(long SubscriptionId, string FullReference)> ResolveSubscribers(int itemHandle)
|
||||
{
|
||||
if (!_subscribersByItemHandle.TryGetValue(itemHandle, out var bag)) return [];
|
||||
|
||||
// Each subscription may include the tag once. Walk every active subscription that
|
||||
// claims this handle and pull the full ref from its binding list.
|
||||
var result = new List<(long, string)>();
|
||||
foreach (var subId in bag.Distinct())
|
||||
{
|
||||
if (!_bySubscriptionId.TryGetValue(subId, out var entry)) continue;
|
||||
var binding = entry.Bindings.FirstOrDefault(b => b.ItemHandle == itemHandle);
|
||||
if (binding is { FullReference: { } fullRef })
|
||||
result.Add((subId, fullRef));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Snapshot every active binding for diagnostic output.</summary>
|
||||
public IReadOnlyList<TagBinding> SnapshotAllBindings() =>
|
||||
[.. _bySubscriptionId.Values.SelectMany(entry => entry.Bindings)];
|
||||
|
||||
private sealed record SubscriptionEntry(long SubscriptionId, IReadOnlyList<TagBinding> Bindings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One (full reference, gw item handle) pair returned by SubscribeBulk. Item handle is
|
||||
/// zero or negative when the gateway rejected this individual tag (bad name, duplicate);
|
||||
/// the registry keeps the binding so the caller can surface a per-tag failure status.
|
||||
/// </summary>
|
||||
internal sealed record TagBinding(string FullReference, int ItemHandle);
|
||||
Reference in New Issue
Block a user