using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.LoadHarness;
///
/// In-process stand-in for an OPC UA / MxGateway server, registered on the REAL
/// DataConnectionFactory under the protocol so the
/// whole Data Connection Layer above it — DataConnectionManagerActor,
/// DataConnectionActor, its _instancesByTag fan-out and the
/// TagValueUpdate hand-off to Instance Actors — runs unmodified.
///
///
/// It implements because the batch path is
/// the one a real site takes at this scale; the per-tag fallback would make
/// subscribe setup, not steady-state throughput, the thing being measured.
///
///
/// Why faking here does not invalidate the measurement. Everything this class
/// replaces is on the far side of the process boundary: socket I/O, the OPC UA SDK's
/// own session/subscription machinery, and the device. The system under test — the
/// actor hierarchy, the site stream, store-and-forward, health, and the streaming
/// relay — begins at the invocation, which is
/// exactly where the real adapter hands off. Driving 375,000 genuine monitored items
/// would measure the OPC UA stack, not ScadaBridge.
///
///
public sealed class SimulatedDataConnection : IDataConnection, IBatchSubscribableConnection
{
/// Protocol discriminator this adapter registers under on the factory.
public const string ProtocolName = "LoadSim";
private SubscriptionCallback? _callback;
private int _subscriptionCounter;
///
/// The data connection name this adapter was created for, taken from the connection
/// details. The DCL factory creates adapters as its manager actor processes the
/// CreateConnectionCommands, so creation order is not connection order — the driver
/// resolves an adapter by name rather than by index.
///
public string ConnectionName { get; private set; } = string.Empty;
/// Key under which the connection name travels in the connection details.
public const string ConnectionNameKey = "connectionName";
///
public ConnectionHealth Status { get; private set; } = ConnectionHealth.Disconnected;
///
public event Action? Disconnected;
///
/// The callback captured at subscribe time. The tag driver invokes this to inject
/// a value change, mirroring what the OPC UA SDK's notification thread does.
/// Null until the site's Instance Actors have subscribed.
///
public SubscriptionCallback? ValueCallback => _callback;
/// Number of tag paths this connection has accepted subscriptions for.
public int SubscribedTagCount => _subscriptionCounter;
///
public Task ConnectAsync(IDictionary connectionDetails, CancellationToken cancellationToken = default)
{
if (connectionDetails.TryGetValue(ConnectionNameKey, out var name))
ConnectionName = name;
Status = ConnectionHealth.Connected;
return Task.CompletedTask;
}
///
public Task DisconnectAsync(CancellationToken cancellationToken = default)
{
Status = ConnectionHealth.Disconnected;
Disconnected?.Invoke();
return Task.CompletedTask;
}
///
public Task SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default)
{
_callback = callback;
return Task.FromResult($"sub-{Interlocked.Increment(ref _subscriptionCounter)}");
}
///
public Task> SubscribeBatchAsync(
IReadOnlyList tagPaths,
SubscriptionCallback callback,
CancellationToken cancellationToken = default)
{
_callback = callback;
var results = new List(tagPaths.Count);
foreach (var path in tagPaths)
{
results.Add(new TagSubscribeResult(
path, true, $"sub-{Interlocked.Increment(ref _subscriptionCounter)}", null));
}
return Task.FromResult>(results);
}
///
public Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
///
public Task UnsubscribeBatchAsync(IReadOnlyList subscriptionIds, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
///
public Task ReadAsync(string tagPath, CancellationToken cancellationToken = default)
=> Task.FromResult(new ReadResult(true, new TagValue(0d, QualityCode.Good, DateTimeOffset.UtcNow), null));
///
public Task> ReadBatchAsync(
IEnumerable tagPaths, CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var results = new Dictionary();
foreach (var path in tagPaths)
results[path] = new ReadResult(true, new TagValue(0d, QualityCode.Good, now), null);
return Task.FromResult>(results);
}
///
public Task WriteAsync(string tagPath, object? value, CancellationToken cancellationToken = default)
=> Task.FromResult(new WriteResult(true, null));
///
public Task> WriteBatchAsync(
IDictionary values, CancellationToken cancellationToken = default)
{
var results = new Dictionary();
foreach (var key in values.Keys)
results[key] = new WriteResult(true, null);
return Task.FromResult>(results);
}
///
public Task WriteBatchAndWaitAsync(
IDictionary values, string flagPath, object? flagValue,
string responsePath, object? responseValue, TimeSpan timeout,
CancellationToken cancellationToken = default)
=> Task.FromResult(true);
///
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}