5 Commits

Author SHA1 Message Date
Marc Schier 04f3d12fa8 [security] Bound the participant metric tag to per-kind cardinality (MEDIUM-4)
plans/security-assessment.md MEDIUM-4 (unbounded metric cardinality):

opcua.channel.participant.timeout.count and
opcua.channel.participant.recreate.count emit a "participant" tag
whose value was the full per-instance IReconnectParticipant.Id. Two
problems:
  - ClientChannelReconnectParticipant uses
    "{idPrefix}-{Guid.NewGuid():N}" — the suffix made every instance
    permanent in cardinality-retaining metric backends (Prometheus,
    OTLP, Application Insights).
  - Session.ChannelManager.cs:45 used a bare Guid with NO prefix at
    all, so the metric tag was a 32-hex-char string with no way for
    operators to bucket the data.

Fix:
  - Session.ChannelManager.cs:45 — prefix the GUID with "Session-" so
    Session participants follow the same prefix-then-suffix shape as
    ClientChannelReconnectParticipant.
  - ClientChannelManagerMetrics — add a GetParticipantKind helper
    that returns everything before the first '-' (or the full id if
    no '-'). Use it in CreateEndpointParticipantTags and
    CreateEndpointParticipantSuccessTags so the metric tag stays at
    per-kind cardinality ("Session", "Client", …).
  - Full per-instance id continues to flow through Activity tags and
    EventSource events (separate code path in
    ClientChannelManagerDiagnostics) so distributed traces remain
    correlatable.
  - Docs/Sessions.md callout updated to describe the now-bounded
    behavior (was a warning before, now describes the implemented
    semantic and the convention required of custom participants).

Build verified: Stack/Opc.Ua.Core, Libraries/Opc.Ua.Client net10.0 —
0 W, 0 E.
2026-06-11 12:26:54 +02:00
Marc Schier 81acd947a6 Close channel-manager production gaps and eliminate Tests/.../Gaps/
Closes the three carry-forward gaps documented in
Tests/Opc.Ua.Stress.Tests/Channels/Gaps/ (formerly each documented via
an [Explicit] failing test). The realigned tests are relocated to
Channels/Contract/, the [Category("Gaps")] disappears, and the
Gaps/ folder is removed entirely.

G1. Faulted-entry recovery
  Already closed implicitly by Phase E SwapFaultedEntryAsync (PR #3852).
  Test realigned from "expects BadSecureChannelClosed throw" to
  "auto-resets on next ReconnectAsync and reaches Ready" and renamed
  FaultedEntryAutoResetsOnNextReconnectAsync. New file location:
  Tests/Opc.Ua.Stress.Tests/Channels/Contract/FaultedEntryRecoveryTests.cs.
  No production change required; this was a test-vs-design alignment.

G2. Bounded participant timeout
  New ParticipantTimeout on IChannelReconnectPolicy. Default
  Timeout.InfiniteTimeSpan via DIM on net8+/netstandard2.1; legacy TFMs
  opt in via IParticipantTimeoutPolicy (mirrors the existing
  IBudgetAwareChannelReconnectPolicy pattern). ExponentialBackoffChannel-
  ReconnectPolicy sets a sensible default of 30 seconds.
  ChannelEntry.ReactivateParticipantsAsync wraps the participant's
  OnReconnectAsync in WaitAsync(timeout, ct); on timeout the participant
  is reported as TransientFailure for this cycle (the channel-level
  retry policy retries normally; eventually escalates to Faulted via
  the existing MaxAttempts path). A new participant.timeout.count
  metric is emitted per timeout.
  Test realigned from "documents indefinite hang" to "times out within
  bounded wait and surfaces as TransientFailure", renamed
  HungParticipantTimesOutAfterBoundedWaitAsync, and merged into
  Tests/Opc.Ua.Stress.Tests/Channels/Contract/HungParticipantTests.cs.
  The sibling Contract/HungParticipantTests.HungParticipantBlocks-
  ReconnectIndefinitely is also realigned + renamed to
  HungParticipantTimesOutAndOtherParticipantsRecoverAsync. A new
  positive contract test (BoundedParticipantTimeoutHonorsTimeoutAsync)
  guards against false positives. A new live-server integration test
  exercises the timeout via a real ManagedSession in
  Tests/Opc.Ua.Sessions.Tests/ChannelManagerSessionLifecycleIntegration-
  Tests.cs.

G3. RequiresSessionRecreate plumbing
  New RecreateAsync(ct) on IReconnectParticipant. Default no-op via DIM
  on net8+/netstandard2.1; legacy TFMs opt in via
  IRecreateAwareReconnectParticipant. ChannelEntry's switch on
  RequiresSessionRecreate now fire-and-forgets a DispatchRecreate(participant)
  task that invokes RecreateAsync; the manager does NOT block its
  transition to Ready waiting for the recreate (matches the existing
  enum doc that says "the participant is responsible for completing
  its own recreation out of band"). New participant.recreate.count
  + participant.recreate.failure.count metrics emit per dispatch.
  Session.ChannelManager.cs (the Session's IReconnectParticipant
  adapter) overrides RecreateAsync to delegate to the existing
  Session.RecreateInPlaceAsync — this is the actual "wired-through"
  deliverable flagged as PR #3852 carry-forward.
  Test realigned from "documents that nothing is invoked" to "fire-
  and-forget invokes RecreateAsync exactly once", renamed
  RequiresSessionRecreateInvokesRecreateAsync, moved to
  Tests/Opc.Ua.Stress.Tests/Channels/Contract/RecreateDispatchTests.cs.
  The 1 remaining [Ignore]d integration test from PR #3852 is replaced
  by a positive integration test in
  Tests/Opc.Ua.Sessions.Tests/ChannelManagerSessionLifecycleIntegration-
  Tests.cs that simulates BadSessionIdInvalid on reactivation.

Cleanup
  - GapTestBase.cs deleted; unique helpers folded into ContractTestBase.
  - Tests/Opc.Ua.Stress.Tests/Channels/Gaps/ folder deleted.
  - [Category("Gaps")] removed from the codebase.
  - Tests/Opc.Ua.Stress.Tests/README.md "category overview" loses the
    [Explicit] / L5 / Channels/Gaps/ row and the "Add known production
    gaps to Channels/Gaps/" guidance bullet.
  - Docs/Sessions.md "Participant model" subsection documents the new
    RecreateAsync callback and ParticipantTimeout policy property.
  - Pre-existing build-blocker in Applications/Quickstarts.Servers/
    ReferenceServer/ReferenceServerConfigurationNodeManager.cs
    (ObjectIds.SecurityGroups / KeyPushTargets are not on the curated
    Stack/Opc.Ua.Types/Internal/ObjectIds.cs surface) is replaced with
    inline NodeId literals (i=15443 / i=25440) per the docstring above
    the array. This was a separate fix required for the merged tree
    to build end-to-end.

Verified locally on net10.0:
  - Full UA.slnx Release build (CodeQL parity): 0 errors.
  - Tests/Opc.Ua.Stress.Tests Contract filter (faulted recovery,
    recreate dispatch, hung participant, participant result
    aggregation): 9/9 passed.
  - Tests/Opc.Ua.Client.Tests ClientChannelManager filter: 29/29 passed.
  - Tests/Opc.Ua.Sessions.Tests ChannelManager filter: 9/9 passed
    (was 7/7+1 skipped before; the G3 RequiresSessionRecreate
    integration test that was carry-forward [Ignore] is now
    implemented and counted as a pass).
  - No remaining Gaps namespace / Category("Gaps") / GapTestBase
    references anywhere in the tree.
2026-06-08 18:12:18 +02:00
Marc Schier 06fbbb571f Add channel-manager stress/chaos test suite (Tests/Opc.Ua.Channels.Stress.Tests)
Layered pyramid (per rubber-duck plan) to exercise every meaningful combination
of the channel-manager + session reconnect machinery shipped in PR #3852:

L1 — Contract (deterministic, fake transport, runs in every PR):
  - CoalescingTests, ParticipantResultAggregationTests,
    RetryBudgetEnforcementTests, HungParticipantTests,
    LeaseLifecycleTests, GateAndBypassTests, KeyAndSharingTests,
    CertRotationContractTests, LeakAccuracyTests
  - 27 tests, all pass.

L2 — Integration (in-process server stop/start, runs in every PR):
  - ServerOutageRecoveryTests, CertRotationLiveTests, FailoverLeaseSwapTests
  - 6 tests, all pass.

L3 — Chaos (TCP proxy with drop/block-accept/stall, nightly category=ChaosTCP):
  - TransparentReconnectChaosTests, SubscriptionSurvivalChaosTests,
    AcceptButStallChaosTests, BlockAcceptChaosTests
  - Seed-driven via ChaosSchedule for reproducibility.

L4 — Soak (manual category=Soak):
  - LongSoakTests (60 min randomized chaos),
    CombinatorialMatrixTests (engine/transfer/subscribe/sessions matrix),
    MemoryStabilitySoakTests (30 min memory snapshots).

L5 — Known-failing gaps ([Explicit], document carry-forward):
  - FaultedEntryResetGapTests, SessionRecreatePlumbingGapTests,
    ParticipantTimeoutGapTests.

Infrastructure in Fakes/ and Helpers/:
  - FakeTransport, FakeChannelBindings, FakeParticipant (configurable fault modes).
  - ChaosBarrier (deterministic barrier eliminates timing flakiness in coalescing tests).
  - TcpChaosProxy (~200 lines; DropAllConnectionsAsync, BlockAcceptAsync, StallForwarding).
  - StressRunner (concurrent workload generator with latency percentiles).
  - ChaosSchedule + ChaosScheduleRunner (pre-generated deterministic event schedule).
  - WaitForQuiescence (ForManagerAsync, EntryRefcountReachesAsync, EntryGoneAsync).
  - MetricsCollector (subscribes to Opc.Ua.ChannelManager EventSource + IMeter).
  - LeakCounters (snapshot-based refcount/cert/entry validation).

CI wiring:
  - .github/workflows/buildandtest.yml now runs Category=Contract|Integration in PR CI.
  - .github/workflows/channel-manager-stress-test.yml runs Category=ChaosTCP nightly with random seed.

Small production-code tweaks discovered while building tests:
  - ClientChannelManager.Managed.cs / .Metrics.cs: minor refactors for testability.
  - Session.cs / Session.ChannelManager.cs / ManagedSession.cs: small adjustments to
    make IClientChannelManager-aware flows easier to assert.

Docs:
  - Docs/Sessions.md section 4 now has a "Testing the channel manager" subsection
    pointing at the test categories with run-and-reproduce commands.

Build: 0 warnings, 0 errors on net10.0.
Tests: 33/33 pass in Category=Contract|Integration.
2026-06-05 23:17:33 +02:00
Marc Schier 82347dc1da Address PR #3852 review feedback
- Move all channel-related types into `Stack/Opc.Ua.Core/Stack/Client/Channels/`
  subfolder (#13).
- Un-obsolete `SessionReconnectHandler` and the SessionExtensions
  `ReconnectAsync(connection, ct)` / `(channel, ct)` extensions; remove
  all SRH-related `#pragma warning disable CS0618` suppressions from
  applications and tests. AttachChannel/DetachChannel remain obsolete (#1, #4).
- Remove the shared-budget section from MigrationGuide.md (#3).
- Mark `ManagedSession.AttachChannel`/`DetachChannel` `[Obsolete]` so the
  warning surfaces all the way up the supported API surface (#9).
- Break the `<see cref=…>` line in `Session.ChannelManager.cs` to stay under
  140 chars (#10).
- Move `ChannelManagerSessionFactory` from `Opc.Ua.Gds.Client.Common` into
  `Opc.Ua.Client` as a public, documented session-factory option (#11).
- Audit other client classes for `IClientChannelManager`-aware overloads;
  add `ISessionFactory`-accepting overloads on `GlobalDiscoveryServerClient`,
  `LocalDiscoveryServerClient`, `ServerPushConfigurationClient` so any
  session factory (including `ChannelManagerSessionFactory`) works (#12).
- Refactor MCP server (`OpcUaSessionManager`, `Program`) to use
  `ManagedSession` + DI-resolved `IClientChannelManager`; remove
  `SessionReconnectHandler` and manual keep-alive reconnect (#2).
- ConnectionStateMachine code-style fixes: use named delegate types,
  collapse multi-line callback properties to single lines, swap
  `timeProvider`/`maxTotalReconnectTime` constructor param order,
  fix multi-line declaration (#6, #7, #8).
- HTTPS resilience: confirmed `Microsoft.Extensions.Http` and
  `Microsoft.Extensions.Http.Resilience` 10.6.0 support
  `net472`/`net48`/`netstandard2.1`; removed `#if NET8_0_OR_GREATER`
  gating from `ManagedSessionBuilder` and `OpcUaClientBuilderExtensions`
  and HTTPS csproj package refs (#5).

Verification:
- `Opc.Ua.Client` + `Opc.Ua.Core` + `Opc.Ua.Gds.Client.Common` build
  clean (0 warnings, 0 errors).
- 56/56 core channel-manager / retry-budget / HTTPS factory tests pass.
- 129/129 client (ManagedSession + legacy SRH back-compat + SessionExtensions)
  tests pass.
2026-06-05 17:22:41 +02:00
Marc Schier 36d67b3d3a [Client] Central channel manager with shared, ref-counted transport channels (#3288)
Introduces `IClientChannelManager` as the central registry for client-side
transport channels. Sessions and discovery clients now share ref-counted
managed channels per (endpoint, reverse-connect identity); reconnect is
coalesced and notified to attached participants via a new
`IReconnectParticipant` callback interface; service calls are gated by a
three-state lifecycle (`TransportReconnecting` -> `TransportConnectedSessionReactivating`
-> `Ready`) until reactivation completes.

New types in `Stack/Opc.Ua.Core/Stack/Client/`:
- `ManagedChannelKey`, `ChannelState`, `ChannelStateChange`, `ParticipantReconnectResult`
- `IReconnectParticipant`, `IManagedTransportChannel`, `IClientChannelManager`
- `IChannelReconnectPolicy` + `ExponentialBackoffChannelReconnectPolicy`
- `IRetryBudget` + `RetryBudget` (shared retry deadline across the two layers)
- `ClientChannelManager.Managed/Entry/Lease/Metrics/Diagnostics/CertRotation.cs`
  partials with refcount, coalesced reconnect, IMeter instruments, ActivitySource
  + EventSource, and automatic ReconnectAllAsync on certificate rotation.

Session integration (Libraries/Opc.Ua.Client):
- `Session.CreateAsync(IClientChannelManager, ...)` overload constructs a
  Session that shares its channel with co-located participants.
- `Session` implements `IReconnectParticipant`; reactivation handler
  distinguishes Reactivated / RequiresSessionRecreate / TransientFailure /
  FatalForParticipant / FatalForChannel.
- `Session.ReconnectAsync(ct)` automatically delegates to the channel
  manager when wired.
- `Session.RecreateInPlaceAsync` swaps managed-channel leases for
  failover-to-different-endpoint; same-key recreates delegate to manager
  reconnect; explicit channel/connection callers retain legacy behavior.
- KA failure routes through `IClientChannelManager.ReconnectAsync(channel)`.

ManagedSession integration:
- `WithChannelManager(...)` builder + ctor param.
- Two-level retry: channel-mgr handles transparent reconnect; outer
  `ConnectionStateMachine` + `IReconnectPolicy` only triggers on terminal
  channel `Faulted`. Outer-state churn suppressed during channel-mgr
  cycles via `_channelReconnectInProgress` flag wired through
  `IManagedTransportChannel.StateChanged`.
- New `ManagedSession.ChannelStateChanged` event surfaces transparent
  reconnects to UI / health dashboards.
- `ConnectionStateChangedEventArgs.UnderlyingChannelState` populated when
  outer reconnect was triggered by a channel fault.
- Shared `IRetryBudget` propagated to channel-mgr so the two layers
  enforce a single max-total-time instead of compounding multiplicatively.

Discovery/Registration/GDS:
- Channel-manager-aware `CreateAsync` overloads on `DiscoveryClient`,
  `RegistrationClient`, `ServerPushConfigurationClient`,
  `LocalDiscoveryServerClient`, `GlobalDiscoveryServerClient`. Each
  registers a minimal `IReconnectParticipant`; co-located sessions and
  discovery probes targeting the same endpoint share a channel.

HTTPS transport (`Stack/Opc.Ua.Bindings.Https/`):
- New `IOpcUaHttpClientFactory` abstraction.
- HTTPS / OPC-HTTPS channels migrated to `IHttpClientFactory` +
  `Microsoft.Extensions.Http.Resilience` (10.6.0, AOT-compatible on net10
  via source generators; legacy direct-HTTP path retained for older TFMs
  / no-DI consumers).

DI:
- `AddOpcUa().AddClient(...)` registers `IClientChannelManager` and the
  named HTTPS HttpClient + standard resilience handler automatically.
- `ManagedSessionBuilder` resolves the manager from DI when present.

Obsoletions (functional, `[Obsolete]` with guidance):
- `IClientBase.AttachChannel`/`DetachChannel`
- `SessionReconnectHandler`
- `SessionExtensions.ReconnectAsync(connection, ct)` and `(channel, ct)`
  legacy overloads

Tests (~70 new unit + 4 live-server integration fixtures):
- Core: `ClientChannelManagerManagedTests`, `ClientChannelManagerCertRotationTests`,
  `RetryBudgetTests`, `OpcUaHttpClientFactoryTests` (56/56 pass).
- Client: `ManagedSession*Tests` updated (129/129 pass) + back-compat
  suites pass with `#pragma warning disable CS0618`.
- Sessions integration: `ChannelManagerSharing/TransparentReconnect/SessionLifecycle/CertRotation`
  (6 pass, 2 skipped with documented blockers for faulted-entry reset and
  RequiresSessionRecreate end-to-end plumbing).

Builds clean (0 warnings / 0 errors) across all six TFMs (net472, net48,
netstandard2.1, net8.0, net9.0, net10.0).

Docs:
- `Docs/Sessions.md` new section 4 documents the channel manager,
  three-state gating, two-level retry with shared `IRetryBudget`, and
  HTTPS resilience layering.
- `Docs/MigrationGuide.md` migration recipes for the four obsoleted API
  groups.
- `Docs/DependencyInjection.md` channel-manager and HTTPS factory
  registration.
2026-06-05 15:37:23 +02:00