mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 17:16:16 +08:00
## Summary Backports the fix for ServerTimestamp updating on Read operations for stored/static nodes to master378 (1.5.378), addressing issue #4257 (part 2). ## Details - In CustomNodeManager and CoreNodeManager, ensure ServerTimestamp is stamped with the read time for stored/static nodes whose SourceTimestamp is older than eadTime (e.g. after a Write). - Retain matching SourceTimestamp and ServerTimestamp for dynamically produced values generated during Read (such as ServerStatus children). - Adds unit test ReadStaticVariableStampsFreshServerTimestampAfterWriteAsync in ReferenceServerTest.cs to verify that after writing to a static variable, subsequent reads return ServerTimestamp stamped with the current read time. Fixes #4257 (ServerTimestamp behavior) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -714,6 +714,8 @@ namespace Opc.Ua.Server
|
||||
// owned by this node manager.
|
||||
nodeToRead.Processed = true;
|
||||
|
||||
DateTime readTime = DateTime.UtcNow;
|
||||
|
||||
// read the default value (also verifies that the attribute id is valid for the node).
|
||||
ServiceResult error = node.Read(context, nodeToRead.AttributeId, value);
|
||||
|
||||
@@ -779,14 +781,16 @@ namespace Opc.Ua.Server
|
||||
// Set SourceTimestamp if not already set by the node
|
||||
if (value.SourceTimestamp == DateTime.MinValue)
|
||||
{
|
||||
value.SourceTimestamp = DateTime.UtcNow;
|
||||
value.SourceTimestamp = readTime;
|
||||
}
|
||||
|
||||
// Set ServerTimestamp to match SourceTimestamp for Value attributes
|
||||
// This ensures ServerTimestamp and SourceTimestamp are equal,
|
||||
// which is important for nodes like ServerStatus children where
|
||||
// the node's read callback sets a specific timestamp
|
||||
value.ServerTimestamp = value.SourceTimestamp;
|
||||
// Set timestamps after ReadAttribute. ServerTimestamp reflects
|
||||
// when the Server verified the value. A value whose
|
||||
// SourceTimestamp was produced as part of this read (e.g.
|
||||
// ServerStatus children) keeps ServerTimestamp aligned with
|
||||
// SourceTimestamp; a stored/static value is verified now, so
|
||||
// ServerTimestamp is stamped with the current read time.
|
||||
value.ServerTimestamp = value.SourceTimestamp >= readTime ? value.SourceTimestamp : readTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1670,6 +1670,8 @@ namespace Opc.Ua.Server
|
||||
continue;
|
||||
}
|
||||
|
||||
DateTime readTime = DateTime.UtcNow;
|
||||
|
||||
// read the attribute value.
|
||||
errors[ii] = handle.Node.ReadAttribute(
|
||||
systemContext,
|
||||
@@ -1678,24 +1680,24 @@ namespace Opc.Ua.Server
|
||||
nodeToRead.DataEncoding,
|
||||
value);
|
||||
|
||||
// Set timestamps after ReadAttribute to ensure consistency
|
||||
// For Value attributes, match ServerTimestamp to SourceTimestamp
|
||||
// For other attributes, just ensure ServerTimestamp is set
|
||||
// Set timestamps after ReadAttribute. ServerTimestamp reflects
|
||||
// when the Server verified the value. A value whose
|
||||
// SourceTimestamp was produced as part of this read (e.g.
|
||||
// ServerStatus children) keeps ServerTimestamp aligned with
|
||||
// SourceTimestamp; a stored/static value is verified now, so
|
||||
// ServerTimestamp is stamped with the current read time.
|
||||
if (nodeToRead.AttributeId == Attributes.Value)
|
||||
{
|
||||
if (value.SourceTimestamp == DateTime.MinValue)
|
||||
{
|
||||
value.SourceTimestamp = DateTime.UtcNow;
|
||||
value.SourceTimestamp = readTime;
|
||||
}
|
||||
value.ServerTimestamp = value.SourceTimestamp;
|
||||
value.ServerTimestamp = value.SourceTimestamp >= readTime ? value.SourceTimestamp : readTime;
|
||||
}
|
||||
else
|
||||
else if (value.ServerTimestamp == DateTime.MinValue)
|
||||
{
|
||||
// For non-value attributes, only ServerTimestamp is relevant
|
||||
if (value.ServerTimestamp == DateTime.MinValue)
|
||||
{
|
||||
value.ServerTimestamp = DateTime.UtcNow;
|
||||
}
|
||||
value.ServerTimestamp = readTime;
|
||||
}
|
||||
#if DEBUG
|
||||
if (nodeToRead.AttributeId == Attributes.Value)
|
||||
|
||||
@@ -543,6 +543,76 @@ namespace Opc.Ua.Server.Tests
|
||||
"Array SourceTimestamp should be close to the second read time");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that reading a static variable after a write returns a fresh ServerTimestamp.
|
||||
/// </summary>
|
||||
[Test]
|
||||
[NonParallelizable]
|
||||
public async Task ReadStaticVariableStampsFreshServerTimestampAfterWriteAsync()
|
||||
{
|
||||
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
||||
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();
|
||||
|
||||
NodeId nodeId = ExpandedNodeId.ToNodeId(
|
||||
CommonTestWorkers.NodeIdTestSetStatic[2],
|
||||
m_server.CurrentInstance.NamespaceUris);
|
||||
var nodesToRead = new ReadValueIdCollection
|
||||
{
|
||||
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }
|
||||
};
|
||||
|
||||
// 1. Write a value to the static variable
|
||||
var writeNodes = new WriteValueCollection
|
||||
{
|
||||
new WriteValue
|
||||
{
|
||||
NodeId = nodeId,
|
||||
AttributeId = Attributes.Value,
|
||||
Value = new DataValue { WrappedValue = new Variant(42) }
|
||||
}
|
||||
};
|
||||
|
||||
RequestHeader writeRequestHeader = m_requestHeader;
|
||||
writeRequestHeader.Timestamp = DateTime.UtcNow;
|
||||
WriteResponse writeResponse = await m_server.WriteAsync(
|
||||
m_secureChannelContext,
|
||||
writeRequestHeader,
|
||||
writeNodes,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("Write result: {StatusCode}", writeResponse.Results[0]);
|
||||
NUnit.Framework.Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True, $"Write failed with: {writeResponse.Results[0]}");
|
||||
|
||||
// Wait a short delay so the subsequent read time is measurably greater than write time
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
|
||||
// 2. Read the variable back
|
||||
RequestHeader readRequestHeader = m_requestHeader;
|
||||
readRequestHeader.Timestamp = DateTime.UtcNow;
|
||||
DateTime timeBeforeRead = DateTime.UtcNow;
|
||||
|
||||
ReadResponse readResponse = await m_server.ReadAsync(
|
||||
m_secureChannelContext,
|
||||
readRequestHeader,
|
||||
kMaxAge,
|
||||
TimestampsToReturn.Both,
|
||||
nodesToRead,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
NUnit.Framework.Assert.That(readResponse.Results.Count, Is.EqualTo(1));
|
||||
DataValue readValue = readResponse.Results[0];
|
||||
NUnit.Framework.Assert.That(StatusCode.IsGood(readValue.StatusCode), Is.True);
|
||||
|
||||
logger.LogInformation(
|
||||
"After write read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
|
||||
readValue.SourceTimestamp,
|
||||
readValue.ServerTimestamp);
|
||||
|
||||
// ServerTimestamp should reflect when the server processed the read, not be stuck at write time
|
||||
NUnit.Framework.Assert.That(readValue.ServerTimestamp, Is.GreaterThanOrEqualTo(timeBeforeRead.AddSeconds(-1)));
|
||||
NUnit.Framework.Assert.That(readValue.ServerTimestamp, Is.GreaterThan(readValue.SourceTimestamp));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update static Nodes, read modify write.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user