From abd8dbd68f7575e1d56a3d3b39c5fe0f8d7513a9 Mon Sep 17 00:00:00 2001 From: Marc Schier Date: Mon, 8 Jan 2024 18:19:25 +0100 Subject: [PATCH] * Sending errors as heartbeats and keyframes * Improve synchronization between writer and subscription handle * Fix Connectivity status * Leverage ability to inherit stack resources instead of linking * Update dependencies and .net stack to 116-preview --- common.props | 4 +- .../IIoTPlatform-E2E-Tests.csproj | 8 +- .../OpcPublisher-AE-E2E-Tests.csproj | 8 +- samples/Http/BrowseAll/BrowseAll.csproj | 4 +- .../GetConfiguration/GetConfiguration.csproj | 4 +- .../ReadCurrentTime/ReadCurrentTime.csproj | 4 +- .../SetConfiguration/SetConfiguration.csproj | 4 +- .../WriteReadbackValue.csproj | 4 +- .../ApproveRejected/ApproveRejected.csproj | 4 +- .../BrowseCertificates.csproj | 4 +- .../GetCertificate/GetApplicationCert.csproj | 4 +- .../GetConfiguration/GetConfiguration.csproj | 4 +- .../MonitorMessages/MonitorMessages.csproj | 2 +- .../ReadCurrentTime/ReadCurrentTime.csproj | 4 +- .../WriteReadbackValue.csproj | 4 +- .../src/WriterGroupDiagnosticModel.cs | 22 + ...e.IIoT.OpcUa.Publisher.Models.Tests.csproj | 2 +- .../Azure.IIoT.OpcUa.Publisher.Module.csproj | 3 +- .../src/Program.cs | 1 - ...e.IIoT.OpcUa.Publisher.Module.Tests.csproj | 2 +- .../AdvancedPubSubIntegrationTests.cs | 4 +- .../BasicSamplesIntegrationTests.cs | 1 + ...IIoT.OpcUa.Publisher.Service.WebApi.csproj | 2 +- ...pcUa.Publisher.Service.WebApi.Tests.csproj | 2 +- ....IIoT.OpcUa.Publisher.Service.Tests.csproj | 2 +- ...IoT.OpcUa.Publisher.Testing.Servers.csproj | 2 +- .../Azure.IIoT.OpcUa.Publisher.Testing.csproj | 2 +- .../src/Azure.IIoT.OpcUa.Publisher.csproj | 4 +- .../src/IMessageSource.cs | 2 +- .../src/Runtime/PublisherConfig.cs | 1 - .../src/Services/NetworkMessageSink.cs | 2 +- .../Services/PublisherDiagnosticCollector.cs | 18 +- .../src/Services/PublisherService.cs | 12 +- .../src/Services/RuntimeStateReporter.cs | 69 +- .../src/Services/WriterGroupDataSource.cs | 372 +++-- .../SubscriptionConfigurationModelEx.cs | 30 - .../Stack/Extensions/SubscriptionModelEx.cs | 47 - .../src/Stack/IClientAccessor.cs | 10 - .../src/Stack/IOpcUaClient.cs | 23 +- ...entState.cs => IOpcUaClientDiagnostics.cs} | 8 +- .../src/Stack/IOpcUaMonitoredItem.cs | 18 +- .../src/Stack/IOpcUaSubscription.cs | 75 +- .../src/Stack/IOpcUaSubscriptionManager.cs | 17 +- .../Stack/IOpcUaSubscriptionNotification.cs | 2 +- .../src/Stack/ISessionServices.cs | 40 +- .../src/Stack/ISubscriptionCallbacks.cs | 59 + .../src/Stack/ISubscriptionHandle.cs | 51 +- .../Models/SubscriptionNotificationModel.cs | 2 +- .../src/Stack/Runtime/OpcUaClientConfig.cs | 2 - .../src/Stack/Services/OpcUaClient.cs | 395 ++++-- .../src/Stack/Services/OpcUaClientCapture.cs | 2 - .../src/Stack/Services/OpcUaClientManager.cs | 32 +- .../src/Stack/Services/OpcUaMonitoredItem.cs | 497 ++++--- .../src/Stack/Services/OpcUaSession.cs | 376 ++--- .../src/Stack/Services/OpcUaSubscription.cs | 1250 ++++++++--------- .../src/Storage/PublishedNodesConverter.cs | 1 - .../Azure.IIoT.OpcUa.Publisher.Tests.csproj | 2 +- .../tests/Services/Encoder/NetworkMessage.cs | 21 +- .../tests/Stack/GetSimpleEventFilterTests.cs | 24 +- .../tests/Stack/OpcUaMonitoredItemTests.cs | 106 +- .../src/Azure.IIoT.OpcUa.csproj | 2 +- .../tests/Azure.IIoT.OpcUa.Tests.csproj | 2 +- 62 files changed, 1935 insertions(+), 1750 deletions(-) delete mode 100644 src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/SubscriptionConfigurationModelEx.cs delete mode 100644 src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/SubscriptionModelEx.cs rename src/Azure.IIoT.OpcUa.Publisher/src/Stack/{IOpcUaClientState.cs => IOpcUaClientDiagnostics.cs} (88%) create mode 100644 src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionCallbacks.cs diff --git a/common.props b/common.props index 0df106a55e..e7a9f994b3 100644 --- a/common.props +++ b/common.props @@ -42,8 +42,8 @@ - - + + diff --git a/e2e-tests/IIoTPlatform-E2E-Tests/IIoTPlatform-E2E-Tests.csproj b/e2e-tests/IIoTPlatform-E2E-Tests/IIoTPlatform-E2E-Tests.csproj index 9c71f25808..7546698f8f 100644 --- a/e2e-tests/IIoTPlatform-E2E-Tests/IIoTPlatform-E2E-Tests.csproj +++ b/e2e-tests/IIoTPlatform-E2E-Tests/IIoTPlatform-E2E-Tests.csproj @@ -11,12 +11,12 @@ - + - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/e2e-tests/OpcPublisher-E2E-Tests/OpcPublisher-AE-E2E-Tests.csproj b/e2e-tests/OpcPublisher-E2E-Tests/OpcPublisher-AE-E2E-Tests.csproj index c599cb4ea0..0ca21f7b95 100644 --- a/e2e-tests/OpcPublisher-E2E-Tests/OpcPublisher-AE-E2E-Tests.csproj +++ b/e2e-tests/OpcPublisher-E2E-Tests/OpcPublisher-AE-E2E-Tests.csproj @@ -16,12 +16,12 @@ - + - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/samples/Http/BrowseAll/BrowseAll.csproj b/samples/Http/BrowseAll/BrowseAll.csproj index ec10db5945..c3acc35148 100644 --- a/samples/Http/BrowseAll/BrowseAll.csproj +++ b/samples/Http/BrowseAll/BrowseAll.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/Http/GetConfiguration/GetConfiguration.csproj b/samples/Http/GetConfiguration/GetConfiguration.csproj index ec10db5945..c3acc35148 100644 --- a/samples/Http/GetConfiguration/GetConfiguration.csproj +++ b/samples/Http/GetConfiguration/GetConfiguration.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/Http/ReadCurrentTime/ReadCurrentTime.csproj b/samples/Http/ReadCurrentTime/ReadCurrentTime.csproj index ec10db5945..c3acc35148 100644 --- a/samples/Http/ReadCurrentTime/ReadCurrentTime.csproj +++ b/samples/Http/ReadCurrentTime/ReadCurrentTime.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/Http/SetConfiguration/SetConfiguration.csproj b/samples/Http/SetConfiguration/SetConfiguration.csproj index ec10db5945..c3acc35148 100644 --- a/samples/Http/SetConfiguration/SetConfiguration.csproj +++ b/samples/Http/SetConfiguration/SetConfiguration.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/Http/WriteReadbackValue/WriteReadbackValue.csproj b/samples/Http/WriteReadbackValue/WriteReadbackValue.csproj index ec10db5945..c3acc35148 100644 --- a/samples/Http/WriteReadbackValue/WriteReadbackValue.csproj +++ b/samples/Http/WriteReadbackValue/WriteReadbackValue.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/IoTHub/ApproveRejected/ApproveRejected.csproj b/samples/IoTHub/ApproveRejected/ApproveRejected.csproj index ec10db5945..c3acc35148 100644 --- a/samples/IoTHub/ApproveRejected/ApproveRejected.csproj +++ b/samples/IoTHub/ApproveRejected/ApproveRejected.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/IoTHub/BrowseCertificates/BrowseCertificates.csproj b/samples/IoTHub/BrowseCertificates/BrowseCertificates.csproj index ec10db5945..c3acc35148 100644 --- a/samples/IoTHub/BrowseCertificates/BrowseCertificates.csproj +++ b/samples/IoTHub/BrowseCertificates/BrowseCertificates.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/IoTHub/GetCertificate/GetApplicationCert.csproj b/samples/IoTHub/GetCertificate/GetApplicationCert.csproj index ec10db5945..c3acc35148 100644 --- a/samples/IoTHub/GetCertificate/GetApplicationCert.csproj +++ b/samples/IoTHub/GetCertificate/GetApplicationCert.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/IoTHub/GetConfiguration/GetConfiguration.csproj b/samples/IoTHub/GetConfiguration/GetConfiguration.csproj index ec10db5945..c3acc35148 100644 --- a/samples/IoTHub/GetConfiguration/GetConfiguration.csproj +++ b/samples/IoTHub/GetConfiguration/GetConfiguration.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/IoTHub/MonitorMessages/MonitorMessages.csproj b/samples/IoTHub/MonitorMessages/MonitorMessages.csproj index 3ff9f7bfb7..9bea864c12 100644 --- a/samples/IoTHub/MonitorMessages/MonitorMessages.csproj +++ b/samples/IoTHub/MonitorMessages/MonitorMessages.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/IoTHub/ReadCurrentTime/ReadCurrentTime.csproj b/samples/IoTHub/ReadCurrentTime/ReadCurrentTime.csproj index ec10db5945..c3acc35148 100644 --- a/samples/IoTHub/ReadCurrentTime/ReadCurrentTime.csproj +++ b/samples/IoTHub/ReadCurrentTime/ReadCurrentTime.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/samples/IoTHub/WriteReadbackValue/WriteReadbackValue.csproj b/samples/IoTHub/WriteReadbackValue/WriteReadbackValue.csproj index ec10db5945..c3acc35148 100644 --- a/samples/IoTHub/WriteReadbackValue/WriteReadbackValue.csproj +++ b/samples/IoTHub/WriteReadbackValue/WriteReadbackValue.csproj @@ -6,10 +6,10 @@ enable - + - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupDiagnosticModel.cs b/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupDiagnosticModel.cs index 594082053b..3b37ce5db0 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupDiagnosticModel.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/WriterGroupDiagnosticModel.cs @@ -266,6 +266,28 @@ public record class WriterGroupDiagnosticModel EmitDefaultValue = true)] public double MinPublishRequestsRatio { get; set; } + /// + /// Number of endpoints connected + /// + [DataMember(Name = "NumberOfConnectedEndpoints", Order = 36, + EmitDefaultValue = true)] + public int NumberOfConnectedEndpoints { get; set; } + + /// + /// Number of endpoints disconnected + /// + [DataMember(Name = "NumberOfDisconnectedEndpoints", Order = 37, + EmitDefaultValue = true)] + public int NumberOfDisconnectedEndpoints { get; set; } + + /// + /// Number values or events that were not assignable to + /// the items in the subscription. + /// + [DataMember(Name = "IngressUnassignedChanges", Order = 38, + EmitDefaultValue = true)] + public long IngressUnassignedChanges { get; set; } + /// /// Publisher version /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj index 376a8985e9..3803f4e4da 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj @@ -10,7 +10,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj index 51de271332..87f664b723 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj @@ -38,7 +38,6 @@ - @@ -46,7 +45,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Program.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Program.cs index 8ade062980..fd063f2245 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Program.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Program.cs @@ -16,7 +16,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Module using System.Linq; using System.Threading; using System.Threading.Tasks; - using Azure.IIoT.OpcUa.Publisher.Stack.Runtime; /// /// Module diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj index 617522a187..6477e01db4 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs index 97a5c905ea..c21191b73b 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs @@ -312,11 +312,11 @@ public async Task RestartConfigurationTest() const string name2 = nameof(RestartConfigurationTest) + "new"; WritePublishedNodes(name2, "./Resources/DataItems2.json"); var diagnostics = await PublisherApi.GetDiagnosticInfoAsync(); - for (var i = 0; i < 10 && + for (var i = 0; i < 12 && (diagnostics.Count != 1 || diagnostics[0].Endpoint.DataSetWriterGroup != name2); i++) { _output.WriteLine($"######### {i}: Failed to get diagnosticsinfo."); - await Task.Delay(1000); + await Task.Delay(5000); diagnostics = await PublisherApi.GetDiagnosticInfoAsync(); } var diag = Assert.Single(diagnostics); diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicSamplesIntegrationTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicSamplesIntegrationTests.cs index 4cd896ad29..a78c42456d 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicSamplesIntegrationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicSamplesIntegrationTests.cs @@ -83,6 +83,7 @@ public async Task CanSendHeartbeatToIoTHubTest(MessageTimestamp timestamp, Heart Assert.NotEmpty(message.GetProperty("ApplicationUri").GetString()); Assert.NotEmpty(message.GetProperty("Timestamp").GetString()); Assert.True(message.GetProperty("SequenceNumber").GetUInt32() > 0); + _output.WriteLine(message.ToJsonString()); Assert.Equal("en-US", message.GetProperty("Value").GetProperty("Value").EnumerateArray().First().GetString()); var timestamps = new HashSet { message.GetProperty("Timestamp").GetDateTime() }; diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj index 8ed473f699..4bf26b566f 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj index c295e7e8f7..8dc34ec76b 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj index 21d28548be..c4f7373e7a 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj @@ -12,7 +12,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj index 18b0582dfe..339123df38 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj @@ -57,6 +57,6 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj index 149511ba29..a0cda17a14 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj index 0d37aac3da..8c386a28c5 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/IMessageSource.cs b/src/Azure.IIoT.OpcUa.Publisher/src/IMessageSource.cs index b5f31a4beb..d8b63068e3 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/IMessageSource.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/IMessageSource.cs @@ -14,7 +14,7 @@ namespace Azure.IIoT.OpcUa.Publisher /// /// Writer group /// - public interface IMessageSource : IAsyncDisposable + public interface IMessageSource : IDisposable { /// /// Subscribe to writer messages diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Runtime/PublisherConfig.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Runtime/PublisherConfig.cs index f08190fef3..335405d144 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Runtime/PublisherConfig.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Runtime/PublisherConfig.cs @@ -6,7 +6,6 @@ namespace Azure.IIoT.OpcUa.Publisher { using Azure.IIoT.OpcUa.Publisher.Models; - using Azure.IIoT.OpcUa.Publisher.Stack.Runtime; using Furly.Extensions.Configuration; using Furly.Extensions.Hosting; using Furly.Extensions.Messaging; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs index 49252263eb..61c095fc19 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs @@ -180,7 +180,7 @@ public async ValueTask DisposeAsync() finally { _diagnostics?.Dispose(); - await Source.DisposeAsync().ConfigureAwait(false); + Source.Dispose(); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs index ed1cac4f2d..9676cbcd4c 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs @@ -195,10 +195,14 @@ internal WriterGroupDiagnosticModel AggregateModel writers.Sum(w => w.MonitoredOpcNodesFailedCount), MonitoredOpcNodesSucceededCount = MonitoredOpcNodesSucceededCount + writers.Sum(w => w.MonitoredOpcNodesSucceededCount), - ConnectionRetries = writers.Count == 0 ? 0 : (int) - writers.Average(w => w.ConnectionRetries), - OpcEndpointConnected = OpcEndpointConnected || - writers.Any(w => w.OpcEndpointConnected), + ConnectionRetries = ConnectionRetries + + writers.Sum(w => w.ConnectionRetries), + NumberOfDisconnectedEndpoints = NumberOfDisconnectedEndpoints + + writers.Sum(w => w.NumberOfDisconnectedEndpoints), + NumberOfConnectedEndpoints = NumberOfConnectedEndpoints + + writers.Sum(w => w.NumberOfConnectedEndpoints), + OpcEndpointConnected = NumberOfConnectedEndpoints != 0 || + writers.Any(w => w.NumberOfConnectedEndpoints != 0), PublishRequestsRatio = PublishRequestsRatio + writers.Sum(w => w.PublishRequestsRatio), BadPublishRequestsRatio = BadPublishRequestsRatio + @@ -254,6 +258,8 @@ internal WriterGroupDiagnosticModel AggregateModel (d, i) => d.IngressCyclicReads = (long)i, ["iiot_edge_publisher_event_notifications"] = (d, i) => d.IngressEventNotifications = (long)i, + ["iiot_edge_publisher_unassigned_notification_count"] = + (d, i) => d.IngressUnassignedChanges = (long)i, ["iiot_edge_publisher_estimated_message_chunks_per_day"] = (d, i) => d.EstimatedIoTChunksPerDay = (double)i, ["iiot_edge_publisher_messages_per_second"] = @@ -287,7 +293,9 @@ internal WriterGroupDiagnosticModel AggregateModel ["iiot_edge_publisher_bad_nodes"] = (d, i) => d.MonitoredOpcNodesFailedCount = (long)i, ["iiot_edge_publisher_is_connection_ok"] = - (d, i) => d.OpcEndpointConnected = ((int)i) != 0, + (d, i) => d.NumberOfConnectedEndpoints = (int)i, + ["iiot_edge_publisher_is_disconnected"] = + (d, i) => d.NumberOfDisconnectedEndpoints = (int)i, ["iiot_edge_publisher_connection_retries"] = (d, i) => d.ConnectionRetries = (long)i, ["iiot_edge_publisher_subscriptions"] = diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherService.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherService.cs index d38f82a14f..809274dc7d 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherService.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherService.cs @@ -178,7 +178,7 @@ private async Task RunAsync(CancellationToken ct) { try { - await group.Value.DisposeAsync().ConfigureAwait(false); + group.Value.Dispose(); _logger.LogInformation("Writer group job {Job} stopped.", group.Key); } @@ -240,7 +240,7 @@ private async ValueTask ProcessChangesAsync(TaskCompletionSource task, { try { - await delete.DisposeAsync().ConfigureAwait(false); + delete.Dispose(); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -276,7 +276,7 @@ private async ValueTask ProcessChangesAsync(TaskCompletionSource task, /// /// Job context /// - private sealed class WriterGroupJob : IAsyncDisposable + private sealed class WriterGroupJob : IDisposable { /// /// Immutable writer group identifier @@ -337,7 +337,7 @@ public static async ValueTask CreateAsync(PublisherService outer catch (Exception ex) { outer._logger.LogError(ex, "Failed to create writer group job {Name}", context.Id); - await context.DisposeAsync().ConfigureAwait(false); + context.Dispose(); throw; } } @@ -373,11 +373,11 @@ public async ValueTask UpdateAsync(int version, WriterGroupModel writerGroup, } /// - public async ValueTask DisposeAsync() + public void Dispose() { try { - await Source.DisposeAsync().ConfigureAwait(false); + Source.Dispose(); } catch (Exception ex) { diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/RuntimeStateReporter.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/RuntimeStateReporter.cs index 5def8b3dce..38adc935f9 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/RuntimeStateReporter.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/RuntimeStateReporter.cs @@ -29,7 +29,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Services using System.Text; using System.Threading; using System.Threading.Tasks; - using Azure.IIoT.OpcUa.Publisher.Stack.Runtime; /// /// This class manages reporting of runtime state. @@ -473,24 +472,34 @@ private static void WriteDiagnosticsToConsole(IEnumerable<(string, WriterGroupDi StringBuilder Append(StringBuilder builder, string writerGroupId, WriterGroupDiagnosticModel info) { - var ingestionDuration = info.IngestionDuration; - var valueChangesPerSec = info.IngressValueChanges / ingestionDuration.TotalSeconds; - var dataChangesPerSec = info.IngressDataChanges / ingestionDuration.TotalSeconds; - var dataChangesLastMin = info.IngressDataChangesInLastMinute - .ToString("D2", CultureInfo.CurrentCulture); - var valueChangesPerSecLastMin = info.IngressValueChangesInLastMinute / - Math.Min(ingestionDuration.TotalSeconds, 60d); - var dataChangesPerSecLastMin = info.IngressDataChangesInLastMinute / - Math.Min(ingestionDuration.TotalSeconds, 60d); - - var dataChangesPerSecFormatted = info.IngressDataChanges > 0 && ingestionDuration.TotalSeconds > 0 - ? $"(All time ~{dataChangesPerSec:0.##}/s; {dataChangesLastMin} in last 60s ~{dataChangesPerSecLastMin:0.##}/s)" + var s = info.IngestionDuration.TotalSeconds == 0 ? 1 : info.IngestionDuration.TotalSeconds; + var min = info.IngestionDuration.TotalMinutes == 0 ? 1 : info.IngestionDuration.TotalMinutes; + + var eventsPerSec = info.IngressEvents / s; + var eventNotificationsPerSec = info.IngressEventNotifications / s; + + var dataChangesPerSecLastMin = info.IngressDataChangesInLastMinute / Math.Min(s, 60d); + var dataChangesPerSecFormatted = info.IngressDataChanges > 0 + ? $"(All time ~{info.IngressDataChanges / s:0.##}/s; {info.IngressDataChangesInLastMinute} in last 60s ~{dataChangesPerSecLastMin:0.##}/s)" + : string.Empty; + var valueChangesPerSecLastMin = info.IngressValueChangesInLastMinute / Math.Min(s, 60d); + var valueChangesPerSecFormatted = info.IngressValueChanges > 0 + ? $"(All time ~{info.IngressValueChanges / s:0.##}/s; {info.IngressValueChangesInLastMinute} in last 60s ~{valueChangesPerSecLastMin:0.##}/s)" + : string.Empty; + var sentMessagesPerSecFormatted = info.OutgressIoTMessageCount > 0 + ? $"({info.SentMessagesPerSec:0.##}/s)" + : string.Empty; + var keepAliveChangesPerSecFormatted = info.IngressKeepAliveNotifications > 0 + ? $"(All time ~{info.IngressKeepAliveNotifications / min:0.##}/min)" : string.Empty; - var valueChangesPerSecFormatted = info.IngressValueChanges > 0 && ingestionDuration.TotalSeconds > 0 - ? $"(All time ~{valueChangesPerSec:0.##}/s; {dataChangesLastMin} in last 60s ~{valueChangesPerSecLastMin:0.##}/s)" + var eventsPerSecFormatted = info.IngressEventNotifications > 0 + ? $"(All time ~{info.IngressEventNotifications / s:0.##}/s)" : string.Empty; - var sentMessagesPerSecFormatted = info.OutgressIoTMessageCount > 0 && ingestionDuration.TotalSeconds > 0 - ? $"({info.SentMessagesPerSec:0.##}/s)" : ""; + var eventNotificationsPerSecFormatted = info.IngressEventNotifications > 0 + ? $"(All time ~{info.IngressEventNotifications / s:0.##}/s)" + : string.Empty; + var connectivityState = info.NumberOfConnectedEndpoints > 0 ? (info.NumberOfDisconnectedEndpoints > 0 + ? "(Partially Connected)" : "(Connected)") : "(Disconnected)"; return builder.AppendLine() .Append(" DIAGNOSTICS INFORMATION for : ") @@ -501,11 +510,12 @@ StringBuilder Append(StringBuilder builder, string writerGroupId, .AppendFormat(CultureInfo.CurrentCulture, "{0,14:O}", info.Timestamp) .AppendLine() .Append(" # Ingestion duration : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:dd\\:hh\\:mm\\:ss}", ingestionDuration) + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:dd\\:hh\\:mm\\:ss}", info.IngestionDuration) .AppendLine(" (dd:hh:mm:ss)") - .Append(" # Opc endpoint connected? : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0}", info.OpcEndpointConnected) - .AppendLine() + .Append(" # Endpoints connected/disconnected : ") + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0}", info.NumberOfConnectedEndpoints).Append(" | ") + .AppendFormat(CultureInfo.CurrentCulture, "{0:0}", info.NumberOfDisconnectedEndpoints).Append(' ') + .AppendLine(connectivityState) .Append(" # Connection retries : ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:0}", info.ConnectionRetries) .AppendLine() @@ -530,17 +540,20 @@ StringBuilder Append(StringBuilder builder, string writerGroupId, .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressValueChanges).Append(' ') .AppendLine(valueChangesPerSecFormatted) .Append(" # Ingress events : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressEvents) + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressEvents).Append(' ') + .AppendLine(eventsPerSecFormatted) + .Append(" # Ingress values/events unassignable : ") + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressUnassignedChanges) .AppendLine() .Append(" # Received Data Change Notifications : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressDataChanges) - .Append(' ').AppendLine(dataChangesPerSecFormatted) + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressDataChanges).Append(' ') + .AppendLine(dataChangesPerSecFormatted) .Append(" # Received Event Notifications : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressEventNotifications) - .AppendLine() + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressEventNotifications).Append(' ') + .AppendLine(eventNotificationsPerSecFormatted) .Append(" # Received Keep Alive Notifications : ") - .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressKeepAliveNotifications) - .AppendLine() + .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressKeepAliveNotifications).Append(' ') + .AppendLine(keepAliveChangesPerSecFormatted) .Append(" # Generated Cyclic read Notifications: ") .AppendFormat(CultureInfo.CurrentCulture, "{0,14:n0}", info.IngressCyclicReads) .AppendLine() diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs index d094586a87..de0692f04a 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs @@ -22,11 +22,12 @@ namespace Azure.IIoT.OpcUa.Publisher.Services using System.Threading.Tasks; using System.Timers; using Timer = System.Timers.Timer; + using Opc.Ua.Client; /// /// Triggers dataset writer messages on subscription changes /// - public sealed class WriterGroupDataSource : IMessageSource, IDisposable + public sealed class WriterGroupDataSource : IMessageSource { /// public event EventHandler? OnMessage; @@ -76,8 +77,9 @@ public async ValueTask StartAsync(CancellationToken ct) foreach (var writer in _writerGroup.DataSetWriters ?? Enumerable.Empty()) { // Create writer subscriptions - var writerSubscription = await DataSetWriterSubscription.CreateAsync( - this, writer, ct).ConfigureAwait(false); +#pragma warning disable CA2000 // Dispose objects before losing scope + var writerSubscription = new DataSetWriterSubscription(this, writer); +#pragma warning restore CA2000 // Dispose objects before losing scope _subscriptions.AddOrUpdate(writerSubscription.Id, writerSubscription); } } @@ -100,7 +102,7 @@ public async ValueTask UpdateAsync(WriterGroupModel writerGroup, CancellationTok { foreach (var subscription in _subscriptions.Values) { - await subscription.DisposeAsync().ConfigureAwait(false); + subscription.Dispose(); } _logger.LogInformation("Removed all subscriptions from writer group {Name}.", writerGroup.WriterGroupId); @@ -133,7 +135,7 @@ public async ValueTask UpdateAsync(WriterGroupModel writerGroup, CancellationTok { if (_subscriptions.Remove(id, out var s)) { - await s.DisposeAsync().ConfigureAwait(false); + s.Dispose(); } } else @@ -141,7 +143,7 @@ public async ValueTask UpdateAsync(WriterGroupModel writerGroup, CancellationTok // Update if (_subscriptions.TryGetValue(id, out var s)) { - await s.UpdateAsync(writer, ct).ConfigureAwait(false); + s.Update(writer); } } } @@ -151,8 +153,9 @@ public async ValueTask UpdateAsync(WriterGroupModel writerGroup, CancellationTok if (!_subscriptions.ContainsKey(writer.Key)) { // Add - var writerSubscription = await DataSetWriterSubscription.CreateAsync( - this, writer.Value, ct).ConfigureAwait(false); +#pragma warning disable CA2000 // Dispose objects before losing scope + var writerSubscription = new DataSetWriterSubscription(this, writer.Value); +#pragma warning restore CA2000 // Dispose objects before losing scope _subscriptions.AddOrUpdate(writerSubscription.Id, writerSubscription); } } @@ -178,7 +181,11 @@ public void Dispose() { try { - DisposeAsync().AsTask().GetAwaiter().GetResult(); + foreach (var s in _subscriptions.Values) + { + s.Dispose(); + } + _subscriptions.Clear(); } finally { @@ -187,16 +194,6 @@ public void Dispose() } } - /// - public async ValueTask DisposeAsync() - { - foreach (var s in _subscriptions.Values) - { - await s.DisposeAsync().ConfigureAwait(false); - } - _subscriptions.Clear(); - } - /// /// Safe clone the writer group model /// @@ -260,7 +257,8 @@ private WriterGroupModel Copy(WriterGroupModel model) /// /// Helper to manage subscriptions /// - private sealed class DataSetWriterSubscription : IMetricsContext, IAsyncDisposable + private sealed class DataSetWriterSubscription : IDisposable, ISubscriptionCallbacks, + IMetricsContext { /// public TagList TagList { get; } @@ -273,14 +271,14 @@ private sealed class DataSetWriterSubscription : IMetricsContext, IAsyncDisposab /// /// Active subscription /// - public IOpcUaSubscription? Subscription { get; set; } + public ISubscriptionHandle? Subscription { get; set; } /// /// Create subscription from a DataSetWriterModel template /// /// /// - private DataSetWriterSubscription(WriterGroupDataSource outer, + public DataSetWriterSubscription(WriterGroupDataSource outer, DataSetWriterModel dataSetWriter) { _outer = outer ?? throw new ArgumentNullException(nameof(outer)); @@ -289,6 +287,9 @@ private DataSetWriterSubscription(WriterGroupDataSource outer, _subscriptionInfo = _dataSetWriter.ToSubscriptionModel( _outer._subscriptionConfig.Value, outer._writerGroup.WriterGroupId); + _outer._logger.LogDebug("Open new writer with subscription {Id} in writer group {Name}...", + Id, _outer._writerGroup.WriterGroupId ?? Constants.DefaultWriterGroupId); + var dataSetClassId = dataSetWriter.DataSet?.DataSetMetaData?.DataSetClassId ?? Guid.Empty; var builder = new TopicBuilder(_outer._options, new Dictionary { @@ -313,32 +314,29 @@ private DataSetWriterSubscription(WriterGroupDataSource outer, new KeyValuePair(Constants.DataSetWriterIdTag, dataSetWriter.DataSetWriterName) }; - } - /// - /// Create subscription - /// - /// - /// - /// - public static async ValueTask CreateAsync( - WriterGroupDataSource outer, DataSetWriterModel dataSetWriter, - CancellationToken ct) - { - var dataSetSubscription = new DataSetWriterSubscription(outer, dataSetWriter); + // + // Creating inner OPC UA subscription object. This will create a session + // if none already exist and transfer the subscription into the session + // management realm + // + _outer._subscriptionManager.CreateSubscription(_subscriptionInfo, this, this); - // Open subscription which creates the underlying OPC UA subscription - await dataSetSubscription.OpenAsync(ct).ConfigureAwait(false); + _frameCount = 0; + InitializeMetaDataTrigger(); + InitializeKeepAlive(); - return dataSetSubscription; + _metadataTimer?.Start(); + _outer._logger.LogInformation( + "New writer with subscription {Id} in writer group {Name} opened.", + Id, _outer._writerGroup.WriterGroupId ?? Constants.DefaultWriterGroupId); } /// /// Update subscription content /// /// - /// - public async ValueTask UpdateAsync(DataSetWriterModel dataSetWriter, CancellationToken ct) + public void Update(DataSetWriterModel dataSetWriter) { _outer._logger.LogDebug("Updating writer with subscription {Id} in writer group {Name}...", Id, _outer._writerGroup.WriterGroupId ?? Constants.DefaultWriterGroupId); @@ -347,7 +345,8 @@ public async ValueTask UpdateAsync(DataSetWriterModel dataSetWriter, Cancellatio _subscriptionInfo = _dataSetWriter.ToSubscriptionModel( _outer._subscriptionConfig.Value, _outer._writerGroup.WriterGroupId); - if (Subscription == null) + var subscription = Subscription; + if (subscription == null) { _outer._logger.LogWarning("Writer does not have a subscription to update yet!"); return; @@ -357,24 +356,25 @@ public async ValueTask UpdateAsync(DataSetWriterModel dataSetWriter, Cancellatio InitializeKeepAlive(); // Apply changes - await Subscription.UpdateAsync(_subscriptionInfo, ct).ConfigureAwait(false); + subscription.Update(_subscriptionInfo); _outer._logger.LogInformation("Updated subscription for writer {Id} in writer group {Name}.", Id, _outer._writerGroup.WriterGroupId ?? Constants.DefaultWriterGroupId); } /// - public async ValueTask DisposeAsync() + public void Dispose() { try { - if (Subscription == null) + if (_disposed) { return; } + _disposed = true; _metadataTimer?.Stop(); - await CloseAsync().ConfigureAwait(false); + Close(); } finally { @@ -383,72 +383,133 @@ public async ValueTask DisposeAsync() } } - /// - /// Open subscription - /// - /// - /// - private async ValueTask OpenAsync(CancellationToken ct) + /// + public void OnSubscriptionUpdated(ISubscriptionHandle? subscription) { - _outer._logger.LogDebug("Open new writer with subscription {Id} in writer group {Name}...", - Id, _outer._writerGroup.WriterGroupId ?? Constants.DefaultWriterGroupId); + Subscription = subscription; - // - // Creating inner OPC UA subscription object. This will create a session - // if none already exist and transfer the subscription into the session - // management realm - // - Subscription = await _outer._subscriptionManager.CreateSubscriptionAsync( - _subscriptionInfo, this, ct).ConfigureAwait(false); + if (subscription != null) + { + _outer._logger.LogInformation("Writer received updated subscription!"); + } + else + { + _outer._logger.LogInformation("Writer subscription removed after close!"); + } + } - _frameCount = 0; - InitializeMetaDataTrigger(); - InitializeKeepAlive(); + /// + public void OnSubscriptionDataChange(IOpcUaSubscriptionNotification notification) + { + CallMessageReceiverDelegates(ProcessKeyFrame(notification)); - Subscription.OnSubscriptionKeepAlive - += OnSubscriptionKeepAliveNotification; - Subscription.OnSubscriptionDataChange - += OnSubscriptionDataChangeNotification; - Subscription.OnSubscriptionEventChange - += OnSubscriptionEventNotification; - Subscription.OnSubscriptionDataDiagnosticsChange - += OnSubscriptionDataDiagnosticsChanged; - Subscription.OnSubscriptionEventDiagnosticsChange - += OnSubscriptionEventDiagnosticsChanged; + IOpcUaSubscriptionNotification ProcessKeyFrame(IOpcUaSubscriptionNotification notification) + { + var keyFrameCount = _dataSetWriter.KeyFrameCount + ?? _outer._subscriptionConfig.Value.DefaultKeyFrameCount ?? 0; + if (keyFrameCount > 0) + { + var frameCount = Interlocked.Increment(ref _frameCount); + if (((frameCount - 1) % keyFrameCount) == 0) + { + notification.TryUpgradeToKeyFrame(); + } + } + return notification; + } + } - _metadataTimer?.Start(); - _outer._logger.LogInformation("New writer with subscription {Id} in writer group {Name} opened.", - Id, _outer._writerGroup.WriterGroupId ?? Constants.DefaultWriterGroupId); + /// + public void OnSubscriptionKeepAlive(IOpcUaSubscriptionNotification notification) + { + Interlocked.Increment(ref _outer._keepAliveCount); + if (_sendKeepAlives) + { + CallMessageReceiverDelegates(notification); + } + } + + /// + public void OnSubscriptionDataDiagnosticsChange(bool liveData, int notificationCounts, + int heartbeat, int cyclic) + { + lock (_lock) + { + _outer._heartbeatsCount += heartbeat; + _outer._cyclicReadsCount += cyclic; + if (liveData) + { + if (_outer.DataChangesCount >= kNumberOfInvokedMessagesResetThreshold || + _outer.ValueChangesCount >= kNumberOfInvokedMessagesResetThreshold) + { + // reset both + _outer._logger.LogDebug( + "Notifications counter in subscription {Id} has been reset to prevent" + + " overflow. So far, {DataChangesCount} data changes and {ValueChangesCount} " + + "value changes were invoked by message source.", + Id, _outer.DataChangesCount, _outer.ValueChangesCount); + _outer.DataChangesCount = 0; + _outer.ValueChangesCount = 0; + _outer._heartbeatsCount = 0; + _outer._cyclicReadsCount = 0; + _outer.OnCounterReset?.Invoke(this, EventArgs.Empty); + } + + _outer.ValueChangesCount += notificationCounts; + _outer.DataChangesCount++; + } + } + } + + /// + public void OnSubscriptionEventChange(IOpcUaSubscriptionNotification notification) + { + CallMessageReceiverDelegates(notification); + } + + /// + public void OnSubscriptionEventDiagnosticsChange(bool liveData, int notificationCounts) + { + lock (_lock) + { + if (_outer._eventCount >= kNumberOfInvokedMessagesResetThreshold || + _outer._eventNotificationCount >= kNumberOfInvokedMessagesResetThreshold) + { + // reset both + _outer._logger.LogDebug( + "Notifications counter in subscription {Id} has been reset to prevent" + + " overflow. So far, {EventChangesCount} event changes and {EventValueChangesCount} " + + "event value changes were invoked by message source.", + Id, _outer._eventCount, _outer._eventNotificationCount); + _outer._eventCount = 0; + _outer._eventNotificationCount = 0; + _outer.OnCounterReset?.Invoke(this, EventArgs.Empty); + } + + _outer._eventNotificationCount += notificationCounts; + if (liveData) + { + _outer._eventCount++; + } + } } /// /// Close subscription /// /// - private async ValueTask CloseAsync() + private void Close() { - if (Subscription == null) + var subscription = Subscription; + if (subscription == null) { return; } _outer._logger.LogDebug("Closing writer with subscription {Id} in writer group {Name}...", Id, _outer._writerGroup.WriterGroupId ?? Constants.DefaultWriterGroupId); - await Subscription.CloseAsync().ConfigureAwait(false); - - Subscription.OnSubscriptionKeepAlive - -= OnSubscriptionKeepAliveNotification; - Subscription.OnSubscriptionDataChange - -= OnSubscriptionDataChangeNotification; - Subscription.OnSubscriptionEventChange - -= OnSubscriptionEventNotification; - Subscription.OnSubscriptionDataDiagnosticsChange - -= OnSubscriptionDataDiagnosticsChanged; - Subscription.OnSubscriptionEventDiagnosticsChange - -= OnSubscriptionEventDiagnosticsChanged; - - Subscription.Dispose(); - Subscription = null; + + subscription.Close(); _outer._logger.LogInformation("Writer with subscription {Id} in writer group {Name} closed.", Id, _outer._writerGroup.WriterGroupId ?? Constants.DefaultWriterGroupId); @@ -524,7 +585,7 @@ private void MetadataTimerElapsed(object? sender, ElapsedEventArgs e) if (notification != null) { // This call udpates the message type, so no need to do it here. - CallMessageReceiverDelegates(this, notification, true); + CallMessageReceiverDelegates(notification, true); } else { @@ -533,127 +594,13 @@ private void MetadataTimerElapsed(object? sender, ElapsedEventArgs e) } } - /// - /// Handle subscription data change messages - /// - /// - /// - private void OnSubscriptionDataChangeNotification(object? sender, IOpcUaSubscriptionNotification notification) - { - CallMessageReceiverDelegates(sender, ProcessKeyFrame(notification)); - - IOpcUaSubscriptionNotification ProcessKeyFrame(IOpcUaSubscriptionNotification notification) - { - var keyFrameCount = _dataSetWriter.KeyFrameCount - ?? _outer._subscriptionConfig.Value.DefaultKeyFrameCount ?? 0; - if (keyFrameCount > 0) - { - var frameCount = Interlocked.Increment(ref _frameCount); - if (((frameCount - 1) % keyFrameCount) == 0) - { - notification.TryUpgradeToKeyFrame(); - } - } - return notification; - } - } - - /// - /// Handle subscription keep alive messages - /// - /// - /// - private void OnSubscriptionKeepAliveNotification(object? sender, IOpcUaSubscriptionNotification notification) - { - Interlocked.Increment(ref _outer._keepAliveCount); - if (_sendKeepAlives) - { - CallMessageReceiverDelegates(sender, notification); - } - } - - /// - /// Handle subscription data diagnostics change messages - /// - /// - /// - private void OnSubscriptionDataDiagnosticsChanged(object? sender, (bool, int, int, int) notificationCounts) - { - lock (_lock) - { - _outer._heartbeatsCount += notificationCounts.Item3; - _outer._cyclicReadsCount += notificationCounts.Item4; - if (notificationCounts.Item1) - { - if (_outer.DataChangesCount >= kNumberOfInvokedMessagesResetThreshold || - _outer.ValueChangesCount >= kNumberOfInvokedMessagesResetThreshold) - { - // reset both - _outer._logger.LogDebug("Notifications counter in subscription {Id} has been reset to prevent" + - " overflow. So far, {DataChangesCount} data changes and {ValueChangesCount} " + - "value changes were invoked by message source.", - Id, _outer.DataChangesCount, _outer.ValueChangesCount); - _outer.DataChangesCount = 0; - _outer.ValueChangesCount = 0; - _outer._heartbeatsCount = 0; - _outer._cyclicReadsCount = 0; - _outer.OnCounterReset?.Invoke(this, EventArgs.Empty); - } - - _outer.ValueChangesCount += notificationCounts.Item2; - _outer.DataChangesCount++; - } - } - } - - /// - /// Handle subscription change messages - /// - /// - /// - private void OnSubscriptionEventNotification(object? sender, IOpcUaSubscriptionNotification notification) - { - CallMessageReceiverDelegates(sender, notification); - } - - /// - /// Handle subscription event diagnostics change messages - /// - /// - /// - private void OnSubscriptionEventDiagnosticsChanged(object? sender, (bool, int) notificationCounts) - { - lock (_lock) - { - if (_outer._eventCount >= kNumberOfInvokedMessagesResetThreshold || - _outer._eventNotificationCount >= kNumberOfInvokedMessagesResetThreshold) - { - // reset both - _outer._logger.LogDebug("Notifications counter in subscription {Id} has been reset to prevent" + - " overflow. So far, {EventChangesCount} event changes and {EventValueChangesCount} " + - "event value changes were invoked by message source.", - Id, _outer._eventCount, _outer._eventNotificationCount); - _outer._eventCount = 0; - _outer._eventNotificationCount = 0; - _outer.OnCounterReset?.Invoke(this, EventArgs.Empty); - } - - _outer._eventNotificationCount += notificationCounts.Item2; - if (notificationCounts.Item1) - { - _outer._eventCount++; - } - } - } - /// /// handle subscription change messages /// - /// /// /// - private void CallMessageReceiverDelegates(object? sender, - IOpcUaSubscriptionNotification notification, bool metaDataTimer = false) + private void CallMessageReceiverDelegates(IOpcUaSubscriptionNotification notification, + bool metaDataTimer = false) { try { @@ -683,7 +630,7 @@ private void CallMessageReceiverDelegates(object? sender, () => Interlocked.Increment(ref _metadataSequenceNumber)) }; #pragma warning restore CA2000 // Dispose objects before losing scope - _outer.OnMessage?.Invoke(sender, metadata); + _outer.OnMessage?.Invoke(this, metadata); InitializeMetaDataTrigger(); } } @@ -695,7 +642,7 @@ private void CallMessageReceiverDelegates(object? sender, () => Interlocked.Increment(ref _dataSetSequenceNumber)); _outer._logger.LogTrace("Enqueuing notification: {Notification}", notification.ToString()); - _outer.OnMessage?.Invoke(sender, notification); + _outer.OnMessage?.Invoke(this, notification); } } } @@ -761,7 +708,7 @@ public sealed record class MetadataNotificationModel : public object? Context { get; set; } /// - public IServiceMessageContext? ServiceMessageContext { get; set; } + public IServiceMessageContext ServiceMessageContext { get; set; } /// public IList Notifications { get; } @@ -808,6 +755,7 @@ public void Dispose() private uint _currentMetadataMajorVersion; private uint _currentMetadataMinorVersion; private bool _sendKeepAlives; + private bool _disposed; } /// diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/SubscriptionConfigurationModelEx.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/SubscriptionConfigurationModelEx.cs deleted file mode 100644 index 57cd23f289..0000000000 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/SubscriptionConfigurationModelEx.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. -// ------------------------------------------------------------ - -namespace Azure.IIoT.OpcUa.Publisher.Stack.Models -{ - using Azure.IIoT.OpcUa.Publisher.Models; - using System.Diagnostics.CodeAnalysis; - - /// - /// Subscription model extensions - /// - public static class SubscriptionConfigurationModelEx - { - /// - /// Clone - /// - /// - /// - [return: NotNullIfNotNull(nameof(model))] - public static SubscriptionConfigurationModel? Clone(this SubscriptionConfigurationModel? model) - { - return model == null ? null : (model with - { - MetaData = model.MetaData.Clone() - }); - } - } -} diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/SubscriptionModelEx.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/SubscriptionModelEx.cs deleted file mode 100644 index 39e65a9a7d..0000000000 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/SubscriptionModelEx.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. -// ------------------------------------------------------------ - -namespace Azure.IIoT.OpcUa.Publisher.Stack.Models -{ - using System.Diagnostics.CodeAnalysis; - using System.Linq; - - /// - /// Subscription model extensions - /// - public static class SubscriptionModelEx - { - /// - /// Clone - /// - /// - /// - [return: NotNullIfNotNull(nameof(model))] - public static SubscriptionModel? Clone(this SubscriptionModel? model) - { - return model == null ? null : (model with - { - Configuration = model.Configuration.Clone(), - Id = model.Id.Clone(), - MonitoredItems = model.MonitoredItems?.ToList() - }); - } - - /// - /// Clone id - /// - /// - /// - [return: NotNullIfNotNull(nameof(model))] - public static SubscriptionIdentifier? Clone(this SubscriptionIdentifier? model) - { - if (model is null) - { - return null; - } - return new SubscriptionIdentifier(model.Connection, model.Id); - } - } -} diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IClientAccessor.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IClientAccessor.cs index 108f034c25..18ae7925bc 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IClientAccessor.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IClientAccessor.cs @@ -18,15 +18,5 @@ internal interface IClientAccessor : IClientSampler /// /// IOpcUaClient GetOrCreateClient(T connection); - - /// - /// Get a client handle for a connection or null - /// if the client does not exist. The session might - /// be disconnected at point it is returned. The client - /// handle must be disposed when not used anymore. - /// - /// - /// - IOpcUaClient? GetClient(T connection); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClient.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClient.cs index d63259a61f..21f162f239 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClient.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClient.cs @@ -33,33 +33,12 @@ internal interface IOpcUaClient : IDisposable ValueTask GetSessionHandleAsync( CancellationToken ct = default); - /// - /// Register a subscription which takes a reference on the session - /// handle. Must be unregistered to release the reference count. - /// Reference count going to 1 means that the connect thread is - /// started to unblock the writer lock on the session once connected. - /// Once the session is connected the subcription state is applied. - /// If the session is already connected it is applied inline. - /// - /// - /// - void RegisterSubscription(ISubscriptionHandle subscription); - /// /// Trigger the client to manage the subscription. This is a /// no op if the subscription is not registered or the client /// is not connected. /// /// - void ManageSubscription(ISubscriptionHandle subscription); - - /// - /// Removes a subscription and releases the reference count. If the - /// refernce count goes to 0 the session is disconnected and the - /// writer lock is aquired until it is going back to 1 or higher. - /// - /// - /// - void UnregisterSubscription(ISubscriptionHandle subscription); + void ManageSubscription(IOpcUaSubscription subscription); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientState.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientDiagnostics.cs similarity index 88% rename from src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientState.cs rename to src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientDiagnostics.cs index e970ab72ac..379cc59292 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientState.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientDiagnostics.cs @@ -5,10 +5,12 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack { + using Azure.IIoT.OpcUa.Publisher.Models; + /// /// Safely access client state for diagnostics /// - internal interface IOpcUaClientState + internal interface IOpcUaClientDiagnostics { /// /// Bad publish requests tracked by this client @@ -31,9 +33,9 @@ internal interface IOpcUaClientState int SubscriptionCount { get; } /// - /// Is connected + /// Connectivity state /// - bool IsConnected { get; } + EndpointConnectivityState State { get; } /// /// Connection attempts diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs index acdd6e2d58..0a59474b2f 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs @@ -35,15 +35,15 @@ public delegate void UpdateNodeId(NodeId nodeId, public interface IOpcUaMonitoredItem : IDisposable { /// - /// Monitored item once added to the subscription. Contract: - /// The item will be null until the subscription calls + /// The item is valid once added to the subscription. Contract: + /// The item will be invalid until the subscription calls /// /// to add it to the subscription. After removal the item - /// is still valid, but the Handle is null. The item is - /// again null after is + /// is still Valid, but not Created. The item is + /// again invalid after is /// called. /// - MonitoredItem? Item { get; } + bool Valid { get; } /// /// Data set name @@ -67,7 +67,7 @@ public interface IOpcUaMonitoredItem : IDisposable /// Get the display name for the node. This is called after /// the node is resolved and registered as applicable. /// - (string NodeId, UpdateString Update)? DisplayName { get; } + (string NodeId, UpdateString Update)? GetDisplayName { get; } /// /// Add the item to the subscription @@ -127,12 +127,6 @@ ValueTask GetMetaDataAsync(IOpcUaSession session, NodeIdDictionary dataTypes, CancellationToken ct); - /// - /// Subscription state changed - /// - /// - void OnMonitoredItemStateChanged(bool online); - /// /// Try get monitored item notifications from /// the subscription's monitored item event payload. diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscription.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscription.cs index 2f53a41c6d..88ee0dec6c 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscription.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscription.cs @@ -5,79 +5,34 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack { - using Azure.IIoT.OpcUa.Publisher.Stack.Models; - using Azure.IIoT.OpcUa.Publisher.Models; - using System; + using Opc.Ua.Client; using System.Threading; using System.Threading.Tasks; /// - /// Subscription abstraction + /// The opc ua subscription is an internal interface + /// between opc ua client and the subscription owned + /// by the client. /// - public interface IOpcUaSubscription : IDisposable + internal interface IOpcUaSubscription { /// - /// Subscription keep alive events + /// Apply the current subscription configuration to + /// the session. /// - event EventHandler? OnSubscriptionKeepAlive; - - /// - /// Subscription data change events - /// - event EventHandler? OnSubscriptionDataChange; - - /// - /// Subscription event change events - /// - event EventHandler? OnSubscriptionEventChange; - - /// - /// Subscription data change diagnostics events - /// - event EventHandler<(bool, int, int, int)>? OnSubscriptionDataDiagnosticsChange; - - /// - /// Subscription event change diagnostics events - /// - event EventHandler<(bool, int)>? OnSubscriptionEventDiagnosticsChange; - - /// - /// Identifier of the subscription - /// - string? Name { get; } - - /// - /// Assigned index - /// - ushort Id { get; } - - /// - /// Connection - /// - ConnectionModel? Connection { get; } - - /// - /// Create a keep alive notification - /// - /// - IOpcUaSubscriptionNotification? CreateKeepAlive(); - - /// - /// Apply desired state of the subscription and its monitored items. - /// This will attempt a differential update of the subscription - /// and monitored items state. It is called periodically, when the - /// configuration is updated or when a session is reconnected and - /// the subscription needs to be recreated. - /// - /// + /// /// - ValueTask UpdateAsync(SubscriptionModel configuration, + /// + ValueTask SyncWithSessionAsync(ISession session, CancellationToken ct = default); /// - /// Close and delete subscription + /// Try get the current position in the out stream. /// + /// + /// /// - ValueTask CloseAsync(); + bool TryGetCurrentPosition(out uint subscriptionId, + out uint sequenceNumber); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionManager.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionManager.cs index 1a584795dd..35c64ed412 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionManager.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionManager.cs @@ -6,8 +6,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack { using Azure.IIoT.OpcUa.Publisher.Stack.Models; - using System.Threading; - using System.Threading.Tasks; /// /// Subscription manager @@ -15,14 +13,15 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack public interface IOpcUaSubscriptionManager { /// - /// Get or create new subscription + /// Create new subscription with the subscription model + /// The callback will have been called with the new subscription + /// which then can be used to manage the subscription. /// - /// - /// - /// + /// The subscription template + /// Callbacks from the subscription + /// Additional metrics information /// - ValueTask CreateSubscriptionAsync( - SubscriptionModel subscription, IMetricsContext metrics, - CancellationToken ct = default); + void CreateSubscription(SubscriptionModel subscription, + ISubscriptionCallbacks callback, IMetricsContext metrics); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionNotification.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionNotification.cs index 750d8b9625..04817d9666 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionNotification.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionNotification.cs @@ -24,7 +24,7 @@ public interface IOpcUaSubscriptionNotification : IDisposable /// /// Service message context /// - IServiceMessageContext? ServiceMessageContext { get; } + IServiceMessageContext ServiceMessageContext { get; } /// /// Notification diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISessionServices.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISessionServices.cs index 0b657b3eb2..13bef75257 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISessionServices.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISessionServices.cs @@ -22,7 +22,7 @@ public interface ISessionServices /// /// /// - Task AddNodesAsync(RequestHeader requestHeader, + ValueTask AddNodesAsync(RequestHeader requestHeader, AddNodesItemCollection nodesToAdd, CancellationToken ct); /// @@ -32,7 +32,7 @@ Task AddNodesAsync(RequestHeader requestHeader, /// /// /// - Task AddReferencesAsync(RequestHeader requestHeader, + ValueTask AddReferencesAsync(RequestHeader requestHeader, AddReferencesItemCollection referencesToAdd, CancellationToken ct); /// @@ -44,7 +44,7 @@ Task AddReferencesAsync(RequestHeader requestHeader, /// /// /// - Task BrowseAsync(RequestHeader requestHeader, + ValueTask BrowseAsync(RequestHeader requestHeader, ViewDescription? view, uint requestedMaxReferencesPerNode, BrowseDescriptionCollection nodesToBrowse, CancellationToken ct); @@ -56,7 +56,7 @@ Task BrowseAsync(RequestHeader requestHeader, /// /// /// - Task BrowseNextAsync(RequestHeader requestHeader, + ValueTask BrowseNextAsync(RequestHeader requestHeader, bool releaseContinuationPoints, ByteStringCollection continuationPoints, CancellationToken ct); @@ -67,7 +67,7 @@ Task BrowseNextAsync(RequestHeader requestHeader, /// /// /// - Task CallAsync(RequestHeader requestHeader, + ValueTask CallAsync(RequestHeader requestHeader, CallMethodRequestCollection methodsToCall, CancellationToken ct); /// @@ -77,7 +77,7 @@ Task CallAsync(RequestHeader requestHeader, /// /// /// - Task DeleteNodesAsync(RequestHeader requestHeader, + ValueTask DeleteNodesAsync(RequestHeader requestHeader, DeleteNodesItemCollection nodesToDelete, CancellationToken ct); /// @@ -87,7 +87,7 @@ Task DeleteNodesAsync(RequestHeader requestHeader, /// /// /// - Task DeleteReferencesAsync(RequestHeader requestHeader, + ValueTask DeleteReferencesAsync(RequestHeader requestHeader, DeleteReferencesItemCollection referencesToDelete, CancellationToken ct); /// @@ -100,7 +100,7 @@ Task DeleteReferencesAsync(RequestHeader requestHeader /// /// /// - Task HistoryReadAsync(RequestHeader requestHeader, + ValueTask HistoryReadAsync(RequestHeader requestHeader, ExtensionObject? historyReadDetails, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, HistoryReadValueIdCollection nodesToRead, CancellationToken ct); @@ -112,7 +112,7 @@ Task HistoryReadAsync(RequestHeader requestHeader, /// /// /// - Task HistoryUpdateAsync(RequestHeader requestHeader, + ValueTask HistoryUpdateAsync(RequestHeader requestHeader, ExtensionObjectCollection historyUpdateDetails, CancellationToken ct); /// @@ -126,7 +126,7 @@ Task HistoryUpdateAsync(RequestHeader requestHeader, /// /// /// - Task QueryFirstAsync(RequestHeader requestHeader, + ValueTask QueryFirstAsync(RequestHeader requestHeader, ViewDescription view, NodeTypeDescriptionCollection nodeTypes, ContentFilter filter, uint maxDataSetsToReturn, uint maxReferencesToReturn, CancellationToken ct); @@ -138,7 +138,7 @@ Task QueryFirstAsync(RequestHeader requestHeader, /// /// /// - Task QueryNextAsync(RequestHeader requestHeader, + ValueTask QueryNextAsync(RequestHeader requestHeader, bool releaseContinuationPoint, byte[] continuationPoint, CancellationToken ct); /// @@ -150,7 +150,7 @@ Task QueryNextAsync(RequestHeader requestHeader, /// /// /// - Task ReadAsync(RequestHeader requestHeader, double maxAge, + ValueTask ReadAsync(RequestHeader requestHeader, double maxAge, TimestampsToReturn timestampsToReturn, ReadValueIdCollection nodesToRead, CancellationToken ct); @@ -161,9 +161,19 @@ Task ReadAsync(RequestHeader requestHeader, double maxAge, /// /// /// - Task RegisterNodesAsync(RequestHeader requestHeader, + ValueTask RegisterNodesAsync(RequestHeader requestHeader, NodeIdCollection nodesToRegister, CancellationToken ct); + /// + /// Unregister nodes + /// + /// + /// + /// + /// + ValueTask UnregisterNodesAsync(RequestHeader requestHeader, + NodeIdCollection nodesToUnregister, CancellationToken ct); + /// /// Translate browse paths /// @@ -171,7 +181,7 @@ Task RegisterNodesAsync(RequestHeader requestHeader, /// /// /// - Task TranslateBrowsePathsToNodeIdsAsync( + ValueTask TranslateBrowsePathsToNodeIdsAsync( RequestHeader requestHeader, BrowsePathCollection browsePaths, CancellationToken ct); @@ -182,7 +192,7 @@ Task TranslateBrowsePathsToNodeIdsAsync( /// /// /// - Task WriteAsync(RequestHeader requestHeader, + ValueTask WriteAsync(RequestHeader requestHeader, WriteValueCollection nodesToWrite, CancellationToken ct); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionCallbacks.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionCallbacks.cs new file mode 100644 index 0000000000..7e9c46e2d1 --- /dev/null +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionCallbacks.cs @@ -0,0 +1,59 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +namespace Azure.IIoT.OpcUa.Publisher.Stack +{ + /// + /// Subscription callbacks + /// + public interface ISubscriptionCallbacks + { + /// + /// Called when the subscription is updated + /// + /// + public void OnSubscriptionUpdated( + ISubscriptionHandle? subscriptionHandle); + + /// + /// Called when a keep alive notification is received + /// + /// + public void OnSubscriptionKeepAlive( + IOpcUaSubscriptionNotification notification); + + /// + /// Called when subscription data changes + /// + /// + public void OnSubscriptionDataChange( + IOpcUaSubscriptionNotification notification); + + /// + /// Called when event changes + /// + /// + public void OnSubscriptionEventChange( + IOpcUaSubscriptionNotification notification); + + /// + /// Diagnostics for data change notifications + /// + /// + /// + /// + /// + void OnSubscriptionDataDiagnosticsChange(bool liveData, + int valueChanges, int heartbeats, int cyclicReads); + + /// + /// Event diagnostics + /// + /// + /// + void OnSubscriptionEventDiagnosticsChange(bool liveData, + int events); + } +} diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs index 8eafb7e66c..e46a8ad582 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs @@ -5,46 +5,47 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack { + using Azure.IIoT.OpcUa.Publisher.Stack.Models; using System.Threading; using System.Threading.Tasks; /// - /// The subscription handle is an internal interface - /// between opc ua client and the subscription owned - /// by the client. + /// Subscription handle is a safe abstraction that allows the owner of the + /// subscription to update and control without requiring access to the + /// underlying state in the opc ua client session. /// - internal interface ISubscriptionHandle + public interface ISubscriptionHandle { /// - /// Apply the current subscription configuration to - /// the session. + /// Identifier of the subscription + /// + string Name { get; } + + /// + /// Assigned index + /// + ushort LocalIndex { get; } + + /// + /// Create a keep alive notification /// - /// - /// - /// /// - ValueTask SyncWithSessionAsync(IOpcUaSession session, - bool sessionIsNew, CancellationToken ct = default); + IOpcUaSubscriptionNotification? CreateKeepAlive(); /// - /// Called to signal the underlying session is - /// disconnected and the subscription is offline, or - /// when it is reconnected and the session is back online. - /// This is the case during reconnect handler execution - /// or when the subscription was disconnected. + /// Apply desired state of the subscription and its monitored items. + /// This will attempt a differential update of the subscription + /// and monitored items state. It is called periodically, when the + /// configuration is updated or when a session is reconnected and + /// the subscription needs to be recreated. /// - /// - /// - void OnSubscriptionStateChanged(bool online, - IOpcUaClientState state); + /// + void Update(SubscriptionModel configuration); /// - /// Try get the current position in the out stream. + /// Close and delete subscription /// - /// - /// /// - bool TryGetCurrentPosition(out uint subscriptionId, - out uint sequenceNumber); + void Close(); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/SubscriptionNotificationModel.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/SubscriptionNotificationModel.cs index 72ac6fa26b..094752eef0 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/SubscriptionNotificationModel.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/SubscriptionNotificationModel.cs @@ -53,7 +53,7 @@ public sealed record class SubscriptionNotificationModel : public object? Context { get; set; } /// - public IServiceMessageContext? ServiceMessageContext { get; set; } + public IServiceMessageContext ServiceMessageContext { get; set; } = null!; /// public IList Notifications { get; set; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs index 3722d68cc6..97813ab069 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs @@ -11,8 +11,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Runtime using System; using System.Globalization; using System.IO; - using System.Reflection; - using System.Runtime.InteropServices; using System.Text; /// diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs index 3a69d742ec..0f16c33641 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs @@ -26,13 +26,19 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Services using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; + using System.Security.Cryptography.X509Certificates; /// /// OPC UA Client based on official ua client reference sample. /// - internal sealed class OpcUaClient : IOpcUaClient, ISessionAccessor, - IOpcUaClientState + internal sealed class OpcUaClient : DefaultSessionFactory, IOpcUaClient, ISessionAccessor, + IOpcUaClientDiagnostics { + /// + /// Client namespace + /// + internal const string Namespace = "http://opcfoundation.org/UA/Client/Types.xsd"; + /// /// The session keepalive interval to be used in ms. /// @@ -78,29 +84,35 @@ internal sealed class OpcUaClient : IOpcUaClient, ISessionAccessor, /// public bool? DisableComplexTypePreloading { get; set; } - /// + /// + /// Client is connected + /// public bool IsConnected - => _session?.Session.Connected ?? false; + => _session?.Connected ?? false; + + /// + public EndpointConnectivityState State + => _lastState; /// public int BadPublishRequestCount - => _session?.Session?.DefunctRequestCount ?? 0; + => _session?.DefunctRequestCount ?? 0; /// public int GoodPublishRequestCount - => _session?.Session?.GoodPublishRequestCount ?? 0; + => _session?.GoodPublishRequestCount ?? 0; /// public int OutstandingRequestCount - => _session?.Session?.OutstandingRequestCount ?? 0; + => _session?.OutstandingRequestCount ?? 0; /// public int SubscriptionCount - => _session?.Session?.Subscriptions.Count(s => s.Created) ?? 0; + => _session?.Subscriptions.Count(s => s.Created) ?? 0; /// public int MinPublishRequestCount - => _session?.Session?.MinPublishRequestCount ?? 0; + => _session?.MinPublishRequestCount ?? 0; /// public int ReconnectCount => _numberOfConnectRetries; @@ -108,7 +120,7 @@ public int MinPublishRequestCount /// /// Disconnected state /// - internal static IOpcUaClientState Disconnected { get; } + internal static IOpcUaClientDiagnostics Disconnected { get; } = new DisconnectState(); /// @@ -121,7 +133,6 @@ public int MinPublishRequestCount /// /// /// - /// /// /// /// @@ -130,7 +141,7 @@ public OpcUaClient(ApplicationConfiguration configuration, ConnectionIdentifier connection, IJsonSerializer serializer, ILoggerFactory loggerFactory, Meter meter, IMetricsContext metrics, EventHandler? notifier, - ISessionFactory sessionFactory, ReverseConnectManager? reverseConnectManager, + ReverseConnectManager? reverseConnectManager, TimeSpan? maxReconnectPeriod = null, string? sessionName = null) { if (connection?.Connection?.Endpoint?.Url == null) @@ -152,8 +163,6 @@ public OpcUaClient(ApplicationConfiguration configuration, throw new ArgumentNullException(nameof(serializer)); _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - _sessionFactory = sessionFactory ?? - throw new ArgumentNullException(nameof(sessionFactory)); _notifier = notifier; InitializeMetrics(); @@ -162,7 +171,6 @@ public OpcUaClient(ApplicationConfiguration configuration, _tokens = new Dictionary(); _lastState = EndpointConnectivityState.Disconnected; _sessionName = sessionName ?? connection.ToString(); - _subscriptions = ImmutableHashSet.Empty; _maxReconnectPeriod = maxReconnectPeriod ?? TimeSpan.Zero; if (_maxReconnectPeriod == TimeSpan.Zero) { @@ -204,64 +212,102 @@ public async ValueTask GetSessionHandleAsync( /// public bool TryGetSession([NotNullWhen(true)] out ISession? session) { - session = _session?.Session; + session = _session; return session != null; } /// - public void RegisterSubscription(ISubscriptionHandle subscription) + public void ManageSubscription(IOpcUaSubscription subscription) { - try - { - lock (_subscriptionsLock) - { - if (_subscriptions.Contains(subscription)) - { - return; - } - _subscriptions = _subscriptions.Add(subscription); - } - AddRef(); - } - finally - { - TriggerConnectionEvent(ConnectionEvent.SubscriptionChange, - subscription); - } + TriggerConnectionEvent(ConnectionEvent.SubscriptionManage, subscription); } /// - public void ManageSubscription(ISubscriptionHandle subscription) + public override Session Create(ISessionChannel channel, ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint) { - TriggerConnectionEvent(ConnectionEvent.SubscriptionManage, - subscription); + return new OpcUaSession(this, _serializer, _loggerFactory.CreateLogger(), + (ITransportChannel)channel, configuration, endpoint); } /// - public void UnregisterSubscription(ISubscriptionHandle subscription) + public override Session Create(ITransportChannel channel, ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, X509Certificate2? clientCertificate, + EndpointDescriptionCollection? availableEndpoints, + StringCollection? discoveryProfileUris) { - try - { - lock (_subscriptionsLock) - { - if (!_subscriptions.Contains(subscription)) - { - _logger.LogWarning("Subscription {Subscription} not found in {Client}.", - subscription, this); - return; - } - _subscriptions = _subscriptions.Remove(subscription); - } - Release(); + return new OpcUaSession(this, _serializer, _loggerFactory.CreateLogger(), + channel, configuration, endpoint, + clientCertificate, availableEndpoints, discoveryProfileUris); + } - _logger.LogDebug( - "Subscription {Subscription} unregistered from {Client} (remaining:{Now}).", - subscription, this, _subscriptions.Count); - } - finally + /// + public override Task CreateAsync(ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, bool updateBeforeConnect, string sessionName, + uint sessionTimeout, IUserIdentity identity, IList preferredLocales, + CancellationToken ct) + { + return CreateAsync(configuration, endpoint, updateBeforeConnect, false, + sessionName, sessionTimeout, identity, preferredLocales, ct); + } + + /// + public async override Task CreateAsync(ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, bool updateBeforeConnect, bool checkDomain, + string sessionName, uint sessionTimeout, IUserIdentity identity, IList preferredLocales, + CancellationToken ct) + { + return await Session.Create(this, configuration, (ITransportWaitingConnection?)null, endpoint, + updateBeforeConnect, checkDomain, sessionName, sessionTimeout, + identity, preferredLocales, ct).ConfigureAwait(false); + } + + /// + public async override Task CreateAsync(ApplicationConfiguration configuration, + ITransportWaitingConnection connection, ConfiguredEndpoint endpoint, bool updateBeforeConnect, + bool checkDomain, string sessionName, uint sessionTimeout, IUserIdentity identity, + IList preferredLocales, CancellationToken ct) + { + return await Session.Create(this, configuration, connection, endpoint, updateBeforeConnect, + checkDomain, sessionName, sessionTimeout, identity, preferredLocales, ct).ConfigureAwait(false); + } + + /// + public override ISession Create(ApplicationConfiguration configuration, ITransportChannel channel, + ConfiguredEndpoint endpoint, X509Certificate2 clientCertificate, + EndpointDescriptionCollection? availableEndpoints, StringCollection? discoveryProfileUris) + { + return Session.Create(this, configuration, channel, endpoint, clientCertificate, + availableEndpoints, discoveryProfileUris); + } + + /// + public async override Task CreateAsync(ApplicationConfiguration configuration, + ReverseConnectManager? reverseConnectManager, ConfiguredEndpoint endpoint, bool updateBeforeConnect, + bool checkDomain, string sessionName, uint sessionTimeout, IUserIdentity userIdentity, + IList preferredLocales, CancellationToken ct) + { + if (reverseConnectManager == null) { - TriggerConnectionEvent(ConnectionEvent.SubscriptionChange); + return await CreateAsync(configuration, endpoint, updateBeforeConnect, + checkDomain, sessionName, sessionTimeout, userIdentity, preferredLocales, ct).ConfigureAwait(false); } + ITransportWaitingConnection? connection; + do + { + connection = await reverseConnectManager.WaitForConnection(endpoint.EndpointUrl, + endpoint.ReverseConnect?.ServerUri, ct).ConfigureAwait(false); + if (updateBeforeConnect) + { + await endpoint.UpdateFromServerAsync(endpoint.EndpointUrl, connection, + endpoint.Description.SecurityMode, endpoint.Description.SecurityPolicyUri, + ct).ConfigureAwait(false); + updateBeforeConnect = false; + connection = null; + } + } while (connection == null); + return await CreateAsync(configuration, connection, endpoint, false, checkDomain, + sessionName, sessionTimeout, userIdentity, preferredLocales, ct).ConfigureAwait(false); } /// @@ -575,10 +621,12 @@ internal void Release(string? token = null) private async Task ManageSessionStateMachineAsync(CancellationToken ct) { var currentSessionState = SessionState.Disconnected; - var currentSubscriptions = ImmutableHashSet.Empty; + IReadOnlyList currentSubscriptions; + var queuedSubscriptions = new HashSet(); var reconnectPeriod = 0; var reconnectTimer = new Timer(_ => TriggerConnectionEvent(ConnectionEvent.ConnectRetry)); + currentSubscriptions = Array.Empty(); await using (reconnectTimer.ConfigureAwait(false)) { try @@ -622,8 +670,13 @@ private async Task ManageSessionStateMachineAsync(CancellationToken ct) _disconnectLock.Dispose(); _disconnectLock = null; - currentSubscriptions = _subscriptions; - await ApplySubscriptionAsync(currentSubscriptions, true, true, + currentSubscriptions = _session.SubscriptionHandles; + // + // Equality is through subscriptionidentifer therefore only subscriptions + // that are not yet created inside the session remain in queued state. + // + queuedSubscriptions.ExceptWith(currentSubscriptions); + await ApplySubscriptionAsync(currentSubscriptions, queuedSubscriptions, ct).ConfigureAwait(false); currentSessionState = SessionState.Connected; @@ -638,34 +691,20 @@ await ApplySubscriptionAsync(currentSubscriptions, true, true, } break; - case ConnectionEvent.SubscriptionChange: // Sent when the subscription list changed - var changedSubscription = context as ISubscriptionHandle; - var subscriptions = _subscriptions; // Snapshot the list of subscriptions - switch (currentSessionState) - { - case SessionState.Connected: - // What changed? - var diff = currentSubscriptions.Except(subscriptions); - if (changedSubscription != null && - subscriptions.Contains(changedSubscription) && - !diff.Contains(changedSubscription)) - { - diff = diff.Add(changedSubscription); - } - await ApplySubscriptionAsync(diff, cancellationToken: ct).ConfigureAwait(false); - currentSubscriptions = subscriptions; - break; - } - break; - case ConnectionEvent.SubscriptionManage: + var item = context as IOpcUaSubscription; + Debug.Assert(item != null); switch (currentSessionState) { case SessionState.Connected: - var item = context as ISubscriptionHandle; - Debug.Assert(item != null); - var diff = ImmutableHashSet.Create(item); - await ApplySubscriptionAsync(diff, cancellationToken: ct).ConfigureAwait(false); + queuedSubscriptions.Remove(item); + await ApplySubscriptionAsync(new[] { item }, queuedSubscriptions, + cancellationToken: ct).ConfigureAwait(false); + break; + case SessionState.Disconnected: + break; + default: + queuedSubscriptions.Add(item); break; } break; @@ -676,16 +715,13 @@ await ApplySubscriptionAsync(currentSubscriptions, true, true, case SessionState.Connected: // only valid when connected. Debug.Assert(_reconnectHandler.State == SessionReconnectHandler.ReconnectState.Ready); - await ApplySubscriptionAsync(currentSubscriptions, false, false, - ct).ConfigureAwait(false); - // Ensure no more access to the session through reader locks Debug.Assert(_disconnectLock == null); _disconnectLock = await _lock.WriterLockAsync(ct); _logger.LogInformation("Reconnecting session {Session} due to error {Error}...", _sessionName, context as ServiceResult); - var state = _reconnectHandler.BeginReconnect(_session!.Session, + var state = _reconnectHandler.BeginReconnect(_session, _reverseConnectManager, GetMinReconnectPeriod(), (sender, evt) => { if (ReferenceEquals(sender, _reconnectHandler)) @@ -727,7 +763,7 @@ await ApplySubscriptionAsync(currentSubscriptions, false, false, // if (reconnected == null) { - reconnected = _reconnectingSession?.Session; + reconnected = _reconnectingSession; } Debug.Assert(reconnected != null, "reconnected should never be null"); @@ -757,8 +793,13 @@ await ApplySubscriptionAsync(currentSubscriptions, false, false, _disconnectLock.Dispose(); _disconnectLock = null; - currentSubscriptions = _subscriptions; - await ApplySubscriptionAsync(currentSubscriptions, true, isNew, + currentSubscriptions = _session.SubscriptionHandles; // Snapshot + // + // Equality is through subscriptionidentifer therefore only subscriptions + // that are not yet created inside the session remain in queued state. + // + queuedSubscriptions.ExceptWith(currentSubscriptions); + await ApplySubscriptionAsync(currentSubscriptions, queuedSubscriptions, ct).ConfigureAwait(false); _reconnectRequired = 0; @@ -782,9 +823,8 @@ await ApplySubscriptionAsync(currentSubscriptions, true, isNew, _reconnectHandler.CancelReconnect(); reconnectTimer.Change(Timeout.Infinite, Timeout.Infinite); - await ApplySubscriptionAsync(currentSubscriptions, false, false, - ct).ConfigureAwait(false); - currentSubscriptions = ImmutableHashSet.Empty; + queuedSubscriptions.Clear(); + currentSubscriptions = Array.Empty(); // if not already disconnected, aquire writer lock if (_disconnectLock == null) @@ -807,11 +847,12 @@ await ApplySubscriptionAsync(currentSubscriptions, false, false, } } + NotifyConnectivityStateChange(EndpointConnectivityState.Disconnected); + // Clean up await CloseSessionAsync().ConfigureAwait(false); Debug.Assert(_session == null); - NotifyConnectivityStateChange(EndpointConnectivityState.Disconnected); currentSessionState = SessionState.Disconnected; break; } @@ -831,28 +872,31 @@ await ApplySubscriptionAsync(currentSubscriptions, false, false, } } - async ValueTask ApplySubscriptionAsync(ImmutableHashSet subscriptions, - bool? online = null, bool newSession = false, CancellationToken cancellationToken = default) + async ValueTask ApplySubscriptionAsync(IReadOnlyList subscriptions, + HashSet extra, CancellationToken cancellationToken = default) { - _logger.LogDebug("Applying changes to {Count} subscriptions...", subscriptions.Count); + var numberOfSubscriptions = subscriptions.Count + extra.Count; + _logger.LogDebug("Applying changes to {Count} subscriptions...", numberOfSubscriptions); var sw = Stopwatch.StartNew(); var session = _session; - Debug.Assert(session != null || online == false, $"Session is null but online is {online}"); + Debug.Assert(session != null, "Session is null"); - await Task.WhenAll(subscriptions.Select(async subscription => + try + { + // Reload namespace tables should they have changed... + await session.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); + } + catch (ServiceResultException sre) + { + _logger.LogWarning(sre, "Failed to fetch namespace table..."); + } + + await Task.WhenAll(subscriptions.Concat(extra).Select(async subscription => { try { - if (online != false && session != null) - { - await subscription.SyncWithSessionAsync(session, newSession, - cancellationToken).ConfigureAwait(false); - } - if (online != null) - { - subscription.OnSubscriptionStateChanged(online.Value, this); - } + await subscription.SyncWithSessionAsync(session, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { } catch (Exception ex) @@ -864,13 +908,13 @@ await subscription.SyncWithSessionAsync(session, newSession, EnsureMinimumNumberOfPublishRequestsQueued(); - if (subscriptions.Count > 1) + if (numberOfSubscriptions > 1) { // Clear the node cache - TODO: we should have a real node cache here session?.NodeCache.Clear(); _logger.LogInformation("Applying changes to {Count} subscription(s) took {Duration}.", - subscriptions.Count, sw.Elapsed); + numberOfSubscriptions, sw.Elapsed); } } @@ -890,13 +934,14 @@ int GetMinReconnectPeriod() } private const int kMinPublishRequestCount = 3; + private const int kPublishTimeoutsMultiplier = 3; /// /// Ensure min publish requests are queued /// private void EnsureMinimumNumberOfPublishRequestsQueued() { - var session = _session?.Session; + var session = _session; if (session == null) { return; @@ -1016,13 +1061,16 @@ private async ValueTask TryConnectAsync(CancellationToken ct) }.ToList(); var sessionTimeout = SessionTimeout ?? TimeSpan.FromSeconds(30); - var session = await _sessionFactory.CreateAsync(_configuration, + var session = await CreateAsync(_configuration, _reverseConnectManager, endpoint, // Update endpoint through discovery updateBeforeConnect: _reverseConnectManager != null, checkDomain: false, // Domain must match on connect _sessionName, (uint)sessionTimeout.TotalMilliseconds, userIdentity, preferredLocales, ct).ConfigureAwait(false); + + session.RenewUserIdentity += (_, _) => userIdentity; + // Assign the created session var isNew = await UpdateSessionAsync(session).ConfigureAwait(false); Debug.Assert(isNew); @@ -1051,13 +1099,13 @@ private async ValueTask TryConnectAsync(CancellationToken ct) /// /// /// - private void Session_HandlePublishError(ISession session, PublishErrorEventArgs e) + internal void Session_HandlePublishError(ISession session, PublishErrorEventArgs e) { switch (e.Status.Code) { case StatusCodes.BadTooManyPublishRequests: _maxPublishRequests = GoodPublishRequestCount; - _logger.LogDebug("Limit publish requests to {Limit}...", + _logger.LogDebug("Limiting number of queued publish requests to {Limit}...", _maxPublishRequests); break; case StatusCodes.BadSessionIdInvalid: @@ -1065,22 +1113,27 @@ private void Session_HandlePublishError(ISession session, PublishErrorEventArgs case StatusCodes.BadSessionClosed: case StatusCodes.BadConnectionClosed: case StatusCodes.BadNoCommunication: - if (Interlocked.Increment(ref _reconnectRequired) == 1) - { - // Ensure we reconnect - TriggerConnectionEvent(ConnectionEvent.StartReconnect, e.Status); - } + TriggerReconnect(e.Status); break; case StatusCodes.BadRequestTimeout: case StatusCodes.BadTimeout: - // TODO: Count and also handle if above (threshold * #subscriptions) - _logger.LogDebug("Timeout during publishing."); - break; + var threshold = MinPublishRequestCount * kPublishTimeoutsMultiplier; + if (Interlocked.Increment(ref _publishTimeoutCounter) > threshold) + { + _logger.LogError( + "{Count} Timeouts (> {Threshold}) during publishing. Reconnecting...", + _publishTimeoutCounter, threshold); + TriggerReconnect(e.Status); + } + return; case StatusCodes.BadTooManyOperations: SetCode(e.Status, StatusCodes.BadServerHalted); break; } + // Reset timeout counter - we only care about subsequent timeouts + _publishTimeoutCounter = 0; + // Reach into the private field and update it. static void SetCode(ServiceResult status, uint fixup) { @@ -1095,9 +1148,12 @@ static void SetCode(ServiceResult status, uint fixup) /// /// /// - private void Session_PublishSequenceNumbersToAcknowledge(ISession session, + internal void Session_PublishSequenceNumbersToAcknowledge(ISession session, PublishSequenceNumbersToAcknowledgeEventArgs e) { + // Reset timeout counter + _publishTimeoutCounter = 0; + var acks = e.AcknowledgementsToSend .Concat(e.DeferredAcknowledgementsToSend) .ToHashSet(); @@ -1107,7 +1163,8 @@ private void Session_PublishSequenceNumbersToAcknowledge(ISession session, } e.AcknowledgementsToSend.Clear(); e.DeferredAcknowledgementsToSend.Clear(); - foreach (var subscription in _subscriptions) + + foreach (var subscription in ((OpcUaSession)session).SubscriptionHandles) { if (!subscription.TryGetCurrentPosition(out var sid, out var seq)) { @@ -1145,12 +1202,12 @@ static string ToString(SubscriptionAcknowledgementCollection acks) /// /// /// - private void Session_KeepAlive(ISession session, KeepAliveEventArgs e) + internal void Session_KeepAlive(ISession session, KeepAliveEventArgs e) { try { // check for events from discarded sessions. - if (!ReferenceEquals(session, _session?.Session)) + if (!ReferenceEquals(session, _session)) { return; } @@ -1158,10 +1215,7 @@ private void Session_KeepAlive(ISession session, KeepAliveEventArgs e) // start reconnect sequence on communication error. if (ServiceResult.IsBad(e.Status)) { - if (Interlocked.Increment(ref _reconnectRequired) == 1) - { - TriggerConnectionEvent(ConnectionEvent.StartReconnect, e.Status); - } + TriggerReconnect(e.Status); _logger.LogInformation( "Got Keep Alive error: {Error} ({TimeStamp}:{ServerState}", @@ -1174,6 +1228,19 @@ private void Session_KeepAlive(ISession session, KeepAliveEventArgs e) } } + /// + /// Trigger reconnect + /// + /// + void TriggerReconnect(ServiceResult sr) + { + if (Interlocked.Increment(ref _reconnectRequired) == 1) + { + // Ensure we reconnect + TriggerConnectionEvent(ConnectionEvent.StartReconnect, sr); + } + } + /// /// Trigger connection event /// @@ -1190,6 +1257,9 @@ private void TriggerConnectionEvent(ConnectionEvent evt, object? context = null) /// private async ValueTask UpdateSessionAsync(ISession session) { + _publishTimeoutCounter = 0; + Debug.Assert(session is OpcUaSession); + if (_session == null) { _session = _reconnectingSession; @@ -1197,20 +1267,14 @@ private async ValueTask UpdateSessionAsync(ISession session) } Debug.Assert(_reconnectingSession == null); - if (ReferenceEquals(_session?.Session, session)) + if (ReferenceEquals(_session, session)) { // Not a new session return false; } await CloseSessionAsync().ConfigureAwait(false); - _session = new OpcUaSession(session, Session_KeepAlive, - KeepAliveInterval ?? TimeSpan.FromSeconds(30), - OperationTimeout ?? TimeSpan.FromMinutes(1), - _serializer, _loggerFactory.CreateLogger(), - Session_HandlePublishError, - Session_PublishSequenceNumbersToAcknowledge, - DisableComplexTypePreloading != true); + _session = (OpcUaSession)session; NotifyConnectivityStateChange(EndpointConnectivityState.Ready); kSessions.Add(1, _metrics.TagList); @@ -1234,11 +1298,25 @@ private async ValueTask CloseSessionAsync() _session = null; } + _publishTimeoutCounter = 0; + async ValueTask DisposeAsync(OpcUaSession session) { - await session.CloseAsync(default).ConfigureAwait(false); - session.Dispose(); - kSessions.Add(-1, _metrics.TagList); + try + { + await session.CloseAsync(CancellationToken.None).ConfigureAwait(false); + + _logger.LogDebug("Successfully closed session {Session}.", session); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to close session {Session}.", session); + } + finally + { + session.Dispose(); + kSessions.Add(-1, _metrics.TagList); + } } } @@ -1489,7 +1567,6 @@ private enum ConnectionEvent Disconnect, StartReconnect, ReconnectComplete, - SubscriptionChange, SubscriptionManage } @@ -1678,22 +1755,29 @@ static void NotifyAll(uint seq, ReadValueIdCollection nodesToRead, uint statusCo /// /// Disconnected state /// - private sealed class DisconnectState : IOpcUaClientState + private sealed class DisconnectState : IOpcUaClientDiagnostics { /// - public int BadPublishRequestCount => 0; + public int BadPublishRequestCount + => 0; /// - public int GoodPublishRequestCount => 0; + public int GoodPublishRequestCount + => 0; /// - public int OutstandingRequestCount => 0; + public int OutstandingRequestCount + => 0; /// - public int SubscriptionCount => 0; + public int SubscriptionCount + => 0; /// - public bool IsConnected => false; + public EndpointConnectivityState State + => EndpointConnectivityState.Disconnected; /// - public int ReconnectCount => 0; + public int ReconnectCount + => 0; /// - public int MinPublishRequestCount => 0; + public int MinPublishRequestCount + => 0; } /// @@ -1705,7 +1789,7 @@ private void InitializeMetrics() () => new Measurement((int)_lastState, _metrics.TagList), "EndpointConnectivityState", "Client connectivity state."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_client_subscription_count", - () => new Measurement(_subscriptions.Count, _metrics.TagList), + () => new Measurement(SubscriptionCount, _metrics.TagList), "Subscriptions", "Number of client managed subscriptions."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_client_connectivity_retry_count", () => new Measurement(_numberOfConnectRetries, _metrics.TagList), @@ -1725,6 +1809,9 @@ private void InitializeMetrics() _meter.CreateObservableUpDownCounter("iiot_edge_publisher_client_outstanding_requests_count", () => new Measurement(OutstandingRequestCount, _metrics.TagList), "Requests", "Number of outstanding requests."); + _meter.CreateObservableUpDownCounter("iiot_edge_publisher_client_publish_timeout_count", + () => new Measurement(_publishTimeoutCounter, + _metrics.TagList), "Requests", "Number of timed out requests."); } private static readonly UpDownCounter kSessions = Diagnostics.Meter.CreateUpDownCounter( @@ -1737,14 +1824,12 @@ private void InitializeMetrics() private IDisposable? _disconnectLock; #pragma warning restore CA2213 // Disposable fields should be disposed private EndpointConnectivityState _lastState; - private ImmutableHashSet _subscriptions; private int _numberOfConnectRetries; private bool _disposed; private int _refCount; private int? _maxPublishRequests; - private readonly object _subscriptionsLock = new(); + private int _publishTimeoutCounter; private readonly ReverseConnectManager? _reverseConnectManager; - private readonly ISessionFactory _sessionFactory; private readonly AsyncReaderWriterLock _lock = new(); private readonly ApplicationConfiguration _configuration; private readonly IJsonSerializer _serializer; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs index bea516cc6c..73604430c6 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs @@ -13,8 +13,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Services using SharpPcap.LibPcap; #endif using System.Collections.Generic; - using System.Linq; - using System.IO; using Autofac; /// diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientManager.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientManager.cs index dbc80a528f..78739149d6 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientManager.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientManager.cs @@ -43,10 +43,9 @@ internal sealed class OpcUaClientManager : IOpcUaClientManager, /// /// /// - /// public OpcUaClientManager(ILoggerFactory loggerFactory, IJsonSerializer serializer, IOptions options, IOpcUaConfiguration configuration, - IMetricsContext? metrics = null, ISessionFactory? sessionFactory = null) + IMetricsContext? metrics = null) { _metrics = metrics ?? IMetricsContext.Empty; @@ -60,7 +59,6 @@ public OpcUaClientManager(ILoggerFactory loggerFactory, IJsonSerializer serializ throw new ArgumentNullException(nameof(configuration)); _logger = _loggerFactory.CreateLogger(); - _sessionFactory = sessionFactory ?? DefaultSessionFactory.Instance; _reverseConnectManager = new ReverseConnectManager(); _reverseConnectStartException = new Lazy( StartReverseConnectManager, isThreadSafe: true); @@ -69,14 +67,16 @@ public OpcUaClientManager(ILoggerFactory loggerFactory, IJsonSerializer serializ } /// - public ValueTask CreateSubscriptionAsync( - SubscriptionModel subscription, IMetricsContext metrics, - CancellationToken ct) + public void CreateSubscription(SubscriptionModel subscription, + ISubscriptionCallbacks callback, IMetricsContext metrics) { ObjectDisposedException.ThrowIf(_disposed, this); - return OpcUaSubscription.CreateAsync(this, _options, subscription, - _loggerFactory, new OpcUaClientTagList( - subscription.Id.Connection, metrics ?? _metrics), ct); + // Create subscription which will register with callback/client +#pragma warning disable CA2000 // Dispose objects before losing scope + _ = new OpcUaSubscription(this, callback, subscription, + _options, _loggerFactory, new OpcUaClientTagList( + subscription.Id.Connection, metrics ?? _metrics)); +#pragma warning restore CA2000 // Dispose objects before losing scope } /// @@ -87,15 +87,6 @@ public IOpcUaClient GetOrCreateClient(ConnectionModel connection) return GetOrAddClient(connection); } - /// - public IOpcUaClient? GetClient(ConnectionModel connection) - { - ObjectDisposedException.ThrowIf(_disposed, this); - var client = FindClient(connection); - client?.AddRef(); - return client; - } - /// public IAsyncDisposable Sample(ConnectionModel connection, TimeSpan samplingRate, ReadValueId nodeToRead, Action callback) @@ -142,7 +133,7 @@ public async Task TestConnectionAsync( _configuration.Value).ConfigureAwait(false); try { - using var session = await _sessionFactory.CreateAsync( + using var session = await DefaultSessionFactory.Instance.CreateAsync( _configuration.Value, reverseConnectManager: null, configuredEndpoint, updateBeforeConnect: true, // Update endpoint through discovery checkDomain: false, // Domain must match on connect @@ -547,7 +538,7 @@ private OpcUaClient GetOrAddClient(ConnectionModel connection) var client = _clients.GetOrAdd(id, id => { var client = new OpcUaClient(_configuration.Value, id, _serializer, - _loggerFactory, _meter, _metrics, OnConnectionStateChange, _sessionFactory, + _loggerFactory, _meter, _metrics, OnConnectionStateChange, reverseConnect ? _reverseConnectManager : null, _options.Value.MaxReconnectDelay) { @@ -622,7 +613,6 @@ private void InitializeMetrics() private readonly ReverseConnectManager _reverseConnectManager; private readonly Lazy _reverseConnectStartException; private readonly ConcurrentDictionary _clients = new(); - private readonly ISessionFactory _sessionFactory; private readonly IMetricsContext _metrics; private readonly Meter _meter = Diagnostics.NewMeter(); } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs index e937b0e990..808803c279 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs @@ -13,7 +13,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Services using Opc.Ua.Client; using Opc.Ua.Client.ComplexTypes; using Opc.Ua.Extensions; - using MonitoringMode = Publisher.Models.MonitoringMode; using System; using System.Collections.Generic; using System.Data; @@ -21,32 +20,33 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Services using System.Linq; using System.Threading; using System.Threading.Tasks; + using System.Runtime.Serialization; /// /// Monitored item /// - internal abstract class OpcUaMonitoredItem : IOpcUaMonitoredItem + internal abstract class OpcUaMonitoredItem : MonitoredItem, IOpcUaMonitoredItem { /// /// Assigned monitored item id on server /// - public uint? ServerId => Item?.Status.Id; + public uint? RemoteId => Created ? Status.Id : null; /// - public MonitoredItem? Item { get; protected internal set; } + public bool Valid { get; protected internal set; } /// public virtual string? DataSetName { get; } /// - public bool AttachedToSubscription { get; protected internal set; } + public bool AttachedToSubscription { get; protected internal set; } // TODO: Use Subscription property != null /// public virtual (string NodeId, UpdateNodeId Update)? Register => null; /// - public virtual (string NodeId, UpdateString Update)? DisplayName + public virtual (string NodeId, UpdateString Update)? GetDisplayName => null; /// @@ -61,7 +61,7 @@ public virtual (string NodeId, string[] Path, UpdateNodeId Update)? Resolve /// /// Last saved value /// - public IEncodeable? LastValue { get; private set; } + public IEncodeable? LastReceivedValue { get; private set; } /// /// Create item @@ -74,6 +74,34 @@ protected OpcUaMonitoredItem(ILogger logger, string nodeId) _logger = logger; } + /// + /// Copy constructor + /// + /// + /// + /// + protected OpcUaMonitoredItem(OpcUaMonitoredItem item, + bool copyEventHandlers, bool copyClientHandle) + : base(item, copyEventHandlers, copyClientHandle) + { + NodeId = item.NodeId; + _logger = item._logger; + + LastReceivedValue = item.LastReceivedValue; + AttachedToSubscription = item.AttachedToSubscription; + Valid = item.Valid; + } + + /// + public override abstract MonitoredItem CloneMonitoredItem( + bool copyEventHandlers, bool copyClientHandle); + + /// + public override object Clone() + { + return CloneMonitoredItem(true, true); + } + /// /// Create items /// @@ -82,7 +110,7 @@ protected OpcUaMonitoredItem(ILogger logger, string nodeId) /// /// /// - public static IEnumerable Create( + public static IEnumerable Create( IEnumerable items, ILoggerFactory factory, IClientSampler? clients = null, ConnectionIdentifier? connection = null) @@ -140,12 +168,6 @@ public void Dispose() GC.SuppressFinalize(this); } - /// - public virtual void OnMonitoredItemStateChanged(bool online) - { - // No op - } - /// public abstract ValueTask GetMetaDataAsync(IOpcUaSession session, ComplexTypeSystem? typeSystem, FieldMetaDataCollection fields, @@ -157,7 +179,7 @@ public abstract ValueTask GetMetaDataAsync(IOpcUaSession session, /// protected virtual void Dispose(bool disposing) { - if (disposing && Item != null) + if (disposing && Valid) { if (AttachedToSubscription) { @@ -165,7 +187,7 @@ protected virtual void Dispose(bool disposing) _logger.LogError("Unexpected state: Item {Item} must " + "already be removed from subscription, but wasn't.", this); } - Item = null; + Valid = false; } } @@ -173,9 +195,9 @@ protected virtual void Dispose(bool disposing) public virtual bool AddTo(Subscription subscription, IOpcUaSession session, out bool metadataChanged) { - if (Item != null) + if (Valid) { - subscription.AddItem(Item); + subscription.AddItem(this); _logger.LogDebug( "Added monitored item {Item} to subscription #{SubscriptionId}.", this, subscription.Id); @@ -197,7 +219,7 @@ public virtual bool RemoveFrom(Subscription subscription, { if (AttachedToSubscription) { - subscription.RemoveItem(Item); + subscription.RemoveItem(this); _logger.LogDebug( "Removed monitored item {Item} from subscription #{SubscriptionId}.", this, subscription.Id); @@ -214,7 +236,7 @@ public virtual bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, Action, bool> cb) { - if (Item == null) + if (!Valid) { return false; } @@ -223,59 +245,58 @@ public virtual bool TryCompleteChanges(Subscription subscription, { _logger.LogDebug( "Item {Item} removed from subscription #{SubscriptionId} with {Status}.", - this, subscription.Id, Item.Status.Error); + this, subscription.Id, Status.Error); // Complete removal return true; } - if (Item.Status.MonitoringMode == Opc.Ua.MonitoringMode.Disabled) + if (Status.MonitoringMode == Opc.Ua.MonitoringMode.Disabled) { return false; } - if (Item.Status.Error != null && - StatusCode.IsNotGood(Item.Status.Error.StatusCode)) + if (Status.Error == null || !StatusCode.IsNotGood(Status.Error.StatusCode)) { - _logger.LogWarning("Error adding monitored item {Item} " + - "to subscription #{SubscriptionId} due to {Status}.", - this, subscription.Id, Item.Status.Error); - - // Not needed, mode changes applied after - // applyChanges = true; - return false; - } - - if (Item.SamplingInterval != Item.Status.SamplingInterval || - Item.QueueSize != Item.Status.QueueSize) - { - _logger.LogInformation( - @"Server has revised {Item} ('{Name}') in subscription #{SubscriptionId} + if (SamplingInterval != Status.SamplingInterval || + QueueSize != Status.QueueSize) + { + _logger.LogInformation( + @"Server has revised {Item} ('{Name}') in subscription #{SubscriptionId} The item's actual/desired states: SamplingInterval {CurrentSamplingInterval}/{SamplingInterval}, QueueSize {CurrentQueueSize}/{QueueSize}", - Item.StartNodeId, Item.DisplayName, subscription.Id, - Item.Status.SamplingInterval, Item.SamplingInterval, - Item.Status.QueueSize, Item.QueueSize); - } - else - { - _logger.LogDebug( - "Item {Item} added to subscription #{SubscriptionId} successfully.", - this, subscription.Id); + StartNodeId, DisplayName, subscription.Id, + Status.SamplingInterval, SamplingInterval, + Status.QueueSize, QueueSize); + } + else + { + _logger.LogDebug( + "Item {Item} added to subscription #{SubscriptionId} successfully.", + this, subscription.Id); + } + return true; } - return true; + + _logger.LogWarning( + "Error adding monitored item {Item} to subscription #{SubscriptionId} due to {Status}.", + this, subscription.Id, Status.Error); + + // Not needed, mode changes applied after + // applyChanges = true; + return false; } /// public virtual Opc.Ua.MonitoringMode? GetMonitoringModeChange() { - if (!AttachedToSubscription || Item == null) + if (!AttachedToSubscription || !Valid) { return null; } - var currentMode = Item.Status?.MonitoringMode + var currentMode = Status?.MonitoringMode ?? Opc.Ua.MonitoringMode.Disabled; - var desiredMode = Item.MonitoringMode; + var desiredMode = MonitoringMode; return currentMode != desiredMode ? desiredMode : null; } @@ -283,12 +304,11 @@ public virtual bool TryCompleteChanges(Subscription subscription, public virtual bool TryGetMonitoredItemNotifications(uint sequenceNumber, DateTime timestamp, IEncodeable evt, IList notifications) { - var item = Item; - if (item == null) + if (!Valid) { return false; } - LastValue = evt; + LastReceivedValue = evt; return true; } @@ -296,11 +316,11 @@ public virtual bool TryGetMonitoredItemNotifications(uint sequenceNumber, DateTi public virtual bool TryGetLastMonitoredItemNotifications(uint sequenceNumber, IList notifications) { - var lastValue = LastValue; - if (lastValue == null || Item?.Status?.Error != null) + var lastValue = LastReceivedValue; + if (lastValue == null || Status?.Error != null) { return TryGetErrorMonitoredItemNotifications(sequenceNumber, - Item?.Status?.Error.StatusCode ?? StatusCodes.GoodNoData, + Status?.Error.StatusCode ?? StatusCodes.GoodNoData, notifications); } return TryGetMonitoredItemNotifications(sequenceNumber, DateTime.UtcNow, @@ -333,7 +353,7 @@ protected bool MergeWith(T template, T desired, out T updated, metadataChanged = false; updated = template; - if (Item == null) + if (!Valid) { return false; } @@ -346,7 +366,7 @@ protected bool MergeWith(T template, T desired, out T updated, this, updated.DiscardNew ?? false, desired.DiscardNew ?? false); updated = updated with { DiscardNew = desired.DiscardNew }; - Item.DiscardOldest = !(updated.DiscardNew ?? false); + DiscardOldest = !(updated.DiscardNew ?? false); itemChange = true; } if (updated.QueueSize != desired.QueueSize) @@ -355,17 +375,17 @@ protected bool MergeWith(T template, T desired, out T updated, this, updated.QueueSize, desired.QueueSize); updated = updated with { QueueSize = desired.QueueSize }; - Item.QueueSize = updated.QueueSize; + QueueSize = updated.QueueSize; itemChange = true; } - if ((updated.MonitoringMode ?? MonitoringMode.Reporting) != - (desired.MonitoringMode ?? MonitoringMode.Reporting)) + if ((updated.MonitoringMode ?? Publisher.Models.MonitoringMode.Reporting) != + (desired.MonitoringMode ?? Publisher.Models.MonitoringMode.Reporting)) { _logger.LogDebug("{Item}: Changing monitoring mode from {Old} to {New}", - this, updated.MonitoringMode ?? MonitoringMode.Reporting, - desired.MonitoringMode ?? MonitoringMode.Reporting); + this, updated.MonitoringMode ?? Publisher.Models.MonitoringMode.Reporting, + desired.MonitoringMode ?? Publisher.Models.MonitoringMode.Reporting); updated = updated with { MonitoringMode = desired.MonitoringMode }; - Item.MonitoringMode = updated.MonitoringMode.ToStackType() + MonitoringMode = updated.MonitoringMode.ToStackType() ?? Opc.Ua.MonitoringMode.Reporting; // Not a change yet, will be done as bulk update @@ -388,7 +408,7 @@ protected bool MergeWith(T template, T desired, out T updated, updated.DisplayName != desired.DisplayName) { updated = updated with { DataSetFieldName = desired.DataSetFieldName }; - Item.DisplayName = updated.DisplayName; + DisplayName = updated.DisplayName; metadataChanged = true; itemChange = true; } @@ -563,6 +583,10 @@ static DataTypeDescription GetDefault(Node dataType, BuiltInType builtInType) /// /// Extension Field item /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(DataChangeFilter))] + [KnownType(typeof(EventFilter))] + [KnownType(typeof(AggregateFilter))] internal class FieldItem : OpcUaMonitoredItem { /// @@ -581,6 +605,26 @@ public FieldItem(ExtensionFieldModel template, Template = template; } + /// + /// Copy constructor + /// + /// + /// + /// + protected FieldItem(FieldItem item, bool copyEventHandlers, + bool copyClientHandle) + : base(item, copyEventHandlers, copyClientHandle) + { + Template = item.Template; + } + + /// + public override MonitoredItem CloneMonitoredItem( + bool copyEventHandlers, bool copyClientHandle) + { + return new FieldItem(this, copyEventHandlers, copyClientHandle); + } + /// public override bool Equals(object? obj) { @@ -635,7 +679,7 @@ public override bool AddTo(Subscription subscription, { metadataChanged = true; _value = new DataValue(session.Codec.Decode(Template.Value, BuiltInType.Variant)); - Item = new MonitoredItem(); + Valid = true; return true; } @@ -652,7 +696,7 @@ public override bool RemoveFrom(Subscription subscription, out bool metadataChan { metadataChanged = true; _value = new DataValue(); - Item = null; + Valid = false; return true; } @@ -668,7 +712,7 @@ public override bool TryCompleteChanges(Subscription subscription, public override bool TryGetLastMonitoredItemNotifications(uint sequenceNumber, IList notifications) { - if (Item == null) + if (!Valid) { return false; } @@ -700,7 +744,7 @@ protected override bool TryGetErrorMonitoredItemNotifications( /// protected MonitoredItemNotificationModel ToMonitoredItemNotification(uint sequenceNumber) { - Debug.Assert(Item != null); + Debug.Assert(Valid); Debug.Assert(Template != null); return new MonitoredItemNotificationModel @@ -721,24 +765,28 @@ protected MonitoredItemNotificationModel ToMonitoredItemNotification(uint sequen /// /// Data item /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(DataChangeFilter))] + [KnownType(typeof(EventFilter))] + [KnownType(typeof(AggregateFilter))] internal class DataItem : OpcUaMonitoredItem { /// public override (string NodeId, string[] Path, UpdateNodeId Update)? Resolve => Template.RelativePath != null && - (ResolvedNodeId == Template.StartNodeId || string.IsNullOrEmpty(ResolvedNodeId)) ? + (TheResolvedNodeId == Template.StartNodeId || string.IsNullOrEmpty(TheResolvedNodeId)) ? (Template.StartNodeId, Template.RelativePath.ToArray(), - (v, context) => ResolvedNodeId = NodeId + (v, context) => TheResolvedNodeId = NodeId = v.AsString(context, Template.NamespaceFormat) ?? string.Empty) : null; /// public override (string NodeId, UpdateNodeId Update)? Register - => Template.RegisterRead && !string.IsNullOrEmpty(ResolvedNodeId) ? - (ResolvedNodeId, (v, context) => NodeId + => Template.RegisterRead && !string.IsNullOrEmpty(TheResolvedNodeId) ? + (TheResolvedNodeId, (v, context) => NodeId = v.AsString(context, Template.NamespaceFormat) ?? string.Empty) : null; /// - public override (string NodeId, UpdateString Update)? DisplayName + public override (string NodeId, UpdateString Update)? GetDisplayName => Template.FetchDataSetFieldName == true && Template.DataSetFieldName != null && !string.IsNullOrEmpty(NodeId) ? (NodeId, v => Template = Template with { DataSetFieldName = v }) : null; @@ -751,7 +799,7 @@ public override (string NodeId, UpdateString Update)? DisplayName /// /// Resolved node id /// - protected string ResolvedNodeId { get; private set; } + protected string TheResolvedNodeId { get; private set; } /// /// Field identifier either configured or randomly assigned @@ -775,7 +823,28 @@ public DataItem(DataMonitoredItemModel template, // We also track the resolved node id so we distinguish it // from the registered and thus effective node id // - ResolvedNodeId = template.StartNodeId; + TheResolvedNodeId = template.StartNodeId; + } + + /// + /// Copy constructor + /// + /// + /// + /// + protected DataItem(DataItem item, bool copyEventHandlers, + bool copyClientHandle) + : base(item, copyEventHandlers, copyClientHandle) + { + TheResolvedNodeId = item.TheResolvedNodeId; + Template = item.Template; + } + + /// + public override MonitoredItem CloneMonitoredItem( + bool copyEventHandlers, bool copyClientHandle) + { + return new DataItem(this, copyEventHandlers, copyClientHandle); } /// @@ -839,8 +908,8 @@ public override int GetHashCode() /// public override string ToString() { - return $"Data Item '{Template.StartNodeId}' with server id {ServerId} - " + - $"{(Item?.Status?.Created == true ? "" : "not ")}created"; + return $"Data Item '{Template.StartNodeId}' with server id {RemoteId} - " + + $"{(Status?.Created == true ? "" : "not ")}created"; } /// @@ -882,23 +951,21 @@ public override bool AddTo(Subscription subscription, IOpcUaSession session, return false; } - Item = new MonitoredItem - { - DisplayName = Template.DisplayName, - AttributeId = (uint)(Template.AttributeId ?? - (NodeAttribute)Attributes.Value), - IndexRange = Template.IndexRange, - StartNodeId = nodeId, - MonitoringMode = Template.MonitoringMode.ToStackType() - ?? Opc.Ua.MonitoringMode.Reporting, - QueueSize = Template.QueueSize, - SamplingInterval = (int)Template.SamplingInterval. - GetValueOrDefault(TimeSpan.FromSeconds(1)).TotalMilliseconds, - Filter = Template.DataChangeFilter.ToStackModel() ?? - (MonitoringFilter?)Template.AggregateFilter.ToStackModel( - session.MessageContext), - DiscardOldest = !(Template.DiscardNew ?? false) - }; + DisplayName = Template.DisplayName; + AttributeId = (uint)(Template.AttributeId ?? + (NodeAttribute)Attributes.Value); + IndexRange = Template.IndexRange; + StartNodeId = nodeId; + MonitoringMode = Template.MonitoringMode.ToStackType() + ?? Opc.Ua.MonitoringMode.Reporting; + QueueSize = Template.QueueSize; + SamplingInterval = (int)Template.SamplingInterval. + GetValueOrDefault(TimeSpan.FromSeconds(1)).TotalMilliseconds; + Filter = Template.DataChangeFilter.ToStackModel() ?? + (MonitoringFilter?)Template.AggregateFilter.ToStackModel( + session.MessageContext); + DiscardOldest = !(Template.DiscardNew ?? false); + Valid = true; if (!TrySetSkipFirst(Template.SkipFirst)) { @@ -924,7 +991,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, out bool metadataChanged) { metadataChanged = false; - if (item is not DataItem model || Item == null) + if (item is not DataItem model || !Valid) { return false; } @@ -945,7 +1012,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, model.Template.SamplingInterval.GetValueOrDefault( TimeSpan.FromSeconds(1)).TotalMilliseconds); Template = Template with { SamplingInterval = model.Template.SamplingInterval }; - Item.SamplingInterval = + SamplingInterval = (int)Template.SamplingInterval.GetValueOrDefault( TimeSpan.FromSeconds(1)).TotalMilliseconds; itemChange = true; @@ -965,7 +1032,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, { Template = Template with { DataChangeFilter = model.Template.DataChangeFilter }; _logger.LogDebug("{Item}: Changing data change filter.", this); - Item.Filter = Template.DataChangeFilter.ToStackModel(); + Filter = Template.DataChangeFilter.ToStackModel(); itemChange = true; } @@ -974,7 +1041,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, { Template = Template with { AggregateFilter = model.Template.AggregateFilter }; _logger.LogDebug("{Item}: Changing aggregate change filter.", this); - Item.Filter = Template.AggregateFilter.ToStackModel(session.MessageContext); + Filter = Template.AggregateFilter.ToStackModel(session.MessageContext); itemChange = true; } if (model.Template.SkipFirst != Template.SkipFirst) @@ -1045,7 +1112,7 @@ protected virtual bool ProcessMonitoredItemNotification(uint sequenceNumber, protected MonitoredItemNotificationModel ToMonitoredItemNotification( uint sequenceNumber, DataValue dataValue) { - Debug.Assert(Item != null); + Debug.Assert(Valid); Debug.Assert(Template != null); return new MonitoredItemNotificationModel @@ -1121,6 +1188,10 @@ enum SkipSetting /// cache on the subscription object caching _all_ values received. /// /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(DataChangeFilter))] + [KnownType(typeof(EventFilter))] + [KnownType(typeof(AggregateFilter))] internal sealed class DataItemWithHeartbeat : DataItem { /// @@ -1139,6 +1210,29 @@ public DataItemWithHeartbeat(DataMonitoredItemModel dataTemplate, _heartbeatTimer = new Timer(_ => SendHeartbeatNotifications()); } + /// + /// Copy constructor + /// + /// + /// + /// + private DataItemWithHeartbeat(DataItemWithHeartbeat item, bool copyEventHandlers, + bool copyClientHandle) + : base(item, copyEventHandlers, copyClientHandle) + { + _heartbeatInterval = item._heartbeatInterval; + _timerInterval = item._timerInterval; + _heartbeatBehavior = item._heartbeatBehavior; + _heartbeatTimer = new Timer(_ => SendHeartbeatNotifications()); + } + + /// + public override MonitoredItem CloneMonitoredItem( + bool copyEventHandlers, bool copyClientHandle) + { + return new DataItemWithHeartbeat(this, copyEventHandlers, copyClientHandle); + } + /// public override bool Equals(object? obj) { @@ -1160,7 +1254,7 @@ public override string ToString() { return $"Data Item '{Template.StartNodeId}' " + $"(with {Template.HeartbeatBehavior ?? HeartbeatBehavior.WatchdogLKV} Heartbeat) " + - $"with server id {ServerId} - {(Item?.Status?.Created == true ? "" : + $"with server id {RemoteId} - {(Status?.Created == true ? "" : "not ")}created"; } @@ -1179,10 +1273,10 @@ protected override bool ProcessMonitoredItemNotification(uint sequenceNumber, DateTime timestamp, MonitoredItemNotification monitoredItemNotification, IList notifications) { - Debug.Assert(Item != null); + Debug.Assert(Valid); // Last value should be this notification - Debug.Assert(monitoredItemNotification == LastValue); + Debug.Assert(monitoredItemNotification == LastReceivedValue); if ((_heartbeatBehavior & HeartbeatBehavior.PeriodicLKV) == 0) { _heartbeatTimer.Change(_timerInterval, _timerInterval); @@ -1197,7 +1291,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, out bool metadataChanged) { metadataChanged = false; - if (item is not DataItemWithHeartbeat model || Item == null) + if (item is not DataItemWithHeartbeat model || !Valid) { return false; } @@ -1231,10 +1325,10 @@ public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, Action, bool> cb) { - var result = base.TryCompleteChanges(subscription, ref applyChanges, - cb); - - if (!AttachedToSubscription || !result) + var result = base.TryCompleteChanges(subscription, ref applyChanges, cb); + if (!AttachedToSubscription || + (!result && (_heartbeatBehavior & HeartbeatBehavior.WatchdogLKG) + != HeartbeatBehavior.WatchdogLKG)) { _callback = null; // Stop heartbeat @@ -1285,24 +1379,31 @@ private static bool IsGoodDataValue(DataValue? value) private void SendHeartbeatNotifications() { var callback = _callback; - var item = Item; - - if (callback == null || item == null || LastValue == null) + if (callback == null || !Valid) { return; } - var lastNofication = LastValue as MonitoredItemNotification; + var lastNofication = LastReceivedValue as MonitoredItemNotification; if ((_heartbeatBehavior & HeartbeatBehavior.WatchdogLKG) == HeartbeatBehavior.WatchdogLKG && !IsGoodDataValue(lastNofication?.Value)) { - // Currently no good value to send + // Currently no last known good value (LKG) to send return; } - var lastValue = lastNofication?.Value ?? - new DataValue(item.Status?.Error?.StatusCode ?? StatusCodes.GoodNoData); + var lastValue = lastNofication?.Value; + if (lastValue == null && Status?.Error?.StatusCode != null) + { + lastValue = new DataValue(Status.Error.StatusCode); + } + + if (lastValue == null) + { + // Currently no last known value (LKV) to send + return; + } if ((_heartbeatBehavior & HeartbeatBehavior.WatchdogLKVWithUpdatedTimestamps) == HeartbeatBehavior.WatchdogLKVWithUpdatedTimestamps) { @@ -1349,6 +1450,10 @@ private void SendHeartbeatNotifications() /// execute through the client sampler periodically (at the configured /// sampling rate). /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(DataChangeFilter))] + [KnownType(typeof(EventFilter))] + [KnownType(typeof(AggregateFilter))] internal sealed class DataItemWithCyclicRead : DataItem { /// @@ -1363,18 +1468,39 @@ public DataItemWithCyclicRead(IClientSampler sampler, ILogger logger) : base(template with { // Always ensure item is disabled - MonitoringMode = MonitoringMode.Disabled + MonitoringMode = Publisher.Models.MonitoringMode.Disabled }, logger) { _sampler = sampler; _connection = connection; - LastValue = new MonitoredItemNotification + LastReceivedValue = new MonitoredItemNotification { Value = new DataValue(StatusCodes.GoodNoData) }; } + /// + /// Copy constructor + /// + /// + /// + /// + private DataItemWithCyclicRead(DataItemWithCyclicRead item, bool copyEventHandlers, + bool copyClientHandle) + : base(item, copyEventHandlers, copyClientHandle) + { + _sampler = item._sampler; + _connection = item._connection; + } + + /// + public override MonitoredItem CloneMonitoredItem( + bool copyEventHandlers, bool copyClientHandle) + { + return new DataItemWithCyclicRead(this, copyEventHandlers, copyClientHandle); + } + /// public override bool Equals(object? obj) { @@ -1442,14 +1568,14 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, } else if (_sampling == null) { - Debug.Assert(Item?.MonitoringMode == Opc.Ua.MonitoringMode.Disabled); + Debug.Assert(MonitoringMode == Opc.Ua.MonitoringMode.Disabled); _sampling = _sampler.Sample(_connection.Connection, - TimeSpan.FromMilliseconds(Item.SamplingInterval), + TimeSpan.FromMilliseconds(SamplingInterval), new ReadValueId { - AttributeId = Item.AttributeId, - IndexRange = Item.IndexRange, - NodeId = Item.ResolvedNodeId + AttributeId = AttributeId, + IndexRange = IndexRange, + NodeId = ResolvedNodeId }, OnSampledDataValueReceived); _logger.LogDebug("Item {Item} successfully registered with sampler.", @@ -1464,7 +1590,7 @@ public override bool TryCompleteChanges(Subscription subscription, Action, bool> cb) { // Dont call base implementation as it is not what we want. - if (Item == null) + if (!Valid) { return false; } @@ -1533,14 +1659,14 @@ internal DataValue LastSampledValue { get { - Debug.Assert(LastValue is MonitoredItemNotification); - return ((MonitoredItemNotification)LastValue).Value + Debug.Assert(LastReceivedValue is MonitoredItemNotification); + return ((MonitoredItemNotification)LastReceivedValue).Value ?? new DataValue(StatusCodes.GoodNoData); } set { - Debug.Assert(LastValue is MonitoredItemNotification); - ((MonitoredItemNotification)LastValue).Value = value; + Debug.Assert(LastReceivedValue is MonitoredItemNotification); + ((MonitoredItemNotification)LastReceivedValue).Value = value; } } @@ -1555,10 +1681,14 @@ internal DataValue LastSampledValue /// /// Event monitored item /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(DataChangeFilter))] + [KnownType(typeof(EventFilter))] + [KnownType(typeof(AggregateFilter))] internal class EventItem : OpcUaMonitoredItem { /// - public override (string NodeId, UpdateString Update)? DisplayName + public override (string NodeId, UpdateString Update)? GetDisplayName => Template.FetchDataSetFieldName == true && !string.IsNullOrEmpty(Template.EventFilter.TypeDefinitionId) && Template.DataSetFieldName == null ? @@ -1599,6 +1729,27 @@ public EventItem(EventMonitoredItemModel template, Template = template; } + /// + /// Copy constructor + /// + /// + /// + /// + protected EventItem(EventItem item, bool copyEventHandlers, + bool copyClientHandle) + : base(item, copyEventHandlers, copyClientHandle) + { + Fields = item.Fields; + Template = item.Template; + } + + /// + public override MonitoredItem CloneMonitoredItem( + bool copyEventHandlers, bool copyClientHandle) + { + return new EventItem(this, copyEventHandlers, copyClientHandle); + } + /// public override bool Equals(object? obj) { @@ -1656,8 +1807,8 @@ public override int GetHashCode() /// public override string ToString() { - return $"Event Item '{Template.StartNodeId}' with server id {ServerId} - " + - $"{(Item?.Status?.Created == true ? "" : "not ")}created"; + return $"Event Item '{Template.StartNodeId}' with server id {RemoteId} - " + + $"{(Status?.Created == true ? "" : "not ")}created"; } /// @@ -1665,7 +1816,7 @@ public override async ValueTask GetMetaDataAsync(IOpcUaSession session, ComplexTypeSystem? typeSystem, FieldMetaDataCollection fields, NodeIdDictionary dataTypes, CancellationToken ct) { - if (Item?.Filter is not EventFilter eventFilter) + if (Filter is not EventFilter eventFilter) { return; } @@ -1714,10 +1865,10 @@ public override bool TryCompleteChanges(Subscription subscription, { return false; } - Debug.Assert(Item != null); + Debug.Assert(Valid); // TODO: Instead figure out how to get the filter status and inspect - return TestWhereClauseAsync(subscription.Session, Item.Filter as EventFilter).Result; + return TestWhereClauseAsync(subscription.Session, Filter as EventFilter).Result; } /// @@ -1730,19 +1881,18 @@ public override bool AddTo(Subscription subscription, metadataChanged = false; return false; } - Item = new MonitoredItem - { - DisplayName = Template.DisplayName, - AttributeId = (uint)(Template.AttributeId - ?? (NodeAttribute)Attributes.EventNotifier), - MonitoringMode = Template.MonitoringMode.ToStackType() - ?? Opc.Ua.MonitoringMode.Reporting, - StartNodeId = Template.StartNodeId.ToNodeId(session.MessageContext), - QueueSize = Template.QueueSize, - SamplingInterval = 0, - Filter = GetEventFilter(session), - DiscardOldest = !(Template.DiscardNew ?? false) - }; + DisplayName = Template.DisplayName; + AttributeId = (uint)(Template.AttributeId + ?? (NodeAttribute)Attributes.EventNotifier); + MonitoringMode = Template.MonitoringMode.ToStackType() + ?? Opc.Ua.MonitoringMode.Reporting; + StartNodeId = Template.StartNodeId.ToNodeId(session.MessageContext); + QueueSize = Template.QueueSize; + SamplingInterval = 0; + Filter = GetEventFilter(session); + DiscardOldest = !(Template.DiscardNew ?? false); + Valid = true; + return base.AddTo(subscription, session, out metadataChanged); } @@ -1751,7 +1901,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, out bool metadataChanged) { metadataChanged = false; - if (item is not EventItem model || Item == null) + if (item is not EventItem model || !Valid) { return false; } @@ -1777,7 +1927,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, if (metadataChanged) { - Item.Filter = GetEventFilter(session); + Filter = GetEventFilter(session); } return itemChange; } @@ -1848,7 +1998,7 @@ protected virtual bool ProcessEventNotification(uint sequenceNumber, DateTime ti protected IEnumerable ToMonitoredItemNotifications( uint sequenceNumber, EventFieldList eventFields) { - Debug.Assert(Item != null); + Debug.Assert(Valid); Debug.Assert(Template != null); if (Fields.Count >= eventFields.EventFields.Count) @@ -2178,6 +2328,10 @@ await ParseFieldsAsync(session, fieldNames, componentNode, /// /// Condition item /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(DataChangeFilter))] + [KnownType(typeof(EventFilter))] + [KnownType(typeof(AggregateFilter))] internal class Condition : EventItem { /// @@ -2197,6 +2351,29 @@ public Condition(EventMonitoredItemModel template, _conditionTimer = new Timer(OnConditionTimerElapsed); } + /// + /// Copy constructor + /// + /// + /// + /// + private Condition(Condition item, bool copyEventHandlers, + bool copyClientHandle) + : base(item, copyEventHandlers, copyClientHandle) + { + _snapshotInterval = item._snapshotInterval; + _updateInterval = item._updateInterval; + _conditionHandlingState = item._conditionHandlingState; + _conditionTimer = new Timer(OnConditionTimerElapsed); + } + + /// + public override MonitoredItem CloneMonitoredItem( + bool copyEventHandlers, bool copyClientHandle) + { + return new Condition(this, copyEventHandlers, copyClientHandle); + } + /// public override bool Equals(object? obj) { @@ -2217,8 +2394,8 @@ public override int GetHashCode() public override string ToString() { return - $"Condition Item '{Template.StartNodeId}' with server id {ServerId}" + - $" - {(Item?.Status?.Created == true ? "" : "not ")}created"; + $"Condition Item '{Template.StartNodeId}' with server id {RemoteId}" + + $" - {(Status?.Created == true ? "" : "not ")}created"; } /// @@ -2235,10 +2412,10 @@ protected override void Dispose(bool disposing) protected override bool ProcessEventNotification(uint sequenceNumber, DateTime timestamp, EventFieldList eventFields, IList notifications) { - Debug.Assert(Item != null); + Debug.Assert(Valid); Debug.Assert(Template != null); - var evFilter = Item.Filter as EventFilter; + var evFilter = Filter as EventFilter; var eventTypeIndex = evFilter?.SelectClauses.IndexOf( evFilter.SelectClauses .FirstOrDefault(x => x.TypeDefinitionId == ObjectTypeIds.BaseEventType @@ -2275,23 +2452,23 @@ protected override bool ProcessEventNotification(uint sequenceNumber, DateTime t _logger.LogInformation("{Item}: Issuing ConditionRefresh for " + "item {Name} on subscription {Subscription} due to receiving " + "a RefreshRequired event", this, Template.DisplayName, - Item.Subscription.DisplayName); + Subscription.DisplayName); try { - Item.Subscription.ConditionRefresh(); + Subscription.ConditionRefresh(); } catch (Exception e) { _logger.LogInformation("{Item}: ConditionRefresh for item {Name} " + "on subscription {Subscription} failed with error '{Message}'", - this, Template.DisplayName, Item.Subscription.DisplayName, e.Message); + this, Template.DisplayName, Subscription.DisplayName, e.Message); noErrorFound = false; } if (noErrorFound) { _logger.LogInformation("{Item}: ConditionRefresh for item {Name} " + "on subscription {Subscription} has completed", this, - Template.DisplayName, Item.Subscription.DisplayName); + Template.DisplayName, Subscription.DisplayName); } return true; } @@ -2339,7 +2516,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, out bool metadataChanged) { metadataChanged = false; - if (item is not Condition model || Item == null) + if (item is not Condition model || !Valid) { return false; } @@ -2469,7 +2646,7 @@ private void OnConditionTimerElapsed(object? sender) var state = _conditionHandlingState; try { - if (Item?.Created != true) + if (!Created) { return; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSession.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSession.cs index 73d1916120..cf99b8499e 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSession.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSession.cs @@ -22,12 +22,17 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Services using System.Linq; using System.Threading; using System.Threading.Tasks; + using System.Security.Cryptography.X509Certificates; + using System.Runtime.Serialization; /// - /// OPC UA Client based on official ua client reference sample. + /// OPC UA session extends the SDK session /// - internal sealed class OpcUaSession : IOpcUaSession, ISessionServices, - ISessionAccessor, IDisposable + [DataContract(Namespace = OpcUaClient.Namespace)] + [KnownType(typeof(OpcUaSubscription))] + [KnownType(typeof(OpcUaMonitoredItem))] + internal sealed class OpcUaSession : Session, IOpcUaSession, + ISessionServices, ISessionAccessor { /// public IVariantEncoder Codec { get; } @@ -35,122 +40,127 @@ internal sealed class OpcUaSession : IOpcUaSession, ISessionServices, /// public ISessionServices Services => this; - /// - public ITypeTable TypeTree => Session.TypeTree; - - /// - public INodeCache NodeCache => Session.NodeCache; - - /// - public IServiceMessageContext MessageContext => Session.MessageContext; - - /// - public ISystemContext SystemContext => Session.SystemContext; - /// - /// The underlying session + /// Type system has loaded /// - internal ISession Session { get; } + internal bool IsTypeSystemLoaded + => _complexTypeSystem?.IsCompletedSuccessfully ?? false; /// - /// Type system has loaded + /// Get list of subscription handles registered in the session /// - internal bool IsTypeSystemLoaded => _complexTypeSystem?.IsCompleted ?? false; + internal List SubscriptionHandles + { + get + { + lock (SyncRoot) + { + return Subscriptions.OfType().ToList(); + } + } + } /// /// Create session /// - /// - /// - /// - /// + /// /// /// - /// - /// - /// + /// + /// + /// + /// + /// + /// /// - public OpcUaSession(ISession session, KeepAliveEventHandler keepAlive, - TimeSpan keepAliveInterval, TimeSpan operationTimeout, + public OpcUaSession(OpcUaClient client, IJsonSerializer serializer, ILogger logger, - PublishErrorEventHandler? errorHandler = null, - PublishSequenceNumbersToAcknowledgeEventHandler? ackHandler = null, - bool preloadComplexTypeSystem = true) + ITransportChannel channel, ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, X509Certificate2? clientCertificate = null, + EndpointDescriptionCollection? availableEndpoints = null, + StringCollection? discoveryProfileUris = null) + : base(channel, configuration, endpoint, clientCertificate, + availableEndpoints, discoveryProfileUris) { - _logger = logger ?? - throw new ArgumentNullException(nameof(logger)); - _keepAlive = keepAlive ?? - throw new ArgumentNullException(nameof(keepAlive)); - Session = session ?? - throw new ArgumentNullException(nameof(session)); - - // support transfer - Session.DeleteSubscriptionsOnClose = false; - Session.TransferSubscriptionsOnReconnect = true; - Session.KeepAliveInterval = (int)keepAliveInterval.TotalMilliseconds; - Session.OperationTimeout = (int)operationTimeout.TotalMilliseconds; - - _authenticationToken = (NodeId?)typeof(ClientBase).GetProperty( - "AuthenticationToken", - System.Reflection.BindingFlags.NonPublic | - System.Reflection.BindingFlags.Instance)?.GetValue(session) - ?? NodeId.Null; - - Codec = new JsonVariantEncoder(session.MessageContext, serializer); - if (errorHandler != null) - { - Session.PublishError += errorHandler; - _errorHandler = errorHandler; - } - if (ackHandler != null) - { - Session.PublishSequenceNumbersToAcknowledge += ackHandler; - _ackHandler = ackHandler; - } - Session.KeepAlive += keepAlive; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _client = client ?? throw new ArgumentNullException(nameof(client)); + _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); - _cts = new CancellationTokenSource(); - _complexTypeSystem = preloadComplexTypeSystem ? - LoadComplexTypeSystemAsync() : null; + Initialize(); + Codec = new JsonVariantEncoder(MessageContext, serializer); + } + + /// + /// Copy constructor + /// + /// + /// + /// + /// + private OpcUaSession(OpcUaSession session, + ITransportChannel channel, Session template, bool copyEventHandlers) + : base(channel, template, copyEventHandlers) + { + _logger = session._logger; + _client = session._client; + _serializer = session._serializer; + + _complexTypeSystem = session._complexTypeSystem; + _history = session._history; + _limits = session._limits; + _server = session._server; + + Initialize(); + Codec = new JsonVariantEncoder(MessageContext, _serializer); } /// - public void Dispose() + protected override void Dispose(bool disposing) { - try + if (disposing && !_disposed) { - _cts.Cancel(); - Session.KeepAlive -= _keepAlive; - if (_ackHandler != null) + _disposed = true; + + PublishError -= + _client.Session_HandlePublishError; + PublishSequenceNumbersToAcknowledge -= + _client.Session_PublishSequenceNumbersToAcknowledge; + KeepAlive -= + _client.Session_KeepAlive; + SessionConfigurationChanged -= + Session_SessionConfigurationChanged; + + try { - Session.PublishSequenceNumbersToAcknowledge -= _ackHandler; + _cts.Cancel(); + _logger.LogDebug("Session {Name} disposed.", SessionName); } - if (_errorHandler != null) + finally { - Session.PublishError -= _errorHandler; + _activitySource.Dispose(); + _cts.Dispose(); } - - Session.Dispose(); - _logger.LogDebug("Session {Name} disposed.", Session.SessionName); - } - finally - { - _activitySource.Dispose(); - _cts.Dispose(); } + base.Dispose(disposing); + } + + /// + public override Session CloneSession(ITransportChannel channel, bool copyEventHandlers) + { + return new OpcUaSession(this, channel, this, copyEventHandlers); } /// public bool TryGetSession([NotNullWhen(true)] out ISession? session) { - session = Session; + session = this; return true; } /// public override string? ToString() { - return Session.SessionName; + return SessionName; } /// @@ -211,29 +221,33 @@ public async ValueTask GetHistoryCapabilitiesAsy /// public async ValueTask GetComplexTypeSystemAsync(CancellationToken ct) { - try + for (var attempt = 0; attempt < 2; attempt++) { - Debug.Assert(_complexTypeSystem != null); - return await _complexTypeSystem.WaitAsync(ct).ConfigureAwait(false); - } - catch (OperationCanceledException) when (ct.IsCancellationRequested) - { - // Throw any cancellation token exception - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to get complex type system for client {Client}.", this); + try + { + Debug.Assert(_complexTypeSystem != null); + return await _complexTypeSystem.WaitAsync(ct).ConfigureAwait(false); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + // Throw any cancellation token exception + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, + "Attempt #{Attempt}. Failed to get complex type system for client {Client}.", + attempt, this); - // Try again. TODO: Throttle using a timer or so... - _complexTypeSystem = LoadComplexTypeSystemAsync(); - return null; + // Try again. TODO: Throttle using a timer or so... + _complexTypeSystem = LoadComplexTypeSystemAsync(); + } } + return null; } /// - public async Task AddNodesAsync(RequestHeader requestHeader, + async ValueTask ISessionServices.AddNodesAsync(RequestHeader requestHeader, AddNodesItemCollection nodesToAdd, CancellationToken ct) { using var activity = Begin(requestHeader); @@ -246,13 +260,13 @@ public async Task AddNodesAsync(RequestHeader requestHeader, RequestHeader = requestHeader, NodesToAdd = nodesToAdd }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task AddReferencesAsync( + async ValueTask ISessionServices.AddReferencesAsync( RequestHeader requestHeader, AddReferencesItemCollection referencesToAdd, CancellationToken ct) { @@ -266,13 +280,13 @@ public async Task AddReferencesAsync( RequestHeader = requestHeader, ReferencesToAdd = referencesToAdd }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task DeleteNodesAsync( + async ValueTask ISessionServices.DeleteNodesAsync( RequestHeader requestHeader, DeleteNodesItemCollection nodesToDelete, CancellationToken ct) { @@ -286,13 +300,13 @@ public async Task DeleteNodesAsync( RequestHeader = requestHeader, NodesToDelete = nodesToDelete }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task DeleteReferencesAsync( + async ValueTask ISessionServices.DeleteReferencesAsync( RequestHeader requestHeader, DeleteReferencesItemCollection referencesToDelete, CancellationToken ct) { @@ -306,13 +320,13 @@ public async Task DeleteReferencesAsync( RequestHeader = requestHeader, ReferencesToDelete = referencesToDelete }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task BrowseAsync( + async ValueTask ISessionServices.BrowseAsync( RequestHeader requestHeader, ViewDescription? view, uint requestedMaxReferencesPerNode, BrowseDescriptionCollection nodesToBrowse, CancellationToken ct) @@ -329,13 +343,13 @@ public async Task BrowseAsync( RequestedMaxReferencesPerNode = requestedMaxReferencesPerNode, NodesToBrowse = nodesToBrowse }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task BrowseNextAsync( + async ValueTask ISessionServices.BrowseNextAsync( RequestHeader requestHeader, bool releaseContinuationPoints, ByteStringCollection continuationPoints, CancellationToken ct) { @@ -350,13 +364,13 @@ public async Task BrowseNextAsync( ReleaseContinuationPoints = releaseContinuationPoints, ContinuationPoints = continuationPoints }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task TranslateBrowsePathsToNodeIdsAsync( + async ValueTask ISessionServices.TranslateBrowsePathsToNodeIdsAsync( RequestHeader requestHeader, BrowsePathCollection browsePaths, CancellationToken ct) { @@ -371,13 +385,13 @@ public async Task TranslateBrowsePathsToN RequestHeader = requestHeader, BrowsePaths = browsePaths }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task RegisterNodesAsync( + async ValueTask ISessionServices.RegisterNodesAsync( RequestHeader requestHeader, NodeIdCollection nodesToRegister, CancellationToken ct) { @@ -391,13 +405,13 @@ public async Task RegisterNodesAsync( RequestHeader = requestHeader, NodesToRegister = nodesToRegister }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task UnregisterNodesAsync( + async ValueTask ISessionServices.UnregisterNodesAsync( RequestHeader requestHeader, NodeIdCollection nodesToUnregister, CancellationToken ct) { @@ -411,13 +425,13 @@ public async Task UnregisterNodesAsync( RequestHeader = requestHeader, NodesToUnregister = nodesToUnregister }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task QueryFirstAsync( + async ValueTask ISessionServices.QueryFirstAsync( RequestHeader requestHeader, ViewDescription view, NodeTypeDescriptionCollection nodeTypes, ContentFilter filter, uint maxDataSetsToReturn, uint maxReferencesToReturn, @@ -437,13 +451,13 @@ public async Task QueryFirstAsync( MaxDataSetsToReturn = maxDataSetsToReturn, MaxReferencesToReturn = maxReferencesToReturn }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task QueryNextAsync( + async ValueTask ISessionServices.QueryNextAsync( RequestHeader requestHeader, bool releaseContinuationPoint, byte[] continuationPoint, CancellationToken ct) { @@ -458,13 +472,13 @@ public async Task QueryNextAsync( ReleaseContinuationPoint = releaseContinuationPoint, ContinuationPoint = continuationPoint }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task ReadAsync(RequestHeader requestHeader, + async ValueTask ISessionServices.ReadAsync(RequestHeader requestHeader, double maxAge, Opc.Ua.TimestampsToReturn timestampsToReturn, ReadValueIdCollection nodesToRead, CancellationToken ct) { @@ -480,13 +494,13 @@ public async Task ReadAsync(RequestHeader requestHeader, TimestampsToReturn = timestampsToReturn, NodesToRead = nodesToRead }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task HistoryReadAsync( + async ValueTask ISessionServices.HistoryReadAsync( RequestHeader requestHeader, ExtensionObject? historyReadDetails, Opc.Ua.TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, HistoryReadValueIdCollection nodesToRead, CancellationToken ct) @@ -504,13 +518,13 @@ public async Task HistoryReadAsync( ReleaseContinuationPoints = releaseContinuationPoints, NodesToRead = nodesToRead }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task WriteAsync(RequestHeader requestHeader, + async ValueTask ISessionServices.WriteAsync(RequestHeader requestHeader, WriteValueCollection nodesToWrite, CancellationToken ct) { using var activity = Begin(requestHeader); @@ -523,13 +537,13 @@ public async Task WriteAsync(RequestHeader requestHeader, RequestHeader = requestHeader, NodesToWrite = nodesToWrite }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task HistoryUpdateAsync( + async ValueTask ISessionServices.HistoryUpdateAsync( RequestHeader requestHeader, ExtensionObjectCollection historyUpdateDetails, CancellationToken ct) { @@ -543,13 +557,13 @@ public async Task HistoryUpdateAsync( RequestHeader = requestHeader, HistoryUpdateDetails = historyUpdateDetails }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } /// - public async Task CallAsync(RequestHeader requestHeader, + async ValueTask ISessionServices.CallAsync(RequestHeader requestHeader, CallMethodRequestCollection methodsToCall, CancellationToken ct) { using var activity = Begin(requestHeader); @@ -562,24 +576,67 @@ public async Task CallAsync(RequestHeader requestHeader, RequestHeader = requestHeader, MethodsToCall = methodsToCall }; - var response = await Session.TransportChannel.SendRequestAsync( + var response = await TransportChannel.SendRequestAsync( request, ct).ConfigureAwait(false); return activity.ValidateResponse(response); } - /// - public async ValueTask CloseAsync(CancellationToken ct) + /// + /// Called when session is created + /// + /// + /// + public override void SessionCreated(NodeId sessionId, NodeId sessionCookie) { - try - { - await Session.CloseAsync(ct).ConfigureAwait(false); + base.SessionCreated(sessionId, sessionCookie); + //PreloadComplexTypeSystem(); + } - _logger.LogDebug("Successfully closed session {Session}.", this); - } - catch (Exception ex) + /// + /// Called when session configuration changed + /// + /// + /// + private void Session_SessionConfigurationChanged(object? sender, EventArgs e) + { + PreloadComplexTypeSystem(); + } + + /// + /// Preload type system + /// + private void PreloadComplexTypeSystem() + { + if (_complexTypeSystem != null || !Connected || + _client.DisableComplexTypePreloading == true) { - _logger.LogError(ex, "Failed to close session {Session}.", this); + return; } + _complexTypeSystem = LoadComplexTypeSystemAsync(); + } + + /// + /// Initialize session settings from client configuration + /// + private void Initialize() + { + SessionFactory = _client; + DeleteSubscriptionsOnClose = false; + TransferSubscriptionsOnReconnect = true; + + PublishError += + _client.Session_HandlePublishError; + PublishSequenceNumbersToAcknowledge += + _client.Session_PublishSequenceNumbersToAcknowledge; + KeepAlive += + _client.Session_KeepAlive; + SessionConfigurationChanged += + Session_SessionConfigurationChanged; + + KeepAliveInterval = + (int)(_client.KeepAliveInterval ?? TimeSpan.FromSeconds(30)).TotalMilliseconds; + OperationTimeout = + (int)(_client.OperationTimeout ?? TimeSpan.FromMinutes(1)).TotalMilliseconds; } /// @@ -592,7 +649,7 @@ public async ValueTask CloseAsync(CancellationToken ct) CancellationToken ct) { // Fetch limits into the session using the new api - var maxNodesPerRead = Validate32(Session.OperationLimits.MaxNodesPerRead); + var maxNodesPerRead = Validate32(OperationLimits.MaxNodesPerRead); // Read once more to ensure we have all we need and also correctly show what is not provided. var nodes = new[] { @@ -626,7 +683,7 @@ public async ValueTask CloseAsync(CancellationToken ct) NodeId = n, AttributeId = Attributes.Value })); - var response = await Session.ReadAsync(header, 0, + var response = await ReadAsync(header, 0, Opc.Ua.TimestampsToReturn.Both, requests, ct).ConfigureAwait(false); var results = response.Validate(response.Results, d => d.StatusCode, response.DiagnosticInfos, requests); @@ -877,17 +934,21 @@ private Task LoadComplexTypeSystemAsync() { return Task.Run(async () => { - if (Session?.Connected == true) + if (Connected) { - var complexTypeSystem = new ComplexTypeSystem(Session); + var complexTypeSystem = new ComplexTypeSystem(this); await complexTypeSystem.Load().ConfigureAwait(false); - _logger.LogInformation( - "Complex type system loaded into client {Client}.", this); - // Clear cache to release memory. - // TODO: we should have a real node cache here - NodeCache.Clear(); - return complexTypeSystem; + if (Connected) + { + _logger.LogInformation( + "Complex type system loaded into client {Client}.", this); + + // Clear cache to release memory. + // TODO: we should have a real node cache here + NodeCache?.Clear(); + return complexTypeSystem; + } } throw new ServiceResultException(StatusCodes.BadNotConnected); }, _cts.Token); @@ -903,7 +964,7 @@ private SessionActivity Begin(RequestHeader header) where T : IServiceResponse, new() { var activity = new SessionActivity(this, typeof(T).Name[0..^8]); - if (!Session.Connected) + if (!Connected) { var error = new T(); error.ResponseHeader.ServiceResult = StatusCodes.BadNotConnected; @@ -924,8 +985,8 @@ private SessionActivity Begin(RequestHeader header) } else { - header.RequestHandle = Session.NewRequestHandle(); - header.AuthenticationToken = _authenticationToken; + header.RequestHandle = NewRequestHandle(); + header.AuthenticationToken = AuthenticationToken; header.Timestamp = DateTime.UtcNow; } return activity; @@ -1021,12 +1082,11 @@ private sealed record class LogScope(string name, Stopwatch sw, ILogger logger); private OperationLimitsModel? _limits; private HistoryServerCapabilitiesModel? _history; private Task? _complexTypeSystem; - private readonly CancellationTokenSource _cts; - private readonly NodeId _authenticationToken; - private readonly KeepAliveEventHandler _keepAlive; + private bool _disposed; + private readonly CancellationTokenSource _cts = new(); private readonly ILogger _logger; - private readonly PublishErrorEventHandler? _errorHandler; - private readonly PublishSequenceNumbersToAcknowledgeEventHandler? _ackHandler; + private readonly OpcUaClient _client; + private readonly IJsonSerializer _serializer; private readonly ActivitySource _activitySource = Diagnostics.NewActivitySource(); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs index fc8c80af51..47b0c4d8c9 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs @@ -17,43 +17,34 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Services using Opc.Ua.Extensions; using System; using System.Collections.Generic; - using System.Collections.Immutable; + using System.Collections.Frozen; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; - using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; + using System.Runtime.Serialization; /// /// Subscription implementation /// - internal sealed class OpcUaSubscription : IOpcUaSubscription, ISubscriptionHandle + [DataContract(Namespace = OpcUaClient.Namespace)] + [KnownType(typeof(OpcUaMonitoredItem))] + [KnownType(typeof(OpcUaMonitoredItem.DataItem))] + [KnownType(typeof(OpcUaMonitoredItem.DataItemWithCyclicRead))] + [KnownType(typeof(OpcUaMonitoredItem.DataItemWithHeartbeat))] + [KnownType(typeof(OpcUaMonitoredItem.EventItem))] + [KnownType(typeof(OpcUaMonitoredItem.Condition))] + [KnownType(typeof(OpcUaMonitoredItem.FieldItem))] + internal sealed class OpcUaSubscription : Subscription, ISubscriptionHandle, + IOpcUaSubscription { /// - public string? Name => _subscription?.Id.Id; + public string Name => _template.Id.Id; /// - public ushort Id { get; } - - /// - public ConnectionModel? Connection => _subscription?.Id.Connection; - - /// - public event EventHandler? OnSubscriptionKeepAlive; - - /// - public event EventHandler? OnSubscriptionDataChange; - - /// - public event EventHandler? OnSubscriptionEventChange; - - /// - public event EventHandler<(bool, int, int, int)>? OnSubscriptionDataDiagnosticsChange; - - /// - public event EventHandler<(bool, int)>? OnSubscriptionEventDiagnosticsChange; + public ushort LocalIndex { get; } /// /// Current metadata @@ -61,84 +52,126 @@ internal sealed class OpcUaSubscription : IOpcUaSubscription, ISubscriptionHandl internal DataSetMetaDataType? CurrentMetaData => _metaDataLoader.IsValueCreated ? _metaDataLoader.Value.MetaData : null; + /// + /// Whether the subscription is online + /// + internal bool IsOnline + => Handle != null && Session?.Connected == true && !_closed; + + /// + /// Client state + /// + internal IOpcUaClientDiagnostics State + => (_client as IOpcUaClientDiagnostics) ?? OpcUaClient.Disconnected; + /// /// Subscription /// /// + /// + /// /// /// /// - private OpcUaSubscription(IClientAccessor clients, + internal OpcUaSubscription(IClientAccessor clients, + ISubscriptionCallbacks callbacks, SubscriptionModel template, IOptions options, ILoggerFactory loggerFactory, - IMetricsContext? metrics) + IMetricsContext metrics) { - _clients = clients ?? - throw new ArgumentNullException(nameof(clients)); - _options = options ?? - throw new ArgumentNullException(nameof(options)); - _loggerFactory = loggerFactory ?? - throw new ArgumentNullException(nameof(loggerFactory)); - _metrics = metrics ?? - throw new ArgumentNullException(nameof(metrics)); - - _logger = loggerFactory.CreateLogger(); - _lock = new SemaphoreSlim(1, 1); - _currentlyMonitored = ImmutableDictionary.Empty; + _clients = clients ?? throw new ArgumentNullException(nameof(clients)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics)); + _callbacks = callbacks ?? throw new ArgumentNullException(nameof(callbacks)); + _template = ValidateSubscriptionInfo(template); + + _logger = _loggerFactory.CreateLogger(); + _currentlyMonitored = FrozenDictionary.Empty; + LocalIndex = Opc.Ua.SequenceNumber.Increment16(ref _lastIndex); + + Initialize(); _metaDataLoader = new Lazy(() => new MetaDataLoader(this), true); - Id = SequenceNumber.Increment16(ref _lastIndex); _timer = new Timer(OnSubscriptionManagementTriggered); _keepAliveWatcher = new Timer(OnKeepAliveMissing); + InitializeMetrics(); + TriggerManageSubscription(true); } /// - /// Create subscription + /// Copy constructor /// - /// - /// /// - /// - /// - /// - /// - internal static async ValueTask CreateAsync( - IClientAccessor outer, IOptions options, - SubscriptionModel subscription, ILoggerFactory loggerFactory, - IMetricsContext? metrics, CancellationToken ct = default) + /// + private OpcUaSubscription(OpcUaSubscription subscription, bool copyEventHandlers) + : base(subscription, copyEventHandlers) { - // Create object - var newSubscription = new OpcUaSubscription(outer, options, loggerFactory, metrics); + _clients = subscription._clients; + _options = subscription._options; + _loggerFactory = subscription._loggerFactory; + _metrics = subscription._metrics; + _template = ValidateSubscriptionInfo(subscription._template); + _callbacks = subscription._callbacks; + + LocalIndex = subscription.LocalIndex; + _client = subscription._client; + _useDeferredAcknoledge = subscription._useDeferredAcknoledge; + _logger = subscription._logger; + _sequenceNumber = subscription._sequenceNumber; + + // TODO: Should copy? + _currentlyMonitored = subscription._currentlyMonitored; + _currentSequenceNumber = subscription._currentSequenceNumber; + _previousSequenceNumber = subscription._previousSequenceNumber; + _continuouslyMissingKeepAlives = subscription._continuouslyMissingKeepAlives; + _closed = subscription._closed; + + Initialize(); + _metaDataLoader = new Lazy(() => new MetaDataLoader(this), true); + _timer = new Timer(OnSubscriptionManagementTriggered); + _keepAliveWatcher = new Timer(OnKeepAliveMissing); - // Initialize - await newSubscription.UpdateAsync(subscription, ct).ConfigureAwait(false); + InitializeMetrics(); + } - return newSubscription; + /// + public override object Clone() + { + return new OpcUaSubscription(this, true); } /// - public override string? ToString() + public override Subscription CloneSubscription(bool copyEventHandlers) { - var subscriptionName = _subscription?.Id?.ToString() ?? ""; - var subscriptionId = _currentSubscription?.Id.ToString(CultureInfo.CurrentCulture) ?? ""; - return $"{subscriptionName}:{subscriptionId}"; + return new OpcUaSubscription(this, copyEventHandlers); } /// - public void OnSubscriptionStateChanged(bool online, IOpcUaClientState state) + public override string? ToString() { - _state = state; - _online = online; + return $"{_template.Id.Id}:{Id}"; + } - foreach (var monitoredItem in _currentlyMonitored.Values) + /// + public override bool Equals(object? obj) + { + if (obj is not OpcUaSubscription subscription) { - monitoredItem.OnMonitoredItemStateChanged(online); + return false; } + return subscription._template.Id.Equals(_template.Id); + } + + /// + public override int GetHashCode() + { + return _template.Id.GetHashCode(); } /// public bool TryGetCurrentPosition(out uint subscriptionId, out uint sequenceNumber) { - subscriptionId = _currentSubscription?.Id ?? 0; + subscriptionId = Id; sequenceNumber = _currentSequenceNumber; return _useDeferredAcknoledge; } @@ -146,183 +179,164 @@ public bool TryGetCurrentPosition(out uint subscriptionId, out uint sequenceNumb /// public IOpcUaSubscriptionNotification? CreateKeepAlive() { - _lock.Wait(); - try + lock (_lock) { - var subscription = _currentSubscription; - if (subscription == null) + if (_disposed) { + _logger.LogError("Subscription {Subscription} already DISPOSED!", this); return null; } - return new Notification(this, subscription.Id) + try { - ServiceMessageContext = subscription.Session.MessageContext, - ApplicationUri = subscription.Session.Endpoint.Server.ApplicationUri, - EndpointUrl = subscription.Session.Endpoint.EndpointUrl, - SubscriptionName = Name, - SequenceNumber = SequenceNumber.Increment32(ref _sequenceNumber), - SubscriptionId = Id, - MessageType = MessageType.KeepAlive - }; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to create a subscription notification for subscription {Subscription}.", - this); - return null; - } - finally - { - _lock.Release(); + var session = Session; + if (session == null) + { + return null; + } + return new Notification(this, Id, session.MessageContext) + { + ApplicationUri = session.Endpoint.Server.ApplicationUri, + EndpointUrl = session.Endpoint.EndpointUrl, + SubscriptionName = Name, + SequenceNumber = Opc.Ua.SequenceNumber.Increment32(ref _sequenceNumber), + SubscriptionId = LocalIndex, + MessageType = MessageType.KeepAlive + }; + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to create a subscription notification for subscription {Subscription}.", + this); + return null; + } } } /// - public async ValueTask UpdateAsync(SubscriptionModel subscription, - CancellationToken ct) + public void Update(SubscriptionModel subscription) { - ArgumentNullException.ThrowIfNull(subscription); - if (subscription.Configuration == null) - { - throw new ArgumentException("Missing configuration", nameof(subscription)); - } - if (subscription.Id?.Connection == null) + Debug.Assert(!_closed); + lock (_lock) { - throw new ArgumentException("Missing connection information", nameof(subscription)); - } - await _lock.WaitAsync(ct).ConfigureAwait(false); - try - { - var previousSubscription = _subscription?.Id; + if (_disposed) + { + _logger.LogError("Subscription {Subscription} already DISPOSED!", this); + return; + } // Update subscription configuration - _subscription = subscription.Clone(); + var previousTemplateId = _template.Id; + + _template = ValidateSubscriptionInfo(subscription, previousTemplateId.Id); + Debug.Assert(Name == previousTemplateId.Id, "The name must not change"); - if (previousSubscription is not null && previousSubscription != _subscription.Id) + // But connection information could have changed + if (previousTemplateId != _template.Id) { - // - // TODO: Do we need to remove any session from session manager - // if the subscription was transfered to new connection model - // has changed? - // - _logger.LogError("Upgrading existing subscription to different session."); - } + _logger.LogError("Upgrading subscription to different session."); - // try to get a session using the provided configuration - using var client = _clients.GetOrCreateClient(_subscription.Id.Connection); + // Force closing of the subscription and ... + _forceRecreate = true; - // - // Now register with the session to ensure the state is re-applied when - // session changes or connects if unconnected. - // Registering takes a ref count on the client which will stay alive and - // hopefully connected through the lifetime of the subscription. - // Note that we can call Register many times and only a single ref count - // is taken. - // - client.RegisterSubscription(this); - } - finally - { - _lock.Release(); + // ... release client handle to cause closing of session if last reference. + var client = _client; + _client = null; + + client?.Dispose(); + } + + TriggerManageSubscription(true); } } /// - public async ValueTask SyncWithSessionAsync(IOpcUaSession handle, bool sessionIsNew, - CancellationToken ct) + public void Close() { - if (_closed) + lock (_lock) { - return; - } - - // If we get here we are online - _online = true; - - // Lock access to the subscription state while we are applying the state. - await _lock.WaitAsync(ct).ConfigureAwait(false); - try - { - try + if (_disposed) { - await SyncWithSessionInternalAsync(handle, sessionIsNew, ct).ConfigureAwait(false); + _logger.LogError("Subscription {Subscription} already DISPOSED!", this); return; } - catch (Exception e) - { - _online = false; // Set true when retry comes back in - - _logger.LogDebug(e, - "Failed to apply state to Subscription {Subscription} in session {Session}...", - this, handle); - // Retry in 2 seconds - TriggerSubscriptionManagementCallbackIn( - _options.Value.SubscriptionErrorRetryDelay, kDefaultErrorRetryDelay); - } - } - finally - { - _lock.Release(); + Debug.Assert(!_closed); + _closed = true; + TriggerManageSubscription(true); } } /// - public async ValueTask CloseAsync() + public async ValueTask SyncWithSessionAsync(ISession session, CancellationToken ct) { - await _lock.WaitAsync().ConfigureAwait(false); - IOpcUaClient? client = null; + if (_disposed) + { + return; + } try { - var connection = _subscription?.Id.Connection; - if (_closed || connection == null) + if (_closed) // Finalize closing the subscription { - return; - } - _closed = true; + _callbacks.OnSubscriptionUpdated(null); - client = _clients.GetClient(connection); - if (client == null) - { - _logger.LogWarning( - "Failed to unregister subscription '{Subscription}'. " + - "The client for the connection could not be found.", this); + // Does not throw + await CloseCurrentSubscriptionAsync().ConfigureAwait(false); + + _client?.Dispose(); + _client = null; return; } - // Unregister subscription from session - client.UnregisterSubscription(this); + await SyncWithSessionInternalAsync(session, ct).ConfigureAwait(false); } - catch (ObjectDisposedException) { } // client accessor already disposed - finally + catch (Exception e) { - _currentlyMonitored = ImmutableDictionary.Empty; - NumberOfCreatedItems = 0; - NumberOfNotCreatedItems = 0; + _logger.LogDebug(e, + "Failed to apply state to Subscription {Subscription} in session {Session}...", + this, session); - // Does not throw - await CloseCurrentSubscriptionAsync().ConfigureAwait(false); - client?.Dispose(); - _state = OpcUaClient.Disconnected; - _lock.Release(); + // Retry in 2 seconds + TriggerSubscriptionManagementCallbackIn( + _options.Value.SubscriptionErrorRetryDelay, kDefaultErrorRetryDelay); } } /// - public void Dispose() + protected override void Dispose(bool disposing) { - if (!_closed) + try { - Try.Async(() => CloseAsync().AsTask()).Wait(); + if (disposing) + { + lock (_lock) + { + if (!_disposed) + { + _disposed = true; - Debug.Assert(_closed); - } + FastDataChangeCallback = null; + FastEventCallback = null; + FastKeepAliveCallback = null; + + PublishStatusChanged -= OnPublishStatusChange; + StateChanged -= OnStateChange; - _keepAliveWatcher.Dispose(); - _timer.Dispose(); - _meter.Dispose(); - _lock.Dispose(); + _keepAliveWatcher.Dispose(); + _timer.Dispose(); + _meter.Dispose(); + } + } + } + + Debug.Assert(!_disposed || FastDataChangeCallback == null); + Debug.Assert(!_disposed || FastKeepAliveCallback == null); + Debug.Assert(!_disposed || FastEventCallback == null); + } + finally + { + base.Dispose(disposing); + } } /// @@ -335,69 +349,69 @@ public void Dispose() internal void SendNotification(MessageType messageType, string? dataSetName, IEnumerable notifications, bool diagnosticsOnly) { - var subscription = _currentSubscription; - if (subscription == null || subscription.Id == 0) + if (!Created) { return; } - if (messageType == MessageType.Event || messageType == MessageType.Condition) + var session = Session; + if (session?.MessageContext == null) { - var onSubscriptionEventDiagnosticsChange = OnSubscriptionEventDiagnosticsChange; - var onSubscriptionEventChange = OnSubscriptionEventChange; - if (onSubscriptionEventChange == null) - { - return; - } + return; + } + #pragma warning disable CA2000 // Dispose objects before losing scope - var message = CreateMessage(notifications, messageType, dataSetName, subscription); + var message = new Notification(this, Id, session.MessageContext, notifications) + { + ApplicationUri = session.Endpoint?.Server?.ApplicationUri, + EndpointUrl = session.Endpoint?.EndpointUrl, + SubscriptionName = Name, + DataSetName = dataSetName, + SubscriptionId = LocalIndex, + SequenceNumber = Opc.Ua.SequenceNumber.Increment32(ref _sequenceNumber), + MessageType = messageType + }; #pragma warning restore CA2000 // Dispose objects before losing scope + + if (messageType == MessageType.Event || messageType == MessageType.Condition) + { if (!diagnosticsOnly) { - onSubscriptionEventChange.Invoke(this, message); + _callbacks.OnSubscriptionEventChange(message); } - if (message.Notifications.Count > 0 && onSubscriptionEventDiagnosticsChange != null) + if (message.Notifications.Count > 0) { - onSubscriptionEventDiagnosticsChange.Invoke(this, - (false, message.Notifications.Count)); + _callbacks.OnSubscriptionEventDiagnosticsChange(false, message.Notifications.Count); } } else { - var onSubscriptionDataDiagnosticsChange = OnSubscriptionDataDiagnosticsChange; - var onSubscriptionDataChange = OnSubscriptionDataChange; - if (onSubscriptionDataChange == null) - { - return; - } -#pragma warning disable CA2000 // Dispose objects before losing scope - var message = CreateMessage(notifications, messageType, dataSetName, subscription); -#pragma warning restore CA2000 // Dispose objects before losing scope if (!diagnosticsOnly) { - onSubscriptionDataChange.Invoke(this, message); + _callbacks.OnSubscriptionDataChange(message); } - if (message.Notifications.Count > 0 && onSubscriptionDataDiagnosticsChange != null) + if (message.Notifications.Count > 0) { - onSubscriptionDataDiagnosticsChange.Invoke(this, - (false, message.Notifications.Count, message.Heartbeats, message.CyclicReads)); + _callbacks.OnSubscriptionDataDiagnosticsChange(false, + message.Notifications.Count, message.Heartbeats, message.CyclicReads); } } + } - Notification CreateMessage(IEnumerable notifications, - MessageType messageType, string? dataSetName, Subscription subscription) - { - return new Notification(this, subscription.Id, notifications) - { - ServiceMessageContext = subscription.Session?.MessageContext, - ApplicationUri = subscription.Session?.Endpoint?.Server?.ApplicationUri, - EndpointUrl = subscription.Session?.Endpoint?.EndpointUrl, - SubscriptionName = Name, - DataSetName = dataSetName, - SubscriptionId = Id, - SequenceNumber = SequenceNumber.Increment32(ref _sequenceNumber), - MessageType = messageType - }; - } + /// + /// Initialize state + /// + private void Initialize() + { + FastKeepAliveCallback = OnSubscriptionKeepAliveNotification; + FastDataChangeCallback = OnSubscriptionDataChangeNotification; + FastEventCallback = OnSubscriptionEventNotificationList; + PublishStatusChanged += OnPublishStatusChange; + StateChanged += OnStateChange; + TimestampsToReturn = Opc.Ua.TimestampsToReturn.Both; + DisableMonitoredItemCache = true; + RepublishAfterTransfer = true; + + _callbacks.OnSubscriptionUpdated(_closed ? null : this); } /// @@ -406,39 +420,37 @@ Notification CreateMessage(IEnumerable notificat /// private async Task CloseCurrentSubscriptionAsync() { - Debug.Assert(_lock.CurrentCount == 0); // Should be always under lock - var subscription = _currentSubscription; - if (subscription == null) + ResetKeepAliveTimer(); + if (Handle == null) { // Already closed return; } - _currentSubscription = null; - ResetKeepAliveTimer(); + + Handle = null; try { -#if !DEBUG // Get the wrong subscription callbacks in debug - subscription.FastDataChangeCallback = null; - subscription.FastEventCallback = null; - subscription.FastKeepAliveCallback = null; -#endif _logger.LogDebug("Closing subscription '{Subscription}'...", this); + + _currentlyMonitored = FrozenDictionary.Empty; _currentSequenceNumber = 0; + NumberOfCreatedItems = 0; + NumberOfNotCreatedItems = 0; await Try.Async( - () => subscription.SetPublishingModeAsync(false)).ConfigureAwait(false); + () => SetPublishingModeAsync(false)).ConfigureAwait(false); await Try.Async( - () => subscription.DeleteItemsAsync(default)).ConfigureAwait(false); + () => DeleteItemsAsync(default)).ConfigureAwait(false); await Try.Async( - () => subscription.ApplyChangesAsync()).ConfigureAwait(false); + () => ApplyChangesAsync()).ConfigureAwait(false); _logger.LogDebug("Deleted monitored items for '{Subscription}'.", this); await Try.Async( - () => subscription.DeleteAsync(true)).ConfigureAwait(false); + () => DeleteAsync(true)).ConfigureAwait(false); - if (subscription.Session != null) + if (Session != null) { - await subscription.Session.RemoveSubscriptionAsync(subscription).ConfigureAwait(false); + await Session.RemoveSubscriptionAsync(this).ConfigureAwait(false); } _logger.LogInformation("Subscription '{Subscription}' closed.", this); @@ -447,44 +459,35 @@ await Try.Async( { _logger.LogError(e, "Failed to close subscription {Subscription}", this); } - finally - { - subscription.PublishStatusChanged -= OnPublishStatusChange; - subscription.StateChanged -= OnStateChange; - - subscription.Dispose(); - } } /// /// Synchronize monitored items in subscription (no lock) /// - /// - /// /// /// - private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscription, - IOpcUaSession sessionHandle, IEnumerable monitoredItems, - CancellationToken ct) + private async Task SynchronizeMonitoredItemsAsync( + IEnumerable monitoredItems, CancellationToken ct) { - Debug.Assert(_lock.CurrentCount == 0); + var session = Session as OpcUaSession; + Debug.Assert(session != null); TriggerSubscriptionManagementCallbackIn(Timeout.InfiniteTimeSpan); // Get limits to batch requests during resolve - var operationLimits = await sessionHandle.GetOperationLimitsAsync( + var operationLimits = await session.GetOperationLimitsAsync( ct).ConfigureAwait(false); #pragma warning disable CA2000 // Dispose objects before losing scope var desired = OpcUaMonitoredItem .Create(monitoredItems, _loggerFactory, _clients, - _subscription?.Id.Connection == null ? null : - new ConnectionIdentifier(_subscription.Id.Connection)) + _template.Id.Connection == null ? null : + new ConnectionIdentifier(_template.Id.Connection)) .ToHashSet(); - var previouslyMonitored = _currentlyMonitored.Values.ToImmutableHashSet(); - var remove = previouslyMonitored.Except(desired); - var add = desired.Except(previouslyMonitored).ToImmutableHashSet(); + var previouslyMonitored = _currentlyMonitored.Values.ToHashSet(); + var remove = previouslyMonitored.Except(desired).ToHashSet(); + var add = desired.Except(previouslyMonitored).ToHashSet(); var same = previouslyMonitored.ToHashSet(); same.IntersectWith(desired); @@ -505,14 +508,14 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip foreach (var resolvers in allResolvers.Batch( (int?)operationLimits.MaxNodesPerTranslatePathsToNodeIds ?? 1)) { - var response = await sessionHandle.Services.TranslateBrowsePathsToNodeIdsAsync( + var response = await session.Services.TranslateBrowsePathsToNodeIdsAsync( new RequestHeader(), new BrowsePathCollection(resolvers .Select(a => new BrowsePath { StartingNode = a!.Value.NodeId.ToNodeId( - sessionHandle.MessageContext), + session.MessageContext), RelativePath = a.Value.Path.ToRelativePath( - sessionHandle.MessageContext) + session.MessageContext) })), ct).ConfigureAwait(false); var results = response.Validate(response.Results, s => s.StatusCode, @@ -531,7 +534,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip if (result.ErrorInfo == null && result.Result.Targets.Count == 1) { resolvedId = result.Result.Targets[0].TargetId.ToNodeId( - sessionHandle.MessageContext.NamespaceUris); + session.MessageContext.NamespaceUris); } else { @@ -539,7 +542,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip "in {Subscription} due to '{ServiceResult}'", result.Request!.Value.NodeId, this, result.ErrorInfo); } - result.Request!.Value.Update(resolvedId, sessionHandle.MessageContext); + result.Request!.Value.Update(resolvedId, session.MessageContext); } } } @@ -560,16 +563,16 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip foreach (var registrations in allRegistrations.Batch( (int?)operationLimits.MaxNodesPerRegisterNodes ?? 1)) { - var response = await sessionHandle.Services.RegisterNodesAsync( + var response = await session.Services.RegisterNodesAsync( new RequestHeader(), new NodeIdCollection(registrations - .Select(a => a!.Value.NodeId.ToNodeId(sessionHandle.MessageContext))), + .Select(a => a!.Value.NodeId.ToNodeId(session.MessageContext))), ct).ConfigureAwait(false); foreach (var result in response.RegisteredNodeIds.Zip(registrations)) { Debug.Assert(result.Second != null); if (!NodeId.IsNull(result.First)) { - result.Second.Value.Update(result.First, sessionHandle.MessageContext); + result.Second.Value.Update(result.First, session.MessageContext); } } } @@ -577,7 +580,6 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip var metadataChanged = false; var applyChanges = false; - var session = rawSubscription.Session; var updated = 0; var errors = 0; @@ -592,7 +594,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip Debug.Assert(toUpdate.GetType() == theUpdate.GetType()); try { - if (toUpdate.MergeWith(theUpdate, sessionHandle, out var metadata)) + if (toUpdate.MergeWith(theUpdate, session, out var metadata)) { _logger.LogDebug( "Trying to update monitored item '{Item}' in {Subscription}...", @@ -623,7 +625,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip { try { - if (toRemove.RemoveFrom(rawSubscription, out var metadata)) + if (toRemove.RemoveFrom(this, out var metadata)) { _logger.LogDebug( "Trying to remove monitored item '{Item}' from {Subscription}...", @@ -651,7 +653,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip desired.Remove(toAdd); try { - if (toAdd.AddTo(rawSubscription, sessionHandle, out var metadata)) + if (toAdd.AddTo(this, session, out var metadata)) { _logger.LogDebug( "Adding monitored item '{Item}' to {Subscription}...", @@ -678,11 +680,15 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip if (applyChanges) { - await rawSubscription.ApplyChangesAsync(ct).ConfigureAwait(false); - if (rawSubscription.MonitoredItemCount == 0 && - _subscription?.Configuration?.EnableImmediatePublishing != true) + await ApplyChangesAsync(ct).ConfigureAwait(false); + if (MonitoredItemCount == 0 && + _template.Configuration?.EnableImmediatePublishing != true) { - await rawSubscription.SetPublishingModeAsync(false, ct).ConfigureAwait(false); + await SetPublishingModeAsync(false, ct).ConfigureAwait(false); + + _logger.LogInformation( + "Disabled empty Subscription {Subscription} in session {Session}.", + this, session); } } @@ -708,7 +714,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip // necessary. // var allDisplayNameUpdates = desiredMonitoredItems - .Select(a => a.DisplayName) + .Select(a => a.GetDisplayName) .Where(a => a != null) .ToList(); if (allDisplayNameUpdates.Count > 0) @@ -716,11 +722,11 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip foreach (var displayNameUpdates in allDisplayNameUpdates.Batch( (int?)operationLimits.MaxNodesPerRead ?? 1)) { - var response = await sessionHandle.Services.ReadAsync(new RequestHeader(), + var response = await session.Services.ReadAsync(new RequestHeader(), 0, Opc.Ua.TimestampsToReturn.Neither, new ReadValueIdCollection( displayNameUpdates.Select(a => new ReadValueId { - NodeId = a!.Value.NodeId.ToNodeId(sessionHandle.MessageContext), + NodeId = a!.Value.NodeId.ToNodeId(session.MessageContext), AttributeId = (uint)NodeAttribute.DisplayName })), ct).ConfigureAwait(false); var results = response.Validate(response.Results, @@ -762,25 +768,26 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip "Completing {Count} items in subscription {Subscription}...", desiredMonitoredItems.Count, this); - var successfullyCompletedItems = new List(); + var set = MonitoredItems + .OfType() + .ToHashSet(); foreach (var monitoredItem in desiredMonitoredItems) { - if (!monitoredItem.TryCompleteChanges( - rawSubscription, ref applyChanges, SendNotification)) + if (!monitoredItem.TryCompleteChanges(this, ref applyChanges, SendNotification)) { // Apply any changes from this second pass invalidItems++; } else { - successfullyCompletedItems.Add(monitoredItem); + set.Add(monitoredItem); } } if (applyChanges) { // Apply any additional changes - await rawSubscription.ApplyChangesAsync(ct).ConfigureAwait(false); + await ApplyChangesAsync(ct).ConfigureAwait(false); } // @@ -795,15 +802,19 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip // metadata. Then we need a way to retain the previous metadata until // switching over. // - var set = successfullyCompletedItems.ToImmutableHashSet(); + // Create currently monitored items list + Debug.Assert(set.Select(m => m.ClientHandle).Distinct().Count() == set.Count, + "Client handles are not distinct or one of the items is null"); + var currentlyMonitored = set + .ToFrozenDictionary(m => m.ClientHandle, m => m); if (metadataChanged) { var threshold = - _subscription?.Configuration?.AsyncMetaDataLoadThreshold + _template.Configuration?.AsyncMetaDataLoadThreshold ?? 30; // Synchronous loading for 30 or less items - var tcs = (set.Count <= threshold) ? new TaskCompletionSource() : null; - var args = new MetaDataLoader.MetaDataLoaderArguments(tcs, sessionHandle, - session.NamespaceUris, set); + var tcs = (currentlyMonitored.Count <= threshold) ? new TaskCompletionSource() : null; + var args = new MetaDataLoader.MetaDataLoaderArguments(tcs, session, + session.NamespaceUris, currentlyMonitored.Values.ToList()); _metaDataLoader.Value.Reload(args); if (tcs != null) { @@ -814,7 +825,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip _logger.LogDebug( "Setting monitoring mode on {Count} items in subscription {Subscription}...", - successfullyCompletedItems.Count, this); + set.Count, this); // // Finally change the monitoring mode as required. Batch the requests @@ -822,8 +833,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip // the monitoring mode was already configured. This is for updates as // they are not applied through ApplyChanges // - foreach (var change in successfullyCompletedItems - .GroupBy(i => i.GetMonitoringModeChange())) + foreach (var change in set.GroupBy(i => i.GetMonitoringModeChange())) { if (change.Key == null) { @@ -831,16 +841,16 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip continue; } - foreach (var itemsBatch in change.Select(t => t.Item!).Batch( + foreach (var itemsBatch in change.Batch( (int?)operationLimits.MaxMonitoredItemsPerCall ?? 1)) { - var itemsToChange = itemsBatch.ToList(); + var itemsToChange = itemsBatch.Cast().ToList(); _logger.LogInformation( "Set monitoring to {Value} for {Count} items in subscription {Subscription}.", change.Key.Value, itemsToChange.Count, this); - var results = await rawSubscription.SetMonitoringModeAsync( - change.Key.Value, itemsToChange.ToList(), ct).ConfigureAwait(false); + var results = await SetMonitoringModeAsync(change.Key.Value, + itemsToChange, ct).ConfigureAwait(false); if (results != null) { var erroneousResultsCount = results @@ -873,20 +883,14 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip .ToList() .ForEach(m => m.Dispose()); - // Create currently monitored items list - Debug.Assert(set.Select(m => m.Item?.ClientHandle).Distinct().Count() == set.Count, - "Client handles are not distinct or one of the items is null"); - _currentlyMonitored = ImmutableDictionary.Empty.SetItems( - set.Select(m => - new KeyValuePair(m.Item!.ClientHandle, m))); - // Update subscription state + _currentlyMonitored = currentlyMonitored; NumberOfNotCreatedItems = invalidItems; - NumberOfCreatedItems = set.Count; + NumberOfCreatedItems = currentlyMonitored.Count - invalidItems; _logger.LogInformation( - "Now monitoring {Count} nodes in subscription {Subscription}.", - set.Count, this); + "Now monitoring {Count} (Good:{Good}/Bad:{Bad}) nodes in subscription {Subscription}.", + currentlyMonitored.Count, NumberOfCreatedItems, NumberOfNotCreatedItems, this); // Refresh condition if (set.OfType().Any()) @@ -895,7 +899,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip "Issuing ConditionRefresh on subscription {Subscription}", this); try { - await rawSubscription.ConditionRefreshAsync(ct).ConfigureAwait(false); + await ConditionRefreshAsync(ct).ConfigureAwait(false); _logger.LogInformation("ConditionRefresh on subscription " + "{Subscription} has completed.", this); } @@ -920,7 +924,7 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip TriggerSubscriptionManagementCallbackIn( _options.Value.InvalidMonitoredItemRetryDelay, TimeSpan.FromMinutes(5)); } - else if (desiredMonitoredItems.Count != set.Count) + else if (desiredMonitoredItems.Count != currentlyMonitored.Count) { // Try to periodically update the subscription // TODO: Trigger on address space model changes... @@ -942,24 +946,22 @@ private async Task SynchronizeMonitoredItemsAsync(Subscription rawSubscrip /// Resets the operation timeout on the session accrding to the /// publishing intervals on all subscriptions. /// - /// - /// - private void ReapplySessionOperationTimeout(ISession session, Subscription newSubscription) + private void ReapplySessionOperationTimeout() { - if (session == null) + if (Session == null) { return; } var currentOperationTimeout = _options.Value.Quotas.OperationTimeout; var localMaxOperationTimeout = - newSubscription.PublishingInterval * (int)newSubscription.KeepAliveCount; + PublishingInterval * (int)KeepAliveCount; if (currentOperationTimeout < localMaxOperationTimeout) { currentOperationTimeout = localMaxOperationTimeout; } - foreach (var subscription in session.Subscriptions) + foreach (var subscription in Session.Subscriptions) { localMaxOperationTimeout = (int)subscription.CurrentPublishingInterval * (int)subscription.CurrentKeepAliveCount; @@ -968,279 +970,197 @@ private void ReapplySessionOperationTimeout(ISession session, Subscription newSu currentOperationTimeout = localMaxOperationTimeout; } } - if (session.OperationTimeout != currentOperationTimeout) + if (Session.OperationTimeout != currentOperationTimeout) { - session.OperationTimeout = currentOperationTimeout; + Session.OperationTimeout = currentOperationTimeout; } } /// /// Apply state to session /// - /// - /// + /// /// /// - private async ValueTask SyncWithSessionInternalAsync(IOpcUaSession handle, bool sessionIsNew, + private async ValueTask SyncWithSessionInternalAsync(ISession session, CancellationToken ct) { - Debug.Assert(_lock.CurrentCount == 0); - - // Get the raw session object from the session handle to do the heart surgery - if (handle is not ISessionAccessor accessor || - !accessor.TryGetSession(out var session)) // Should never happen. + if (session?.Connected != true) { - _logger.LogInformation( - "Failed to access session in {Session} to update subscription {Subscription}.", - handle, this); + _logger.LogError( + "Session {Session} for {Subscription} not connected.", + session, this); TriggerSubscriptionManagementCallbackIn( _options.Value.CreateSessionTimeout, TimeSpan.FromSeconds(10)); return; } - // - // While we access the session it is valid and what is more connected. - // We are called on the client connection manager thread and thus can - // access the session however we want without anyone disconnecting it. - // - Debug.Assert(session.Connected); - - // Should not happen since we are called under lock. - Debug.Assert(_subscription != null, "No subscription during apply"); - - if (sessionIsNew) - { - // - // If session is new close any current subscription now if it is - // not in the session yet. The one in the session might also be - // new, so grab the new reference here also. - // - var currentSubscription = session.Subscriptions - .FirstOrDefault(s => s.Handle.Equals(Id)); - if (currentSubscription == null) - { - _logger.LogInformation( - "Closing old subscription {Subscription} since session is new...", this); - // Does not throw - await CloseCurrentSubscriptionAsync().ConfigureAwait(false); - } - _currentSubscription = currentSubscription; - ResetKeepAliveTimer(); - } - else + if (_forceRecreate) { - // Should not happen if session has not been updated. - Debug.Assert(session.Subscriptions.FirstOrDefault( - s => s.Handle.Equals(Id)) == _currentSubscription); - - if (_forceRecreate) - { - _forceRecreate = false; - _logger.LogInformation( - "Closing subscription {Subscription} and then re-creating...", this); - // Does not throw - await CloseCurrentSubscriptionAsync().ConfigureAwait(false); - } + _forceRecreate = false; + _logger.LogInformation( + "Closing subscription {Subscription} and then re-creating...", this); + // Does not throw + await CloseCurrentSubscriptionAsync().ConfigureAwait(false); } - // Create or update the subscription inside the raw session object. - var subscription = await AddOrUpdateSubscriptionInSessionAsync(handle, session, - ct).ConfigureAwait(false); + // Synchronize subscription through the session. + await SynchronizeSubscriptionAsync(session, ct).ConfigureAwait(false); + Debug.Assert(Session != null); + Debug.Assert(Session == session); - if (subscription == null) - { - _logger.LogWarning( - "Could not add or update a Subscription {Subscription} in {Session}.", - this, handle); - - TriggerSubscriptionManagementCallbackIn( - _options.Value.SubscriptionErrorRetryDelay, kDefaultErrorRetryDelay); - return; - } - - if (_subscription.MonitoredItems != null) + if (_template.MonitoredItems != null) { // Resolves and sets the monitored items in the subscription - await SynchronizeMonitoredItemsAsync(subscription, handle, - _subscription.MonitoredItems, ct).ConfigureAwait(false); + await SynchronizeMonitoredItemsAsync(_template.MonitoredItems, + ct).ConfigureAwait(false); } - if (subscription.ChangesPending) + if (ChangesPending) { - await subscription.ApplyChangesAsync(ct).ConfigureAwait(false); + await ApplyChangesAsync(ct).ConfigureAwait(false); } var shouldEnable = _currentlyMonitored.Values - .Any(m => m.Item != null && m.Item.MonitoringMode != Opc.Ua.MonitoringMode.Disabled); - var isEnabled = subscription.PublishingEnabled; - if (isEnabled ^ shouldEnable) + .Any(m => m.Valid && m.MonitoringMode != Opc.Ua.MonitoringMode.Disabled); + if (PublishingEnabled ^ shouldEnable) { - await subscription.SetPublishingModeAsync(shouldEnable, ct).ConfigureAwait(false); + await SetPublishingModeAsync(shouldEnable, ct).ConfigureAwait(false); _logger.LogInformation( "{State} Subscription {Subscription} in session {Session}.", - shouldEnable ? "Enabled" : "Disabled", this, handle); + shouldEnable ? "Enabled" : "Disabled", this, session); } } /// /// Get a subscription with the supplied configuration (no lock) /// - /// /// /// /// /// - private async ValueTask AddOrUpdateSubscriptionInSessionAsync( - IOpcUaSession handle, ISession session, CancellationToken ct) + private async ValueTask SynchronizeSubscriptionAsync(ISession session, CancellationToken ct) { Debug.Assert(session.DefaultSubscription != null, "No default subscription template."); - Debug.Assert(_lock.CurrentCount == 0); // Under lock - - await session.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); - GetSubscriptionConfiguration(_currentSubscription ?? session.DefaultSubscription, + GetSubscriptionConfiguration(session.DefaultSubscription, out var configuredPublishingInterval, out var configuredPriority, out var configuredKeepAliveCount, out var configuredLifetimeCount, out var configuredMaxNotificationsPerPublish); - if (_currentSubscription == null) + if (Handle == null) { var enablePublishing = - _subscription?.Configuration?.EnableImmediatePublishing ?? false; + _template.Configuration?.EnableImmediatePublishing ?? false; var sequentialPublishing = - _subscription?.Configuration?.EnableSequentialPublishing ?? false; - -#pragma warning disable CA2000 // Dispose objects before losing scope - var subscription = new Subscription(session.DefaultSubscription) - { - Handle = Id, - DisplayName = Name, - PublishingEnabled = enablePublishing, - TimestampsToReturn = Opc.Ua.TimestampsToReturn.Both, - KeepAliveCount = configuredKeepAliveCount, - PublishingInterval = configuredPublishingInterval, - MaxNotificationsPerPublish = configuredMaxNotificationsPerPublish, - LifetimeCount = configuredLifetimeCount, - Priority = configuredPriority, - // TODO: use a channel and reorder task before calling OnMessage - // to order or else republish is called too often - SequentialPublishing = sequentialPublishing, - DisableMonitoredItemCache = true, // Not needed anymore - RepublishAfterTransfer = true, - FastKeepAliveCallback = OnSubscriptionKeepAliveNotification, - FastDataChangeCallback = OnSubscriptionDataChangeNotification, - FastEventCallback = OnSubscriptionEventNotificationList - }; - - subscription.PublishStatusChanged += OnPublishStatusChange; - subscription.StateChanged += OnStateChange; - -#pragma warning restore CA2000 // Dispose objects before losing scope - - ReapplySessionOperationTimeout(session, subscription); - - var result = session.AddSubscription(subscription); - if (!result) - { - _logger.LogError( - "Failed to add subscription '{Subscription}' to session {Session}", - this, handle); - - return null; - } + _template.Configuration?.EnableSequentialPublishing ?? false; + + Handle = LocalIndex; + DisplayName = Name; + PublishingEnabled = enablePublishing; + KeepAliveCount = configuredKeepAliveCount; + PublishingInterval = configuredPublishingInterval; + MaxNotificationsPerPublish = configuredMaxNotificationsPerPublish; + LifetimeCount = configuredLifetimeCount; + Priority = configuredPriority; + // TODO: use a channel and reorder task before calling OnMessage + // to order or else republish is called too often + SequentialPublishing = sequentialPublishing; + + var result = session.AddSubscription(this); + Debug.Assert(result, "session should not already contain this subscription"); + Debug.Assert(Session == session); + + ReapplySessionOperationTimeout(); _logger.LogInformation( "Creating new {State} subscription {Subscription} in session {Session}.", - subscription.PublishingEnabled ? "enabled" : "disabled", this, handle); + PublishingEnabled ? "enabled" : "disabled", this, session); - Debug.Assert(enablePublishing == subscription.PublishingEnabled); - await subscription.CreateAsync(ct).ConfigureAwait(false); + Debug.Assert(enablePublishing == PublishingEnabled); + await CreateAsync(ct).ConfigureAwait(false); - if (!subscription.Created) + if (!Created) { - await session.RemoveSubscriptionAsync(subscription, ct).ConfigureAwait(false); - + Handle = null; + await session.RemoveSubscriptionAsync(this, ct).ConfigureAwait(false); throw new ServiceResultException(StatusCodes.BadSubscriptionIdInvalid, $"Failed to create subscription {this} in session {session}"); } - LogRevisedValues(subscription, true); - Debug.Assert(subscription.Id != 0); + LogRevisedValues(true); + Debug.Assert(Id != 0); + Debug.Assert(Created); - _useDeferredAcknoledge = _subscription?.Configuration?.UseDeferredAcknoledgements + _useDeferredAcknoledge = _template.Configuration?.UseDeferredAcknoledgements ?? false; - _currentSubscription = subscription; } else { - var subscription = _currentSubscription; - // Apply new configuration on configuration on original subscription var modifySubscription = false; - if (configuredKeepAliveCount != subscription.KeepAliveCount) + if (configuredKeepAliveCount != KeepAliveCount) { _logger.LogInformation( "Change KeepAliveCount to {New} in Subscription {Subscription}...", configuredKeepAliveCount, this); - subscription.KeepAliveCount = configuredKeepAliveCount; + KeepAliveCount = configuredKeepAliveCount; modifySubscription = true; } - if (subscription.PublishingInterval != configuredPublishingInterval) + if (PublishingInterval != configuredPublishingInterval) { _logger.LogInformation( "Change publishing interval to {New} in Subscription {Subscription}...", configuredPublishingInterval, this); - subscription.PublishingInterval = configuredPublishingInterval; + PublishingInterval = configuredPublishingInterval; modifySubscription = true; } - if (subscription.MaxNotificationsPerPublish != configuredMaxNotificationsPerPublish) + if (MaxNotificationsPerPublish != configuredMaxNotificationsPerPublish) { _logger.LogInformation( "Change MaxNotificationsPerPublish to {New} in Subscription {Subscription}", configuredMaxNotificationsPerPublish, this); - subscription.MaxNotificationsPerPublish = configuredMaxNotificationsPerPublish; + MaxNotificationsPerPublish = configuredMaxNotificationsPerPublish; modifySubscription = true; } - if (subscription.LifetimeCount != configuredLifetimeCount) + if (LifetimeCount != configuredLifetimeCount) { _logger.LogInformation( "Change LifetimeCount to {New} in Subscription {Subscription}...", configuredLifetimeCount, this); - subscription.LifetimeCount = configuredLifetimeCount; + LifetimeCount = configuredLifetimeCount; modifySubscription = true; } - if (subscription.Priority != configuredPriority) + if (Priority != configuredPriority) { _logger.LogInformation( "Change Priority to {New} in Subscription {Subscription}...", configuredPriority, this); - subscription.Priority = configuredPriority; + Priority = configuredPriority; modifySubscription = true; } if (modifySubscription) { - await subscription.ModifyAsync(ct).ConfigureAwait(false); + await ModifyAsync(ct).ConfigureAwait(false); _logger.LogInformation( "Subscription {Subscription} in session {Session} successfully modified.", - this, handle); - LogRevisedValues(subscription, false); + this, session); + LogRevisedValues(false); } } ResetKeepAliveTimer(); - return _currentSubscription; } /// /// Log revised values of the subscription /// - /// /// - private void LogRevisedValues(Subscription subscription, bool created) + private void LogRevisedValues(bool created) { _logger.LogInformation(@"Successfully {Action} subscription {Subscription}'. Actual (revised) state/desired state: @@ -1249,10 +1169,10 @@ private void LogRevisedValues(Subscription subscription, bool created) # KeepAliveCount {CurrentKeepAliveCount}/{KeepAliveCount} # LifetimeCount {CurrentLifetimeCount}/{LifetimeCount}", created ? "created" : "modified", this, - subscription.CurrentPublishingEnabled, subscription.PublishingEnabled, - subscription.CurrentPublishingInterval, subscription.PublishingInterval, - subscription.CurrentKeepAliveCount, subscription.KeepAliveCount, - subscription.CurrentLifetimeCount, subscription.LifetimeCount); + CurrentPublishingEnabled, PublishingEnabled, + CurrentPublishingInterval, PublishingInterval, + CurrentKeepAliveCount, KeepAliveCount, + CurrentLifetimeCount, LifetimeCount); } /// @@ -1268,15 +1188,15 @@ private void GetSubscriptionConfiguration(Subscription defaultSubscription, out int publishingInterval, out byte priority, out uint keepAliveCount, out uint lifetimeCount, out uint maxNotificationsPerPublish) { - publishingInterval = (int)((_subscription?.Configuration?.PublishingInterval) ?? + publishingInterval = (int)((_template.Configuration?.PublishingInterval) ?? TimeSpan.FromSeconds(1)).TotalMilliseconds; - keepAliveCount = (_subscription?.Configuration?.KeepAliveCount) ?? + keepAliveCount = (_template.Configuration?.KeepAliveCount) ?? defaultSubscription.KeepAliveCount; - maxNotificationsPerPublish = (_subscription?.Configuration?.MaxNotificationsPerPublish) ?? + maxNotificationsPerPublish = (_template.Configuration?.MaxNotificationsPerPublish) ?? defaultSubscription.MaxNotificationsPerPublish; - lifetimeCount = (_subscription?.Configuration?.LifetimeCount) ?? + lifetimeCount = (_template.Configuration?.LifetimeCount) ?? defaultSubscription.LifetimeCount; - priority = (_subscription?.Configuration?.Priority) ?? + priority = (_template.Configuration?.Priority) ?? defaultSubscription.Priority; } @@ -1313,17 +1233,37 @@ private void TriggerSubscriptionManagementCallbackIn(TimeSpan? delay, /// private void OnSubscriptionManagementTriggered(object? state) { - try + TriggerManageSubscription(false); + } + + /// + /// Trigger managing of this subscription, ensure client exists if it is null + /// + /// + private void TriggerManageSubscription(bool ensureClientExists) + { + // + // Ensure a client and session exists for this subscription. This takes a + // reference that must be released when the subscription is closed or the + // underlying connection information changes. + // + + if (_client == null) { - var connection = _subscription?.Id.Connection; - if (connection != null) + if (!ensureClientExists) { - // try to get a session using the provided configuration - using var client = _clients.GetClient(connection); - client?.ManageSubscription(this); + return; } + _client = _clients.GetOrCreateClient(_template.Id.Connection); } - catch (ObjectDisposedException) { } + + // Execute creation/update on the session management thread inside the client + Debug.Assert(_client != null); + + _logger.LogInformation("Trigger management of subscription {Subscription}...", + this); + + _client.ManageSubscription(this); } /// @@ -1336,7 +1276,7 @@ private void OnSubscriptionManagementTriggered(object? state) private void OnSubscriptionEventNotificationList(Subscription subscription, EventNotificationList notification, IList? stringTable) { - if (subscription == null || subscription.Id != _currentSubscription?.Id) + if (!ReferenceEquals(subscription, this)) { _logger.LogWarning( "EventChange for wrong subscription {Id} received on {Subscription}.", @@ -1357,15 +1297,17 @@ private void OnSubscriptionEventNotificationList(Subscription subscription, return; } - ResetKeepAliveTimer(); - - var onSubscriptionEventChange = OnSubscriptionEventChange; - if (onSubscriptionEventChange == null) + var session = Session; + if (session?.MessageContext == null) { - _logger.LogDebug("No callback registered for event change - skip."); + _logger.LogWarning( + "EventChange for subscription {Subscription} received without a session {Session}.", + this, session); return; } + ResetKeepAliveTimer(); + var sw = Stopwatch.StartNew(); try { @@ -1379,14 +1321,14 @@ private void OnSubscriptionEventNotificationList(Subscription subscription, // Do not log when the sequence number is 1 after reconnect _previousSequenceNumber = 1; } - else if (!SequenceNumber.Validate(sequenceNumber, ref _previousSequenceNumber, + else if (!Opc.Ua.SequenceNumber.Validate(sequenceNumber, ref _previousSequenceNumber, out var missingSequenceNumbers, out var dropped)) { _logger.LogWarning("Event subscription notification for subscription " + "{Subscription} has unexpected sequenceNumber {SequenceNumber} missing " + "{ExpectedSequenceNumber} which were {Dropped}, publishTime {PublishTime}", this, sequenceNumber, - SequenceNumber.ToString(missingSequenceNumbers), dropped ? + Opc.Ua.SequenceNumber.ToString(missingSequenceNumbers), dropped ? "dropped" : "already received", publishTime); } @@ -1394,23 +1336,22 @@ private void OnSubscriptionEventNotificationList(Subscription subscription, foreach (var eventFieldList in notification.Events) { Debug.Assert(eventFieldList != null); - if (_currentlyMonitored.TryGetValue(eventFieldList.ClientHandle, out var wrapper)) + if (_currentlyMonitored.TryGetValue(eventFieldList.ClientHandle, out var monitoredItem)) { #pragma warning disable CA2000 // Dispose objects before losing scope - var message = new Notification(this, subscription.Id, sequenceNumber: sequenceNumber) + var message = new Notification(this, Id, session.MessageContext, sequenceNumber: sequenceNumber) { - ServiceMessageContext = subscription.Session?.MessageContext, - ApplicationUri = subscription.Session?.Endpoint?.Server?.ApplicationUri, - EndpointUrl = subscription.Session?.Endpoint?.EndpointUrl, + ApplicationUri = session.Endpoint?.Server?.ApplicationUri, + EndpointUrl = session.Endpoint?.EndpointUrl, SubscriptionName = Name, - DataSetName = wrapper.DataSetName, - SubscriptionId = Id, - SequenceNumber = SequenceNumber.Increment32(ref _sequenceNumber), + DataSetName = monitoredItem.DataSetName, + SubscriptionId = LocalIndex, + SequenceNumber = Opc.Ua.SequenceNumber.Increment32(ref _sequenceNumber), MessageType = MessageType.Event, PublishTimestamp = publishTime }; - if (!wrapper.TryGetMonitoredItemNotifications(message.SequenceNumber, + if (!monitoredItem.TryGetMonitoredItemNotifications(message.SequenceNumber, publishTime, eventFieldList, message.Notifications)) { _logger.LogDebug("Skipping the monitored item notification for Event " + @@ -1419,7 +1360,7 @@ private void OnSubscriptionEventNotificationList(Subscription subscription, if (message.Notifications.Count > 0) { - onSubscriptionEventChange.Invoke(this, message); + _callbacks.OnSubscriptionEventChange(message); numOfEvents++; } else @@ -1429,14 +1370,19 @@ private void OnSubscriptionEventNotificationList(Subscription subscription, } else { - _logger.LogWarning( - "Monitored item not found with client handle {ClientHandle} " + - "for Event received for subscription {Subscription}.", - eventFieldList.ClientHandle, this); + var found = subscription.FindItemByClientHandle(eventFieldList.ClientHandle); + _unassignedNotifications++; + + if (_logger.IsEnabled(LogLevel.Debug) || found != null) + { + _logger.LogDebug( + "Monitored item not found with client handle {ClientHandle} for " + + "for Event received for subscription {Subscription} ({Count}, {Item}).", + eventFieldList.ClientHandle, this, _currentlyMonitored.Count, found); + } } } - var onSubscriptionEventDiagnosticsChange = OnSubscriptionEventDiagnosticsChange; - onSubscriptionEventDiagnosticsChange?.Invoke(this, (true, numOfEvents)); + _callbacks.OnSubscriptionEventDiagnosticsChange(true, numOfEvents); } catch (Exception e) { @@ -1461,37 +1407,29 @@ private void OnSubscriptionEventNotificationList(Subscription subscription, private void OnSubscriptionKeepAliveNotification(Subscription subscription, NotificationData notification) { - var currentSubscriptionId = _currentSubscription?.Id ?? 0; - if (currentSubscriptionId == 0 || subscription == null) - { - // Nothing to do here - _logger.LogWarning("Got Keep alive without subscription in {Subscription}.", - this); - return; - } - - Debug.Assert(_currentSubscription != null); - if (subscription.Id != currentSubscriptionId) + if (!ReferenceEquals(subscription, this)) { _logger.LogWarning( - "Keep alive for wrong subscription {Id} received on {Subscription}.", - subscription.Id, this); + "Keep Alive for wrong subscription {Id} received on {Subscription}.", + subscription?.Id, this); return; } ResetKeepAliveTimer(); - if (!subscription.PublishingEnabled) + if (!PublishingEnabled) { _logger.LogDebug( "Keep alive event received while publishing is not enabled - skip."); return; } - var onSubscriptionKeepAlive = OnSubscriptionKeepAlive; - if (onSubscriptionKeepAlive == null) + var session = Session; + if (session?.MessageContext == null) { - _logger.LogDebug("No callback registered for keep alive events - skip."); + _logger.LogWarning( + "Keep alive event for subscription {Subscription} received without session {Session}.", + this, session); return; } @@ -1507,20 +1445,19 @@ private void OnSubscriptionKeepAliveNotification(Subscription subscription, this, sequenceNumber, publishTime); #pragma warning disable CA2000 // Dispose objects before losing scope - var message = new Notification(this, subscription.Id) + var message = new Notification(this, Id, session.MessageContext) { - ServiceMessageContext = subscription.Session?.MessageContext, - ApplicationUri = subscription.Session?.Endpoint?.Server?.ApplicationUri, - EndpointUrl = subscription.Session?.Endpoint?.EndpointUrl, + ApplicationUri = session.Endpoint?.Server?.ApplicationUri, + EndpointUrl = session.Endpoint?.EndpointUrl, SubscriptionName = Name, PublishTimestamp = publishTime, - SubscriptionId = Id, - SequenceNumber = SequenceNumber.Increment32(ref _sequenceNumber), + SubscriptionId = LocalIndex, + SequenceNumber = Opc.Ua.SequenceNumber.Increment32(ref _sequenceNumber), MessageType = MessageType.KeepAlive }; #pragma warning restore CA2000 // Dispose objects before losing scope - onSubscriptionKeepAlive.Invoke(this, message); + _callbacks.OnSubscriptionKeepAlive(message); Debug.Assert(message.Notifications != null); } catch (Exception e) @@ -1547,7 +1484,7 @@ private void OnSubscriptionKeepAliveNotification(Subscription subscription, private void OnSubscriptionDataChangeNotification(Subscription subscription, DataChangeNotification notification, IList? stringTable) { - if (subscription == null || subscription.Id != _currentSubscription?.Id) + if (!ReferenceEquals(subscription, this)) { _logger.LogWarning( "DataChange for wrong subscription {Id} received on {Subscription}.", @@ -1555,14 +1492,17 @@ private void OnSubscriptionDataChangeNotification(Subscription subscription, return; } - ResetKeepAliveTimer(); - - var onSubscriptionDataChange = OnSubscriptionDataChange; - if (onSubscriptionDataChange == null) + var session = Session; + if (session?.MessageContext == null) { - _logger.LogDebug("No callback registered for data change events - skip."); + _logger.LogWarning( + "DataChange for subscription {Subscription} received without session {Session}.", + this, session); return; } + + ResetKeepAliveTimer(); + var sw = Stopwatch.StartNew(); try { @@ -1570,15 +1510,14 @@ private void OnSubscriptionDataChangeNotification(Subscription subscription, var publishTime = notification.PublishTime; #pragma warning disable CA2000 // Dispose objects before losing scope - var message = new Notification(this, subscription.Id, sequenceNumber: sequenceNumber) + var message = new Notification(this, Id, session.MessageContext, sequenceNumber: sequenceNumber) { - ServiceMessageContext = subscription.Session?.MessageContext, - ApplicationUri = subscription.Session?.Endpoint?.Server?.ApplicationUri, - EndpointUrl = subscription.Session?.Endpoint?.EndpointUrl, + ApplicationUri = session.Endpoint?.Server?.ApplicationUri, + EndpointUrl = session.Endpoint?.EndpointUrl, SubscriptionName = Name, - SubscriptionId = Id, + SubscriptionId = LocalIndex, PublishTimestamp = publishTime, - SequenceNumber = SequenceNumber.Increment32(ref _sequenceNumber), + SequenceNumber = Opc.Ua.SequenceNumber.Increment32(ref _sequenceNumber), MessageType = MessageType.DeltaFrame }; @@ -1590,23 +1529,23 @@ private void OnSubscriptionDataChangeNotification(Subscription subscription, // Do not log when the sequence number is 1 after reconnect _previousSequenceNumber = 1; } - else if (!SequenceNumber.Validate(sequenceNumber, ref _previousSequenceNumber, + else if (!Opc.Ua.SequenceNumber.Validate(sequenceNumber, ref _previousSequenceNumber, out var missingSequenceNumbers, out var dropped)) { _logger.LogWarning("DataChange notification for subscription " + "{Subscription} has unexpected sequenceNumber {SequenceNumber} " + "missing {ExpectedSequenceNumber} which were {Dropped}, publishTime {PublishTime}", this, sequenceNumber, - SequenceNumber.ToString(missingSequenceNumbers), + Opc.Ua.SequenceNumber.ToString(missingSequenceNumbers), dropped ? "dropped" : "already received", publishTime); } foreach (var item in notification.MonitoredItems.OrderBy(m => m.Value?.SourceTimestamp)) { Debug.Assert(item != null); - if (_currentlyMonitored.TryGetValue(item.ClientHandle, out var wrapper)) + if (_currentlyMonitored.TryGetValue(item.ClientHandle, out var monitoredItem)) { - if (!wrapper.TryGetMonitoredItemNotifications(message.SequenceNumber, + if (!monitoredItem.TryGetMonitoredItemNotifications(message.SequenceNumber, publishTime, item, message.Notifications)) { _logger.LogDebug( @@ -1616,19 +1555,25 @@ private void OnSubscriptionDataChangeNotification(Subscription subscription, } else { - _logger.LogWarning( - "Monitored item not found with client handle {ClientHandle} " + - "for DataChange received for subscription {Subscription}", - item.ClientHandle, this); + var found = subscription.FindItemByClientHandle(item.ClientHandle); + _unassignedNotifications++; + + if (_logger.IsEnabled(LogLevel.Debug) || found != null) + { + _logger.LogWarning( + "Monitored item not found with client handle {ClientHandle} for " + + "for DataChange received for subscription {Subscription} ({Count}, {Item}).", + item.ClientHandle, this, _currentlyMonitored.Count, found); + } } } - onSubscriptionDataChange.Invoke(this, message); + _callbacks.OnSubscriptionDataChange(message); Debug.Assert(message.Notifications != null); - var onSubscriptionDataDiagnosticsChange = OnSubscriptionDataDiagnosticsChange; - if (message.Notifications.Count > 0 && onSubscriptionDataDiagnosticsChange != null) + if (message.Notifications.Count > 0) { - onSubscriptionDataDiagnosticsChange.Invoke(this, (true, message.Notifications.Count, 0, 0)); + _callbacks.OnSubscriptionDataDiagnosticsChange(true, + message.Notifications.Count, 0, 0); } } catch (Exception e) @@ -1654,32 +1599,29 @@ private void OnSubscriptionDataChangeNotification(Subscription subscription, private bool TryGetNotifications(uint sequenceNumber, [NotNullWhen(true)] out IList? notifications) { - _lock.Wait(); - try + lock (_lock) { - var subscription = _currentSubscription; - if (subscription == null) + try { - notifications = null; - return false; + if (Handle == null) + { + notifications = null; + return false; + } + notifications = new List(); + foreach (var item in _currentlyMonitored.Values) + { + item.TryGetLastMonitoredItemNotifications(sequenceNumber, notifications); + } + return true; } - notifications = new List(); - foreach (var item in _currentlyMonitored.Values) + catch (Exception ex) { - item.TryGetLastMonitoredItemNotifications(sequenceNumber, notifications); + notifications = null; + _logger.LogError(ex, "Failed to get a notifications from monitored " + + "items in subscription {Subscription}.", this); + return false; } - return true; - } - catch (Exception ex) - { - notifications = null; - _logger.LogError(ex, "Failed to get a notifications from monitored " + - "items in subscription {Subscription}.", this); - return false; - } - finally - { - _lock.Release(); } } @@ -1690,7 +1632,7 @@ private bool TryGetNotifications(uint sequenceNumber, /// private void AdvancePosition(uint subscriptionId, uint? sequenceNumber) { - if (sequenceNumber.HasValue && _currentSubscription?.Id == subscriptionId) + if (sequenceNumber.HasValue && Id == subscriptionId) { _logger.LogDebug("Advancing stream #{SubscriptionId} to #{Position}", subscriptionId, sequenceNumber); @@ -1704,15 +1646,15 @@ private void AdvancePosition(uint subscriptionId, uint? sequenceNumber) private void ResetKeepAliveTimer() { _continuouslyMissingKeepAlives = 0; - var subscription = _currentSubscription; - if (subscription == null || !_online) + + if (!IsOnline) { _keepAliveWatcher.Change(Timeout.Infinite, Timeout.Infinite); return; } + var keepAliveTimeout = TimeSpan.FromMilliseconds( - (subscription.CurrentPublishingInterval * - (subscription.CurrentKeepAliveCount + 1)) + 1000); + (CurrentPublishingInterval * (CurrentKeepAliveCount + 1)) + 1000); try { _keepAliveWatcher.Change(keepAliveTimeout, keepAliveTimeout); @@ -1729,7 +1671,9 @@ private void ResetKeepAliveTimer() /// private void OnKeepAliveMissing(object? state) { - if (!_online || _closed) + Debug.Assert(!_closed); + + if (!IsOnline) { // Stop watchdog _keepAliveWatcher.Change(Timeout.Infinite, Timeout.Infinite); @@ -1739,30 +1683,21 @@ private void OnKeepAliveMissing(object? state) NumberOfMissingKeepAlives++; _continuouslyMissingKeepAlives++; - var subscription = _currentSubscription; - if (subscription != null) + if (_continuouslyMissingKeepAlives == CurrentLifetimeCount + 1) { - if (_continuouslyMissingKeepAlives == subscription.CurrentLifetimeCount + 1) - { - _logger.LogCritical( - "#{Count}/{Lifetimecount}: Keep alive count exceeded. Resetting {Subscription}...", - _continuouslyMissingKeepAlives, subscription.CurrentLifetimeCount, this); + _logger.LogCritical( + "#{Count}/{Lifetimecount}: Keep alive count exceeded. Resetting {Subscription}...", + _continuouslyMissingKeepAlives, CurrentLifetimeCount, this); - // TODO: option to fail fast here - _forceRecreate = true; - OnSubscriptionManagementTriggered(this); - } - else - { - _logger.LogInformation( - "#{Count}/{Lifetimecount}: Subscription {Subscription} is missing keep alive.", - _continuouslyMissingKeepAlives, subscription.CurrentLifetimeCount, this); - } + // TODO: option to fail fast here + _forceRecreate = true; + OnSubscriptionManagementTriggered(this); } else { - _logger.LogInformation("#{Count}: Subscription {Subscription} is missing keep alive.", - _continuouslyMissingKeepAlives, this); + _logger.LogInformation( + "#{Count}/{Lifetimecount}: Subscription {Subscription} is missing keep alive.", + _continuouslyMissingKeepAlives, CurrentLifetimeCount, this); } } @@ -1774,16 +1709,19 @@ private void OnKeepAliveMissing(object? state) /// private void OnPublishStatusChange(Subscription subscription, PublishStateChangedEventArgs e) { + if (_disposed) + { + return; + } + if (e.Status.HasFlag(PublishStateChangedMask.Stopped)) { _logger.LogInformation("Subscription {Subscription} STOPPED!", this); - _online = false; - ResetKeepAliveTimer(); + _keepAliveWatcher.Change(Timeout.Infinite, Timeout.Infinite); } if (e.Status.HasFlag(PublishStateChangedMask.Recovered)) { _logger.LogInformation("Subscription {Subscription} RECOVERED!", this); - _online = true; ResetKeepAliveTimer(); } if (e.Status.HasFlag(PublishStateChangedMask.Transferred)) @@ -1819,6 +1757,11 @@ private void OnPublishStatusChange(Subscription subscription, PublishStateChange /// private void OnStateChange(Subscription subscription, SubscriptionStateChangedEventArgs e) { + if (_disposed) + { + return; + } + if (e.Status.HasFlag(SubscriptionChangeMask.Created)) { _logger.LogDebug("Subscription {Subscription} created.", this); @@ -1857,6 +1800,36 @@ private void OnStateChange(Subscription subscription, SubscriptionStateChangedEv } } + /// + /// Helper to validate subscription template + /// + /// + /// + /// + private static SubscriptionModel ValidateSubscriptionInfo(SubscriptionModel subscription, + string? subscriptionName = null) + { + ArgumentNullException.ThrowIfNull(subscription); + if (subscription.Configuration == null) + { + throw new ArgumentException("Missing configuration", nameof(subscription)); + } + if (subscription.Id?.Connection == null) + { + throw new ArgumentException("Missing connection information", nameof(subscription)); + } + return subscription with + { + Configuration = subscription.Configuration with + { + MetaData = subscription.Configuration.MetaData.Clone() + }, + Id = new SubscriptionIdentifier(subscription.Id.Connection, + subscriptionName ?? subscription.Id.Id), + MonitoredItems = subscription.MonitoredItems?.ToList() + }; + } + /// /// Subscription notification container /// @@ -1896,7 +1869,7 @@ internal sealed record class Notification : IOpcUaSubscriptionNotification public uint? PublishSequenceNumber { get; } /// - public IServiceMessageContext? ServiceMessageContext { get; internal set; } + public IServiceMessageContext ServiceMessageContext { get; } /// public IList Notifications { get; } @@ -1921,15 +1894,18 @@ internal sealed record class Notification : IOpcUaSubscriptionNotification /// /// /// + /// /// /// public Notification(OpcUaSubscription outer, uint subscriptionId, + IServiceMessageContext messageContext, IEnumerable? notifications = null, uint? sequenceNumber = null) { _outer = outer; PublishSequenceNumber = sequenceNumber; CreatedTimestamp = DateTime.UtcNow; + ServiceMessageContext = messageContext; _subscriptionId = subscriptionId; MetaData = _outer.CurrentMetaData; @@ -2064,7 +2040,7 @@ private async Task StartAsync(CancellationToken ct) internal async Task UpdateMetaDataAsync(MetaDataLoaderArguments args, CancellationToken ct = default) { - if (_subscription._subscription?.Configuration?.MetaData == null) + if (_subscription._template.Configuration?.MetaData == null) { // Metadata disabled MetaData = null; @@ -2103,9 +2079,9 @@ await monitoredItem.GetMetaDataAsync(args.sessionHandle, typeSystem, MetaData = new DataSetMetaDataType { Name = - _subscription._subscription.Configuration.MetaData.Name, + _subscription._template.Configuration.MetaData.Name, DataSetClassId = - (Uuid)_subscription._subscription.Configuration.MetaData.DataSetClassId, + (Uuid)_subscription._template.Configuration.MetaData.DataSetClassId, Namespaces = args.namespaces.ToArray(), EnumDataTypes = @@ -2117,7 +2093,7 @@ await monitoredItem.GetMetaDataAsync(args.sessionHandle, typeSystem, Fields = fields, Description = - _subscription._subscription.Configuration.MetaData.Description, + _subscription._template.Configuration.MetaData.Description, ConfigurationVersion = new ConfigurationVersionDataType { MajorVersion = major, @@ -2128,7 +2104,7 @@ await monitoredItem.GetMetaDataAsync(args.sessionHandle, typeSystem, internal record MetaDataLoaderArguments(TaskCompletionSource? tcs, IOpcUaSession sessionHandle, NamespaceTable namespaces, - ImmutableHashSet monitoredItemsInDataSet); + List monitoredItemsInDataSet); private MetaDataLoaderArguments? _arguments; private readonly Task _loader; private readonly CancellationTokenSource _cts = new(); @@ -2148,6 +2124,9 @@ public void InitializeMetrics() _meter.CreateObservableCounter("iiot_edge_publisher_missing_keep_alives", () => new Measurement(NumberOfMissingKeepAlives, _metrics.TagList), "Keep Alives", "Number of missing keep alives in subscription."); + _meter.CreateObservableCounter("iiot_edge_publisher_unassigned_notification_count", + () => new Measurement(_unassignedNotifications, + _metrics.TagList), "Notifications", "Number of notifications that could not be assigned."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_good_nodes", () => new Measurement(NumberOfCreatedItems, _metrics.TagList), "Monitored items", "Monitored items successfully created."); @@ -2157,54 +2136,57 @@ public void InitializeMetrics() _meter.CreateObservableUpDownCounter("iiot_edge_publisher_monitored_items", () => new Measurement(_currentlyMonitored.Count, _metrics.TagList), "Monitored items", "Monitored item count."); - _meter.CreateObservableUpDownCounter("iiot_edge_publisher_connection_retries", - () => new Measurement(_state.ReconnectCount, + () => new Measurement(State.ReconnectCount, _metrics.TagList), "Attempts", "OPC UA connect retries."); _meter.CreateObservableGauge("iiot_edge_publisher_is_connection_ok", - () => new Measurement(_online && !_closed ? 1 : 0, + () => new Measurement(State.State == EndpointConnectivityState.Ready ? 1 : 0, + _metrics.TagList), "Online", "OPC UA connection success flag."); + _meter.CreateObservableGauge("iiot_edge_publisher_is_disconnected", + () => new Measurement(State.State != EndpointConnectivityState.Ready ? 1 : 0, _metrics.TagList), "Online", "OPC UA connection success flag."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_publish_requests_per_subscription", - () => new Measurement(Ratio(_state.OutstandingRequestCount, _state.SubscriptionCount), + () => new Measurement(Ratio(State.OutstandingRequestCount, State.SubscriptionCount), _metrics.TagList), "Requests per Subscription", "Good publish requests per subsciption."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_good_publish_requests_per_subscription", - () => new Measurement(Ratio(_state.GoodPublishRequestCount, _state.SubscriptionCount), + () => new Measurement(Ratio(State.GoodPublishRequestCount, State.SubscriptionCount), _metrics.TagList), "Requests per Subscription", "Good publish requests per subsciption."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_bad_publish_requests_per_subscription", - () => new Measurement(Ratio(_state.BadPublishRequestCount, _state.SubscriptionCount), + () => new Measurement(Ratio(State.BadPublishRequestCount, State.SubscriptionCount), _metrics.TagList), "Requests per Subscription", "Bad publish requests per subsciption."); _meter.CreateObservableUpDownCounter("iiot_edge_publisher_min_publish_requests_per_subscription", - () => new Measurement(Ratio(_state.MinPublishRequestCount, _state.SubscriptionCount), + () => new Measurement(Ratio(State.MinPublishRequestCount, State.SubscriptionCount), _metrics.TagList), "Requests per Subscription", "Min publish requests queued per subsciption."); static double Ratio(int value, int count) => count == 0 ? 0.0 : (double)value / count; } private static readonly TimeSpan kDefaultErrorRetryDelay = TimeSpan.FromSeconds(2); - private ImmutableDictionary _currentlyMonitored; - private SubscriptionModel? _subscription; + private FrozenDictionary _currentlyMonitored; + private SubscriptionModel _template; #pragma warning disable CA2213 // Disposable fields should be disposed - private Subscription? _currentSubscription; + private IOpcUaClient? _client; #pragma warning restore CA2213 // Disposable fields should be disposed - private IOpcUaClientState _state = OpcUaClient.Disconnected; - private bool _online; private uint _previousSequenceNumber; private bool _useDeferredAcknoledge; private uint _sequenceNumber; private bool _closed; private bool _forceRecreate; + private readonly ISubscriptionCallbacks _callbacks; private readonly Lazy _metaDataLoader; private readonly IClientAccessor _clients; private readonly IOptions _options; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private readonly IMetricsContext _metrics; - private readonly SemaphoreSlim _lock; private readonly Timer _timer; private readonly Timer _keepAliveWatcher; private readonly Meter _meter = Diagnostics.NewMeter(); private static uint _lastIndex; private uint _currentSequenceNumber; private int _continuouslyMissingKeepAlives; + private long _unassignedNotifications; + private bool _disposed; + private readonly object _lock = new(); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs index 7353ef25fd..85e29da36f 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs @@ -8,7 +8,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Storage using Azure.IIoT.OpcUa.Publisher; using Azure.IIoT.OpcUa.Publisher.Config.Models; using Azure.IIoT.OpcUa.Publisher.Models; - using Azure.IIoT.OpcUa.Publisher.Stack.Models; using Furly.Azure.IoT.Edge.Services; using Furly.Exceptions; using Furly.Extensions.Serializers; diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj index 5ed37d0ec8..5368644c1c 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/Encoder/NetworkMessage.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/Encoder/NetworkMessage.cs index fb9fd483d3..e9580528e7 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/tests/Services/Encoder/NetworkMessage.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Services/Encoder/NetworkMessage.cs @@ -11,7 +11,6 @@ namespace Azure.IIoT.OpcUa.Publisher.Tests.Services using Furly.Extensions.Logging; using Furly.Extensions.Messaging; using Opc.Ua; - using Opc.Ua.Client; using System; using System.Collections.Generic; using System.Threading; @@ -192,12 +191,10 @@ public static IList GenerateSampleSubscriptionNot DataSetFieldId = nodeId, DataSetFieldName = displayName, }; - eventItem.Item = new MonitoredItem - { - DisplayName = displayName, - StartNodeId = new NodeId(nodeId, 0), - Handle = eventItem - }; + eventItem.DisplayName = displayName; + eventItem.StartNodeId = new NodeId(nodeId, 0); + eventItem.Handle = eventItem; + eventItem.Valid = true; eventItem.TryGetMonitoredItemNotifications(seq, DateTime.UtcNow, eventFieldList, notifications); } else @@ -218,12 +215,10 @@ public static IList GenerateSampleSubscriptionNot DataSetFieldId = nodeId, DataSetFieldName = displayName, }; - dataItem.Item = new MonitoredItem - { - DisplayName = displayName, - StartNodeId = new NodeId(nodeId, 0), - Handle = dataItem - }; + dataItem.DisplayName = displayName; + dataItem.StartNodeId = new NodeId(nodeId, 0); + dataItem.Handle = dataItem; + dataItem.Valid = true; dataItem.TryGetMonitoredItemNotifications(seq, DateTime.UtcNow, monitoredItemNotification, notifications); } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/GetSimpleEventFilterTests.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/GetSimpleEventFilterTests.cs index 7b45130c7e..48add893b7 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/GetSimpleEventFilterTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/GetSimpleEventFilterTests.cs @@ -33,12 +33,12 @@ public void SetupSimpleFilterForBaseEventType() }; // Act - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem; + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem; // Assert - Assert.NotNull(monitoredItemWrapper.Item.Filter); - Assert.IsType(monitoredItemWrapper.Item.Filter); - var eventFilter = (EventFilter)monitoredItemWrapper.Item.Filter; + Assert.NotNull(monitoredItem.Filter); + Assert.IsType(monitoredItem.Filter); + var eventFilter = (EventFilter)monitoredItem.Filter; Assert.NotNull(eventFilter.SelectClauses); Assert.Equal(2, eventFilter.SelectClauses.Count); @@ -74,12 +74,12 @@ public void SetupSimpleFilterForConditionType() }; // Act - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem; + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem; // Assert - Assert.NotNull(monitoredItemWrapper.Item.Filter); - Assert.IsType(monitoredItemWrapper.Item.Filter); - var eventFilter = (EventFilter)monitoredItemWrapper.Item.Filter; + Assert.NotNull(monitoredItem.Filter); + Assert.IsType(monitoredItem.Filter); + var eventFilter = (EventFilter)monitoredItem.Filter; Assert.NotNull(eventFilter.SelectClauses); Assert.Equal(7, eventFilter.SelectClauses.Count); @@ -131,12 +131,12 @@ public void SetupSimpleFilterForConditionTypeWithConditionHandlingEnabled() }; // Act - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem; + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem; // Assert - Assert.NotNull(monitoredItemWrapper.Item.Filter); - Assert.IsType(monitoredItemWrapper.Item.Filter); - var eventFilter = (EventFilter)monitoredItemWrapper.Item.Filter; + Assert.NotNull(monitoredItem.Filter); + Assert.IsType(monitoredItem.Filter); + var eventFilter = (EventFilter)monitoredItem.Filter; Assert.NotNull(eventFilter.SelectClauses); Assert.Equal(8, eventFilter.SelectClauses.Count); diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaMonitoredItemTests.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaMonitoredItemTests.cs index 8ee4bd1b01..c721fd7bba 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaMonitoredItemTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaMonitoredItemTests.cs @@ -26,13 +26,13 @@ public void SetDefaultValuesWhenPropertiesAreNullInBaseTemplate() StartNodeId = "i=2258", DiscardNew = null }; - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; - Assert.Equal(Attributes.Value, monitoredItemWrapper.Item.AttributeId); - Assert.Equal(Opc.Ua.MonitoringMode.Reporting, monitoredItemWrapper.Item.MonitoringMode); - Assert.Equal(1000, monitoredItemWrapper.Item.SamplingInterval); - Assert.True(monitoredItemWrapper.Item.DiscardOldest); - Assert.False(monitoredItemWrapper.SkipMonitoredItemNotification()); + Assert.Equal(Attributes.Value, monitoredItem.AttributeId); + Assert.Equal(Opc.Ua.MonitoringMode.Reporting, monitoredItem.MonitoringMode); + Assert.Equal(1000, monitoredItem.SamplingInterval); + Assert.True(monitoredItem.DiscardOldest); + Assert.False(monitoredItem.SkipMonitoredItemNotification()); } [Fact] @@ -43,17 +43,17 @@ public void SetSkipFirstBeforeFirstNotificationProcessedSucceedsTests() StartNodeId = "i=2258", SkipFirst = true }; - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; - Assert.False(monitoredItemWrapper.TrySetSkipFirst(true)); - Assert.True(monitoredItemWrapper.TrySetSkipFirst(false)); - Assert.True(monitoredItemWrapper.TrySetSkipFirst(true)); - Assert.True(monitoredItemWrapper.TrySetSkipFirst(false)); - Assert.True(monitoredItemWrapper.TrySetSkipFirst(true)); - Assert.True(monitoredItemWrapper.SkipMonitoredItemNotification()); + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; + Assert.False(monitoredItem.TrySetSkipFirst(true)); + Assert.True(monitoredItem.TrySetSkipFirst(false)); + Assert.True(monitoredItem.TrySetSkipFirst(true)); + Assert.True(monitoredItem.TrySetSkipFirst(false)); + Assert.True(monitoredItem.TrySetSkipFirst(true)); + Assert.True(monitoredItem.SkipMonitoredItemNotification()); // This is allowed since it does not matter - Assert.True(monitoredItemWrapper.TrySetSkipFirst(false)); - Assert.False(monitoredItemWrapper.TrySetSkipFirst(true)); - Assert.False(monitoredItemWrapper.SkipMonitoredItemNotification()); + Assert.True(monitoredItem.TrySetSkipFirst(false)); + Assert.False(monitoredItem.TrySetSkipFirst(true)); + Assert.False(monitoredItem.SkipMonitoredItemNotification()); } [Fact] @@ -64,15 +64,15 @@ public void SetSkipFirstAfterFirstNotificationProcessedFailsTests() StartNodeId = "i=2258", SkipFirst = true }; - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; - Assert.True(monitoredItemWrapper.SkipMonitoredItemNotification()); - Assert.False(monitoredItemWrapper.TrySetSkipFirst(true)); + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; + Assert.True(monitoredItem.SkipMonitoredItemNotification()); + Assert.False(monitoredItem.TrySetSkipFirst(true)); // This is allowed since it does not matter - Assert.True(monitoredItemWrapper.TrySetSkipFirst(false)); - Assert.False(monitoredItemWrapper.TrySetSkipFirst(true)); + Assert.True(monitoredItem.TrySetSkipFirst(false)); + Assert.False(monitoredItem.TrySetSkipFirst(true)); // This is allowed since it does not matter - Assert.True(monitoredItemWrapper.TrySetSkipFirst(false)); - Assert.False(monitoredItemWrapper.SkipMonitoredItemNotification()); + Assert.True(monitoredItem.TrySetSkipFirst(false)); + Assert.False(monitoredItem.SkipMonitoredItemNotification()); } [Fact] @@ -82,10 +82,10 @@ public void NotsetSkipFirstAfterFirstNotificationProcessedFailsSettingTests() { StartNodeId = "i=2258" }; - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; - Assert.False(monitoredItemWrapper.SkipMonitoredItemNotification()); - Assert.False(monitoredItemWrapper.TrySetSkipFirst(true)); - Assert.False(monitoredItemWrapper.SkipMonitoredItemNotification()); + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; + Assert.False(monitoredItem.SkipMonitoredItemNotification()); + Assert.False(monitoredItem.TrySetSkipFirst(true)); + Assert.False(monitoredItem.SkipMonitoredItemNotification()); } [Fact] @@ -106,19 +106,19 @@ public void SetBaseValuesWhenPropertiesAreSetInBaseTemplate() SamplingInterval = TimeSpan.FromMilliseconds(10000), DiscardNew = true }; - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem.DataItem; - Assert.Equal("DisplayName", monitoredItemWrapper.Item.DisplayName); - Assert.Equal((uint)NodeAttribute.Value, monitoredItemWrapper.Item.AttributeId); - Assert.Equal("5:20", monitoredItemWrapper.Item.IndexRange); - Assert.Null(monitoredItemWrapper.Item.RelativePath); - Assert.Equal(Opc.Ua.MonitoringMode.Sampling, monitoredItemWrapper.Item.MonitoringMode); - Assert.Equal("i=2258", monitoredItemWrapper.Item.StartNodeId); - Assert.Equal(10u, monitoredItemWrapper.Item.QueueSize); - Assert.Equal(10000, monitoredItemWrapper.Item.SamplingInterval); - Assert.False(monitoredItemWrapper.Item.DiscardOldest); - Assert.Null(monitoredItemWrapper.Item.Handle); - Assert.True(monitoredItemWrapper.SkipMonitoredItemNotification()); + Assert.Equal("DisplayName", monitoredItem.DisplayName); + Assert.Equal((uint)NodeAttribute.Value, monitoredItem.AttributeId); + Assert.Equal("5:20", monitoredItem.IndexRange); + Assert.Null(monitoredItem.RelativePath); + Assert.Equal(Opc.Ua.MonitoringMode.Sampling, monitoredItem.MonitoringMode); + Assert.Equal("i=2258", monitoredItem.StartNodeId); + Assert.Equal(10u, monitoredItem.QueueSize); + Assert.Equal(10000, monitoredItem.SamplingInterval); + Assert.False(monitoredItem.DiscardOldest); + Assert.Null(monitoredItem.Handle); + Assert.True(monitoredItem.SkipMonitoredItemNotification()); } [Fact] @@ -134,12 +134,12 @@ public void SetDataChangeFilterWhenBaseTemplateIsDataTemplate() DeadbandValue = 10.0 } }; - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem; + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem; - Assert.NotNull(monitoredItemWrapper.Item.Filter); - Assert.IsType(monitoredItemWrapper.Item.Filter); + Assert.NotNull(monitoredItem.Filter); + Assert.IsType(monitoredItem.Filter); - var dataChangeFilter = (DataChangeFilter)monitoredItemWrapper.Item.Filter; + var dataChangeFilter = (DataChangeFilter)monitoredItem.Filter; Assert.Equal(DataChangeTrigger.StatusValue, dataChangeFilter.Trigger); Assert.Equal((uint)Opc.Ua.DeadbandType.Percent, dataChangeFilter.DeadbandType); Assert.Equal(10.0, dataChangeFilter.DeadbandValue); @@ -174,12 +174,12 @@ public void SetEventFilterWhenBaseTemplateIsEventTemplate() } } }; - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem; + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem; - Assert.NotNull(monitoredItemWrapper.Item.Filter); - Assert.IsType(monitoredItemWrapper.Item.Filter); + Assert.NotNull(monitoredItem.Filter); + Assert.IsType(monitoredItem.Filter); - var eventFilter = (EventFilter)monitoredItemWrapper.Item.Filter; + var eventFilter = (EventFilter)monitoredItem.Filter; Assert.NotEmpty(eventFilter.SelectClauses); Assert.Equal(ObjectTypeIds.BaseEventType, eventFilter.SelectClauses[0].TypeDefinitionId); Assert.Equal("EventId", eventFilter.SelectClauses[0].BrowsePath.ElementAtOrDefault(0)); @@ -206,12 +206,12 @@ public void AddConditionTypeSelectClausesWhenPendingAlarmsIsSetInEventTemplate() UpdateInterval = 20 } }; - var monitoredItemWrapper = GetMonitoredItem(template) as OpcUaMonitoredItem; + var monitoredItem = GetMonitoredItem(template) as OpcUaMonitoredItem; - Assert.NotNull(monitoredItemWrapper.Item.Filter); - Assert.IsType(monitoredItemWrapper.Item.Filter); + Assert.NotNull(monitoredItem.Filter); + Assert.IsType(monitoredItem.Filter); - var eventFilter = (EventFilter)monitoredItemWrapper.Item.Filter; + var eventFilter = (EventFilter)monitoredItem.Filter; Assert.NotNull(eventFilter.SelectClauses); Assert.Equal(11, eventFilter.SelectClauses.Count); Assert.Equal(Attributes.NodeId, eventFilter.SelectClauses[9].AttributeId); @@ -263,7 +263,7 @@ public void SetupFieldNameWithNamespaceNameWhenNamespaceIndexIsUsed() }); var eventItem = GetMonitoredItem(template, namespaceTable) as OpcUaMonitoredItem.EventItem; - Assert.Equal(((EventFilter)eventItem.Item.Filter).SelectClauses.Count, eventItem.Fields.Count); + Assert.Equal(((EventFilter)eventItem.Filter).SelectClauses.Count, eventItem.Fields.Count); Assert.Equal("http://opcfoundation.org/Quickstarts/SimpleEvents#CycleId", eventItem.Fields[0].Name); Assert.Equal("http://opcfoundation.org/Quickstarts/SimpleEvents#CurrentStep", eventItem.Fields[1].Name); } @@ -308,7 +308,7 @@ public void UseDefaultFieldNameWhenNamespaceTableIsEmpty() var eventItem = GetMonitoredItem(template, namespaceUris) as OpcUaMonitoredItem.EventItem; - Assert.Equal(((EventFilter)eventItem.Item.Filter).SelectClauses.Count, eventItem.Fields.Count); + Assert.Equal(((EventFilter)eventItem.Filter).SelectClauses.Count, eventItem.Fields.Count); Assert.Equal("http://opcfoundation.org/Quickstarts/SimpleEvents#CycleId", eventItem.Fields[0].Name); Assert.Equal("http://opcfoundation.org/Quickstarts/SimpleEvents#CurrentStep", eventItem.Fields[1].Name); } diff --git a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj index 73bdc6bde3..17b478d22a 100644 --- a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj +++ b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj index dbb074154f..c7ef25d4b7 100644 --- a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj +++ b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj @@ -9,7 +9,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers