diff --git a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs index 9131da63a6..4c1f766bfc 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs @@ -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; } } } diff --git a/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs index cae0583bfc..0dd18bd972 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs @@ -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) diff --git a/Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs b/Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs index fb05909cee..3ce3447ffa 100644 --- a/Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs +++ b/Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs @@ -543,6 +543,76 @@ namespace Opc.Ua.Server.Tests "Array SourceTimestamp should be close to the second read time"); } + /// + /// Test that reading a static variable after a write returns a fresh ServerTimestamp. + /// + [Test] + [NonParallelizable] + public async Task ReadStaticVariableStampsFreshServerTimestampAfterWriteAsync() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + ILogger logger = telemetry.CreateLogger(); + + 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)); + } + /// /// Update static Nodes, read modify write. ///