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,116 @@
|
||||
using MxGateway.Client;
|
||||
using MxGateway.Contracts.Proto;
|
||||
// Use the generated nested status enum for the SetBufferedUpdateInterval reply check.
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Production <see cref="IGalaxySubscriber"/> over a connected
|
||||
/// <see cref="GalaxyMxSession"/>. Forwards SubscribeBulk / UnsubscribeBulk to the
|
||||
/// gateway and streams MxEvents via the gw's bidirectional events RPC.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// PR 6.3 wired the per-call <c>buffered_update_interval_ms</c> through
|
||||
/// <see cref="SubscribeBulkAsync"/>. The gw's contract is session-level
|
||||
/// (<c>SetBufferedUpdateInterval</c> applies to all buffered subscriptions on the
|
||||
/// server handle), so we cache the last-applied value and skip redundant calls.
|
||||
/// </remarks>
|
||||
public sealed class GatewayGalaxySubscriber : IGalaxySubscriber
|
||||
{
|
||||
private readonly GalaxyMxSession _session;
|
||||
private readonly Lock _intervalLock = new();
|
||||
private int _lastAppliedIntervalMs = -1; // -1 = never applied; 0 = explicit "use gw default"
|
||||
|
||||
public GatewayGalaxySubscriber(GalaxyMxSession session)
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the gateway's session-level <c>SetBufferedUpdateInterval</c> 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.
|
||||
/// </summary>
|
||||
private async Task EnsureSessionIntervalAsync(
|
||||
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);
|
||||
|
||||
if (reply.ProtocolStatus?.Code is not (ProtocolStatusCode.Ok or ProtocolStatusCode.MxaccessFailure))
|
||||
{
|
||||
// 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 the warning here gives ops the signal.
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_intervalLock)
|
||||
{
|
||||
_lastAppliedIntervalMs = intervalMs;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UnsubscribeBulkAsync(IReadOnlyList<int> 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<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var session = _session.Session
|
||||
?? throw new InvalidOperationException(
|
||||
"GalaxyMxSession is not connected. StreamEventsAsync called before ConnectAsync.");
|
||||
return session.StreamEventsAsync(afterWorkerSequence: 0, cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user