Files
UA-.NETStandard/Libraries/Opc.Ua.Client/Subscription/ISubscriptionNotificationHandler.cs
Marc Schier 49305d9085 V2 subscription engine close gap for test and feature parity with classic engine (#3824)
# 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.
2026-06-03 04:39:08 +02:00

177 lines
7.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;
using System.Threading.Tasks;
using Opc.Ua.Client.Subscriptions.MonitoredItems;
namespace Opc.Ua.Client.Subscriptions
{
/// <summary>
/// <para>
/// Data value change observed on a monitored item during the V2
/// publish dispatch.
/// </para>
/// <para>
/// Lifetime note (pooled notifications): when the V2 subscription
/// manager has <see cref="ISubscriptionManager.PoolNotifications"/>
/// enabled, the underlying <c>MonitoredItemNotification</c> that
/// supplied this struct's fields is recycled back to its
/// activator pool after the handler returns. This struct's
/// references (<see cref="Value"/>, <see cref="DiagnosticInfo"/>)
/// project the inner <c>DataValue</c> / <c>DiagnosticInfo</c>
/// instances directly — those are not themselves pooled (out of
/// scope), so a handler may safely copy this struct by value and
/// continue using <see cref="Value"/> after the call. Pool-aware
/// projection audit: confirmed safe — no reference to a pooled
/// instance is surfaced.
/// </para>
/// </summary>
/// <param name="MonitoredItem"></param>
/// <param name="Value"></param>
/// <param name="DiagnosticInfo"></param>
public record struct DataValueChange(IMonitoredItem? MonitoredItem,
DataValue Value, DiagnosticInfo? DiagnosticInfo);
/// <summary>
/// <para>
/// Event notification observed during the V2 publish dispatch.
/// </para>
/// <para>
/// Lifetime note (pooled notifications): when the V2 subscription
/// manager has <see cref="ISubscriptionManager.PoolNotifications"/>
/// enabled, the underlying <c>EventFieldList</c> is recycled back
/// to its activator pool after the handler returns. The
/// <see cref="Fields"/> property is captured by value as an
/// <see cref="ArrayOf{T}"/> wrapping the original event-fields
/// array. Arrays are not pooled in this design (out of scope), so
/// the captured backing array survives the recycle. Pool-aware
/// projection audit: confirmed safe — no reference to a pooled
/// instance is surfaced.
/// </para>
/// </summary>
/// <param name="MonitoredItem"></param>
/// <param name="Fields"></param>
public record struct EventNotification(IMonitoredItem? MonitoredItem,
ArrayOf<Variant> Fields);
/// <summary>
/// Notification handler
/// </summary>
public interface ISubscriptionNotificationHandler
{
/// <summary>
/// Process data change notification
/// </summary>
/// <param name="subscription"></param>
/// <param name="sequenceNumber"></param>
/// <param name="publishTime"></param>
/// <param name="notification"></param>
/// <param name="publishStateMask"></param>
/// <param name="stringTable"></param>
/// <returns></returns>
ValueTask OnDataChangeNotificationAsync(ISubscription subscription,
uint sequenceNumber, DateTime publishTime,
ReadOnlyMemory<DataValueChange> notification,
PublishState publishStateMask, IReadOnlyList<string> stringTable);
/// <summary>
/// Process event notification
/// </summary>
/// <param name="subscription"></param>
/// <param name="sequenceNumber"></param>
/// <param name="publishTime"></param>
/// <param name="notification"></param>
/// <param name="publishStateMask"></param>
/// <param name="stringTable"></param>
/// <returns></returns>
ValueTask OnEventDataNotificationAsync(ISubscription subscription,
uint sequenceNumber, DateTime publishTime,
ReadOnlyMemory<EventNotification> notification,
PublishState publishStateMask, IReadOnlyList<string> stringTable);
/// <summary>
/// Process keep alive notification
/// </summary>
/// <param name="subscription"></param>
/// <param name="sequenceNumber"></param>
/// <param name="publishTime"></param>
/// <param name="publishStateMask"></param>
/// <returns></returns>
ValueTask OnKeepAliveNotificationAsync(ISubscription subscription,
uint sequenceNumber, DateTime publishTime,
PublishState publishStateMask);
/// <summary>
/// Surface a transition in the subscription's lifecycle
/// (<paramref name="state"/>) or publish-side status
/// (<paramref name="publishStateMask"/>). Fires from the V2
/// engine on:
/// <list type="bullet">
/// <item><description>Lifecycle transitions —
/// <see cref="SubscriptionState.Opened"/>,
/// <see cref="SubscriptionState.Created"/>,
/// <see cref="SubscriptionState.Modified"/>,
/// <see cref="SubscriptionState.Deleted"/>.</description></item>
/// <item><description>Publish-side transitions —
/// <see cref="PublishState.Republish"/> (a gap was detected and
/// a republish was issued), <see cref="PublishState.Recovered"/>
/// (a missing message arrived or publishing resumed after a
/// stop), <see cref="PublishState.Transferred"/> (the
/// subscription was taken over on a different session via
/// <c>TransferSubscriptions</c>).</description></item>
/// </list>
/// <para>
/// Handlers that need derived state (publishing stopped, last
/// keep-alive, republish-pending, etc.) maintain it themselves
/// by responding to this callback — V2 is handler-centric and
/// the engine deliberately does not expose those as polled
/// properties on <see cref="ISubscription"/>.
/// </para>
/// <para>
/// Per-subscription delivery ordering is guaranteed by the V2
/// prioritized publish-ack channel: data / event / keep-alive
/// callbacks for a given subscription always fire in
/// publish-sequence order. State-change callbacks interleave
/// with the notification stream at the moment the transition
/// is observed.
/// </para>
/// </summary>
/// <param name="subscription"></param>
/// <param name="state"></param>
/// <param name="publishStateMask"></param>
/// <param name="ct"></param>
ValueTask OnSubscriptionStateChangedAsync(ISubscription subscription,
SubscriptionState state, PublishState publishStateMask,
CancellationToken ct = default);
}
}