mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-23 02:00:00 +08:00
8634a18884
# Description Server shutdown had grown into roughly 500 lines of coordination machinery in `StandardServer.OnServerStoppingAsync` and `ServerBase.StopAsync`: a `ServerShutdownState` state machine, request-admission-close gates, lifecycle-waiter drain coordination, multi-stage base-resource disposal, and a large set of `ForTest` hooks. Most of that existed to guarantee no request was still dispatching into the address space when it was torn down. That guarantee is now provided directly by disposing the request queue, so the surrounding machinery is no longer needed. ## Approach - **Run once, re-armed on restart.** `ServerBase.StopAsync` now uses a shared-task guard (`m_stopTask ??= StopCoreAsync(...)` under a `System.Threading.Lock`), so concurrent and repeat callers await the same completion. `InitializeRequestQueue` (which already runs on every start) resets the guard, re-arming shutdown for a restarted server. - **Drain via the request queue.** Teardown disposes the request queue first through the new `StopRequestQueue()`, which cancels in-flight processing and waits for the workers before any NodeManager is stopped. `RequestQueue.Dispose` is now idempotent because the queue can be disposed both by shutdown and later by base disposal / re-init. - **Linear teardown.** `OnServerStoppingAsync` is now short and linear: shutdown delay, stop the registration timer, unregister from discovery, then an ordered teardown (lifecycle begin, subscription manager, session manager, node managers, lifecycle complete, dispose) that collects per-stage failures into an `AggregateException`. `Dispose`/`DisposeAsync` reduce to the same shared-task pattern. - **Trim the lifecycle extension.** Removed the shutdown-only request-admission members from `RequestManager` and `RequestManagerLifecycleExtension` (admission close, admission validation, repeat-until-idle drain). The lifecycle-waiter machinery that live NodeManager replacement (`NodeManagerLifecycle`) still depends on is kept intact. Net effect is about 1500 lines removed against roughly 190 added. ## Notes for reviewers - The drain is cancellation-based rather than a graceful wait: disposing the queue cancels in-flight request processing and drains queued requests to `BadServerHalted`. This is the intended behavior for stop/dispose. - `RequestManagerLifecycleExtension` is deliberately only partially trimmed. Its waiter/exclusion logic is shared with `NodeManagerLifecycle`, so only the members that were exclusive to the old shutdown path were removed. - The removed `ForTest` hooks drove three now-deleted test files' worth of coordination assertions; those tests were rewritten or removed. A minimal `BaseResourcesDisposedForTest` hook is kept for the rewritten dispose tests. ## Related Issues N/A ## 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 or that my feature works and increased code coverage. - [ ] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
239 lines
8.0 KiB
C#
239 lines
8.0 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Configuration;
|
|
using Opc.Ua.Server.TestFramework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Server.Tests
|
|
{
|
|
/// <summary>
|
|
/// Tests the deterministic synchronous and asynchronous disposal contracts of
|
|
/// <see cref="StandardServer"/>.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("StandardServer")]
|
|
[NonParallelizable]
|
|
public class StandardServerDisposeAsyncTests
|
|
{
|
|
private sealed class TestableStandardServer : StandardServer
|
|
{
|
|
private int m_serverStoppingCount;
|
|
|
|
public TestableStandardServer(ITelemetryContext telemetry)
|
|
: base(telemetry)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// The number of times the orderly stop sequence entered
|
|
/// <see cref="OnServerStoppingAsync"/>.
|
|
/// </summary>
|
|
public int ServerStoppingCount => Volatile.Read(ref m_serverStoppingCount);
|
|
|
|
protected override ValueTask OnServerStoppingAsync(
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Interlocked.Increment(ref m_serverStoppingCount);
|
|
return base.OnServerStoppingAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task DisposeAsyncDisposesBaseResources()
|
|
{
|
|
ServerFixture<TestableStandardServer> fixture = CreateFixture();
|
|
TestableStandardServer server = null;
|
|
|
|
try
|
|
{
|
|
server = await fixture.StartAsync().ConfigureAwait(false);
|
|
|
|
await server.DisposeAsync().ConfigureAwait(false);
|
|
|
|
AssertReleased(server);
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(fixture, server).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task DisposeAsyncIsIdempotentAndDisposeAfterDisposeAsyncIsSafe()
|
|
{
|
|
TestableStandardServer server = CreateServer();
|
|
|
|
await server.DisposeAsync().ConfigureAwait(false);
|
|
await server.DisposeAsync().ConfigureAwait(false);
|
|
|
|
Assert.DoesNotThrow(server.Dispose);
|
|
AssertReleased(server);
|
|
}
|
|
|
|
[Test]
|
|
public async Task ConcurrentDisposeAndDisposeAsyncBothCompleteAndReleaseOnce()
|
|
{
|
|
ServerFixture<TestableStandardServer> fixture = CreateFixture();
|
|
TestableStandardServer server = null;
|
|
|
|
try
|
|
{
|
|
server = await fixture.StartAsync().ConfigureAwait(false);
|
|
|
|
Task disposeTask = Task.Run(server.Dispose);
|
|
Task disposeAsyncTask = server.DisposeAsync().AsTask();
|
|
|
|
await Task.WhenAll(disposeTask, disposeAsyncTask).ConfigureAwait(false);
|
|
|
|
AssertReleased(server);
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(fixture, server).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task DisposeAsyncThenDisposeIsSafeAndReleasesOnce()
|
|
{
|
|
ServerFixture<TestableStandardServer> fixture = CreateFixture();
|
|
TestableStandardServer server = null;
|
|
|
|
try
|
|
{
|
|
server = await fixture.StartAsync().ConfigureAwait(false);
|
|
|
|
await server.DisposeAsync().ConfigureAwait(false);
|
|
Assert.DoesNotThrow(server.Dispose);
|
|
|
|
AssertReleased(server);
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(fixture, server).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task DisposeAsyncOnNeverStartedServerReleasesCleanly()
|
|
{
|
|
TestableStandardServer server = CreateServer();
|
|
|
|
await server.DisposeAsync().ConfigureAwait(false);
|
|
|
|
AssertReleased(server);
|
|
}
|
|
|
|
[Test]
|
|
public async Task DisposeBlocksUntilShutdownCompletionAndReleasesOnce()
|
|
{
|
|
ServerFixture<TestableStandardServer> fixture = CreateFixture();
|
|
TestableStandardServer server = null;
|
|
|
|
try
|
|
{
|
|
server = await fixture.StartAsync().ConfigureAwait(false);
|
|
|
|
await Task.Run(server.Dispose).ConfigureAwait(false);
|
|
|
|
AssertReleased(server);
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(fixture, server).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ConcurrentAndRepeatStopAsyncRunTeardownOnce()
|
|
{
|
|
ServerFixture<TestableStandardServer> fixture = CreateFixture();
|
|
TestableStandardServer server = null;
|
|
|
|
try
|
|
{
|
|
server = await fixture.StartAsync().ConfigureAwait(false);
|
|
|
|
// Concurrent callers must join the single shared stop task.
|
|
Task stopA = server.StopAsync().AsTask();
|
|
Task stopB = server.StopAsync().AsTask();
|
|
await Task.WhenAll(stopA, stopB).ConfigureAwait(false);
|
|
|
|
// A later caller joins the same completed stop task and does not re-run teardown.
|
|
await server.StopAsync().ConfigureAwait(false);
|
|
|
|
Assert.That(server.ServerStoppingCount, Is.EqualTo(1));
|
|
}
|
|
finally
|
|
{
|
|
await CleanupAsync(fixture, server).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private static ServerFixture<TestableStandardServer> CreateFixture()
|
|
{
|
|
return new ServerFixture<TestableStandardServer>(
|
|
telemetry => new TestableStandardServer(telemetry))
|
|
{
|
|
SecurityNone = true
|
|
};
|
|
}
|
|
|
|
private static TestableStandardServer CreateServer()
|
|
{
|
|
return new TestableStandardServer(NUnitTelemetryContext.Create());
|
|
}
|
|
|
|
private static void AssertReleased(TestableStandardServer server)
|
|
{
|
|
Assert.That(server.BaseResourcesDisposedForTest, Is.True);
|
|
}
|
|
|
|
private static async Task CleanupAsync(
|
|
ServerFixture<TestableStandardServer> fixture,
|
|
TestableStandardServer server)
|
|
{
|
|
if (server is not null && !server.BaseResourcesDisposedForTest)
|
|
{
|
|
await server.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
IApplicationInstance application = fixture.Application;
|
|
if (application is not null)
|
|
{
|
|
await application.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
}
|