Files
cd88bfac23 Server: decouple reverse connect connection admission from global sessions (#4863) (#4889)
Hey everyone! 👋 

Just wanted to contribute! I learned about the OPC Foundation during my
last internship and got introduced to open source coding, so here we
are! I noticed issue #4863 while exploring the codebase and wanted to
help address it.

---

# Description

## Problem
In `ReverseConnectServer.OnReverseConnect`
(`src/Opc.Ua.Server/Server/ReverseConnectServer.cs`), reverse connection
admission was evaluated using a combination of `MaxSessionCount` and
`ServerInternal.SessionManager.GetSessions().Count`:

```csharp
if (reverseConnection.Enabled &&
    (
        reverseConnection.MaxSessionCount == 0 ||
        (
            reverseConnection.MaxSessionCount == 1 &&
            reverseConnection.LastState == ReverseConnectState.Closed
        ) ||
        reverseConnection.MaxSessionCount > ServerInternal.SessionManager
            .GetSessions()
            .Count))
```

As reported in #4863, this logic causes several issues:
1. **Redundant connection attempts:** When `MaxSessionCount == 0` (the
default if omitted or unconfigured), `MaxSessionCount == 0` is always
true. Every 15-second timer tick dispatches another `CreateConnection`
call, even if the connection is already `Connected` or `Connecting`.
2. **Unrelated server sessions block reverse connect:** When
`MaxSessionCount >= 2`,
`ServerInternal.SessionManager.GetSessions().Count` queries the global
session count across all endpoints (including local/direct OPC UA
clients). Once direct sessions reach `MaxSessionCount`, reverse
connections are permanently prevented from establishing even when in
state `Closed`.
3. **Premature connections under `MaxSessionCount == 1`:** If a reverse
connection is established (`Connected`) but the client has not yet
activated an OPC UA session (`GetSessions().Count == 0`), `1 > 0`
evaluates to true, triggering duplicate connection attempts.
4. **Stuck `Errored` state:** If `CreateConnection` throws an exception,
`LastState` is set to `Errored`. If other sessions exist on the server,
`1 > 1` is false, leaving the connection permanently stuck in `Errored`
without retrying.

---

## Code Explanation

Following maintainer guidance on #4863 to remove session-count coupling
from connection admission:

In `src/Opc.Ua.Server/Server/ReverseConnectServer.cs`:
```csharp
// try the reverse connect
if (reverseConnection.Enabled &&
    (reverseConnection.LastState == ReverseConnectState.Closed ||
     reverseConnection.LastState == ReverseConnectState.Errored))
```

- **Connection state drives admission:** A reverse connection is only
initiated if it is enabled and currently `Closed` (needs connection) or
`Errored` (retrying after a previous failure).
- **Prevents duplicate sockets:** When already `Connected` or
`Connecting`, no redundant connection attempts are dispatched.
- **Respects reject backoff:** When in state `Rejected`, the connection
remains idle until `m_rejectTimeout` elapses and recharges the state to
`Closed`.
- **Decoupled from global sessions:** Unrelated client sessions on the
server no longer interfere with reverse connection establishment.
- **Zero API breakage:** Kept changes minimal and surgical—no public
signatures were changed, ensuring full backward compatibility.

---

## Commits Breakdown

The changes were structured into two focused commits:

1. **`31fc2fd95` — `test(server): add regression tests for reverse
connect session accounting (#4863)`**
- Adds unit regression tests in
`tests/Opc.Ua.Server.Tests/ReverseConnectServerSessionAccountingTests.cs`
covering all edge cases (`MaxSessionCount == 0`, `MaxSessionCount == 1`,
`MaxSessionCount >= 2`, `Errored` retry, reject timeout expiry, and
disabled connections) using `FakeTimeProvider`.

2. **`b820dbfed` — `fix(server): decouple reverse connect connection
admission from global sessions (#4863)`**
- Applies the surgical fix to `ReverseConnectServer.OnReverseConnect` to
evaluate `Closed` and `Errored` states directly.

---

## Testing & Verification

- **New regression suite:** All 7 tests in
`ReverseConnectServerSessionAccountingTests` pass on `net10.0` (98 ms).
- **Existing suite:** All 43 tests in category `ReverseConnect` pass on
`net10.0` (12 s).
- **Analyzers:** Zero compiler warnings and zero Roslynator analyzer
warnings (`TreatWarningsAsErrors=true`).

---

## Related Issues
Fixes #4863

---

## Checklist
- [] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran the affected tests locally against .NET 10 and all passed.
- [] I have addressed all PR feedback received.

---------

Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Co-authored-by: Marc <marcschier@hotmail.com>
2026-10-28 20:10:14 +01:00

144 lines
6.2 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<ApplicationConfiguration
xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd"
xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd">
<ApplicationName>Full Server Test</ApplicationName>
<ApplicationUri>urn:localhost:fullservertest</ApplicationUri>
<ProductUri>http://test.org/fullserver</ProductUri>
<ApplicationType>Server_0</ApplicationType>
<SecurityConfiguration>
<ApplicationCertificate>
<StoreType>Directory</StoreType>
<StorePath>%LocalApplicationData%/OPC/fullserver</StorePath>
<SubjectName>CN=FullServer, O=Test</SubjectName>
</ApplicationCertificate>
<TrustedIssuerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalApplicationData%/OPC/issuers</StorePath>
</TrustedIssuerCertificates>
<TrustedPeerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalApplicationData%/OPC/trusted</StorePath>
</TrustedPeerCertificates>
<NonceLength>32</NonceLength>
<RejectSHA1SignedCertificates>true</RejectSHA1SignedCertificates>
<MinimumCertificateKeySize>2048</MinimumCertificateKeySize>
</SecurityConfiguration>
<ServerConfiguration>
<BaseAddresses>
<ua:String>opc.tcp://localhost:4840</ua:String>
<ua:String>https://localhost:4843</ua:String>
</BaseAddresses>
<AlternateBaseAddresses>
<ua:String>opc.tcp://myhost:4840</ua:String>
</AlternateBaseAddresses>
<SecurityPolicies>
<ServerSecurityPolicy>
<SecurityMode>SignAndEncrypt_3</SecurityMode>
<SecurityPolicyUri>http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256</SecurityPolicyUri>
</ServerSecurityPolicy>
<ServerSecurityPolicy>
<SecurityMode>Sign_2</SecurityMode>
<SecurityPolicyUri>http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep</SecurityPolicyUri>
</ServerSecurityPolicy>
</SecurityPolicies>
<MinRequestThreadCount>5</MinRequestThreadCount>
<MaxRequestThreadCount>50</MaxRequestThreadCount>
<MaxQueuedRequestCount>500</MaxQueuedRequestCount>
<UserTokenPolicies>
<ua:UserTokenPolicy>
<ua:TokenType>Anonymous_0</ua:TokenType>
</ua:UserTokenPolicy>
<ua:UserTokenPolicy>
<ua:TokenType>UserName_1</ua:TokenType>
</ua:UserTokenPolicy>
</UserTokenPolicies>
<DiagnosticsEnabled>true</DiagnosticsEnabled>
<MaxSessionCount>200</MaxSessionCount>
<MaxChannelCount>500</MaxChannelCount>
<MinSessionTimeout>5000</MinSessionTimeout>
<MaxSessionTimeout>1800000</MaxSessionTimeout>
<MaxBrowseContinuationPoints>20</MaxBrowseContinuationPoints>
<MaxQueryContinuationPoints>15</MaxQueryContinuationPoints>
<MaxHistoryContinuationPoints>50</MaxHistoryContinuationPoints>
<MaxRequestAge>300000</MaxRequestAge>
<MinPublishingInterval>50</MinPublishingInterval>
<MaxPublishingInterval>1800000</MaxPublishingInterval>
<PublishingResolution>50</PublishingResolution>
<MaxSubscriptionLifetime>1800000</MaxSubscriptionLifetime>
<MaxMessageQueueSize>20</MaxMessageQueueSize>
<AvailableSamplingRates>
<SamplingRateGroup>
<Start>5</Start>
<Increment>5</Increment>
<Count>20</Count>
</SamplingRateGroup>
<SamplingRateGroup>
<Start>100</Start>
<Increment>100</Increment>
<Count>4</Count>
</SamplingRateGroup>
</AvailableSamplingRates>
<MaxNotificationQueueSize>200</MaxNotificationQueueSize>
<MaxNotificationsPerPublish>500</MaxNotificationsPerPublish>
<MinSupportedSamplingInterval>2500</MinSupportedSamplingInterval>
<MaxRegistrationInterval>60000</MaxRegistrationInterval>
<MinSubscriptionLifetime>5000</MinSubscriptionLifetime>
<MaxPublishRequestCount>50</MaxPublishRequestCount>
<MaxSubscriptionCount>200</MaxSubscriptionCount>
<MaxEventQueueSize>5000</MaxEventQueueSize>
<ServerProfileArray>
<ua:String>http://opcfoundation.org/UA-Profile/Server/StandardUA2017</ua:String>
<ua:String>http://opcfoundation.org/UA-Profile/Server/DataAccess</ua:String>
</ServerProfileArray>
<ShutdownDelay>10</ShutdownDelay>
<ServerCapabilities>
<ua:String>DA</ua:String>
<ua:String>HD</ua:String>
</ServerCapabilities>
<SupportedPrivateKeyFormats>
<ua:String>PFX</ua:String>
<ua:String>PEM</ua:String>
</SupportedPrivateKeyFormats>
<MaxTrustListSize>65536</MaxTrustListSize>
<MultiCastDnsEnabled>true</MultiCastDnsEnabled>
<ReverseConnect>
<Clients>
<ReverseConnectClient>
<EndpointUrl>opc.tcp://client1:65300</EndpointUrl>
<Timeout>5000</Timeout>
<Enabled>true</Enabled>
</ReverseConnectClient>
<ReverseConnectClient>
<EndpointUrl>opc.tcp://client2:65301</EndpointUrl>
<Timeout>10000</Timeout>
<Enabled>false</Enabled>
</ReverseConnectClient>
</Clients>
<ConnectInterval>20000</ConnectInterval>
<ConnectTimeout>45000</ConnectTimeout>
<RejectTimeout>90000</RejectTimeout>
</ReverseConnect>
<OperationLimits>
<MaxNodesPerRead>1000</MaxNodesPerRead>
<MaxNodesPerHistoryReadData>500</MaxNodesPerHistoryReadData>
<MaxNodesPerHistoryReadEvents>500</MaxNodesPerHistoryReadEvents>
<MaxNodesPerWrite>1000</MaxNodesPerWrite>
<MaxNodesPerHistoryUpdateData>200</MaxNodesPerHistoryUpdateData>
<MaxNodesPerHistoryUpdateEvents>200</MaxNodesPerHistoryUpdateEvents>
<MaxNodesPerMethodCall>100</MaxNodesPerMethodCall>
<MaxNodesPerBrowse>2000</MaxNodesPerBrowse>
<MaxNodesPerRegisterNodes>1500</MaxNodesPerRegisterNodes>
<MaxNodesPerTranslateBrowsePathsToNodeIds>1500</MaxNodesPerTranslateBrowsePathsToNodeIds>
<MaxNodesPerNodeManagement>500</MaxNodesPerNodeManagement>
<MaxMonitoredItemsPerCall>3000</MaxMonitoredItemsPerCall>
</OperationLimits>
<AuditingEnabled>true</AuditingEnabled>
<HttpsMutualTls>true</HttpsMutualTls>
<DurableSubscriptionsEnabled>true</DurableSubscriptionsEnabled>
<MaxDurableNotificationQueueSize>100000</MaxDurableNotificationQueueSize>
<MaxDurableEventQueueSize>50000</MaxDurableEventQueueSize>
<MaxDurableSubscriptionLifetimeInHours>24</MaxDurableSubscriptionLifetimeInHours>
</ServerConfiguration>
</ApplicationConfiguration>