mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-22 01:26:20 +08:00
9b034f8291
# Description Nullable reference types are now enabled once, in `common.props`, and every project in the repository builds warning-clean with them. **Configuration** - `<Nullable>enable</Nullable>` lives in `common.props`. All per-project `<Nullable>` settings are removed: 178 project files (enable, annotations, and the `disable` in the three Aot test projects). - 419 per-file `#nullable enable`/`restore` directives and the 4 `#nullable disable` directives are removed. - Kept on purpose: - The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`, `PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the project setting in generated code, so the directive is required there. - `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against the 1.5.378 packages and does not import the repository props. - `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now describe the central setting. **Migration** (the 43 unset and 13 annotations-only projects, roughly 10k diagnostics) - **Production code: annotated at the declarations.** Covers `tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the LDS sample. - Members, parameters and returns are nullable where null actually flows. - Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`, `IsMethodTypeDesign`). - `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take `params object?[]`. `Template.AddReplacement` takes `string?` (rendering already skips null replacements). - `IRegisteredServerStore.RegisterAsync` returns `Task<RegistrationEntry?>`: it returns null when a registration is removed. - Dereferences backed by an invariant (for example, the validated model) use `!`, so runtime behaviour is unchanged. - **Behavioural changes in production code** (small, deliberate): - `NodeSetToModelDesign` throws `InvalidOperationException` when the file is not a NodeSet. Before, it threw a NullReferenceException on the next line. - `ModelDesignValidator` starts its lookup tables empty instead of null. A redundant `if (m_nodes == null) return;` guard is gone. - **Test code: minimal annotations.** - `?` on values that can be null, `!` where the test asserts non-null. - `= null!` for fixture members assigned in SetUp. - `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the `IEncodeable` constraint. - A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`) folded into the adjacent `IsNull`/`Count` checks. **Notes for reviewers** - The diff is large (~1,500 files) but almost entirely mechanical: `?`, `!` and directive removal. The non-annotation changes are listed above. - Known follow-up, not done here: `IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay non-nullable even though the validator returns null before it loads a dictionary. Making them nullable would ripple through every generator call site. ## Related Issues - Fixes #4439 ## 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. - [ ] 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. The "no new warnings" box covers net10.0 and net48: full non-incremental builds of `UA.slnx`, `tools/SourceGeneration.slnx`, `tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0 warnings and 0 errors under the repository's warnings-as-errors settings. net9.0 and net8.0 also had no nullable diagnostics, but they were not rebuilt after the final fixes. Tests have not been run locally yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
930 lines
43 KiB
C#
930 lines
43 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 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;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Net;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Core.Tests.Stack.Server
|
|
{
|
|
[TestFixture]
|
|
[Category("Server")]
|
|
[Parallelizable(ParallelScope.All)]
|
|
public sealed class CustomRequestParkingTests
|
|
{
|
|
[Test]
|
|
public async Task CustomHandlerReleasesOnlyExecutionAndLetsPeerUseTheSingleWorkerAsync()
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
await using var server = new QueueServer(provider)
|
|
{
|
|
RequestParkingPolicy = new DelegateRequestParkingPolicy(static request => request is CallRequest)
|
|
};
|
|
var held = new Handler();
|
|
var peer = new Handler();
|
|
var endpoint = new TestEndpoint(server, held, peer);
|
|
Task<IServiceResponse> response = endpoint.SendAsync(new CallRequest(), "held");
|
|
try
|
|
{
|
|
IRequestParkSink sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(sink, Is.Not.Null, "An opted-in handler needs the queue's real parking sink.");
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
sink.NotifyParked();
|
|
sink.NotifyParked();
|
|
await provider.WaitForReleaseAsync(ResourceIsolationStage.RequestExecution, 1, deadline.Token)
|
|
.ConfigureAwait(false);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.Zero);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.Zero);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
|
|
Task<IServiceResponse> peerResponse = endpoint.SendAsync(new ReadRequest(), "peer");
|
|
await peer.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(response.IsCompleted, Is.False);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
peer.Release();
|
|
held.Release();
|
|
Assert.That(await response.WaitAsync(deadline.Token).ConfigureAwait(false), Is.TypeOf<CallResponse>());
|
|
Assert.That(await peerResponse.WaitAsync(deadline.Token).ConfigureAwait(false),
|
|
Is.TypeOf<ReadResponse>());
|
|
sink.NotifyParked();
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
peer.Release();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task SelectedRequestReservesParkedCapacityBeforeDispatchEvenWithoutSignalingAsync()
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
await using var server = new QueueServer(provider, policy: CustomPolicy());
|
|
var held = new Handler();
|
|
var peer = new Handler();
|
|
var endpoint = new TestEndpoint(server, held, peer);
|
|
Task<IServiceResponse> blocker = endpoint.SendAsync(new ReadRequest(), "peer");
|
|
try
|
|
{
|
|
await peer.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Task<IServiceResponse> selected = endpoint.SendAsync(new CallRequest());
|
|
Assert.That(held.Started.Task.IsCompleted, Is.False);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(1));
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
IServiceResponse rejected = await endpoint.SendAsync(new CallRequest())
|
|
.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(rejected.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadServerTooBusy));
|
|
peer.Release();
|
|
await blocker.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false), Is.Not.Null);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
rejected = await endpoint.SendAsync(new CallRequest()).WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(rejected.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadServerTooBusy));
|
|
held.Release();
|
|
Assert.That(await selected.WaitAsync(deadline.Token).ConfigureAwait(false), Is.TypeOf<CallResponse>());
|
|
await provider.WaitForReleaseAsync(ResourceIsolationStage.ParkedRequest, 1, deadline.Token)
|
|
.ConfigureAwait(false);
|
|
Assert.That(await endpoint.SendAsync(new CallRequest()).WaitAsync(deadline.Token).ConfigureAwait(false),
|
|
Is.TypeOf<CallResponse>(), "Completion must return the reserved slot.");
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
peer.Release();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ParkingDoesNotReturnRetainedCostToAdmitAnotherRequestAsync()
|
|
{
|
|
var provider = new TrackingProvider(costCapacity: 10);
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
await using var server = new QueueServer(provider, policy: CustomPolicy());
|
|
var held = new Handler();
|
|
var peer = new Handler();
|
|
var endpoint = new TestEndpoint(server, held, peer);
|
|
Task<IServiceResponse> response = endpoint.SendAsync(new CallRequest());
|
|
try
|
|
{
|
|
IRequestParkSink sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
sink.NotifyParked();
|
|
await provider.WaitForReleaseAsync(ResourceIsolationStage.RequestExecution, 1, deadline.Token)
|
|
.ConfigureAwait(false);
|
|
IServiceResponse rejected = await endpoint.SendAsync(new ReadRequest(), "peer")
|
|
.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(rejected.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadServerTooBusy));
|
|
Assert.That(peer.Started.Task.IsCompleted, Is.False);
|
|
Assert.That(response.IsCompleted, Is.False);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
held.Release();
|
|
await response.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
}
|
|
}
|
|
|
|
[TestCase(true, false)]
|
|
[TestCase(false, false)]
|
|
[TestCase(false, true)]
|
|
public async Task CancellationBeforeAndAfterParkingReturnsAllLeasesOnceAsync(bool queued, bool park)
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
using var cancellation = new CancellationTokenSource();
|
|
await using var server = new QueueServer(provider, policy: CustomPolicy());
|
|
var held = new Handler();
|
|
var peer = new Handler();
|
|
var endpoint = new TestEndpoint(server, held, peer);
|
|
Task<IServiceResponse>? blocker = null;
|
|
try
|
|
{
|
|
if (queued)
|
|
{
|
|
blocker = endpoint.SendAsync(new ReadRequest(), "peer");
|
|
await peer.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
}
|
|
Task<IServiceResponse> response = endpoint.SendAsync(
|
|
new CallRequest(), cancellationToken: cancellation.Token);
|
|
IRequestParkSink? sink = null;
|
|
if (!queued)
|
|
{
|
|
sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
if (park)
|
|
{
|
|
sink.NotifyParked();
|
|
await provider.WaitForReleaseAsync(ResourceIsolationStage.RequestExecution, 1, deadline.Token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
await cancellation.CancelAsync().ConfigureAwait(false);
|
|
IServiceResponse fault = await response.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(fault, Is.TypeOf<ServiceFault>());
|
|
Assert.That(fault.ResponseHeader.ServiceResult,
|
|
Is.EqualTo(queued ? StatusCodes.BadRequestCancelledByClient : StatusCodes.BadTimeout));
|
|
Assert.That(held.Started.Task.IsCompleted, Is.EqualTo(!queued));
|
|
sink?.NotifyParked();
|
|
peer.Release();
|
|
if (blocker != null)
|
|
{
|
|
await blocker.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
}
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
peer.Release();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task PreCancelledRequestNeverInvokesHandlerOrAcquiresCapacityAsync()
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var cancellation = new CancellationTokenSource();
|
|
await cancellation.CancelAsync().ConfigureAwait(false);
|
|
await using var server = new QueueServer(provider, policy: CustomPolicy());
|
|
var held = new Handler();
|
|
var endpoint = new TestEndpoint(server, held, new Handler());
|
|
IServiceResponse response = await endpoint.SendAsync(
|
|
new CallRequest(), cancellationToken: cancellation.Token).ConfigureAwait(false);
|
|
Assert.That(response.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadRequestCancelledByClient));
|
|
Assert.That(held.Started.Task.IsCompleted, Is.False);
|
|
await server.StopAsync().ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
Assert.That(provider.TotalGrants, Is.Zero);
|
|
}
|
|
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public async Task CancellationAndStopRetainParkedCapacityUntilActualCompletionAsync(bool stop)
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
using var cancellation = new CancellationTokenSource();
|
|
await using var server = new QueueServer(provider, policy: CustomPolicy());
|
|
var held = new Handler { ObserveCancellation = false };
|
|
var endpoint = new TestEndpoint(server, held, new Handler());
|
|
Task<IServiceResponse> response = endpoint.SendAsync(
|
|
new CallRequest(), cancellationToken: cancellation.Token);
|
|
try
|
|
{
|
|
IRequestParkSink sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
sink.NotifyParked();
|
|
await provider.WaitForReleaseAsync(ResourceIsolationStage.RequestExecution, 1, deadline.Token)
|
|
.ConfigureAwait(false);
|
|
Task? stopping = null;
|
|
if (stop)
|
|
{
|
|
stopping = server.StopAsync(deadline.Token).AsTask();
|
|
}
|
|
else
|
|
{
|
|
await cancellation.CancelAsync().ConfigureAwait(false);
|
|
}
|
|
await held.Cancelled.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
sink.NotifyParked();
|
|
Assert.That(response.IsCompleted, Is.False);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.Zero);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
if (stopping != null)
|
|
{
|
|
Assert.That(stopping.IsCompleted, Is.False);
|
|
}
|
|
held.Release();
|
|
Assert.That(await response.WaitAsync(deadline.Token).ConfigureAwait(false), Is.TypeOf<CallResponse>());
|
|
if (stopping != null)
|
|
{
|
|
await stopping.ConfigureAwait(false);
|
|
}
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
}
|
|
}
|
|
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public async Task HandlerExceptionBeforeOrAfterParkingFaultsOnceAndReturnsCapacityAsync(bool park)
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
await using var server = new QueueServer(provider, policy: CustomPolicy());
|
|
var held = new Handler { ThrowOnCompletion = true };
|
|
var endpoint = new TestEndpoint(server, held, new Handler());
|
|
Task<IServiceResponse> response = endpoint.SendAsync(new CallRequest());
|
|
try
|
|
{
|
|
IRequestParkSink sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
if (park)
|
|
{
|
|
sink.NotifyParked();
|
|
await provider.WaitForReleaseAsync(ResourceIsolationStage.RequestExecution, 1, deadline.Token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
held.Release();
|
|
IServiceResponse fault = await response.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(fault, Is.TypeOf<ServiceFault>());
|
|
Assert.That(fault.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
sink.NotifyParked();
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task StopCancelsRunningParkedAndQueuedEndpointRequestsAsync()
|
|
{
|
|
var provider = new TrackingProvider(parkedCapacity: 2);
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
await using var server = new QueueServer(provider, policy: CustomPolicy());
|
|
var held = new Handler();
|
|
var peer = new Handler();
|
|
var endpoint = new TestEndpoint(server, held, peer);
|
|
Task<IServiceResponse> parked = endpoint.SendAsync(new CallRequest());
|
|
try
|
|
{
|
|
IRequestParkSink sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
sink.NotifyParked();
|
|
Task<IServiceResponse> running = endpoint.SendAsync(new ReadRequest(), "peer");
|
|
await peer.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Task<IServiceResponse> queued = endpoint.SendAsync(new CallRequest());
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(1));
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That((await parked.WaitAsync(deadline.Token).ConfigureAwait(false))
|
|
.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadTimeout));
|
|
Assert.That((await running.WaitAsync(deadline.Token).ConfigureAwait(false))
|
|
.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadTimeout));
|
|
Assert.That((await queued.WaitAsync(deadline.Token).ConfigureAwait(false))
|
|
.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadServerHalted));
|
|
Assert.That((await endpoint.SendAsync(new CallRequest()).ConfigureAwait(false))
|
|
.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadServerHalted));
|
|
sink.NotifyParked();
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
peer.Release();
|
|
}
|
|
}
|
|
|
|
[TestCase(false, true)]
|
|
[TestCase(true, true)]
|
|
[TestCase(true, false)]
|
|
public async Task NonparticipantsAndGlobalOptOutKeepTheExecutionWorkerAsync(bool installPolicy, bool decouple)
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
IRequestParkingPolicy policy = (installPolicy
|
|
? new DelegateRequestParkingPolicy(_ => !decouple)
|
|
: null)!;
|
|
await using var server = new QueueServer(provider, decouple, policy!);
|
|
var held = new Handler();
|
|
var peer = new Handler();
|
|
var endpoint = new TestEndpoint(server, held, peer);
|
|
Task<IServiceResponse> response = endpoint.SendAsync(new CallRequest());
|
|
try
|
|
{
|
|
IRequestParkSink sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(sink is null, Is.EqualTo(decouple));
|
|
sink?.NotifyParked();
|
|
Task<IServiceResponse> peerResponse = endpoint.SendAsync(new ReadRequest(), "peer");
|
|
Assert.That(peer.Started.Task.IsCompleted, Is.False);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(1));
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.Zero);
|
|
held.Release();
|
|
await response.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
await peer.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
peer.Release();
|
|
await peerResponse.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
peer.Release();
|
|
}
|
|
}
|
|
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public async Task PublishRemainsIntrinsicAndBypassesCustomPolicyAsync(bool installPolicy)
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
IRequestParkingPolicy policy = (installPolicy
|
|
? new DelegateRequestParkingPolicy(static _ => throw new InvalidOperationException("Not for Publish."))
|
|
: null)!;
|
|
await using var server = new QueueServer(provider, policy: policy!);
|
|
var held = new Handler();
|
|
var endpoint = new TestEndpoint(server, held, new Handler());
|
|
Task<IServiceResponse> response = endpoint.SendAsync(new PublishRequest());
|
|
try
|
|
{
|
|
IRequestParkSink sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(sink, Is.Not.Null);
|
|
sink.NotifyParked();
|
|
await provider.WaitForReleaseAsync(ResourceIsolationStage.RequestExecution, 1, deadline.Token)
|
|
.ConfigureAwait(false);
|
|
Assert.That(response.IsCompleted, Is.False);
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
held.Release();
|
|
Assert.That(await response.WaitAsync(deadline.Token).ConfigureAwait(false),
|
|
Is.TypeOf<PublishResponse>());
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task PolicyFailureReturnsFaultBeforeAdmissionAndDoesNotStopTheWorkerAsync()
|
|
{
|
|
var provider = new TrackingProvider();
|
|
await using var server = new QueueServer(provider, policy:
|
|
new DelegateRequestParkingPolicy(static _ => throw new InvalidOperationException("Policy failure.")));
|
|
var held = new Handler();
|
|
var peer = new Handler();
|
|
peer.Release();
|
|
var endpoint = new TestEndpoint(server, held, peer);
|
|
IServiceResponse fault = await endpoint.SendAsync(new CallRequest()).ConfigureAwait(false);
|
|
Assert.That(fault, Is.TypeOf<ServiceFault>());
|
|
Assert.That(fault.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
Assert.That(held.Started.Task.IsCompleted, Is.False);
|
|
Assert.That(provider.TotalGrants, Is.Zero);
|
|
server.RequestParkingPolicy = null;
|
|
Assert.That(await endpoint.SendAsync(new ReadRequest()).ConfigureAwait(false), Is.TypeOf<ReadResponse>());
|
|
await server.StopAsync().ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public async Task ExistingServerInterfaceCanOptionallySupplyParkingPolicyAsync(bool supplyPolicy)
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
await using var queueServer = new QueueServer(provider);
|
|
var server = new Mock<IServerBase>();
|
|
server.SetupGet(value => value.MessageContext).Returns(
|
|
ServiceMessageContext.Create(NUnitTelemetryContext.Create()));
|
|
var policy = CustomPolicy();
|
|
if (supplyPolicy)
|
|
{
|
|
server.As<IRequestParkingPolicySource>().SetupGet(value => value.RequestParkingPolicy).Returns(policy);
|
|
}
|
|
server.Setup(value => value.ScheduleIncomingRequest(
|
|
It.IsAny<IEndpointIncomingRequest>(), It.IsAny<CancellationToken>()))
|
|
.Callback<IEndpointIncomingRequest, CancellationToken>(queueServer.ScheduleIncomingRequest);
|
|
var host = new Mock<IServiceHostBase>();
|
|
host.SetupGet(value => value.Server).Returns(server.Object);
|
|
var held = new Handler();
|
|
var endpoint = new TestEndpoint(host.Object, held, new Handler());
|
|
Task<IServiceResponse> response = endpoint.SendAsync(new CallRequest());
|
|
try
|
|
{
|
|
IRequestParkSink sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(sink is not null, Is.EqualTo(supplyPolicy));
|
|
Assert.That(endpoint.RequestParkingPolicy, Is.SameAs(supplyPolicy ? policy : null));
|
|
sink?.NotifyParked();
|
|
held.Release();
|
|
Assert.That(await response.WaitAsync(deadline.Token).ConfigureAwait(false), Is.TypeOf<CallResponse>());
|
|
await queueServer.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void DelegatePolicyRejectsNullAndSelectsByRequestContents()
|
|
{
|
|
Assert.That(() => new DelegateRequestParkingPolicy(null!), Throws.ArgumentNullException);
|
|
var policy = new DelegateRequestParkingPolicy(
|
|
request => request.TypeId == DataTypeIds.CallRequest && request.RequestHeader.RequestHandle == 42);
|
|
Assert.That(() => policy.CanPark(null!), Throws.ArgumentNullException);
|
|
Assert.That(policy.CanPark(new ReadRequest { RequestHeader = new RequestHeader { RequestHandle = 42 } }),
|
|
Is.False);
|
|
Assert.That(policy.CanPark(new CallRequest { RequestHeader = new RequestHeader { RequestHandle = 1 } }),
|
|
Is.False);
|
|
Assert.That(policy.CanPark(new CallRequest { RequestHeader = new RequestHeader { RequestHandle = 42 } }),
|
|
Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public async Task PublicIncomingRequestUsesTheSameParkingAndCompletionLifecycleAsync()
|
|
{
|
|
var provider = new TrackingProvider();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
await using var server = new QueueServer(provider);
|
|
var direct = new DirectRequest();
|
|
server.ScheduleIncomingRequest(direct);
|
|
try
|
|
{
|
|
await direct.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
direct.ParkSink.NotifyParked();
|
|
direct.ParkSink.NotifyParked();
|
|
await provider.WaitForReleaseAsync(ResourceIsolationStage.RequestExecution, 1, deadline.Token)
|
|
.ConfigureAwait(false);
|
|
Assert.That(direct.Completed.Task.IsCompleted, Is.False);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
direct.Release();
|
|
Assert.That(await direct.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false),
|
|
Is.EqualTo(StatusCodes.Good));
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
direct.ParkSink.NotifyParked();
|
|
Assert.That(direct.CompletionCount, Is.EqualTo(1));
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
direct.Release();
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task CustomParkingReleasesFifoWorkerWithoutBypassingProviderAccountingAsync()
|
|
{
|
|
var provider = new TrackingProvider { UseFairScheduling = false };
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
await using var server = new QueueServer(provider, policy: CustomPolicy());
|
|
var held = new Handler();
|
|
var peer = new Handler();
|
|
var endpoint = new TestEndpoint(server, held, peer);
|
|
Task<IServiceResponse> response = endpoint.SendAsync(new CallRequest());
|
|
try
|
|
{
|
|
IRequestParkSink sink = await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
sink.NotifyParked();
|
|
Task<IServiceResponse> peerResponse = endpoint.SendAsync(new ReadRequest(), "peer");
|
|
await peer.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
|
|
Assert.That(response.IsCompleted, Is.False);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.Zero);
|
|
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
|
|
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
|
|
held.Release();
|
|
peer.Release();
|
|
Assert.That(await response.WaitAsync(deadline.Token).ConfigureAwait(false), Is.TypeOf<CallResponse>());
|
|
Assert.That(await peerResponse.WaitAsync(deadline.Token).ConfigureAwait(false),
|
|
Is.TypeOf<ReadResponse>());
|
|
await server.StopAsync(deadline.Token).ConfigureAwait(false);
|
|
provider.AssertAllReleasedOnce();
|
|
}
|
|
finally
|
|
{
|
|
held.Release();
|
|
peer.Release();
|
|
}
|
|
}
|
|
|
|
private static DelegateRequestParkingPolicy CustomPolicy()
|
|
{
|
|
return new DelegateRequestParkingPolicy(static request => request is CallRequest);
|
|
}
|
|
|
|
private sealed class TestEndpoint : EndpointBase
|
|
{
|
|
public TestEndpoint(QueueServer server, Handler held, Handler peer)
|
|
: base(server)
|
|
{
|
|
RegisterServices(held, peer);
|
|
}
|
|
|
|
public TestEndpoint(IServiceHostBase host, Handler held, Handler peer)
|
|
: base(host)
|
|
{
|
|
RegisterServices(held, peer);
|
|
}
|
|
|
|
public Task<IServiceResponse> SendAsync(
|
|
IServiceRequest request,
|
|
string channel = "held",
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
request.RequestHeader = new RequestHeader();
|
|
return ProcessRequestAsync(
|
|
new SecureChannelContext(channel, new EndpointDescription(), RequestEncoding.Binary),
|
|
request,
|
|
cancellationToken).AsTask();
|
|
}
|
|
|
|
private void RegisterServices(Handler held, Handler peer)
|
|
{
|
|
SupportedServices[DataTypeIds.CallRequest] = new ServiceDefinition(
|
|
typeof(CallRequest), held.InvokeAsync);
|
|
SupportedServices[DataTypeIds.ReadRequest] = new ServiceDefinition(
|
|
typeof(ReadRequest), peer.InvokeAsync);
|
|
SupportedServices[DataTypeIds.PublishRequest] = new ServiceDefinition(
|
|
typeof(PublishRequest), held.InvokeAsync);
|
|
}
|
|
}
|
|
|
|
private sealed class Handler
|
|
{
|
|
public bool ObserveCancellation { get; init; } = true;
|
|
|
|
public bool ThrowOnCompletion { get; init; }
|
|
|
|
public TaskCompletionSource<IRequestParkSink> Started { get; } =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
public TaskCompletionSource<bool> Cancelled { get; } =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
public async ValueTask<IServiceResponse> InvokeAsync(
|
|
IServiceRequest request,
|
|
SecureChannelContext context,
|
|
RequestLifetime lifetime)
|
|
{
|
|
using CancellationTokenRegistration registration = lifetime.CancellationToken.Register(
|
|
() => Cancelled.TrySetResult(true));
|
|
Started.TrySetResult(lifetime.ParkSink!);
|
|
await m_release.Task.WaitAsync(
|
|
ObserveCancellation ? lifetime.CancellationToken : CancellationToken.None)
|
|
.ConfigureAwait(false);
|
|
if (ThrowOnCompletion)
|
|
{
|
|
throw new InvalidOperationException("Handler failure.");
|
|
}
|
|
return request switch
|
|
{
|
|
CallRequest => new CallResponse(),
|
|
PublishRequest => new PublishResponse(),
|
|
_ => new ReadResponse()
|
|
};
|
|
}
|
|
|
|
public void Release()
|
|
{
|
|
m_release.TrySetResult(true);
|
|
}
|
|
|
|
private readonly TaskCompletionSource<bool> m_release =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
}
|
|
|
|
private sealed class QueueServer : ServerBase, IServerBase, IAsyncDisposable
|
|
{
|
|
public QueueServer(
|
|
TrackingProvider provider,
|
|
bool decouple = true,
|
|
IRequestParkingPolicy? policy = null)
|
|
: base(NUnitTelemetryContext.Create(), null, policy)
|
|
{
|
|
m_context = ServiceMessageContext.Create(NUnitTelemetryContext.Create());
|
|
m_queue = new RequestQueue(this, 1, 1, 10, decouple, provider, 10);
|
|
}
|
|
|
|
IServiceMessageContext IServerBase.MessageContext => m_context;
|
|
|
|
public override void ScheduleIncomingRequest(
|
|
IEndpointIncomingRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
m_queue.ScheduleIncomingRequest(request, cancellationToken);
|
|
}
|
|
|
|
public override ValueTask StopAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return m_queue.StopAsync(cancellationToken);
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await m_queue.StopAsync().ConfigureAwait(false);
|
|
Dispose();
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
m_queue.Dispose();
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
private readonly IServiceMessageContext m_context;
|
|
private readonly RequestQueue m_queue;
|
|
}
|
|
|
|
private sealed class DirectRequest : IParkableIncomingRequest
|
|
{
|
|
public SecureChannelContext SecureChannelContext { get; } =
|
|
new("held", new EndpointDescription(), RequestEncoding.Binary);
|
|
|
|
public IServiceRequest Request { get; } = new CallRequest { RequestHeader = new RequestHeader() };
|
|
|
|
public RequestParkSink ParkSink { get; } = new();
|
|
|
|
public TaskCompletionSource<bool> Started { get; } =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
public TaskCompletionSource<StatusCode> Completed { get; } =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
public int CompletionCount => Volatile.Read(ref m_completionCount);
|
|
|
|
public async ValueTask CallAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
Started.TrySetResult(true);
|
|
await m_release.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
OperationCompleted(new CallResponse(), ServiceResult.Good);
|
|
}
|
|
|
|
public void OperationCompleted(IServiceResponse? response, ServiceResult error)
|
|
{
|
|
Interlocked.Increment(ref m_completionCount);
|
|
Completed.TrySetResult(error.StatusCode);
|
|
}
|
|
|
|
public void Release()
|
|
{
|
|
m_release.TrySetResult(true);
|
|
}
|
|
|
|
private readonly TaskCompletionSource<bool> m_release =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
private int m_completionCount;
|
|
}
|
|
|
|
private sealed class TrackingProvider : IServerResourceIsolationProvider
|
|
{
|
|
public TrackingProvider(long parkedCapacity = 1, long costCapacity = 100)
|
|
{
|
|
int stages = (int)ResourceIsolationStage.ParkedRequest + 1;
|
|
m_capacity = new long[stages];
|
|
m_used = new long[stages];
|
|
m_grants = new int[stages];
|
|
m_releases = new int[stages];
|
|
for (int ii = 0; ii < stages; ii++)
|
|
{
|
|
m_capacity[ii] = 100;
|
|
}
|
|
m_capacity[(int)ResourceIsolationStage.RequestExecution] = 1;
|
|
m_capacity[(int)ResourceIsolationStage.ParkedRequest] = parkedCapacity;
|
|
m_capacity[(int)ResourceIsolationStage.RequestQueueBytes] = costCapacity;
|
|
m_held = new ResourceIsolationOwner("held", ResourceIsolationClass.Established, 1, m_capacity);
|
|
m_peer = new ResourceIsolationOwner("peer", ResourceIsolationClass.Established, 1, m_capacity);
|
|
}
|
|
|
|
public bool UseFairScheduling { get; init; } = true;
|
|
|
|
public int TotalGrants
|
|
{
|
|
get
|
|
{
|
|
lock (m_gate)
|
|
{
|
|
int total = 0;
|
|
foreach (int grants in m_grants)
|
|
{
|
|
total += grants;
|
|
}
|
|
return total;
|
|
}
|
|
}
|
|
}
|
|
|
|
public event Action<ResourceIsolationStage>? CapacityAvailable;
|
|
|
|
public ResourceIsolationOwner ClassifyConnection(IPEndPoint? remoteEndpoint)
|
|
{
|
|
return m_held;
|
|
}
|
|
|
|
public ResourceIsolationOwner Classify(
|
|
SecureChannelContext channelContext,
|
|
NodeId authenticationToken = default,
|
|
bool sessionEstablishment = false,
|
|
bool controlRequest = false)
|
|
{
|
|
return channelContext.SecureChannelId == "peer" ? m_peer : m_held;
|
|
}
|
|
|
|
public bool IsCurrent(
|
|
ResourceIsolationOwner owner,
|
|
SecureChannelContext channelContext,
|
|
NodeId authenticationToken = default,
|
|
bool sessionEstablishment = false,
|
|
bool controlRequest = false)
|
|
{
|
|
return ReferenceEquals(owner, Classify(channelContext));
|
|
}
|
|
|
|
public bool TryAcquire(
|
|
ResourceIsolationStage stage,
|
|
ResourceIsolationOwner owner,
|
|
long amount,
|
|
[NotNullWhen(true)] out IDisposable lease,
|
|
out ResourceIsolationFailure failure)
|
|
{
|
|
lock (m_gate)
|
|
{
|
|
if (amount > m_capacity[(int)stage] - m_used[(int)stage])
|
|
{
|
|
lease = null!;
|
|
failure = new ResourceIsolationFailure(ResourceIsolationFailureReason.Capacity, TimeSpan.Zero);
|
|
return false;
|
|
}
|
|
m_used[(int)stage] += amount;
|
|
m_grants[(int)stage]++;
|
|
lease = new Lease(this, stage, amount);
|
|
failure = default;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public long Used(ResourceIsolationStage stage)
|
|
{
|
|
lock (m_gate)
|
|
{
|
|
return m_used[(int)stage];
|
|
}
|
|
}
|
|
|
|
public async Task WaitForReleaseAsync(
|
|
ResourceIsolationStage stage,
|
|
int count,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var released = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
CapacityAvailable += OnReleased;
|
|
try
|
|
{
|
|
OnReleased(stage);
|
|
await released.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
CapacityAvailable -= OnReleased;
|
|
}
|
|
|
|
void OnReleased(ResourceIsolationStage releasedStage)
|
|
{
|
|
lock (m_gate)
|
|
{
|
|
if (releasedStage == stage && m_releases[(int)stage] >= count)
|
|
{
|
|
released.TrySetResult(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void AssertAllReleasedOnce()
|
|
{
|
|
lock (m_gate)
|
|
{
|
|
Assert.That(m_used, Is.All.Zero);
|
|
Assert.That(m_releases, Is.EqualTo(m_grants));
|
|
Assert.That(m_duplicateDisposals, Is.Zero);
|
|
}
|
|
}
|
|
|
|
private void Release(ResourceIsolationStage stage, long amount)
|
|
{
|
|
lock (m_gate)
|
|
{
|
|
m_used[(int)stage] -= amount;
|
|
m_releases[(int)stage]++;
|
|
}
|
|
CapacityAvailable?.Invoke(stage);
|
|
}
|
|
|
|
private sealed class Lease(
|
|
TrackingProvider provider, ResourceIsolationStage stage, long amount) : IDisposable
|
|
{
|
|
public void Dispose()
|
|
{
|
|
if (Interlocked.Exchange(ref m_disposed, 1) == 0)
|
|
{
|
|
provider.Release(stage, amount);
|
|
}
|
|
else
|
|
{
|
|
Interlocked.Increment(ref provider.m_duplicateDisposals);
|
|
}
|
|
}
|
|
|
|
private int m_disposed;
|
|
}
|
|
|
|
private readonly Lock m_gate = new();
|
|
private readonly long[] m_capacity;
|
|
private readonly long[] m_used;
|
|
private readonly int[] m_grants;
|
|
private readonly int[] m_releases;
|
|
private readonly ResourceIsolationOwner m_held;
|
|
private readonly ResourceIsolationOwner m_peer;
|
|
private int m_duplicateDisposals;
|
|
}
|
|
}
|
|
}
|