diff --git a/docs/DependencyInjection.md b/docs/DependencyInjection.md index e06e6ab4a7..af5225bd82 100644 --- a/docs/DependencyInjection.md +++ b/docs/DependencyInjection.md @@ -936,7 +936,6 @@ unchanged. The data binds directly from { "EndpointUrl": "opc.tcp://client.example.com:4841", "Timeout": 30000, - "MaxSessionCount": 1, "Enabled": true } ] diff --git a/docs/ReverseConnect.md b/docs/ReverseConnect.md index 570a08fd09..e7b1e875b0 100644 --- a/docs/ReverseConnect.md +++ b/docs/ReverseConnect.md @@ -220,7 +220,6 @@ services.AddOpcUa() { EndpointUrl = "opc.tcp://client.example.com:65300", Timeout = 30000, - MaxSessionCount = 1, Enabled = true }); }); diff --git a/samples/Reference/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml b/samples/Reference/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml index c42ce2a024..6ef730a465 100644 --- a/samples/Reference/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml +++ b/samples/Reference/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml @@ -228,7 +228,6 @@ opc.tcp://localhost:65300 - 0 true diff --git a/samples/Reference/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/samples/Reference/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 6fac3c289e..e9e81756b5 100644 --- a/samples/Reference/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/samples/Reference/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -371,7 +371,6 @@ opc.tcp://localhost:65300 - 0 true diff --git a/src/Opc.Ua.Core/Schema/ApplicationConfiguration.cs b/src/Opc.Ua.Core/Schema/ApplicationConfiguration.cs index 58fc7ae7d7..daeea64b45 100644 --- a/src/Opc.Ua.Core/Schema/ApplicationConfiguration.cs +++ b/src/Opc.Ua.Core/Schema/ApplicationConfiguration.cs @@ -1623,18 +1623,15 @@ namespace Opc.Ua /// /// The maximum count of active reverse connect sessions. - /// 0 or undefined means unlimited number of sessions. - /// 1 means a single connection is created at a time. - /// n disables reverse hello once the total number of sessions - /// in the server reaches n. + /// This setting is ignored and is no longer serialized. /// - [DataTypeField(Order = 2)] + [Obsolete("MaxSessionCount is no longer supported and is ignored.")] public int MaxSessionCount { get; set; } /// /// Specifies whether the sending of reverse connect attempts is enabled. /// - [DataTypeField(Order = 3, DefaultValueHandling = DefaultValueHandling.Emit)] + [DataTypeField(Order = 2, DefaultValueHandling = DefaultValueHandling.Emit)] public bool Enabled { get; set; } = true; } diff --git a/src/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd b/src/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd index fcf424e534..fa338eeece 100644 --- a/src/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd +++ b/src/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd @@ -206,7 +206,6 @@ - diff --git a/src/Opc.Ua.Gds.Server/Hosting/GdsServerHostedService.cs b/src/Opc.Ua.Gds.Server/Hosting/GdsServerHostedService.cs index 6ab0273d36..88d8cf29b8 100644 --- a/src/Opc.Ua.Gds.Server/Hosting/GdsServerHostedService.cs +++ b/src/Opc.Ua.Gds.Server/Hosting/GdsServerHostedService.cs @@ -638,7 +638,6 @@ namespace Opc.Ua.Gds.Server.Hosting { EndpointUrl = c.EndpointUrl, Timeout = c.Timeout, - MaxSessionCount = c.MaxSessionCount, Enabled = c.Enabled }; } diff --git a/src/Opc.Ua.Server/Hosting/OpcUaServerApplicationConfigurationFeature.cs b/src/Opc.Ua.Server/Hosting/OpcUaServerApplicationConfigurationFeature.cs index bb7bec1b66..561374f685 100644 --- a/src/Opc.Ua.Server/Hosting/OpcUaServerApplicationConfigurationFeature.cs +++ b/src/Opc.Ua.Server/Hosting/OpcUaServerApplicationConfigurationFeature.cs @@ -141,7 +141,6 @@ namespace Opc.Ua.Server.Hosting { EndpointUrl = client.EndpointUrl, Timeout = client.Timeout, - MaxSessionCount = client.MaxSessionCount, Enabled = client.Enabled }; } diff --git a/src/Opc.Ua.Server/Hosting/OpcUaServerBuilderExtensions.cs b/src/Opc.Ua.Server/Hosting/OpcUaServerBuilderExtensions.cs index 220fdafb26..bf4607a9b4 100644 --- a/src/Opc.Ua.Server/Hosting/OpcUaServerBuilderExtensions.cs +++ b/src/Opc.Ua.Server/Hosting/OpcUaServerBuilderExtensions.cs @@ -1928,8 +1928,6 @@ namespace Microsoft.Extensions.DependencyInjection BindString(child, nameof(ServerReverseConnectClientOptions.EndpointUrl), value => client.EndpointUrl = value); BindInt32(child, nameof(ServerReverseConnectClientOptions.Timeout), value => client.Timeout = value); - BindInt32(child, nameof(ServerReverseConnectClientOptions.MaxSessionCount), - value => client.MaxSessionCount = value); BindBoolean(child, nameof(ServerReverseConnectClientOptions.Enabled), value => client.Enabled = value); reverseConnect.Clients.Add(client); } diff --git a/src/Opc.Ua.Server/Hosting/ServerReverseConnectOptions.cs b/src/Opc.Ua.Server/Hosting/ServerReverseConnectOptions.cs index a5981c1b64..14327d7945 100644 --- a/src/Opc.Ua.Server/Hosting/ServerReverseConnectOptions.cs +++ b/src/Opc.Ua.Server/Hosting/ServerReverseConnectOptions.cs @@ -94,9 +94,9 @@ namespace Opc.Ua.Server.Hosting /// /// Maximum simultaneous reverse-connect sessions for this client. - /// Zero means unlimited; 1 permits a single connection at a - /// time. + /// This setting is ignored. /// + [System.Obsolete("MaxSessionCount is no longer supported and is ignored.")] public int MaxSessionCount { get; set; } /// diff --git a/src/Opc.Ua.Server/Server/ReverseConnectServer.cs b/src/Opc.Ua.Server/Server/ReverseConnectServer.cs index 2b4f6659f2..1643cbd916 100644 --- a/src/Opc.Ua.Server/Server/ReverseConnectServer.cs +++ b/src/Opc.Ua.Server/Server/ReverseConnectServer.cs @@ -74,6 +74,25 @@ namespace Opc.Ua.Server /// public class ReverseConnectProperty { + /// + /// Initialize a reverse connect server property. + /// + /// The Url of the reverse connect client. + /// The timeout to use for a reverse connect attempt. + /// If this is an application configuration entry. + /// If the connection is enabled. + public ReverseConnectProperty( + Uri clientUrl, + int timeout, + bool configEntry, + bool enabled = true) + { + ClientUrl = clientUrl; + Timeout = timeout > 0 ? timeout : ReverseConnectServer.DefaultReverseConnectTimeout; + ConfigEntry = configEntry; + Enabled = enabled; + } + /// /// Initialize a reverse connect server property. /// @@ -82,18 +101,18 @@ namespace Opc.Ua.Server /// The maximum number of sessions allowed to the client. /// If this is an application configuration entry. /// If the connection is enabled. + [Obsolete("MaxSessionCount is no longer supported and has been removed from configuration.")] public ReverseConnectProperty( Uri clientUrl, int timeout, int maxSessionCount, bool configEntry, bool enabled = true) + : this(clientUrl, timeout, configEntry, enabled) { - ClientUrl = clientUrl; - Timeout = timeout > 0 ? timeout : ReverseConnectServer.DefaultReverseConnectTimeout; +#pragma warning disable CS0618 MaxSessionCount = maxSessionCount; - ConfigEntry = configEntry; - Enabled = enabled; +#pragma warning restore CS0618 } /// @@ -124,6 +143,7 @@ namespace Opc.Ua.Server /// /// The maximum number of sessions allowed to the client. /// + [Obsolete("MaxSessionCount is no longer supported and has been removed from configuration.")] public int MaxSessionCount; /// @@ -232,12 +252,32 @@ namespace Opc.Ua.Server /// Add a reverse connection url. /// /// + [System.Runtime.CompilerServices.OverloadResolutionPriority(1)] + public virtual void AddReverseConnection( + Uri url, + int timeout = 0, + bool enabled = true) + { +#pragma warning disable CS0618 + AddReverseConnection(url, timeout, maxSessionCount: 0, enabled); +#pragma warning restore CS0618 + } + + /// + /// Add a reverse connection url. + /// + /// The Url of the reverse connect client. + /// The timeout to use for a reverse connect attempt. + /// The maximum number of sessions allowed (deprecated and ignored). + /// If the connection is enabled. + [Obsolete("The maxSessionCount parameter is no longer supported and will be ignored.")] public virtual void AddReverseConnection( Uri url, int timeout = 0, int maxSessionCount = 0, bool enabled = true) { + _ = maxSessionCount; if (m_connections.ContainsKey(url)) { throw new ArgumentException( @@ -247,7 +287,6 @@ namespace Opc.Ua.Server var reverseConnection = new ReverseConnectProperty( url, timeout, - maxSessionCount, false, enabled); @@ -329,15 +368,8 @@ namespace Opc.Ua.Server // try the reverse connect if (reverseConnection.Enabled && - ( - reverseConnection.MaxSessionCount == 0 || - ( - reverseConnection.MaxSessionCount == 1 && - reverseConnection.LastState == ReverseConnectState.Closed - ) || - reverseConnection.MaxSessionCount > ServerInternal.SessionManager - .GetSessions() - .Count)) + reverseConnection.LastState != ReverseConnectState.Connecting && + reverseConnection.LastState != ReverseConnectState.Rejected) { try { @@ -526,7 +558,6 @@ namespace Opc.Ua.Server { if (existing.ConfigEntry) { - existing.MaxSessionCount = client.MaxSessionCount; existing.Enabled = client.Enabled; } else @@ -539,7 +570,6 @@ namespace Opc.Ua.Server m_connections[uri] = new ReverseConnectProperty( uri, client.Timeout, - client.MaxSessionCount, true, client.Enabled); m_logger.ReverseConnectionAddedForEndpointUrlUri(uri); diff --git a/tests/Opc.Ua.Aot.Tests/Quickstarts.ReferenceServer.Config.xml b/tests/Opc.Ua.Aot.Tests/Quickstarts.ReferenceServer.Config.xml index a62cc996e6..2390b0d143 100644 --- a/tests/Opc.Ua.Aot.Tests/Quickstarts.ReferenceServer.Config.xml +++ b/tests/Opc.Ua.Aot.Tests/Quickstarts.ReferenceServer.Config.xml @@ -289,7 +289,6 @@ opc.tcp://localhost:65300 - 0 true diff --git a/tests/Opc.Ua.Configuration.Tests/ConfigurationRoundTripTests.cs b/tests/Opc.Ua.Configuration.Tests/ConfigurationRoundTripTests.cs index 840015bef2..f84c86433d 100644 --- a/tests/Opc.Ua.Configuration.Tests/ConfigurationRoundTripTests.cs +++ b/tests/Opc.Ua.Configuration.Tests/ConfigurationRoundTripTests.cs @@ -872,13 +872,11 @@ namespace Opc.Ua.Configuration.Tests rc.Clients[0].EndpointUrl, Is.EqualTo("opc.tcp://client1:65300")); Assert.That(rc.Clients[0].Timeout, Is.EqualTo(5000)); - Assert.That(rc.Clients[0].MaxSessionCount, Is.EqualTo(10)); Assert.That(rc.Clients[0].Enabled, Is.True); Assert.That( rc.Clients[1].EndpointUrl, Is.EqualTo("opc.tcp://client2:65301")); Assert.That(rc.Clients[1].Timeout, Is.EqualTo(10000)); - Assert.That(rc.Clients[1].MaxSessionCount, Is.EqualTo(5)); Assert.That(rc.Clients[1].Enabled, Is.False); Assert.That(rc.ConnectInterval, Is.EqualTo(20000)); Assert.That(rc.ConnectTimeout, Is.EqualTo(45000)); diff --git a/tests/Opc.Ua.Configuration.Tests/test-server-full.xml b/tests/Opc.Ua.Configuration.Tests/test-server-full.xml index b24be686bd..4af8e0a2b9 100644 --- a/tests/Opc.Ua.Configuration.Tests/test-server-full.xml +++ b/tests/Opc.Ua.Configuration.Tests/test-server-full.xml @@ -107,13 +107,11 @@ opc.tcp://client1:65300 5000 - 10 true opc.tcp://client2:65301 10000 - 5 false diff --git a/tests/Opc.Ua.Lens.Tests/Samples/RepositorySampleLiveTests.Identities.cs b/tests/Opc.Ua.Lens.Tests/Samples/RepositorySampleLiveTests.Identities.cs index 1078142587..47b990ca21 100644 --- a/tests/Opc.Ua.Lens.Tests/Samples/RepositorySampleLiveTests.Identities.cs +++ b/tests/Opc.Ua.Lens.Tests/Samples/RepositorySampleLiveTests.Identities.cs @@ -120,8 +120,7 @@ namespace UaLens.Tests.Samples options.ReverseConnect.Clients.Add(new ServerReverseConnectClientOptions { EndpointUrl = listenerUrl, - Timeout = 10000, - MaxSessionCount = 1 + Timeout = 10000 }); } }).AddCertificateManager(manager); diff --git a/tests/Opc.Ua.Server.Tests/Hosting/HostedReverseConnectIntegrationTests.cs b/tests/Opc.Ua.Server.Tests/Hosting/HostedReverseConnectIntegrationTests.cs index 50e1cf4487..c143aebef7 100644 --- a/tests/Opc.Ua.Server.Tests/Hosting/HostedReverseConnectIntegrationTests.cs +++ b/tests/Opc.Ua.Server.Tests/Hosting/HostedReverseConnectIntegrationTests.cs @@ -106,8 +106,7 @@ namespace Opc.Ua.Server.Tests.Hosting options.ReverseConnect.Clients.Add(new ServerReverseConnectClientOptions { EndpointUrl = clientUrl.AbsoluteUri, - Timeout = 10000, - MaxSessionCount = 1 + Timeout = 10000 }); }); } @@ -237,7 +236,6 @@ namespace Opc.Ua.Server.Tests.Hosting { EndpointUrl = clientUrl.AbsoluteUri, Timeout = 10000, - MaxSessionCount = 1, Enabled = true } ] diff --git a/tests/Opc.Ua.Server.Tests/Hosting/OpcUaServerApplicationConfigurationFeatureTests.cs b/tests/Opc.Ua.Server.Tests/Hosting/OpcUaServerApplicationConfigurationFeatureTests.cs index 7f181f37f3..68fa118345 100644 --- a/tests/Opc.Ua.Server.Tests/Hosting/OpcUaServerApplicationConfigurationFeatureTests.cs +++ b/tests/Opc.Ua.Server.Tests/Hosting/OpcUaServerApplicationConfigurationFeatureTests.cs @@ -178,7 +178,6 @@ namespace Opc.Ua.Server.Tests.Hosting { EndpointUrl = "opc.tcp://reverse-client:4841", Timeout = 500, - MaxSessionCount = 2, Enabled = true }); options.EndpointUrls.Add("opc.tcp://localhost:0/ReverseConnect"); @@ -195,7 +194,6 @@ namespace Opc.Ua.Server.Tests.Hosting Assert.That(reverseConnect.Clients.Count, Is.EqualTo(1)); Assert.That(reverseConnect.Clients[0].EndpointUrl, Is.EqualTo("opc.tcp://reverse-client:4841")); Assert.That(reverseConnect.Clients[0].Timeout, Is.EqualTo(500)); - Assert.That(reverseConnect.Clients[0].MaxSessionCount, Is.EqualTo(2)); Assert.That(reverseConnect.Clients[0].Enabled, Is.True); } diff --git a/tests/Opc.Ua.Server.Tests/Hosting/OpcUaServerOptionsCoverageTests.cs b/tests/Opc.Ua.Server.Tests/Hosting/OpcUaServerOptionsCoverageTests.cs index c854a048a9..880aafb1b4 100644 --- a/tests/Opc.Ua.Server.Tests/Hosting/OpcUaServerOptionsCoverageTests.cs +++ b/tests/Opc.Ua.Server.Tests/Hosting/OpcUaServerOptionsCoverageTests.cs @@ -89,7 +89,6 @@ namespace Opc.Ua.Server.Tests.Hosting { EndpointUrl = "opc.tcp://client.local:4841", Timeout = 7000, - MaxSessionCount = 2, Enabled = true }); }); @@ -104,7 +103,6 @@ namespace Opc.Ua.Server.Tests.Hosting ServerReverseConnectClientOptions client = opts.ReverseConnect.Clients[0]; Assert.That(client.EndpointUrl, Is.EqualTo("opc.tcp://client.local:4841")); Assert.That(client.Timeout, Is.EqualTo(7000)); - Assert.That(client.MaxSessionCount, Is.EqualTo(2)); Assert.That(client.Enabled, Is.True); } diff --git a/tests/Opc.Ua.Server.Tests/Hosting/ServerCompositionHostingTests.cs b/tests/Opc.Ua.Server.Tests/Hosting/ServerCompositionHostingTests.cs index e49c6d9d3e..7d423a4b0e 100644 --- a/tests/Opc.Ua.Server.Tests/Hosting/ServerCompositionHostingTests.cs +++ b/tests/Opc.Ua.Server.Tests/Hosting/ServerCompositionHostingTests.cs @@ -1424,8 +1424,7 @@ namespace Opc.Ua.Server.Tests.Hosting options.ReverseConnect.Clients.Add(new ServerReverseConnectClientOptions { EndpointUrl = enabled.ToString(), - Timeout = 3210, - MaxSessionCount = 0 + Timeout = 3210 }); options.ReverseConnect.Clients.Add(new ServerReverseConnectClientOptions { @@ -2132,8 +2131,7 @@ namespace Opc.Ua.Server.Tests.Hosting options.ReverseConnect.Clients.Add(new ServerReverseConnectClientOptions { EndpointUrl = client.ToString(), - Timeout = 1456, - MaxSessionCount = 0 + Timeout = 1456 }); }); await using ConfiguredAsyncDisposable cleanup = fixture.ConfigureAwait(false); @@ -2236,13 +2234,11 @@ namespace Opc.Ua.Server.Tests.Hosting opc.tcp://localhost:0/xml-enabled 1234 - 0 true opc.tcp://localhost:0/xml-disabled 4321 - 0 false diff --git a/tests/Opc.Ua.Server.Tests/ReverseConnectReloadRegressionTests.cs b/tests/Opc.Ua.Server.Tests/ReverseConnectReloadRegressionTests.cs index db60bb7944..d3323a9735 100644 --- a/tests/Opc.Ua.Server.Tests/ReverseConnectReloadRegressionTests.cs +++ b/tests/Opc.Ua.Server.Tests/ReverseConnectReloadRegressionTests.cs @@ -57,7 +57,7 @@ namespace Opc.Ua.Server.Tests CreateConfiguration("configured").ServerConfiguration!.ReverseConnect; ReloadableReverseConnectServer server = await fixture.StartAsync().ConfigureAwait(false); var dynamicUrl = new Uri("opc.tcp://localhost:4840/dynamic"); - server.AddReverseConnection(dynamicUrl, timeout: 1234, maxSessionCount: 2, enabled: false); + server.AddReverseConnection(dynamicUrl, timeout: 1234, enabled: false); ReverseConnectProperty retained = server.GetReverseConnections()[dynamicUrl]; Assert.That(server.GetReverseConnections(), Has.Count.EqualTo(2)); @@ -70,7 +70,6 @@ namespace Opc.Ua.Server.Tests Assert.That(server.GetReverseConnections()[dynamicUrl], Is.SameAs(retained)); Assert.That(retained.ConfigEntry, Is.False); Assert.That(retained.Timeout, Is.EqualTo(1234)); - Assert.That(retained.MaxSessionCount, Is.EqualTo(2)); Assert.That(retained.Enabled, Is.False); } finally @@ -88,7 +87,7 @@ namespace Opc.Ua.Server.Tests { ReloadableReverseConnectServer server = await fixture.StartAsync().ConfigureAwait(false); var dynamicUrl = new Uri("opc.tcp://localhost:4840/dynamic"); - server.AddReverseConnection(dynamicUrl, timeout: 1234, maxSessionCount: 2, enabled: false); + server.AddReverseConnection(dynamicUrl, timeout: 1234, enabled: false); ReverseConnectProperty retained = server.GetReverseConnections()[dynamicUrl]; fixture.Config.ServerConfiguration!.ReverseConnect = new ReverseConnectServerConfiguration { @@ -98,7 +97,6 @@ namespace Opc.Ua.Server.Tests { EndpointUrl = dynamicUrl.AbsoluteUri, Timeout = 9000, - MaxSessionCount = 8, Enabled = true } ] @@ -111,7 +109,6 @@ namespace Opc.Ua.Server.Tests Assert.That(server.GetReverseConnections()[dynamicUrl], Is.SameAs(retained)); Assert.That(retained.ConfigEntry, Is.False); Assert.That(retained.Timeout, Is.EqualTo(1234)); - Assert.That(retained.MaxSessionCount, Is.EqualTo(2)); Assert.That(retained.Enabled, Is.False); } finally @@ -128,7 +125,7 @@ namespace Opc.Ua.Server.Tests { using var server = new ReverseConnectServer(NUnitTelemetryContext.Create(), new FakeTimeProvider()); var dynamicUrl = new Uri("opc.tcp://localhost:4840/dynamic"); - server.AddReverseConnection(dynamicUrl, timeout: 1234, maxSessionCount: 2, enabled: false); + server.AddReverseConnection(dynamicUrl, timeout: 1234, enabled: false); MethodInfo method = typeof(ReverseConnectServer).GetMethod( "UpdateConfiguration", BindingFlags.Instance | BindingFlags.NonPublic)!; #if NET5_0_OR_GREATER @@ -148,7 +145,6 @@ namespace Opc.Ua.Server.Tests ReverseConnectProperty retained = server.GetReverseConnections()[dynamicUrl]; Assert.That(retained.ConfigEntry, Is.False); Assert.That(retained.Timeout, Is.EqualTo(1234)); - Assert.That(retained.MaxSessionCount, Is.EqualTo(2)); Assert.That(retained.Enabled, Is.False); } diff --git a/tests/Opc.Ua.Server.Tests/ReverseConnectServerCoverageTests.cs b/tests/Opc.Ua.Server.Tests/ReverseConnectServerCoverageTests.cs index b68da84d82..610cf27a41 100644 --- a/tests/Opc.Ua.Server.Tests/ReverseConnectServerCoverageTests.cs +++ b/tests/Opc.Ua.Server.Tests/ReverseConnectServerCoverageTests.cs @@ -53,7 +53,6 @@ namespace Opc.Ua.Server.Tests var property = new ReverseConnectProperty( s_clientUrl, timeout: 5000, - maxSessionCount: 3, configEntry: true, enabled: true); @@ -61,7 +60,6 @@ namespace Opc.Ua.Server.Tests { Assert.That(property.ClientUrl, Is.EqualTo(s_clientUrl)); Assert.That(property.Timeout, Is.EqualTo(5000)); - Assert.That(property.MaxSessionCount, Is.EqualTo(3)); Assert.That(property.ConfigEntry, Is.True); Assert.That(property.Enabled, Is.True); Assert.That(property.LastState, Is.EqualTo(ReverseConnectState.Closed)); @@ -76,7 +74,6 @@ namespace Opc.Ua.Server.Tests var property = new ReverseConnectProperty( s_clientUrl, timeout, - maxSessionCount: 0, configEntry: false); Assert.That( @@ -90,7 +87,6 @@ namespace Opc.Ua.Server.Tests var property = new ReverseConnectProperty( s_clientUrl, timeout: 1000, - maxSessionCount: 0, configEntry: false); Assert.That(property.Enabled, Is.True); @@ -102,7 +98,6 @@ namespace Opc.Ua.Server.Tests var property = new ReverseConnectProperty( s_clientUrl, timeout: 1000, - maxSessionCount: 0, configEntry: false, enabled: false); @@ -115,12 +110,10 @@ namespace Opc.Ua.Server.Tests var property = new ReverseConnectProperty( s_clientUrl, timeout: 1000, - maxSessionCount: 0, configEntry: false); var rejectTime = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); property.LastState = ReverseConnectState.Rejected; - property.MaxSessionCount = 7; property.Enabled = false; property.RejectTime = rejectTime; property.ServiceResult = new ServiceResult(StatusCodes.BadTimeout); @@ -128,7 +121,6 @@ namespace Opc.Ua.Server.Tests Assert.Multiple(() => { Assert.That(property.LastState, Is.EqualTo(ReverseConnectState.Rejected)); - Assert.That(property.MaxSessionCount, Is.EqualTo(7)); Assert.That(property.Enabled, Is.False); Assert.That(property.RejectTime, Is.EqualTo(rejectTime)); Assert.That(property.ServiceResult, Is.Not.Null); @@ -158,7 +150,6 @@ namespace Opc.Ua.Server.Tests Assert.Multiple(() => { Assert.That(property.Timeout, Is.EqualTo(ReverseConnectServer.DefaultReverseConnectTimeout)); - Assert.That(property.MaxSessionCount, Is.Zero); Assert.That(property.Enabled, Is.True); Assert.That(property.ConfigEntry, Is.False); }); @@ -172,14 +163,12 @@ namespace Opc.Ua.Server.Tests server.AddReverseConnection( s_clientUrl, timeout: 2500, - maxSessionCount: 4, enabled: false); ReverseConnectProperty property = server.GetReverseConnections()[s_clientUrl]; Assert.Multiple(() => { Assert.That(property.Timeout, Is.EqualTo(2500)); - Assert.That(property.MaxSessionCount, Is.EqualTo(4)); Assert.That(property.Enabled, Is.False); }); } diff --git a/tests/Opc.Ua.Server.Tests/ReverseConnectServerSessionAccountingTests.cs b/tests/Opc.Ua.Server.Tests/ReverseConnectServerSessionAccountingTests.cs new file mode 100644 index 0000000000..ab8e197e81 --- /dev/null +++ b/tests/Opc.Ua.Server.Tests/ReverseConnectServerSessionAccountingTests.cs @@ -0,0 +1,326 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; +using NUnit.Framework; +using Opc.Ua.Server.TestFramework; +using Opc.Ua.Tests; + +namespace Opc.Ua.Server.Tests +{ + /// + /// Regression tests for Issue #4863: reverse connect connection admission must not + /// be blocked by unrelated server sessions or an existing connection. + /// + [TestFixture] + [Category("ReverseConnect")] + [NonParallelizable] + public class ReverseConnectServerSessionAccountingTests + { + private static readonly Uri s_reverseClientUri = new("opc.tcp://reverseclient.local:4840"); + + private sealed class TestableReverseConnectServer : ReverseConnectServer + { + public TestableReverseConnectServer(ITelemetryContext telemetry) + : this(telemetry, new FakeTimeProvider()) + { + } + + public TestableReverseConnectServer(ITelemetryContext telemetry, TimeProvider timeProvider) + : base(telemetry, timeProvider) + { + } + + /// + /// Number of calls dispatched to the legacy overload. + /// + public int LegacyAddReverseConnectionCallCount { get; private set; } + + [Obsolete] + public override void AddReverseConnection( + Uri url, + int timeout = 0, + int maxSessionCount = 0, + bool enabled = true) + { + LegacyAddReverseConnectionCallCount++; + base.AddReverseConnection(url, timeout, maxSessionCount, enabled); + } + + public void InvokeOnReverseConnect(long version) + { + MethodInfo method = typeof(ReverseConnectServer).GetMethod( + "OnReverseConnect", + BindingFlags.Instance | BindingFlags.NonPublic)!; + method.Invoke(this, [version]); + } + } + + /// + /// Verifies that the new overload still dispatches through the legacy virtual overload. + /// + [Test] + public void AddReverseConnectionPreservesLegacyVirtualDispatch() + { + using var server = new TestableReverseConnectServer(NUnitTelemetryContext.Create()); + + server.AddReverseConnection(s_reverseClientUri); + + Assert.That(server.LegacyAddReverseConnectionCallCount, Is.EqualTo(1)); + Assert.That(server.GetReverseConnections(), Contains.Key(s_reverseClientUri)); + } + + [Test] + public void ConnectedReverseConnection_CanBeReconnected() + { + var fakeTime = new FakeTimeProvider(); + using var server = new TestableReverseConnectServer(NUnitTelemetryContext.Create(), fakeTime); + + server.AddReverseConnection(s_reverseClientUri, timeout: 5000, enabled: true); + ReverseConnectProperty property = server.GetReverseConnections()[s_reverseClientUri]; + + // Simulate the reverse connection has successfully established + property.LastState = ReverseConnectState.Connected; + + // Trigger the reverse connect tick + server.InvokeOnReverseConnect(0); + + // A new connection is required to recreate a session without outbound discovery. + Assert.That( + property.LastState, + Is.EqualTo(ReverseConnectState.Errored), + "A Connected reverse connection must allow another connection attempt."); + Assert.That(property.ServiceResult?.AdditionalInfo, Does.Contain("No suitable listener found")); + } + + /// + /// Verifies that an established reverse connection can be replaced when the server has no sessions. + /// + [Test] + public async Task ConnectedReverseConnectionCanBeReplacedWhenStartedServerHasZeroSessionsAsync() + { + var fakeTime = new FakeTimeProvider(); + var fixture = new ServerFixture( + telemetry => new TestableReverseConnectServer(telemetry, fakeTime)); + try + { + await fixture.LoadConfigurationAsync().ConfigureAwait(false); + fixture.Config.ServerConfiguration!.ReverseConnect = new ReverseConnectServerConfiguration + { + Clients = [] + }; + TestableReverseConnectServer server = await fixture.StartAsync().ConfigureAwait(false); + + server.AddReverseConnection(s_reverseClientUri, timeout: 5000, enabled: true); + ReverseConnectProperty property = server.GetReverseConnections()[s_reverseClientUri]; + + // Connection is already established, but server has 0 active sessions + property.LastState = ReverseConnectState.Connected; + + // Fire the server's current timer (initial delay is one second) + fakeTime.Advance(TimeSpan.FromSeconds(2)); + + Assert.That( + property.LastState, + Is.EqualTo(ReverseConnectState.Connecting), + "A Connected reverse connection must allow a replacement connection attempt."); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + + /// + /// Verifies that reverse connections are attempted with a full legacy session threshold. + /// + [TestCase(ReverseConnectState.Closed)] + [TestCase(ReverseConnectState.Errored)] + [TestCase(ReverseConnectState.Connected)] + public async Task ReverseConnectionIsAttemptedWhenLegacySessionThresholdIsReachedAsync( + ReverseConnectState initialState) + { + var fakeTime = new FakeTimeProvider(); + var fixture = new ServerFixture( + telemetry => new TestableReverseConnectServer(telemetry, fakeTime)); + try + { + await fixture.LoadConfigurationAsync().ConfigureAwait(false); + fixture.Config.ServerConfiguration!.ReverseConnect = new ReverseConnectServerConfiguration + { + Clients = [] + }; + TestableReverseConnectServer server = await fixture.StartAsync().ConfigureAwait(false); + await server.CreateAndActivateSessionAsync("reverse-connect-session-1").ConfigureAwait(false); + await server.CreateAndActivateSessionAsync("reverse-connect-session-2").ConfigureAwait(false); + + server.AddReverseConnection(s_reverseClientUri, timeout: 5000, enabled: true); + ReverseConnectProperty property = server.GetReverseConnections()[s_reverseClientUri]; +#pragma warning disable CS0618 + property.MaxSessionCount = 2; +#pragma warning restore CS0618 + property.LastState = initialState; + + Assert.That(server.CurrentInstance.SessionManager.GetSessions(), Has.Count.EqualTo(2)); + + // Fire the current timer after its one-second initial delay. + fakeTime.Advance(TimeSpan.FromSeconds(2)); + + Assert.That( + property.LastState, + Is.EqualTo(ReverseConnectState.Connecting), + $"A {initialState} connection must be attempted even when the legacy session threshold is reached."); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + } + } + + [Test] + public void ConnectingReverseConnection_IsNotReconnected_WhileConnectionInProgress() + { + var fakeTime = new FakeTimeProvider(); + using var server = new TestableReverseConnectServer(NUnitTelemetryContext.Create(), fakeTime); + + server.AddReverseConnection(s_reverseClientUri, timeout: 5000, enabled: true); + ReverseConnectProperty property = server.GetReverseConnections()[s_reverseClientUri]; + + // Simulate connection attempt in flight + property.LastState = ReverseConnectState.Connecting; + + server.InvokeOnReverseConnect(0); + + Assert.That( + property.LastState, + Is.EqualTo(ReverseConnectState.Connecting), + "A reverse connection that is currently Connecting must not trigger a redundant connection attempt."); + } + + [Test] + public void ClosedReverseConnection_InitiatesConnection_RegardlessOfServerSessions() + { + var fakeTime = new FakeTimeProvider(); + using var server = new TestableReverseConnectServer(NUnitTelemetryContext.Create(), fakeTime); + + server.AddReverseConnection(s_reverseClientUri, timeout: 5000, enabled: true); + ReverseConnectProperty property = server.GetReverseConnections()[s_reverseClientUri]; + + Assert.That(property.LastState, Is.EqualTo(ReverseConnectState.Closed)); + + // Tick should attempt connection (which throws ArgumentException because no listener is configured in unit test) + server.InvokeOnReverseConnect(0); + + Assert.That( + property.LastState, + Is.EqualTo(ReverseConnectState.Errored), + "A Closed connection must attempt connection on tick."); + Assert.That( + property.ServiceResult?.AdditionalInfo, + Does.Contain("No suitable listener found")); + } + + [Test] + public void ErroredReverseConnection_IsRetried_OnSubsequentTick() + { + var fakeTime = new FakeTimeProvider(); + using var server = new TestableReverseConnectServer(NUnitTelemetryContext.Create(), fakeTime); + + server.AddReverseConnection(s_reverseClientUri, timeout: 5000, enabled: true); + ReverseConnectProperty property = server.GetReverseConnections()[s_reverseClientUri]; + + // Simulate that an earlier connection attempt errored + property.LastState = ReverseConnectState.Errored; + property.ServiceResult = new ServiceResult(StatusCodes.BadCommunicationError); + + // On the next tick, it should attempt to reconnect (which updates ServiceResult) + server.InvokeOnReverseConnect(0); + + Assert.That(property.ServiceResult, Is.Not.Null); + Assert.That( + property.ServiceResult!.AdditionalInfo, + Does.Contain("No suitable listener found"), + "An Errored reverse connection should be retried on subsequent ticks."); + } + + [Test] + public void RejectedReverseConnection_DoesNotConnect_UntilRejectTimeoutExpires() + { + var fakeTime = new FakeTimeProvider(); + using var server = new TestableReverseConnectServer(NUnitTelemetryContext.Create(), fakeTime); + + server.AddReverseConnection(s_reverseClientUri, timeout: 5000, enabled: true); + ReverseConnectProperty property = server.GetReverseConnections()[s_reverseClientUri]; + + property.LastState = ReverseConnectState.Rejected; + property.RejectTime = fakeTime.GetUtcNow().UtcDateTime; + + // Advance time only by 10s (default reject timeout is 60s) + fakeTime.Advance(TimeSpan.FromSeconds(10)); + server.InvokeOnReverseConnect(0); + + Assert.That( + property.LastState, + Is.EqualTo(ReverseConnectState.Rejected), + "Rejected connection must not connect before reject timeout elapses."); + + // Advance time past the 60s reject timeout + fakeTime.Advance(TimeSpan.FromSeconds(51)); + server.InvokeOnReverseConnect(0); + + // It should now have recharged to Closed and attempted connect (becoming Errored due to no listener in test) + Assert.That( + property.LastState, + Is.EqualTo(ReverseConnectState.Errored), + "Rejected connection should attempt connect once reject timeout expires."); + } + + [Test] + public void DisabledReverseConnection_DoesNotConnect() + { + var fakeTime = new FakeTimeProvider(); + using var server = new TestableReverseConnectServer(NUnitTelemetryContext.Create(), fakeTime); + + server.AddReverseConnection(s_reverseClientUri, timeout: 5000, enabled: false); + ReverseConnectProperty property = server.GetReverseConnections()[s_reverseClientUri]; + + Assert.That(property.LastState, Is.EqualTo(ReverseConnectState.Closed)); + + server.InvokeOnReverseConnect(0); + + Assert.That( + property.LastState, + Is.EqualTo(ReverseConnectState.Closed), + "Disabled reverse connection must not attempt connection."); + } + } +}