mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 15:22:15 +08:00
1cca744b5c
Closes #4201. Closes #4202. Closes #4203. Closes #4215. Closes #4216. (#4215 and #4216 are @marcschier's originals from the #4188 review; #4202 and #4201 are my later duplicates of the same two findings. All four describe the two changes in section 1 and are closed together.) **This PR now carries what was #4221 as well.** The two were a stack; they have been unstacked and combined here, and #4221 is closed. Everything below the "Read the server diagnostics" section is the work that was previously reviewed as #4221. --- ## 1. The browse path guards itself, and the last node locks are gone `AsyncCustomNodeManager` kept two `lock (source)` sites around `CreateBrowser`, carrying a NOTE that browser creation was not internally synchronized. `NodeBrowser` still handed its lock to derived types through `protected Lock DataLock`. `NodeState.CreateBrowser` now holds a browse lock across `PopulateBrowser` and `OnPopulateBrowser`, so browser construction on a node is serialized — which is what the external `lock (source)` was actually providing, and what a handler that mutates the node during population (the historian's lazy config installer) depends on. Both `lock (source)` statements and the NOTE are gone. **What that lock does not do**, stated plainly in the XML docs and in `docs/NodeManagers.md` rather than over-promised: it is not an atomic snapshot across the node's children, notifiers and references. Writers take those collections' own locks, not the browse lock. The browser is a point-in-time copy and each collection is read consistently, but the combination is not a transaction. No `lock` takes a `NodeState` as its sync root any more. That was the last thing keeping `NodeState` from being documentably self-guarding, so the other eleven went with it: `lock (activeNode)`, `lock (target)`, `lock (source.Node)`, `lock (target.Node)`, `lock (notifier)`, `lock (handle.Node)` in `AsyncCustomNodeManager`, and `lock (node)` in `CustomNodeManager2`. Most were a check-then-act pair now served by `NodeState.AddReferenceIfMissing`, which does the check and the insert under the node's own lock. `NodeBrowser.DataLock` is removed. A browser is **single-consumer**: it belongs to whoever created it, and both server browse paths already serialize on the owning side. The exposed lock only added an inheritance-level locking contract that a derived browser could not reason about. ## 2. Read the server diagnostics under the lock every writer takes `OnUpdateDiagnostics` locked the diagnostics **payload** rather than the lock every writer takes through `UpdateServerDiagnostics`, so the snapshot a client reads from the `ServerDiagnostics` node was never excluded from a concurrent writer. Both halves matter, and the second is the one that is easy to miss: ```csharp lock (m_diagnosticsLock) // was: lock (ServerDiagnostics) { value = Variant.FromStructure(ServerDiagnostics, copy: true); // was: copy defaulted to false } ``` `copy: true` is what makes it a snapshot. `FromStructure(value, copy: false)` does `new ExtensionObject(value, copy)` — it wraps the **live** structure without copying it, so the caller would read the fields long after the lock was released and see them change underneath. Swapping the lock alone would have looked like a fix while still excluding nothing. ## 3. Analyzer numbering The browser rule is **UA0027**, not UA0025. This branch had independently taken UA0025 for `NodeBrowser.DataLock` while #4188 published UA0025 for `ILocalNode.DataLock` and UA0026 for `BaseVariableValue.Lock`. Master's are shipped, so they keep the numbers: | | UA0025 | UA0026 | UA0027 | |---|---|---|---| | master (#4188) | `ILocalNode.DataLock` | `BaseVariableValue.Lock` | — | | **this PR** | `ILocalNode.DataLock` | `BaseVariableValue.Lock` | **`NodeBrowser.DataLock`** | Both sides had written a different `UA0025Tests.cs`, which arrived as an add/add conflict. They are now split: `UA0025Tests.cs` holds master's `ILocalNode` tests, `UA0027Tests.cs` holds the `NodeBrowser` ones. UA0024's help text also drops its reference to `ReadServerDiagnostics`, which #4193 removed as unused, and names `ISession.ReadDiagnostics` / `ISubscription.ReadDiagnostics` instead — those do still exist. ## 4. Two more locks reachable from outside their owner **`ApplicationConfiguration.PropertiesLock` is removed.** It returned the properties dictionary itself, so `lock (configuration.PropertiesLock)` was `lock (configuration.Properties)` — the lock *was* the data it guarded, the same shape as #4203. It had exactly one production consumer (`OAuth2CredentialCollection.Load`, a get-or-add with `ParseExtension` called inside the lock), so it could not simply be deleted. `Properties` is now a `ConcurrentDictionary` — same public type, every operation atomic on its own — and the one multi-operation need gets a member of its own: ```csharp public T GetOrAddProperty<T>(string key, Func<T> valueFactory); ``` It deliberately does **not** invoke the factory under a lock, so `ParseExtension` now runs outside any critical section. Holding a lock across a caller-supplied callback is exactly the shape that produced the attribute/diagnostics deadlock earlier in this work. The two existing tests asserted only `PropertiesLock is not null`, so they tested nothing but its presence. They are replaced by tests of the contract: factory runs once for a missing key, not at all for a present one, 32 concurrent callers all observe the same instance, null arguments throw. `PropertiesLock` ships in 1.5.378, so this is a breaking removal — migration guide section plus analyzer **UA0028**. **`SharedDefinitionCache` no longer hands out its lock.** Three sites in `SharedKeyValueSubscriptionStore` took the nested cache's `Lock` to operate on the cache's own dictionary. Both are private now, behind `ReplaceAll` and `CloneWhere`. `SnapshotCommitLock` stays exposed on purpose: it is a `SemaphoreSlim` sequencing an async commit across `await` points, which `System.Threading.Lock` cannot span, so folding it in would change behaviour rather than hide a lock. ## Verification Merged `master` (`0edfe237a`, including #4234) with no conflicts. - `dotnet build UA.slnx` — 0 warnings, 0 errors - `Opc.Ua.Server.Tests` — 4074 passed - `Opc.Ua.Types.Tests` — 8501 passed - `Opc.Ua.MigrationAnalyzer.Tests` — 151 passed on all five TFMs (138 → 151, the new ones being UA0027 and UA0028) - `Opc.Ua.Core.Tests` — 4249 passed - `Opc.Ua.Redundancy.Server.Tests` — 573 passed ## Note for the reviewer @romanett — your approval was given when this PR's diff was the 7 diagnostics files, because it was measured against `marcschier/node-browse-locks`. Now that the two are combined and the base is `master`, the diff is 23 files, and the extra 16 are the node/browser lock work that was #4221. GitHub kept the approval across the retarget, but it did not cover that code, so please re-review rather than let it ride. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e9c13dd0-a6e3-4ad2-8ad6-1c3fe6323041 Copilot-Session: b3c0e146-5b9e-4d08-a0f6-db3f940484b1 Copilot-Session: d18f87e9-2484-44ce-98ce-463ba6cf5f63 Copilot-Session: 0ee22c1d-c319-4257-bf78-a5a077a1824a
723 lines
24 KiB
C#
723 lines
24 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Server;
|
|
using Quickstarts.Servers;
|
|
|
|
namespace MemoryBuffer
|
|
{
|
|
#pragma warning disable CA1001 // Using timers that are disposed in OnAfterDelete
|
|
public partial class MemoryBufferState
|
|
{
|
|
#pragma warning restore CA1001 // Using timers that are disposed in OnAfterDelete
|
|
/// <summary>
|
|
/// Initializes the buffer from the configuration.
|
|
/// </summary>
|
|
public MemoryBufferState(
|
|
ISystemContext context,
|
|
MemoryBufferInstance? configuration)
|
|
: base(null)
|
|
{
|
|
m_logger = context.Telemetry.CreateLogger<MemoryBufferState>();
|
|
|
|
Initialize(context);
|
|
|
|
string dataType = "UInt32";
|
|
string name = dataType;
|
|
int count = 10;
|
|
|
|
if (configuration != null)
|
|
{
|
|
count = configuration.TagCount;
|
|
|
|
if (!string.IsNullOrEmpty(configuration.DataType))
|
|
{
|
|
dataType = configuration.DataType;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(configuration.Name))
|
|
{
|
|
name = dataType;
|
|
}
|
|
}
|
|
|
|
SymbolicName = name;
|
|
|
|
BuiltInType elementType = dataType != "Double" ? BuiltInType.UInt32 : BuiltInType.Double;
|
|
|
|
CreateBuffer(elementType, count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The server that the buffer belongs to.
|
|
/// </summary>
|
|
public IServerInternal Server { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The node manager that the buffer belongs to.
|
|
/// </summary>
|
|
public INodeManager NodeManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The built-in type for the values stored in the buffer.
|
|
/// </summary>
|
|
public BuiltInType ElementType { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The size of each element in the buffer.
|
|
/// </summary>
|
|
public uint ElementSize => (uint)m_elementSize;
|
|
|
|
/// <summary>
|
|
/// The rate at which the buffer is scanned.
|
|
/// </summary>
|
|
public int MaximumScanRate { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
protected override void OnAfterDelete(ISystemContext context)
|
|
{
|
|
base.OnAfterDelete(context);
|
|
m_scanTimer?.Dispose();
|
|
m_scanTimer = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the buffer with enough space to hold the specified number of elements.
|
|
/// </summary>
|
|
/// <param name="elementName">The type of element.</param>
|
|
/// <param name="noOfElements">The number of elements.</param>
|
|
public void CreateBuffer(string elementName, int noOfElements)
|
|
{
|
|
if (string.IsNullOrEmpty(elementName))
|
|
{
|
|
elementName = "UInt32";
|
|
}
|
|
|
|
BuiltInType elementType = elementName != "Double" ? BuiltInType.UInt32 : BuiltInType.Double;
|
|
|
|
CreateBuffer(elementType, noOfElements);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the buffer with enough space to hold the specified number of elements.
|
|
/// </summary>
|
|
/// <param name="elementType">The type of element.</param>
|
|
/// <param name="noOfElements">The number of elements.</param>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public void CreateBuffer(BuiltInType elementType, int noOfElements)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
ElementType = elementType;
|
|
switch (ElementType)
|
|
{
|
|
case BuiltInType.UInt32:
|
|
m_elementSize = 4;
|
|
break;
|
|
case BuiltInType.Double:
|
|
m_elementSize = 8;
|
|
break;
|
|
case >= BuiltInType.Null and <= BuiltInType.Enumeration:
|
|
m_elementSize = 1;
|
|
break;
|
|
default:
|
|
throw ServiceResultException.Unexpected($"Unexpected BuiltInType {ElementType}");
|
|
}
|
|
|
|
m_lastScanTime = DateTimeUtc.Now;
|
|
MaximumScanRate = 1000;
|
|
|
|
m_buffer = new byte[m_elementSize * noOfElements];
|
|
SizeInBytes!.Value = (uint)m_buffer.Length;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an object which can browse the tags in the buffer.
|
|
/// </summary>
|
|
public override INodeBrowser CreateBrowser(
|
|
ISystemContext context,
|
|
ViewDescription? view,
|
|
NodeId referenceType,
|
|
bool includeSubtypes,
|
|
BrowseDirection browseDirection,
|
|
QualifiedName browseName,
|
|
IEnumerable<IReference>? additionalReferences,
|
|
bool internalOnly)
|
|
{
|
|
NodeBrowser browser = new MemoryBufferBrowser(
|
|
context,
|
|
view!,
|
|
referenceType,
|
|
includeSubtypes,
|
|
browseDirection,
|
|
browseName,
|
|
additionalReferences!,
|
|
internalOnly,
|
|
this);
|
|
|
|
PopulateBrowserSynchronized(context, browser);
|
|
|
|
return browser;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the read operation for an invidual tag.
|
|
/// </summary>
|
|
public ServiceResult ReadTagValue(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
NumericRange indexRange,
|
|
QualifiedName dataEncoding,
|
|
ref Variant value,
|
|
ref StatusCode statusCode,
|
|
ref DateTimeUtc timestamp)
|
|
{
|
|
if (node is not MemoryTagState tag)
|
|
{
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
}
|
|
|
|
if (!indexRange.IsNull)
|
|
{
|
|
return StatusCodes.BadIndexRangeInvalid;
|
|
}
|
|
|
|
if (!dataEncoding.IsNull)
|
|
{
|
|
return StatusCodes.BadDataEncodingUnsupported;
|
|
}
|
|
|
|
int offset = (int)tag.Offset;
|
|
|
|
lock (m_dataLock)
|
|
{
|
|
if (offset < 0 || offset >= m_buffer.Length)
|
|
{
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
}
|
|
|
|
if (m_buffer == null)
|
|
{
|
|
return StatusCodes.BadOutOfService;
|
|
}
|
|
|
|
value = GetValueAtOffset(offset);
|
|
}
|
|
|
|
statusCode = StatusCodes.Good;
|
|
timestamp = m_lastScanTime;
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a write operation for an individual tag.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public ServiceResult WriteTagValue(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
NumericRange indexRange,
|
|
QualifiedName dataEncoding,
|
|
ref Variant value,
|
|
ref StatusCode statusCode,
|
|
ref DateTimeUtc timestamp)
|
|
{
|
|
if (node is not MemoryTagState tag)
|
|
{
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
}
|
|
|
|
if (!indexRange.IsNull)
|
|
{
|
|
return StatusCodes.BadIndexRangeInvalid;
|
|
}
|
|
|
|
if (!dataEncoding.IsNull)
|
|
{
|
|
return StatusCodes.BadDataEncodingUnsupported;
|
|
}
|
|
|
|
if (statusCode != StatusCodes.Good)
|
|
{
|
|
return StatusCodes.BadWriteNotSupported;
|
|
}
|
|
|
|
if (timestamp != DateTimeUtc.MinValue)
|
|
{
|
|
return StatusCodes.BadWriteNotSupported;
|
|
}
|
|
|
|
bool changed = false;
|
|
int offset = (int)tag.Offset;
|
|
|
|
lock (m_dataLock)
|
|
{
|
|
if (offset < 0 || offset >= m_buffer.Length)
|
|
{
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
}
|
|
|
|
if (m_buffer == null)
|
|
{
|
|
return StatusCodes.BadOutOfService;
|
|
}
|
|
|
|
byte[] bytes;
|
|
|
|
switch (ElementType)
|
|
{
|
|
case BuiltInType.UInt32:
|
|
{
|
|
if (!value.TryGetValue(out uint valueToWrite))
|
|
{
|
|
return StatusCodes.BadTypeMismatch;
|
|
}
|
|
|
|
bytes = BitConverter.GetBytes(valueToWrite);
|
|
break;
|
|
}
|
|
case BuiltInType.Double:
|
|
{
|
|
if (!value.TryGetValue(out double valueToWrite))
|
|
{
|
|
return StatusCodes.BadTypeMismatch;
|
|
}
|
|
|
|
bytes = BitConverter.GetBytes(valueToWrite);
|
|
break;
|
|
}
|
|
case >= BuiltInType.Null and <= BuiltInType.Enumeration:
|
|
return StatusCodes.BadNodeIdUnknown;
|
|
default:
|
|
throw ServiceResultException.Unexpected($"Unexpected BuiltInType {ElementType}");
|
|
}
|
|
|
|
for (int ii = 0; ii < bytes.Length; ii++)
|
|
{
|
|
if (!changed && m_buffer[offset + ii] != bytes[ii])
|
|
{
|
|
changed = true;
|
|
}
|
|
|
|
m_buffer[offset + ii] = bytes[ii];
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
OnBufferChanged(offset);
|
|
}
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the value at the specified offset.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public Variant GetValueAtOffset(int offset)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
if (offset < 0 || offset >= m_buffer.Length)
|
|
{
|
|
return Variant.Null;
|
|
}
|
|
|
|
if (m_buffer == null)
|
|
{
|
|
return Variant.Null;
|
|
}
|
|
|
|
switch (ElementType)
|
|
{
|
|
case BuiltInType.UInt32:
|
|
return new Variant(BitConverter.ToUInt32(m_buffer, offset));
|
|
case BuiltInType.Double:
|
|
return new Variant(BitConverter.ToDouble(m_buffer, offset));
|
|
case >= BuiltInType.Null and <= BuiltInType.Enumeration:
|
|
return Variant.Null;
|
|
default:
|
|
throw ServiceResultException.Unexpected($"Bad element type {ElementType}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the instance with the context for the node being monitored.
|
|
/// </summary>
|
|
public void InitializeMonitoring(IServerInternal server, INodeManager nodeManager)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
Server = server;
|
|
NodeManager = nodeManager;
|
|
m_nonValueMonitoredItems = [];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new data change monitored item.
|
|
/// </summary>
|
|
public MemoryBufferMonitoredItem CreateDataChangeItem(
|
|
ServerSystemContext context,
|
|
MemoryTagState tag,
|
|
uint subscriptionId,
|
|
uint monitoredItemId,
|
|
ReadValueId itemToMonitor,
|
|
DiagnosticsMasks diagnosticsMasks,
|
|
TimestampsToReturn timestampsToReturn,
|
|
MonitoringMode monitoringMode,
|
|
uint clientHandle,
|
|
double samplingInterval,
|
|
bool createDurable)
|
|
/*
|
|
ISystemContext context,
|
|
MemoryTagState tag,
|
|
uint monitoredItemId,
|
|
uint attributeId,
|
|
DiagnosticsMasks diagnosticsMasks,
|
|
TimestampsToReturn timestampsToReturn,
|
|
MonitoringMode monitoringMode,
|
|
uint clientHandle,
|
|
double samplingInterval)*/
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
var monitoredItem = new MemoryBufferMonitoredItem(
|
|
Server,
|
|
NodeManager.ToAsyncNodeManager(),
|
|
this,
|
|
tag.Offset,
|
|
0,
|
|
monitoredItemId,
|
|
itemToMonitor,
|
|
diagnosticsMasks,
|
|
timestampsToReturn,
|
|
monitoringMode,
|
|
clientHandle,
|
|
null,
|
|
null,
|
|
null,
|
|
samplingInterval,
|
|
0,
|
|
false,
|
|
0,
|
|
createDurable);
|
|
|
|
/*
|
|
MemoryBufferMonitoredItem monitoredItem = new MemoryBufferMonitoredItem(
|
|
this,
|
|
monitoredItemId,
|
|
tag.Offset,
|
|
attributeId,
|
|
diagnosticsMasks,
|
|
timestampsToReturn,
|
|
monitoringMode,
|
|
clientHandle,
|
|
samplingInterval);
|
|
*/
|
|
|
|
AddMonitoredItemInternal(monitoredItem, tag);
|
|
|
|
return monitoredItem;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores data change monitored item after a server restart.
|
|
/// </summary>
|
|
public MemoryBufferMonitoredItem RestoreDataChangeItem(
|
|
ServerSystemContext context,
|
|
MemoryTagState tag,
|
|
IStoredMonitoredItem storedMonitoredItem)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
var monitoredItem = new MemoryBufferMonitoredItem(
|
|
Server,
|
|
NodeManager.ToAsyncNodeManager(),
|
|
this,
|
|
tag.Offset,
|
|
storedMonitoredItem);
|
|
|
|
AddMonitoredItemInternal(monitoredItem, tag);
|
|
|
|
return monitoredItem;
|
|
}
|
|
}
|
|
|
|
private void AddMonitoredItemInternal(
|
|
MemoryBufferMonitoredItem monitoredItem,
|
|
MemoryTagState tag)
|
|
{
|
|
if (monitoredItem.AttributeId != Attributes.Value)
|
|
{
|
|
m_nonValueMonitoredItems!.Add(monitoredItem.Id, monitoredItem);
|
|
return;
|
|
}
|
|
|
|
int elementCount = (int)(SizeInBytes!.Value / ElementSize);
|
|
|
|
if (m_monitoringTable == null)
|
|
{
|
|
m_monitoringTable = new MemoryBufferMonitoredItem[elementCount][];
|
|
TimeProvider timeProvider = (Server as ITimeProviderProvider)?.TimeProvider
|
|
?? TimeProvider.System;
|
|
m_scanTimer?.Dispose();
|
|
m_scanTimer = timeProvider.CreateTimer(
|
|
DoScan,
|
|
null,
|
|
TimeSpan.FromMilliseconds(100),
|
|
TimeSpan.FromMilliseconds(100));
|
|
}
|
|
|
|
int elementOffet = (int)(tag.Offset / ElementSize);
|
|
|
|
MemoryBufferMonitoredItem[]? monitoredItems = m_monitoringTable![elementOffet];
|
|
|
|
if (monitoredItems == null)
|
|
{
|
|
monitoredItems = new MemoryBufferMonitoredItem[1];
|
|
}
|
|
else
|
|
{
|
|
monitoredItems = new MemoryBufferMonitoredItem[monitoredItems.Length + 1];
|
|
m_monitoringTable[elementOffet]!.CopyTo(monitoredItems, 0);
|
|
}
|
|
|
|
monitoredItems[^1] = monitoredItem;
|
|
m_monitoringTable[elementOffet] = monitoredItems;
|
|
m_itemCount++;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans the buffer and updates every other element.
|
|
/// </summary>
|
|
private void DoScan(object? state)
|
|
{
|
|
DateTimeUtc start1 = DateTimeUtc.Now;
|
|
|
|
lock (m_dataLock)
|
|
{
|
|
for (int ii = 0; ii < m_buffer.Length; ii += m_elementSize)
|
|
{
|
|
m_buffer[ii]++;
|
|
|
|
// notify any monitored items that the value has changed.
|
|
OnBufferChanged(ii);
|
|
}
|
|
|
|
m_lastScanTime = DateTimeUtc.Now;
|
|
}
|
|
|
|
DateTimeUtc end1 = DateTimeUtc.Now;
|
|
|
|
double delta1 = (end1 - start1).TotalMilliseconds;
|
|
|
|
if (delta1 > 100)
|
|
{
|
|
m_logger.SamplingDelay(nameof(MemoryBufferState), delta1);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes the monitored item.
|
|
/// </summary>
|
|
public void DeleteItem(MemoryBufferMonitoredItem monitoredItem)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
if (monitoredItem.AttributeId != Attributes.Value)
|
|
{
|
|
m_nonValueMonitoredItems!.Remove(monitoredItem.Id);
|
|
return;
|
|
}
|
|
|
|
if (m_monitoringTable != null)
|
|
{
|
|
int elementOffet = (int)(monitoredItem.Offset / ElementSize);
|
|
|
|
MemoryBufferMonitoredItem[]? monitoredItems = m_monitoringTable[elementOffet];
|
|
|
|
if (monitoredItems != null)
|
|
{
|
|
int index = -1;
|
|
|
|
for (int ii = 0; ii < monitoredItems.Length; ii++)
|
|
{
|
|
if (ReferenceEquals(monitoredItems[ii], monitoredItem))
|
|
{
|
|
index = ii;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (index >= 0)
|
|
{
|
|
m_itemCount--;
|
|
|
|
if (monitoredItems.Length == 1)
|
|
{
|
|
monitoredItems = null;
|
|
}
|
|
else
|
|
{
|
|
monitoredItems = new MemoryBufferMonitoredItem[monitoredItems
|
|
.Length -
|
|
1];
|
|
|
|
Array.Copy(
|
|
m_monitoringTable[elementOffet]!,
|
|
0,
|
|
monitoredItems,
|
|
0,
|
|
index);
|
|
Array.Copy(
|
|
m_monitoringTable[elementOffet]!,
|
|
index + 1,
|
|
monitoredItems,
|
|
index,
|
|
monitoredItems.Length - index);
|
|
}
|
|
|
|
m_monitoringTable[elementOffet] = monitoredItems;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles change events raised by the node.
|
|
/// </summary>
|
|
public void OnBufferChanged(int offset)
|
|
{
|
|
lock (m_dataLock)
|
|
{
|
|
if (m_monitoringTable != null)
|
|
{
|
|
int elementOffet = (int)(offset / ElementSize);
|
|
|
|
MemoryBufferMonitoredItem[]? monitoredItems = m_monitoringTable[elementOffet];
|
|
|
|
if (monitoredItems != null)
|
|
{
|
|
var value = new DataValue(
|
|
GetValueAtOffset(offset),
|
|
StatusCodes.Good,
|
|
m_lastScanTime,
|
|
DateTimeUtc.Now);
|
|
|
|
for (int ii = 0; ii < monitoredItems.Length; ii++)
|
|
{
|
|
monitoredItems[ii].QueueValue(value, null);
|
|
m_updateCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ScanTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
DoScan(null);
|
|
}
|
|
|
|
private void PublishTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
DateTimeUtc start1 = DateTimeUtc.Now;
|
|
|
|
lock (m_dataLock)
|
|
{
|
|
if (m_itemCount > 0 && m_updateCount < m_itemCount &&
|
|
m_logger.IsEnabled(LogLevel.Information))
|
|
{
|
|
m_logger.MemoryBufferReported(
|
|
DateTimeUtc.Now.ToLocalTime(),
|
|
m_updateCount,
|
|
m_itemCount);
|
|
}
|
|
|
|
m_updateCount = 0;
|
|
}
|
|
|
|
DateTimeUtc end1 = DateTimeUtc.Now;
|
|
|
|
double delta1 = (end1 - start1).TotalMilliseconds;
|
|
|
|
if (delta1 > 100)
|
|
{
|
|
m_logger.PublishDelay(nameof(MemoryBufferState), delta1);
|
|
}
|
|
}
|
|
|
|
private readonly ILogger m_logger;
|
|
private readonly Lock m_dataLock = new();
|
|
private MemoryBufferMonitoredItem[]?[]? m_monitoringTable;
|
|
private Dictionary<uint, MemoryBufferMonitoredItem>? m_nonValueMonitoredItems;
|
|
private int m_elementSize;
|
|
private DateTimeUtc m_lastScanTime;
|
|
private byte[] m_buffer = null!;
|
|
private ITimer? m_scanTimer;
|
|
private int m_updateCount;
|
|
private int m_itemCount;
|
|
}
|
|
|
|
internal static partial class MemoryBufferStateLog
|
|
{
|
|
[LoggerMessage(
|
|
EventId = QuickstartsServersEventIds.MemoryBufferState + 0, Level = LogLevel.Warning,
|
|
Message = "{StateName} SAMPLING DELAY ({Delta}ms)")]
|
|
public static partial void SamplingDelay(this ILogger logger, string stateName, double delta);
|
|
|
|
[LoggerMessage(
|
|
EventId = QuickstartsServersEventIds.MemoryBufferState + 1, Level = LogLevel.Information,
|
|
Message = "{Now:HH:mm:ss.fff} MEMORYBUFFER Reported {UpdateCount}/{ItemCount} items ***.")]
|
|
public static partial void MemoryBufferReported(
|
|
this ILogger logger,
|
|
DateTime now,
|
|
int updateCount,
|
|
int itemCount);
|
|
|
|
[LoggerMessage(
|
|
EventId = QuickstartsServersEventIds.MemoryBufferState + 2, Level = LogLevel.Information,
|
|
Message = "{StateName} ****** PUBLISH DELAY ({Delta}ms) ******")]
|
|
public static partial void PublishDelay(this ILogger logger, string stateName, double delta);
|
|
}
|
|
|
|
}
|