feat(opcuaserver): node manager batched WriteValues under one lock
This commit is contained in:
@@ -513,25 +513,60 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
var ns = NamespaceIndexForRealm(realm);
|
||||
var key = MapKey(realm, nodeId);
|
||||
lock (Lock)
|
||||
{
|
||||
// CreateVariable mutates the SDK address space (_root.AddChild + AddPredefinedNode),
|
||||
// so it MUST run under Lock — the SDK's subscription/ConditionRefresh threads take it too.
|
||||
if (!_variables.TryGetValue(key, out var variable))
|
||||
{
|
||||
variable = CreateVariable(nodeId, ns);
|
||||
_variables[key] = variable;
|
||||
}
|
||||
WriteValueLocked(nodeId, ns, key, value, statusCode, sourceTimestampUtc);
|
||||
}
|
||||
|
||||
variable.Value = value;
|
||||
// #519 — the caller's status code reaches the SDK verbatim, sub-code included. This used to
|
||||
// re-expand a 3-state enum, so every Bad became a bare StatusCodes.Bad (0x80000000) and a
|
||||
// driver's BadNoCommunication was indistinguishable from BadTypeMismatch at the client.
|
||||
variable.StatusCode = new StatusCode(statusCode);
|
||||
variable.Timestamp = sourceTimestampUtc;
|
||||
variable.ClearChangeMasks(SystemContext, includeChildren: false);
|
||||
/// <summary>Batched <see cref="WriteValue"/>: one <c>Lock</c> acquisition for the whole batch instead of
|
||||
/// one per value — the publish path's per-value lock take was the steady-state throughput ceiling.
|
||||
/// Per-item semantics are identical to <see cref="WriteValue"/> (create-on-first-write, #519 verbatim
|
||||
/// status codes, ClearChangeMasks per node).</summary>
|
||||
/// <param name="values">The value writes to apply, in order.</param>
|
||||
public void WriteValues(IReadOnlyList<ValueWrite> values)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
if (values.Count == 0) return;
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var v = values[i];
|
||||
WriteValueLocked(v.NodeId, NamespaceIndexForRealm(v.Realm), MapKey(v.Realm, v.NodeId),
|
||||
v.Value, v.StatusCode, v.SourceTimestampUtc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The body of a single value write. <b>Caller MUST hold <c>Lock</c></b> — this is factored out
|
||||
/// solely so <see cref="WriteValues"/> can apply a whole batch under one acquisition; it is byte-identical
|
||||
/// to what <see cref="WriteValue"/> has always done inside its own <c>lock</c>.</summary>
|
||||
/// <param name="nodeId">The bare <c>s=</c> identifier of the variable.</param>
|
||||
/// <param name="ns">The namespace index resolved from the write's realm.</param>
|
||||
/// <param name="key">The realm-qualified map key for <paramref name="nodeId"/>.</param>
|
||||
/// <param name="value">The new value to write.</param>
|
||||
/// <param name="statusCode">The full OPC UA status code, sub-code included.</param>
|
||||
/// <param name="sourceTimestampUtc">The timestamp of the value in UTC.</param>
|
||||
private void WriteValueLocked(string nodeId, ushort ns, string key, object? value, uint statusCode, DateTime sourceTimestampUtc)
|
||||
{
|
||||
// CreateVariable mutates the SDK address space (_root.AddChild + AddPredefinedNode),
|
||||
// so it MUST run under Lock — the SDK's subscription/ConditionRefresh threads take it too.
|
||||
if (!_variables.TryGetValue(key, out var variable))
|
||||
{
|
||||
variable = CreateVariable(nodeId, ns);
|
||||
_variables[key] = variable;
|
||||
}
|
||||
|
||||
variable.Value = value;
|
||||
// #519 — the caller's status code reaches the SDK verbatim, sub-code included. This used to
|
||||
// re-expand a 3-state enum, so every Bad became a bare StatusCodes.Bad (0x80000000) and a
|
||||
// driver's BadNoCommunication was indistinguishable from BadTypeMismatch at the client.
|
||||
variable.StatusCode = new StatusCode(statusCode);
|
||||
variable.Timestamp = sourceTimestampUtc;
|
||||
// Stays INSIDE the lock, per node: this is how the SDK notifies monitored items of the change.
|
||||
variable.ClearChangeMasks(SystemContext, includeChildren: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a full Part 9 alarm-condition write. When a real <see cref="AlarmConditionState"/> has
|
||||
/// been materialised for <paramref name="alarmNodeId"/> (via <see cref="MaterialiseAlarmCondition"/>),
|
||||
|
||||
@@ -24,6 +24,13 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddre
|
||||
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm)
|
||||
=> _nodeManager.WriteValue(nodeId, value, statusCode, sourceTimestampUtc, realm);
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Overrides the interface's default implementation on purpose: that default loops
|
||||
/// <see cref="WriteValue"/>, which would re-take the node manager's SDK-wide <c>Lock</c> once per value.
|
||||
/// Forwarding to the manager's own batched entry point takes it ONCE for the whole batch.</remarks>
|
||||
public void WriteValues(IReadOnlyList<ValueWrite> values)
|
||||
=> _nodeManager.WriteValues(values);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm)
|
||||
=> _nodeManager.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc, realm);
|
||||
|
||||
Reference in New Issue
Block a user