Fix ServerTimestamp update on read of stored/static nodes (#4257) (#4260)

## 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:
romanett
2026-08-18 18:18:56 +02:00
committed by GitHub
parent a240bf8fe3
commit 2ae373f8d3
3 changed files with 92 additions and 16 deletions
@@ -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>