diff --git a/src/Opc.Ua.Server/Server/StandardServer.cs b/src/Opc.Ua.Server/Server/StandardServer.cs index e63b7e32a9..e88d21d69f 100644 --- a/src/Opc.Ua.Server/Server/StandardServer.cs +++ b/src/Opc.Ua.Server/Server/StandardServer.cs @@ -1877,10 +1877,10 @@ public override async ValueTask HistoryUpdateAsync( try { - // check only for BadNothingToDo here - // MaxNodesPerHistoryUpdateEvents & MaxNodesPerHistoryUpdateData - // must be checked in NodeManager (TODO) ValidateOperationLimits(historyUpdateDetails); + ValidateOperationLimits( + historyUpdateDetails.Count, + GetHistoryUpdateOperationLimit(historyUpdateDetails)); (ArrayOf results, ArrayOf diagnosticInfos) = await ServerInternal.NodeManager.HistoryUpdateAsync( @@ -1915,6 +1915,37 @@ await ServerInternal.NodeManager.HistoryUpdateAsync( } } + private PropertyState? GetHistoryUpdateOperationLimit( + ArrayOf historyUpdateDetails) + { + foreach (ExtensionObject details in historyUpdateDetails) + { + if (details.IsNull || !details.TryGetValue(out HistoryUpdateDetails? historyUpdateDetail)) + { + continue; + } + + Type detailsType = historyUpdateDetail!.GetType(); + if (detailsType == typeof(UpdateEventDetails) || + detailsType == typeof(DeleteEventDetails)) + { + return OperationLimits.MaxNodesPerHistoryUpdateEvents; + } + + if (detailsType == typeof(UpdateDataDetails) || + detailsType == typeof(UpdateStructureDataDetails) || + detailsType == typeof(DeleteRawModifiedDetails) || + detailsType == typeof(DeleteAtTimeDetails)) + { + return OperationLimits.MaxNodesPerHistoryUpdateData; + } + + break; + } + + return null; + } + /// public override async ValueTask CreateSubscriptionAsync( SecureChannelContext secureChannelContext, diff --git a/tests/Opc.Ua.History.Tests/HistoricalAccessTests.cs b/tests/Opc.Ua.History.Tests/HistoricalAccessTests.cs index 56d050b8b6..f7f2a02528 100644 --- a/tests/Opc.Ua.History.Tests/HistoricalAccessTests.cs +++ b/tests/Opc.Ua.History.Tests/HistoricalAccessTests.cs @@ -869,6 +869,92 @@ await Session.HistoryUpdateAsync( } } + [Test] + public async Task HistoryUpdateRejectsTooManyDataOperationsAsync() + { + NodeId nodeId = ToNodeId(Constants.ScalarStaticDouble); + var details = new ExtensionObject[1001]; + + for (int i = 0; i < details.Length; i++) + { + details[i] = new ExtensionObject(new UpdateDataDetails + { + NodeId = nodeId, + PerformInsertReplace = PerformUpdateType.Insert, + UpdateValues = + [ + new DataValue( + new Variant((double)i), + StatusCodes.Good, + DateTime.UtcNow.AddSeconds(i)) + ] + }); + } + + uint originalDataLimit = Session.OperationLimits.MaxNodesPerHistoryUpdateData; + uint originalEventLimit = Session.OperationLimits.MaxNodesPerHistoryUpdateEvents; + + try + { + // Disable client-side batching so the oversized request reaches + // the server as a single HistoryUpdate call. + Session.OperationLimits.MaxNodesPerHistoryUpdateData = 0; + Session.OperationLimits.MaxNodesPerHistoryUpdateEvents = 0; + + ServiceResultException ex = Assert.ThrowsAsync( + async () => await Session.HistoryUpdateAsync( + null, + details.ToArrayOf(), + CancellationToken.None).ConfigureAwait(false)); + + Assert.That(ex.StatusCode, Is.EqualTo(StatusCodes.BadTooManyOperations)); + } + finally + { + Session.OperationLimits.MaxNodesPerHistoryUpdateData = originalDataLimit; + Session.OperationLimits.MaxNodesPerHistoryUpdateEvents = originalEventLimit; + } + } + + [Test] + public async Task HistoryUpdateRejectsTooManyEventOperationsAsync() + { + var details = new ExtensionObject[1001]; + + for (int i = 0; i < details.Length; i++) + { + details[i] = new ExtensionObject(new DeleteEventDetails + { + NodeId = ObjectIds.Server, + EventIds = [ByteString.From([(byte)(i & 0xff)])] + }); + } + + uint originalDataLimit = Session.OperationLimits.MaxNodesPerHistoryUpdateData; + uint originalEventLimit = Session.OperationLimits.MaxNodesPerHistoryUpdateEvents; + + try + { + // Disable client-side batching so the oversized request reaches + // the server as a single HistoryUpdate call. + Session.OperationLimits.MaxNodesPerHistoryUpdateData = 0; + Session.OperationLimits.MaxNodesPerHistoryUpdateEvents = 0; + + ServiceResultException ex = Assert.ThrowsAsync( + async () => await Session.HistoryUpdateAsync( + null, + details.ToArrayOf(), + CancellationToken.None).ConfigureAwait(false)); + + Assert.That(ex.StatusCode, Is.EqualTo(StatusCodes.BadTooManyOperations)); + } + finally + { + Session.OperationLimits.MaxNodesPerHistoryUpdateData = originalDataLimit; + Session.OperationLimits.MaxNodesPerHistoryUpdateEvents = originalEventLimit; + } + } + [Description("Verify that HistoryRead can read history for multiple nodes in a single request and returns one result per node.")] [Test] public async Task HistoryReadMultipleNodesAtOnceAsync()