mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 09:06:37 +08:00
ad6430f560
Fixes #4339. ## Cause `PublishControllerAsync` sized the worker pool at the **top** of its loop, before its first `await`. The `SubscriptionManager` constructor calls the controller directly, so that first resize ran *synchronously inside the constructor* — with `MinPublishWorkerCount`/`MaxPublishWorkerCount` still at their defaults (2 / 15), because owners configure them afterwards. A session that already reports subscriptions — a classic, session-owned subscription counts through `ISubscriptionManagerContext.SessionSubscriptionCount` — therefore had `1` clamped **up** to the default minimum of 2, spun up two workers in the constructor, and published that overshoot through `PublishWorkerCount`. The configured `Min = Max = 1` then shrank the pool back asynchronously. Confirmed with a probe before changing anything: ``` PROBE right after ctor: 2 ``` which reproduces the issue's `Worker #0 STARTED / #0 PAUSED / #1 STARTED` log exactly. `ClassicSessionSubscriptionStartsPublishWorkerAsync` pins the pool to one worker and samples the property directly, so it failed whenever it read it before the controller had shrunk the pool back. ## Fix The control loop now waits first and sizes the pool afterwards: ``` wait -> reap exited workers -> resize -> publish the count ``` The pool is only ever sized in response to a signal or a worker exit, never from unconfigured defaults, and the count is assigned once the pool has settled — so a worker that has exited is never counted alongside its replacement (the second half of the issue's *Expected Behavior*). Two supporting changes were required: - **Publish workers now start on the thread pool.** The controller creates them inline, and the worker loop only yields where it awaits something not already complete. With the resize moved behind the wait, workers are typically created while publishing is already running, which made that starvation reachable: I hit it while testing — 430k publishes in 2 s with `PublishControlCycles` stuck at 1 and `PublishWorkerCount` stuck at 0. The same starvation would hang `DisposeAsync`, which awaits the controller. - **`Resume()`, and leaving publishing quiescence, now signal the publish control**, so the pool is sized promptly once publishing starts rather than waiting for an unrelated event. ## Tests - `ClassicSessionSubscriptionStartsPublishWorkerAsync` now settles the pool through the existing `WaitForPublishWorkerCountAsync` helper instead of sampling the property bare. - New `PublishWorkerPoolIsNeverSizedFromDefaultsBeforeConfigurationAsync` pins the constructor behaviour (count stays 0 while the defaults are in force) and asserts the count never leaves 1 while workers publish. ## Verification - `Opc.Ua.Client.Tests`: 2151/2151 pass. - `Opc.Ua.Subscriptions.Tests`: 619 passed, 6 skipped, 0 failed (22 m 18 s). - The two affected tests stressed 15x in a row: green. - `dotnet format whitespace --verify-no-changes` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>