mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 07:02:16 +08:00
49305d9085
# Description V2 subscription engine feature parity round to make the classic engine eligible for deprecation. Splits the test surface so the classic and V2 engines can evolve independently, ports the gaps the classic-engine integration tests had been exercising, and lands the public V2 API additions needed to close them. - Splits `Tests/Opc.Ua.Subscriptions.Tests` into a classic (`Opc.Ua.Subscriptions.Classic.Tests`) and a V2 (`Opc.Ua.Subscriptions.Tests`) project to shrink CI time and let the two engines diverge cleanly. - Adds the V2 public-surface gaps required by the test ports (`Subscription.ServerId` on the class, `SetSubscriptionDurableAsync`, `SendInitialValuesOnTransfer`, unified `OnSubscriptionStateChangedAsync` handler callback, per-item `ConditionRefreshAsync`, fluent `LoadSubscriptionsAsync(Stream, …)` and `Subscription.Snapshot()` / `RestoreAsync()` for save/load + transfer). - Lands handler-centric design (no `PublishingStopped` property handlers maintain derived state); always-sequential per-subscription delivery documented + tested. - **Adds short and long haul stability tests for the V2 `ManagedSession` + subscription engine. Closes #3744** (see "Issue #3744 Haul tests" below). - **Migrates SubscriptionStateSnapshot / MonitoredItemStateSnapshot to `[DataType]` source-generator codecs**. - `SubscriptionDurableV2Tests.cs` 5 V2 ports of the classic durable test patterns. - `V2FollowUpCoverageTests.cs` handler state-change callback, fluent stream-based `LoadSubscriptionsAsync`, `SendInitialValuesOnTransfer`, snapshot edge cases (empty / `DataChangeFilter` round-trip / concurrent mutation). - `SubscriptionFailoverV2Tests.cs` forced channel-break with and without `WithTransferSubscriptionsOnRecreate`. - `MonitoredItemConditionRefreshLiveV2Tests.cs` — live `ConditionRefresh` against the reference server's event source; asserts `RefreshStart` / `RefreshEnd` events flow through the handler. ## Fixes - #3744 Haul tests (V2 ManagedSession + subscription engine) `Tests/Opc.Ua.Sessions.Tests/ManagedSessionStabilityTest.cs` adds the short/long haul coverage requested by [#3744](https://github.com/OPCFoundation/UA-.NETStandard/issues/3744) plus the bonus fault-injection variant: | Test | Duration | Category | Purpose | |---|---|---|---| | `ShortHaulManagedSessionV2Async` | 2 minutes | `ManagedSessionHaul` (`[Explicit]`) | Quick local sanity check of the V2 monotonicity contract. | | `LongHaulManagedSessionV2Async` | 90 minutes (configurable via `TEST_DURATION_MINUTES`) | `ManagedSessionHaul` (`[Explicit]`) | Long-running V2 stability + monotonicity. | | `LongHaulManagedSessionWithFaultInjectionV2Async` | 90 minutes (configurable) + fault every `FAULT_INJECTION_INTERVAL_SECONDS` (default 60s) | `ManagedSessionHaul` (`[Explicit]`) | **Bonus** — force-closes the subscriber transport channel periodically; verifies V2 manager transfers/recreates the subscription and continues monotonic delivery across reconnects. | **Pattern.** A writer `ManagedSession` increments a `uint32` counter and writes it into `Scalar_Static_Mass_UInt32_UInt32_00` every 250ms. A subscriber `ManagedSession` runs a V2 subscription with a single monitored item on the same node. `MonotonicCounterHandler` asserts each received sample is *strictly greater than* the previous one. V2 `ISubscriptionNotificationHandler` per-subscription ordering guarantee. Skipped values (sampling rate < write rate) are tolerated; duplicates and reorderings are violations.
248 lines
8.9 KiB
C#
248 lines
8.9 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;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Opc.Ua.Client.Subscriptions.Engine
|
|
{
|
|
/// <summary>
|
|
/// Abstraction for the V1 Subscription message cache so that the
|
|
/// bridge can feed translated notifications without a direct
|
|
/// assembly reference to Opc.Ua.Client.
|
|
/// </summary>
|
|
internal interface ISubscriptionMessageSink
|
|
{
|
|
/// <summary>
|
|
/// Stores a <see cref="NotificationMessage"/> in the V1
|
|
/// subscription cache.
|
|
/// </summary>
|
|
/// <param name="availableSequenceNumbers">
|
|
/// The currently available sequence numbers on the server.
|
|
/// </param>
|
|
/// <param name="message">
|
|
/// The notification message to cache.
|
|
/// </param>
|
|
void SaveMessageInCache(
|
|
ArrayOf<uint> availableSequenceNumbers,
|
|
NotificationMessage message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bridges V2 subscription notification callbacks to V1 Subscription
|
|
/// event/cache mechanisms, enabling V1 Subscription objects to receive
|
|
/// notifications when the V2 engine is active.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The bridge converts V2 record-based notifications
|
|
/// (<see cref="DataValueChange"/>, <see cref="EventNotification"/>)
|
|
/// into V1 <see cref="NotificationMessage"/> instances and forwards
|
|
/// them to an <see cref="ISubscriptionMessageSink"/>, which is
|
|
/// typically implemented by the V1 <c>Subscription</c> class.
|
|
/// </remarks>
|
|
internal sealed class SubscriptionBridge : ISubscriptionNotificationHandler
|
|
{
|
|
private readonly ISubscriptionMessageSink m_messageSink;
|
|
|
|
/// <summary>
|
|
/// Creates a new bridge for the specified V1 message sink.
|
|
/// </summary>
|
|
/// <param name="messageSink">
|
|
/// The sink that receives translated V1 notification messages.
|
|
/// </param>
|
|
public SubscriptionBridge(ISubscriptionMessageSink messageSink)
|
|
{
|
|
m_messageSink = messageSink
|
|
?? throw new ArgumentNullException(nameof(messageSink));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask OnDataChangeNotificationAsync(
|
|
ISubscription subscription,
|
|
uint sequenceNumber,
|
|
DateTime publishTime,
|
|
ReadOnlyMemory<DataValueChange> notification,
|
|
PublishState publishStateMask,
|
|
IReadOnlyList<string> stringTable)
|
|
{
|
|
ReadOnlySpan<DataValueChange> span = notification.Span;
|
|
|
|
var monitoredItems =
|
|
new MonitoredItemNotification[span.Length];
|
|
DiagnosticInfo[]? diagnosticInfos = null;
|
|
|
|
for (int i = 0; i < span.Length; i++)
|
|
{
|
|
DataValueChange change = span[i];
|
|
uint clientHandle =
|
|
change.MonitoredItem?.ClientHandle ?? 0;
|
|
|
|
monitoredItems[i] = new MonitoredItemNotification
|
|
{
|
|
ClientHandle = clientHandle,
|
|
Value = change.Value
|
|
};
|
|
|
|
if (change.DiagnosticInfo != null)
|
|
{
|
|
diagnosticInfos ??= new DiagnosticInfo[span.Length];
|
|
diagnosticInfos[i] = change.DiagnosticInfo;
|
|
}
|
|
}
|
|
|
|
var dataChange = new DataChangeNotification
|
|
{
|
|
MonitoredItems = new ArrayOf<MonitoredItemNotification>(
|
|
monitoredItems),
|
|
DiagnosticInfos = diagnosticInfos != null
|
|
? new ArrayOf<DiagnosticInfo>(diagnosticInfos)
|
|
: []
|
|
};
|
|
|
|
NotificationMessage message = BuildNotificationMessage(
|
|
sequenceNumber, publishTime, stringTable,
|
|
new ExtensionObject(dataChange));
|
|
|
|
m_messageSink.SaveMessageInCache(
|
|
[], message);
|
|
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask OnEventDataNotificationAsync(
|
|
ISubscription subscription,
|
|
uint sequenceNumber,
|
|
DateTime publishTime,
|
|
ReadOnlyMemory<EventNotification> notification,
|
|
PublishState publishStateMask,
|
|
IReadOnlyList<string> stringTable)
|
|
{
|
|
ReadOnlySpan<EventNotification> span = notification.Span;
|
|
|
|
var events = new EventFieldList[span.Length];
|
|
|
|
for (int i = 0; i < span.Length; i++)
|
|
{
|
|
EventNotification evt = span[i];
|
|
uint clientHandle =
|
|
evt.MonitoredItem?.ClientHandle ?? 0;
|
|
|
|
events[i] = new EventFieldList
|
|
{
|
|
ClientHandle = clientHandle,
|
|
EventFields = evt.Fields
|
|
};
|
|
}
|
|
|
|
var eventList = new EventNotificationList
|
|
{
|
|
Events = new ArrayOf<EventFieldList>(events)
|
|
};
|
|
|
|
NotificationMessage message = BuildNotificationMessage(
|
|
sequenceNumber, publishTime, stringTable,
|
|
new ExtensionObject(eventList));
|
|
|
|
m_messageSink.SaveMessageInCache(
|
|
[], message);
|
|
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask OnKeepAliveNotificationAsync(
|
|
ISubscription subscription,
|
|
uint sequenceNumber,
|
|
DateTime publishTime,
|
|
PublishState publishStateMask)
|
|
{
|
|
// A keep-alive is a NotificationMessage with no
|
|
// notification data entries.
|
|
var message = new NotificationMessage
|
|
{
|
|
SequenceNumber = sequenceNumber,
|
|
PublishTime = publishTime
|
|
};
|
|
|
|
m_messageSink.SaveMessageInCache(
|
|
[], message);
|
|
|
|
return default;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask OnSubscriptionStateChangedAsync(
|
|
ISubscription subscription,
|
|
SubscriptionState state,
|
|
PublishState publishStateMask,
|
|
System.Threading.CancellationToken ct = default)
|
|
{
|
|
// The bridge translates V2 notifications into V1 cache
|
|
// updates; the V1 subscription class drives its own
|
|
// state-change events via its existing PublishStateChanged /
|
|
// StateChanged event pipeline, so we have nothing to forward
|
|
// here.
|
|
return default;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a <see cref="NotificationMessage"/> containing a
|
|
/// single notification data extension object.
|
|
/// </summary>
|
|
private static NotificationMessage BuildNotificationMessage(
|
|
uint sequenceNumber,
|
|
DateTime publishTime,
|
|
IReadOnlyList<string> stringTable,
|
|
ExtensionObject notificationData)
|
|
{
|
|
var message = new NotificationMessage
|
|
{
|
|
SequenceNumber = sequenceNumber,
|
|
PublishTime = publishTime,
|
|
NotificationData = new ArrayOf<ExtensionObject>(
|
|
new[] { notificationData })
|
|
};
|
|
|
|
if (stringTable != null && stringTable.Count > 0)
|
|
{
|
|
string[] table = new string[stringTable.Count];
|
|
for (int i = 0; i < stringTable.Count; i++)
|
|
{
|
|
table[i] = stringTable[i];
|
|
}
|
|
message.StringTable = new ArrayOf<string>(table);
|
|
}
|
|
|
|
return message;
|
|
}
|
|
}
|
|
}
|