Repository navigation
Improve runtime of Client Tests via fixture-level parallelization and fix parallel test isolation - #3655
Conversation
…, add Parallelizable attributes Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/7c095546-89fb-4661-b7df-873e74fb5b72 Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
|
|
Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/7c095546-89fb-4661-b7df-873e74fb5b72 Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3655 +/- ##
==========================================
+ Coverage 69.96% 70.00% +0.03%
==========================================
Files 477 477
Lines 103300 103300
Branches 17400 17400
==========================================
+ Hits 72270 72310 +40
+ Misses 25870 25828 -42
- Partials 5160 5162 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@copilot Test runtime is better, but now there are failing Tests due to insuficficient Test isolation. Improve Test isolation while keeping the runtime at the current Level |
Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/8f93f916-5ad8-4d07-925b-5394f1526eea Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
…n comment Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/8f93f916-5ad8-4d07-925b-5394f1526eea Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Fixed in commit
No test code (fixture parallelism, test methods) was changed — only the underlying port allocation infrastructure. |
|
@copilot Please fix the failing Client Tests, there seems to be another isolation issue. |
…nc retry Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/2db03a79-d996-4dd1-a570-8105197f6713 Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Fixed in commit
Note: |
|
@copilot Please fix the failing Client Tests, there seems to be another isolation issue. |
…ngleton with per-port ConcurrentDictionary Root cause: HttpsTransportListener.Startup.Listener was a static singleton that was overwritten each time a new HTTPS server started. With parallel fixture execution (ClientTest, ClientBatchTest, NodeCacheAsyncTest etc. all running their https/opc.https instances simultaneously), the last server to start captured the global Listener. Requests arriving at any earlier server's Kestrel instance were then dispatched to the wrong listener, causing BadServerHalted / NullReferenceException cascade failures. Fix: Replace Startup.Listener (static singleton) with Startup.Listeners (ConcurrentDictionary<int, HttpsTransportListener> keyed by port). Start() registers the listener by port; Dispose() removes it. The Kestrel middleware now routes each request to the correct listener using context.Connection.LocalPort. Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/a2a66689-1c27-4834-ab0b-32a2a0974fd6 Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
…eview Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/a2a66689-1c27-4834-ab0b-32a2a0974fd6 Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Fixed in commit Root cause: Fix: replaced |
|
@copilot please remove the static Field from HttpsTransportListener Startup completely instead use the integrated DI container of IWebHostBuilder to retrieve the transportlistener to handle a request |
…ebHostBuilder DI Register the listener instance in the web host DI container via IWebHostBuilder.ConfigureServices(services => services.AddSingleton(this)). Startup.Configure receives it as a method-injected parameter, eliminating all static state from the Startup class. Constructor injection was considered but doesn't work on .NET 8+ where HostBuilder activates Startup from the host-level service provider, which does not include services registered via IWebHostBuilder.ConfigureServices. Method injection in Configure() always resolves from the web-host app container, working correctly on all target frameworks. Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/108c8840-5555-4c12-83fd-91ed0990d531 Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Done in commit The listener is now registered in the web host's DI container via public void Configure(IApplicationBuilder appBuilder, HttpsTransportListener listener)
{
appBuilder.Run(context => listener.SendAsync(context));
}Method injection (rather than constructor injection) is used because on .NET 8+ |
Client Tests in CI took ~25 minutes. The bottleneck was sequential execution of multi-transport-scheme fixtures and long combinatorial test methods (TransferSubscription ~5 min, ReconnectWithSavedSessionSecrets ~3 min). After enabling fixture-level parallelism, bugs in the test infrastructure caused test failures due to insufficient test isolation; these have been fixed.
Proposed changes
Fixture-level parallelization for multi-scheme tests
Added
[Parallelizable(ParallelScope.Fixtures)]toClientTest,ClientBatchTest,NodeCacheAsyncTest, andLoadTest. All four use[TestFixtureSource]with opc.tcp / https / opc.https — the three scheme instances now run concurrently.Extract
TransferSubscriptionTest— new parallelizable fixtureMoved
TransferSubscriptionOnlyAsync(and debug variant) fromSubscriptionTestinto a dedicatedTransferSubscriptionTestclass:[TestFixtureSource]with one fixture instance perTransferType(5 values) +[Parallelizable(ParallelScope.Fixtures)]sendInitialValues×sequentialPublishing) with[Parallelizable]on the test methodSingleSession = true— one shared background session for namespace URI lookups; tests create their own origin/target sessions independentlyExtract
ReconnectWithSavedSessionSecretsTest— new parallelizable fixtureMoved
ReconnectWithSavedSessionSecretsOnlyAsync(and ECC variant) fromSubscriptionTestintoReconnectWithSavedSessionSecretsTest:[TestFixtureSource]with one fixture per security policy (None, ECC_nistP256, Basic256Sha256) +[Parallelizable(ParallelScope.Fixtures)]anonymous×sequentialPublishing×sendInitialValues) per fixture, marked[Parallelizable]ClientFixture.SessionFactoryset once inOneTimeSetUpAsync(thread-safe; was previously set per-test)ClientTestFramework— shared helpers promotedCreateSubscriptionsAsyncandCreateMonitoredItemTestSetmoved fromSubscriptionTesttoClientTestFrameworkasprotectedmethods.CreateSubscriptionsAsyncnow usessession.NamespaceUrisfrom the explicitly passed session parameter rather than the fixture's sharedthis.Session, which is required for correct parallel execution.Fix test isolation:
retryStartServerbug, broadenedConnectAsyncretry, and HTTPS static singleton raceretryStartServerflag never reset on success (ServerFixture.StartAsync,ClientFixture.StartReverseConnectHostAsync,Gds.Tests/Common.StartGDSAsync):retryStartServerwas set totruein thecatchblock but never reset tofalseat the top of the do-while loop. When a retry succeeded, the flag stayedtrue, causing the loop to continue and attempt to start another server on the already-bound port—spawning orphaned servers and eventually exhausting all retries. Fixed by addingretryStartServer = falseat the start of each loop iteration, with a random port selection on retry.Port registry removed from
GetNextFreeIPPort()(ServerFixtureUtils.cs): A globals_allocatedPortsregistry introduced to prevent TOCTOU races proved harmful — tests that create a new server per[SetUp](e.g.ClientLockoutIntegrationTests) caused the registry to grow unbounded, eventually exhausting all 100 attempts and throwing. The registry has been removed; random-port retry combined with theretryStartServerfix is sufficient to handle concurrent port allocation safely.HttpRequestExceptionnot retried inConnectAsync(ClientFixture.cs): For HTTPS/opc.https fixtures starting in parallel, a server-not-ready condition throwsHttpRequestException(not aServiceResultException), which the original retry logic did not catch. Fixed by adding a broadcatch (Exception e) when (e is not IgnoreException && attempt < maxAttempts)retry clause.maxAttemptsis also raised from 5 to 25 to give parallel HTTPS servers sufficient startup time under load.Startupstatic state removed fromHttpsTransportListener(Stack/Opc.Ua.Bindings.Https):Startuppreviously held a static field that every HTTPS server overwrote on startup, causing cross-fixture request misrouting when multiple server instances ran in parallel. All static state has been removed. TheHttpsTransportListenerinstance is now registered in each web host's DI container viaIWebHostBuilder.ConfigureServices(services => services.AddSingleton(this))and injected intoStartup.Configureas a method parameter. Method injection (rather than constructor injection) is used because on .NET 8+HostBuilderactivatesStartupfrom the host-level service provider, which does not include services registered viaIWebHostBuilder.ConfigureServices; theConfiguremethod always resolves its parameters from the web-host app container, ensuring correct behaviour on all target frameworks.Types of changes
What types of changes does your code introduce?
Put an
xin the boxes that apply. You can also fill these out after creating the PR.Checklist
Put an
xin the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.Further comments
The parallelization is safe because:
GetNextFreeIPPort+ random-port retry on failure)ClientFixture.ConnectAsyncwhich is stateless per callSingleSession = truesuppresses per-testSetUpAsync/TearDownAsyncsession churn, eliminating shared-state races between parallel test cases within one fixtureretryStartServerfix ensures each server start attempt is independent; orphaned servers (previously started during spurious loop iterations) no longer accumulateConnectAsyncnow retries on all transient errors (includingHttpRequestExceptionfrom HTTPS servers not yet ready) with up to 25 attempts, ensuring robust parallel fixture startupStartupinHttpsTransportListenernow carries no static state; each Kestrel web host resolves its own listener instance from the DI container, ensuring complete isolation between concurrent HTTPS server instances