Commit Graph
3 Commits
Author SHA1 Message Date
Marc Schier 7d0e5870cb Migrate to source-generated logging across Stack, Libraries and Applications (#3908) (#4003)
# Description

Migrates the entire code base (Stack, Libraries and Applications) from
direct

`ILogger.LogInformation/LogError/LogWarning/LogDebug/LogTrace/LogCritical(...)`
calls to **source-generated `[LoggerMessage]` logging**, as tracked in
#3908.
Source-generated logging avoids boxing of value-type arguments, caches
the
message formatter, and emits an efficient `IsEnabled` check so a
disabled log
level costs nothing.

This is a behavior-preserving refactor: message text, levels and
structured
placeholders are unchanged; only the call shape changes.

## What changed

- **36 projects migrated.** Every static log call now goes through a
per-file
  `internal static partial class <Class>Log` holding `[LoggerMessage]`
  extension methods on `ILogger`, so call sites keep the natural
`logger.SomeEvent(...)` shape. Closely-related files that emit the same
messages share one log class (e.g. the encoders/decoders in
`Opc.Ua.Types`
  share `EncodingLog`) to avoid duplication and CS0121 ambiguity.
- **Per-assembly event-id classes.** Each project has one
  `internal static class <AssemblyToken>EventIds` (e.g. `CoreEventIds`,
  `ClientEventIds`, `PubSubEventIds`) with a reserved `public const int`
  offset block per log class. The assembly-token prefix avoids CS0436
  collisions across `InternalsVisibleTo` boundaries.
- **`CA1873` enabled solution-wide** in `.editorconfig` now that the
whole
code base (including test, sample and tooling projects) is migrated, so
any
regression back to a direct `ILogger.Log*` call with an expensive
argument is
flagged. Every occurrence has been guarded with an `IsEnabled` check (or
the
check merged into an existing condition where the log was the sole body
of an
`if`/`else`, preserving side effects); the only suppressions are two Moq
  `Verify` expression-tree sites, which are not executed log calls.
- **Dynamic log levels stay hand-written.** A handful of calls whose
level is
only known at runtime keep the structured `logger.Log(logLevel, ...)`
form
under an `if (logger.IsEnabled(logLevel))` guard — `[LoggerMessage]`
requires
  a compile-time level.
- **netstandard duplicate-generator fix.** Projects that also pull in an
R9
package (`Microsoft.Extensions.Http.Resilience`, `.Compliance`,
`.Telemetry`)
get the `Microsoft.Gen.Logging` generator in addition to the in-box one;
on
  `netstandard` both implement every partial method (CS0757). A single
`Directory.Build.targets` target removes the R9 analyzer on
`netstandard`
  only (no-op elsewhere and where it is not referenced).
- **Shared/linked source files** that are compiled into more than one
project
(e.g. sample files linked into test projects) use self-contained literal
event ids instead of a per-assembly class the consumer does not compile.
- **Docs.** The authoring recipe (event-id convention, log-class
convention,
  parameter rules, the `IsEnabled` guard, shared/linked files, and the
  netstandard generator interaction) lives in the new

[`Docs/DeveloperGuide.md`](Docs/DeveloperGuide.md#add-a-log-message-source-generated);
`Docs/Diagnostics.md` keeps the runtime/observability side and links to
it.
- **Redaction preserved.** Log calls that previously wrapped sensitive
values
with `Redact.Create(...)` keep passing the `RedactionWrapper<T>`
(instead of
an eager `.ToString()`), so format-time redaction still applies — this
also
  clears the CodeQL clear-text alerts introduced during the migration.

## Validation

- Full `UA.slnx` build passes with **0 errors / 0 warnings**; the
multi-target
  test projects force every referenced library to build on
  net472/net48/netstandard2.1/net8.0/net9.0/net10.0.
- No unguarded static `ILogger.Log*` calls remain anywhere (only the
guarded
dynamic-level calls); `CA1873` is clean across the full solution on
net10.0.

## Related Issues

- Fixes #3908

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] 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.
2026-07-15 15:07:25 +02:00
Marc SchierandCopilot 7f409657ad OPC UA Part 4 6.6 Redundancy (server + client) with opt-in distributed high-availability (#3918)
## Summary

This branch implements **OPC 10000-4 §6.6 Redundancy** end to end —
server and client, **transparent and non-transparent**, across all
failover modes (`None`, `Cold`, `Warm`, `Hot`, `HotAndMirrored`,
`Transparent`) — and layers an **opt-in distributed high-availability
(HA)** capability on top so replicas can share address-space, session,
and subscription state and expose redundancy to clients through the
documented OPC UA mechanisms (for example, running a server replica set
across nodes in Kubernetes).

The single-instance, in-memory path stays **zero-overhead**: every
distributed feature is opt-in through dependency injection, with a
direct-construction fallback, and replaced APIs are kept and marked
`[Obsolete]`.

## New packages

| Package | Purpose |
| --- | --- |
| `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy` | §6.6 redundancy
nodes plus opt-in distributed state: shared address space, secure
session sharing, subscription/continuation-point mirroring, and leader
election. |
| `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy.Crdt` |
Active/active, leaderless multi-writer replication via CRDTs (an
extension beyond §6.6). |
| `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy.K8s` | Kubernetes
integration: Lease leader election, EndpointSlice peer discovery, and
ServiceLevel-driven readiness. |

Also adds the worked samples `Applications/RedundantServer` and
`Applications/RedundantClient`, and the guides
`Docs/HighAvailability.md` and `Docs/HighAvailabilityKubernetes.md`.

## Server side (§6.6)

- `AddServerRedundancy(...)` populates the live
`Server.ServerRedundancy` nodes (`RedundancySupport` plus
`RedundantServerArray` / `ServerUriArray` / `CurrentServerId`) and
drives `Server.ServiceLevel` from an `IServiceLevelProvider` (leader
high, standby low). `Server.ServerRedundancy` is typed to the
NonTransparent or Transparent subtype according to the configured mode.
- `AddRequestServerStateChange(...)` implements the §6.6.5
manual-failover method, with `EstimatedReturnTime` and the Maintenance /
NoData `ServiceLevel` sub-ranges.
- Non-transparent peers resolve through `FindServers`
(`ConfiguredRedundantServerSetProvider`) and advertise the `NTRS`
discovery capability for GDS/NTRS registration.
- Deterministic, replica-stable `EventId` synchronization for
Transparent and HotAndMirrored sets (§6.6.2.2), excluding per-replica
fields so de-duplicating clients do not double-process events.

## Client side (§6.6)

- `DefaultServerRedundancyHandler` reads `Server.ServerRedundancy` and
`Server.ServiceLevel` and fails over to the highest-ServiceLevel peer.
It honors Maintenance (Part 4 Table 105 — disconnect to an available
peer), keeps Warm backups disabled until failover (Table 107), and
treats peers known only through `ServerUriArray` as failover candidates.
- `RedundantManagedClient` implements the Cold, Warm, Hot (a), Hot (b),
and HotAndMirrored client patterns: one active session, optional
lightweight ServiceLevel status-check sessions to backups, and — for Hot
(b) — merging of concurrent reporting streams with exact value-identity
de-duplication. HotAndMirrored failover re-activates the mirrored
session with the existing `AuthenticationToken` instead of recreating
it.
- Network redundancy: alternate communication paths via endpoint
selection.

## Distributed state (opt-in; an extension beyond §6.6)

- `ISharedKeyValueStore` (in-memory default) and `INodeStateStore`, with
an `AddressSpaceSynchronizer` that bridges a `CustomNodeManager2`'s
predefined nodes to the shared store, so node and reference
additions/removals and value changes propagate to other replicas.
`IDistributedValueCache` lets read/write callbacks cache the last value
with a freshness bound; monitored items use the normal read pipeline and
therefore participate only when the read path does.
- **Secure session sharing (active/passive fast reconnect), server and
client.** `DistributedSessionManager` (wired through a new additive
`ISessionManagerFactory` seam on `StandardServer`) mirrors **encrypted**
session state across replicas. On a failover reconnect the standby
restores the session and performs the **full `ActivateSession`
client-certificate signature validation** against a **single-use**
`serverNonce` consumed via compare-and-swap across the replica set,
enforcing the same SecurityPolicy and SecurityMode. The
`AuthenticationToken` is a **lookup key only**, never an authenticator:
entries are keyed by the SHA-256 digest of the token, and a
cross-replica restore emits a distinct audit event.
- Subscription, retransmission-queue, and continuation-point mirroring
for subscription transfer and failover.
- Leader election: a static single-leader provider and a shared-store
**lease via compare-and-swap**, supporting active/passive and
active/active (shared-read / master-write).
- Fail-closed security defaults: shared records are protected at rest
through `IRecordProtector`; secret-bearing mirrors require a registered
protector or an explicit opt-out.

## Active/active (CRDT)

`Opc.Ua.Server.Redundancy.Crdt` adds leaderless multi-writer
address-space replication with CRDTs and gossip
(`UseReplicatedAddressSpace`) plus CRDT-backed session metadata
(`UseReplicatedSessions`). Networked gossip is **fail-closed without
authenticated transport** (mutual TLS for TCP; an explicit opt-out for
isolated dev/test). CRDT state is eventually consistent, so exactly-once
decisions (such as the single-use nonce registry) stay on a strongly
consistent store. The package is available on all stack target
frameworks.

## Kubernetes

`Opc.Ua.Server.Redundancy.K8s` provides Kubernetes Lease leader
election, EndpointSlice-based peer discovery, and ServiceLevel-driven
readiness for running an OPC UA server replica set.
`Docs/HighAvailabilityKubernetes.md` covers StatefulSet/Deployment and
Service manifests, RBAC, probes, time synchronization, secrets,
gossip-port NetworkPolicy, and GDS/NTRS registration.

## Compatibility and validation

- **Zero-overhead** single-instance default; additive,
backward-compatible seams (for example `ISessionManagerFactory` and
`IServerStartupTask`); replaced APIs marked `[Obsolete]` for migration.
- Builds across the full stack TFM set (`net472`, `net48`,
`netstandard2.0`, `netstandard2.1`, `net8.0`, `net9.0`, `net10.0`);
NativeAOT-compatible where the runtime supports it.
- Unit and integration tests for server, client, subscriptions, session
mirroring, CRDT, and Kubernetes; the conformance suite stays green.

See `Docs/HighAvailability.md` for the full OPC 10000-4 §6.6 mapping,
the `Add*` (standard nodes) versus `Use*` (extension) builder
convention, and the security/rotation guidance.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-11 04:47:25 +02:00
Marc Schier 4d613d11dc CI: migrate to netstandard Managed DevOps Pool + per-TFM build matrix (#3866)
# Description

End-to-end migration of the Azure DevOps CI pipelines to the
`netstandard` Managed DevOps Pool with the build matrix re-shaped to fit
the new agents and a handful of test/project fixes that fell out of the
migration.

**Pipeline routing & defaults**

- `azure-pipelines.yml` and `azure-pipelines-preview.yml` default
`managedPool=true`, routing Windows/Linux jobs to the `netstandard`
Managed DevOps Pool with `ImageOverride` demands; flipping the
queue-time boolean to `false` falls back to Microsoft-hosted agents.
- Per-platform images come from the root yml (single source of truth):
- Managed pool: `windows-2022-g2` / `ubuntu-22.04-g2` (Mac stays hosted:
`macOS-15`).
  - Hosted: `windows-2025-vs2026` / `ubuntu-24.04` / `macOS-15`.
- `.azurepipelines/test.yml` and `test-aot.yml` declare `hosted` as a
typed `boolean` parameter so `not(parameters.hosted)` actually evaluates
correctly (an untyped value left it as the string `'false'`, which is
truthy and silently routed managed-pool jobs to Microsoft-hosted
`vmImage` demands).
- macOS test jobs always run on the Microsoft-hosted `macOS-15` image
regardless of `managedPool`; the four per-framework test stages queue a
dedicated `hosted: true` Mac invocation unconditionally and the main
matrix carries only Windows+Linux.
- Test-template prep jobs (which only run a PowerShell matrix script)
run on the managed pool whenever a `poolName` is set, even when the
matrix `hosted: true`, so the Microsoft-hosted parallel-job allotment
isn't blocked.

**Build stage — per-(TFM × configuration) matrix**

- `.azurepipelines/get-matrix.ps1` gained a `-Tfms` parameter that fans
the matrix across target frameworks alongside the existing agent /
configuration dimensions and emits a per-entry `targetTfm`.
- `.azurepipelines/sln.yml` accepts a `tfms` parameter and threads
`$(targetTfm)` into both `dotnet restore` and `dotnet build` as
`/p:CustomTestTarget=$(targetTfm)` so each matrix job builds a single
TFM and fits on the Managed DevOps Pool's small (~16 GB) temp disk.
- `azure-pipelines.yml` `build` stage passes `tfms: 'net10.0,net48'` for
PR builds and the full
`net472,net48,netstandard2.0,netstandard2.1,net8.0,net9.0,net10.0` set
for master / scheduled / manual / fork-PR runs.
- `sln.yml`'s restore step uses `command: custom` (`custom: restore`)
because `command: restore` silently drops the `arguments` input —
without it `/p:CustomTestTarget` never reached restore and
`obj/project.assets.json` was populated with the wrong TFM set.
- `sln.yml` no longer uses the `MSBuild@1` task; it now uses
`DotNetCoreCLI@2` with `dotnet build` so the .NET 10 SDK's bundled
MSBuild (which understands `.slnx`) is used instead of Visual Studio
2022's MSBuild on the managed-pool image.

**Project-level fixes that fell out of the matrix split**

- `Applications/McpServer`, `Applications/MinimalBoilerServer`,
`Applications/MinimalCalcServer`, and `Tests/Opc.Ua.Aot.Tests` each get
a `Directory.Build.targets` that turns the project into a no-op (cleared
`Compile` / `ProjectReference` / `PackageReference` / etc.,
`OutputType=Library`) when `CustomTestTarget` restricts the matrix to a
legacy TFM (`net4*` / `netstandard2.*`) the project can't target. Target
overrides are deliberately avoided — same-named targets in
`Directory.Build.targets` ALWAYS replace the SDK definitions, which
would silently break `dotnet publish` / `dotnet test` in the
unrestricted GitHub Actions flows.
- `Tools/Opc.Ua.MigrationAnalyzer.csproj`'s `ProjectReference` to
`Opc.Ua.MigrationAnalyzer.Core` no longer hard-pins
`SetTargetFramework=net10.0`; the pin follows `CustomTestTarget` (and
falls back to `net10.0` when unset).

**Test reliability**

-
`Tests/Opc.Ua.PubSub.Tests/IntervalRunnerTests.FakeTimeProviderDrivesDeterministicSchedulingAsync`
no longer calls a single `fake.Advance(300)` expecting three timers to
fire — it advances one 100 ms interval at a time and waits for each fire
(the runner only registers the next `Delay` after the previous callback
resumes on the thread pool; on slower CI agents the race surfaces
consistently).
- `Tests/Opc.Ua.Server.Tests/Fluent/SimulationBuilderExtensionsTests`
(four "expect ≥ N ticks in 150 ms" tests) replace fixed
`Task.Delay`/`Thread.Sleep` waits with a `WaitForAsync` polling helper,
eliminating thread-pool cold-start flakiness on slow agents (observed on
the hosted `macOS-15` runner).

## Related Issues

_No tracking issue — pipeline tuning to standardize on the netstandard
Managed DevOps Pool, plus the test/project follow-ups required for the
matrix to be green on that pool._

## 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. _(N/A — pipeline +
test-reliability fixes; the two test fixes themselves were verified to
pass 5× in stress runs locally.)_
- [x] I have added all necessary documentation. _(Comments inline in the
new `Directory.Build.targets`, `sln.yml`, and `azure-pipelines.yml`
explain the unusual MSBuild/`DotNetCoreCLI@2` interactions for future
maintainers.)_
- [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. _(Verified
`dotnet build UA.slnx -p:CustomTestTarget=net10.0 -c Release` and
`-p:CustomTestTarget=net48 -c Debug` both complete with 0 errors;
targeted test runs for the two modified test files pass 5/5.)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-10 17:19:41 +02:00