Files
romanett 6ad0d95b26 Fix reverse connect hold time when several Servers share one listener (#4341)
# Description

Fixes the reverse connect problem reported in #3985: a Client which uses
one reverse connect listener port for several Servers connects to the
first Server, and every following `WaitForConnectionAsync` times out. A
separate listener port per Server was the only workaround.

## Root cause

Confirming the analysis in [mrsuciu's comment on
#3985](https://github.com/OPCFoundation/UA-.NETStandard/issues/3985):

- Servers which send `ReverseHello` before the application registered a
waiting connection for them are held in `OnConnectionWaitingAsync` for
`HoldTime`.
- All held connections share a single `CancellationTokenSource`, so
`RegisterWaitingConnection` wakes up **every** held connection, not only
the one it was registered for.
- The hold loop was `while (!matched) { ... break; }` with an
unconditional `break`, so each woken connection got exactly one re-match
attempt. The connections which did not match the new registration fell
out of the loop unaccepted, although most of their own hold time was
left.
- An unaccepted connection is force faulted by the listener
(`TcpReverseConnectChannel`, "The reverse connection was rejected by the
client"), so that Server is dropped and only returns on its next
`ReverseHello` interval, long after the Client timed out waiting for it.

Separate ports avoid the shared wakeup path, which is exactly why that
workaround succeeds.

## Changes

- `ReverseConnectManager.OnConnectionWaitingAsync` re-arms the hold
after a wakeup: it re-matches, and if the registration which caused the
wakeup was for another Server the connection keeps waiting for the
remainder of its own hold time. The loop is still bounded by `HoldTime`,
so a Server which is never registered for is rejected exactly as before.
- A shutdown flag set by `StopService`/`Dispose` (cleared by
`StartService`) releases held connections immediately during teardown,
instead of parking them until the hold time expires. `Dispose` now also
cancels the token source before disposing it, so a held callback can no
longer read a disposed token.
- `Docs/ReverseConnect.md`: new "Sharing a listener across multiple
Servers" section documenting that the listener port staying in
`LISTENING` is expected, that one shared `ReverseConnectManager` should
serve all Servers on the same Client Url, and the
`HoldTime`/`WaitTimeout` semantics.

No public API, signature or serialization change.

## Validation

- New `ReverseConnectManagerUnitTests` (3 tests): the two Server
regression, hold time expiry still rejecting, and dispose releasing a
held connection. They drive the callback through an internal test hook,
so no listener or socket is involved. Verified that the regression test
fails against the previous behaviour with "The reverse connection of the
second server was released before its hold time expired."
- New tests pass on net10.0 and net48.
- Full reverse connect suite (`FullyQualifiedName~ReverseConnect`,
including the existing `ReverseConnectTest` server fixture): 33/33 pass
on net10.0.

## Related Issues

- Relates to #3985.
- `master` carries the identical defect: the same `break` survived #4009
and #4059 and is still present in
`src/Opc.Ua.Client/ReverseConnectManager.cs`. The equivalent fix for
`master` is #4342 (it uses `TimeProvider` and breaks on a pending
callback drain instead of a new flag), so the issue is intentionally not
auto closed here.

## Checklist

- [x] 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 or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 17:04:15 +02:00
..