Files
romanett 359abe68be [master378] Complete each incoming request exactly once on server stop (#4729) (#4738)
## Proposed changes

Fixes #4729 on master378 (1.5.378.x), the branch the issue was reported
against. The master counterpart is #4736.

If a server is stopped while requests are queued, a request could be
completed twice. The second completion landed on a
`ServiceResponsePooledValueTaskSource` that had already gone back to the
process-wide pool and been rented by another request. That request,
possibly on another server in the same process, then received a foreign
response, which showed up as `InvalidCastException` or
`BadSecurityChecksFailed`.

### Changes
- **Leased completion (root defense, same as #4736):** each rental of
the pooled source gets a lease number. `EndpointIncomingRequest`
captures it at construction and completes through `TrySetResult(lease,
response)`, a single CAS on the pending lease. Only the first completion
of the current rental is accepted, and a late completion can never reach
the next owner.
- **`ServerBase.RequestQueue`:**
- `Dispose` drains the queue with `TryDequeue` instead of completing a
`ToList()` snapshot. Each request is now removed by exactly one party,
either a worker or `Dispose`. This replaces the `m_queue.Clear()` that
only compiled under `NETSTANDARD2_1_OR_GREATER`.
- The stop flag is now an `int`. `Dispose` sets it with
`Interlocked.Exchange`, which is a full fence, and is now idempotent.
- `ScheduleIncomingRequest` re-checks the stop flag after enqueuing and
drains the queue instead of letting `ObjectDisposedException` from
`Release()` escape. Previously that exception made `ProcessAsync`
complete the already-queued request a second time.
- A worker that dequeues a request after stop completes it with
`BadServerHalted` instead of running it. Workers also treat a disposed
semaphore as shutdown.
- Completions from the queue go through a helper that catches and logs
exceptions.

## Tests
- `RequestQueueShutdownTests.DisposeCompletesQueuedRequestsOnceAsync`:
fails on the current master378 (a request is completed twice) and passes
with the fix.
- `RequestQueueShutdownTests.ScheduleAfterDisposeCompletesOnce`.
-
`RequestQueueShutdownTests.ConcurrentScheduleAndDisposeCompletesEachRequestOnceAsync`:
a stress guard. It did not reproduce the old bug locally.
- `ServiceResponsePooledValueTaskSourceTests`: a duplicate completion is
ignored, and a stale completion after the source is recycled does not
complete the next rental.
- Local net10.0 runs: `Opc.Ua.Core.Tests` Stack.* 1496 passed, 0 failed;
`Opc.Ua.Server.Tests` 513 passed, 0 failed.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:36:57 +03:00
..