Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
46fc78c
Squash PR #4516 (bounded recovery + remaining audit findings) as base
romanett Sep 24, 2026
801af55
Fix L9-1, L9-2, L9-3, L9-4, L9-5: bound empty browse pages, release p…
romanett Sep 24, 2026
8ad6d7c
Fix L9-9, L9-10: never fall back to a None endpoint when security is …
romanett Sep 24, 2026
ef1bf01
Fix L9-6, L9-8: track missing value slots explicitly, honour cancella…
romanett Sep 24, 2026
dcaa695
Fix L4-2, L4-3: tear down demoted leader session; close standby sessi…
romanett Sep 24, 2026
d93027d
Fix L9-7: fail only the node whose NodeClass does not match
romanett Sep 24, 2026
75957c8
Fix L9-11: keep tracking alive while a joined start is still waiting
romanett Sep 24, 2026
eeae27e
Fix L4-1: avoid Math.Abs(int.MinValue) in classic ack reconciliation
romanett Sep 24, 2026
97a5134
Fix L7-11: MonitoredItem.LastMessage no longer throws on a mismatched…
romanett Sep 24, 2026
8486880
Fix L7-5: cloned monitored items keep their triggering links
romanett Sep 24, 2026
57116f6
Fix L7-9: no int overflow for very long keep-alive intervals
romanett Sep 24, 2026
9acb306
Fix L7-10: a negative MaxMessageCount no longer stops notification de…
romanett Sep 24, 2026
9a4723e
Fix L7-3: Dispose from a keep-alive PublishStatusChanged handler no l…
romanett Sep 24, 2026
d747db4
Fix L3-1: never downgrade endpoint security on reconnect endpoint ref…
romanett Sep 24, 2026
0eb493a
Fix L3-5: bound ReadBytesAsync against servers ignoring the IndexRange
romanett Sep 24, 2026
3fd7977
Fix L3-6: chunk with the client limit when MaxByteStringLength is not…
romanett Sep 24, 2026
545abd1
Fix L3-4, L3-7, L2-1: connection state machine close race, budget val…
romanett Sep 24, 2026
4553a58
Fix L7-6: create restored items that still carry a stale server id
romanett Sep 24, 2026
90fdd41
Fix L7-8: queue an item removed during its create request for deletion
romanett Sep 24, 2026
f53629f
Fix L3-2, L3-3: release batch resources on failure and rebase diagnos…
romanett Sep 24, 2026
5b24747
Fix L8-1, L8-2: reverse connect Undefined strategy and replaced manua…
romanett Sep 24, 2026
f243d9d
Fix L7-4: transfer restored subscriptions with never-created items, d…
romanett Sep 24, 2026
a36b664
Fix L7-1: a Republish response no longer wipes the available sequence…
romanett Sep 24, 2026
9fd2e2f
Fix L7-2: never deliver a late copy of an already processed message a…
romanett Sep 24, 2026
8829f72
Fix L7-7: ignore the Good_SubscriptionTransferred of a transfer this …
romanett Sep 24, 2026
67cb96d
Fix L5-1: skip already-processed messages in transfer recovery
romanett Sep 24, 2026
dc44755
Fix L5-5: recover messages missing before a keep-alive
romanett Sep 24, 2026
4c48372
Fix L5-2: tolerate duplicate client handles in TrySynchronizeHandlesA…
romanett Sep 24, 2026
baad7e1
Fix L5-3: do not retry failed item changes back-to-back in one apply …
romanett Sep 24, 2026
65c8985
Fix L5-4, L6-1: saturate server-revised durations instead of throwing
romanett Sep 24, 2026
db88bec
Fix L5-6: treat null and empty IndexRange as equal
romanett Sep 24, 2026
3ca6aba
Fix L5-7: break lock-order deadlock between reset paths and SetTrigge…
romanett Sep 24, 2026
3fc54e6
Fix L6-2: roll back a group restore when a secondary partition fails
romanett Sep 24, 2026
e1b779c
Fix L6-3: end the publish request before handling an unresolved subsc…
romanett Sep 24, 2026
9cd3d20
Fix L6-4: re-resolve the subscription id before deleting it as an orphan
romanett Sep 24, 2026
7e32548
Fix L6-5: never drop an acknowledgement when the ack wait ends
romanett Sep 24, 2026
db72eae
Fix L6-6: synchronise StreamingSubscription dispose with first subscribe
romanett Sep 24, 2026
a67b314
Fix L1-3: do not drain background work from a handler running on it
romanett Sep 24, 2026
85cbdf0
Fix L1-5: parse the restored server certificate as a chain blob
romanett Sep 24, 2026
f009b79
Fix L1-1: bound the keep-alive read by its TimeoutHint
romanett Sep 24, 2026
0210cfd
Fix L2-4: re-derive channel-reconnect suppression on a lease swap
romanett Sep 24, 2026
3bc76e0
Fix L1-2, L1-4, L1-6: session identity update and recovery completion
romanett Sep 24, 2026
0a35f8c
Fix L4-4: bind prepared backup subscriptions on client-redundancy tak…
romanett Sep 24, 2026
6b60b46
Fix L4-6: apply health checks in the ShouldFailover extension
romanett Sep 24, 2026
7745197
Fix L10-1, L10-2: resolve instance sub-state machine; stop reporting …
romanett Sep 24, 2026
3ba8b96
Fix L10-3, L10-4: page the RoleSet browse; keep role property reads a…
romanett Sep 24, 2026
209952d
Fix L10-5, L10-6, L10-7: DI session accessor lifetime, redundancy opt…
romanett Sep 24, 2026
6c8fbc1
Fix L2-6: ManagedSession.ConfiguredEndpoint follows failover
romanett Sep 24, 2026
fe1001f
Fix L2-7: explicit identity update stops the provider refresh loop
romanett Sep 24, 2026
0d4f27f
Fix L2-2: wait for a far-away identity expiry in timer-sized chunks
romanett Sep 24, 2026
f27a228
Fix L2-3: bound the reconnect/failover wait for in-flight service calls
romanett Sep 24, 2026
c814b27
Fix L11-4: skip null ServerProfileArray elements in GetConformanceInf…
romanett Sep 24, 2026
819dd9d
Fix L11-5, L11-6, L11-7: alias resolver fallback, merging and degrade…
romanett Sep 24, 2026
46c0818
Fix L12-3, L12-4: validate union SwitchField and decode Verbose JSON …
romanett Sep 24, 2026
715a728
Fix L12-1, L12-2: map WebApi transport failures, apply OperationTimeo…
romanett Sep 24, 2026
72ce74f
Fix L11-2: wait for ReadTransfer before returning a generated tempora…
romanett Sep 24, 2026
8b925a6
Merge branch 'romanett/cfx-subs' into romanett/opc-ua-client-bugs-786838
romanett Sep 24, 2026
9b43136
Fix L11-1, L11-3, L11-8: stale path cache, unfiltered empty check, ro…
romanett Sep 24, 2026
f96ae7d
Merge branch 'romanett/cfx-redundancy' into romanett/opc-ua-client-bu…
romanett Sep 24, 2026
436bcda
Merge branch 'romanett/cfx-reconnect' into romanett/opc-ua-client-bug…
romanett Sep 24, 2026
290ebb9
Fix L7-4: accept transfers of cloned subscriptions without server ids
romanett Sep 24, 2026
36ba012
Merge branch 'romanett/cfx-files' into romanett/opc-ua-client-bugs-78…
romanett Sep 24, 2026
d3b0f92
Merge branch 'romanett/cfx-session' into romanett/opc-ua-client-bugs-…
romanett Sep 24, 2026
147e190
Fix CA1861 warnings in new reconnect-batch tests
romanett Sep 24, 2026
653c5ce
Merge branch 'romanett/cfx-classic' into romanett/opc-ua-client-bugs-…
romanett Sep 24, 2026
794ffe8
Match the replica test channel manager by endpoint value
romanett Sep 24, 2026
a0fe969
Fix review G17: accept TLS-protected https None endpoint under useSec…
romanett Sep 25, 2026
2c6f644
Fix review G27: delete a transferred subscription only on definitive …
romanett Sep 25, 2026
dafa310
Fix review G8a: keep the recovery writer exclusive while the channel …
romanett Sep 25, 2026
f5749b5
Fix review G1: demoted token-reuse leader abandons the shared session
romanett Sep 25, 2026
bddbe28
Fix review G20: decide orphan delete on the pending snapshot taken be…
romanett Sep 25, 2026
78041e3
Fix review G21: re-arm the dispatch release signal when a reset wait …
romanett Sep 25, 2026
1168ff4
Fix review G22+G23: track undelivered republishes and advance the kee…
romanett Sep 25, 2026
6097023
Fix review G6: close a managed session from its own handlers without …
romanett Sep 25, 2026
e417e8b
Fix review G9: interrupt operations queued on the reconnect lock too
romanett Sep 25, 2026
6cb8424
Fix review G14: bound the Web API open-time GetEndpoints by Operation…
romanett Sep 25, 2026
4f668d1
Fix review G16: cap empty BrowseNext rounds in role and file system l…
romanett Sep 25, 2026
08194e4
Fix review G12: make UaPath round-trip names ending in '&' or '/'
romanett Sep 25, 2026
8f58b52
Fix review G3: report demotion before and independent of the standby …
romanett Sep 25, 2026
4d31211
Fix review G4: keep an ephemeral key after a failed ECC-to-ECC override
romanett Sep 25, 2026
0fbce43
Fix review G2: promotion retry keeps the adopted mirrored session
romanett Sep 25, 2026
546f91a
Merge branch 'romanett/rvw-fc' into romanett/opc-ua-client-bugs-786838
romanett Sep 25, 2026
8a2b77e
Merge branch 'romanett/rvw-fd' into romanett/opc-ua-client-bugs-786838
romanett Sep 25, 2026
2ad3ad1
Merge branch 'romanett/rvw-fb' into romanett/opc-ua-client-bugs-786838
romanett Sep 25, 2026
cb0764a
Merge branch 'romanett/rvw-fa' into romanett/opc-ua-client-bugs-786838
romanett Sep 25, 2026
7990669
Merge origin/master into romanett/opc-ua-client-bugs-786838
romanett Sep 28, 2026
9f52952
Wait for the spawner work item in the G5 regression test
romanett Sep 28, 2026
8d248f5
Fix CI: net48/netstandard builds and WebApi throttling mapping
romanett Sep 28, 2026
3265665
Address review: tracker stop race, WebApiClient cleartext guard, Idle…
romanett Sep 28, 2026
244741f
Merge remote-tracking branch 'origin/master' into romanett/opc-ua-cli…
Copilot Sep 29, 2026
5e7948b
Merge branch 'master' into romanett/opc-ua-client-bugs-786838
marcschier Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/HighAvailability.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ ArrayOf<TransferResult> results = await coordinator.TransferActiveSubscriptionsA
ct);
```

A transferred subscription needs a client-side owner on the backup session, otherwise the backup's publish engine sees an unknown `SubscriptionId` and deletes it (or never publishes and it expires). Prepare the backup before the takeover: restore the active client's subscriptions with `ISession.Load` (or add `Subscription` objects whose `TransferId` is the active client's subscription id and whose monitored items carry the same client handles). The coordinator then transfers those through the session, which binds them and resumes publishing; ids without a prepared subscription are still moved with the raw service and are left to the caller.

OPC UA does not standardize how active and backup clients exchange `SessionId` or subscription ids. In this stack the client replica set coordinates through the registered client-side shared store (`AddRedundantClientSharedStore` / `AddRaftClientSharedStore` — a CRDT- or [Raft](https://raft.github.io/)-backed `ISharedKeyValueStore` that the `ClientReplicaCoordinator` consumes).

## Network redundancy (as per Part 4 §6.6.4)
Expand Down
21 changes: 19 additions & 2 deletions samples/Reference/ConsoleReferenceClient/UAClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ public async Task<bool> ConnectAsync(
useSecurity,
m_telemetry,
ct
).ConfigureAwait(false);
).ConfigureAwait(false) ??
throw NoMatchingEndpoint(useSecurity);
connection = null;
}
} while (connection == null);
Expand All @@ -253,7 +254,8 @@ public async Task<bool> ConnectAsync(
serverUrl,
useSecurity,
m_telemetry,
ct).ConfigureAwait(false);
ct).ConfigureAwait(false) ??
throw NoMatchingEndpoint(useSecurity);
}

// Get the endpoint by connecting to server's discovery endpoint.
Expand Down Expand Up @@ -322,6 +324,21 @@ public async Task<bool> ConnectAsync(
}
}

/// <summary>
/// The error reported when discovery found no endpoint that satisfies
/// the security requirement (SelectEndpoint returned null).
/// </summary>
private static ServiceResultException NoMatchingEndpoint(bool useSecurity)
{
return useSecurity
? new ServiceResultException(
StatusCodes.BadSecurityPolicyRejected,
"The server offers no secure endpoint supported by this client.")
: new ServiceResultException(
StatusCodes.BadNotFound,
"The server offers no matching endpoint.");
}

private ValueTask<IUserIdentity> CreateUserIdentityAsync(
EndpointDescription endpointDescription,
CancellationToken ct)
Expand Down
10 changes: 6 additions & 4 deletions src/Opc.Ua.Client.ComplexTypes/Types/UnionComplexType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,15 @@ public override void Decode(IDecoder decoder)
bool isJsonDecoder = decoder.EncodingType == EncodingType.Json;
if (unionSelector == 0 && isJsonDecoder)
{
// The Verbose JSON encoding writes no SwitchField, so the
// selector has to be recovered from the member name. Every
// union field is a candidate: union fields are never
// IsOptional (Part 3 §8.51), so filtering by it left the list
// empty and every such union decoded as null.
var fields = new List<string>();
foreach (ComplexTypePropertyInfo property in GetPropertyEnumerator())
{
if (property.IsOptional)
{
fields.Add(property.Name);
}
fields.Add(property.Name);
}

unionSelector = decoder.ReadSwitchField(fields, out _);
Expand Down
6 changes: 5 additions & 1 deletion src/Opc.Ua.Client/AliasNames/AliasNameClientErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ public static Exception Translate(
: string.Empty) +
".");
}
// BadMethodInvalid is what a server returns when the type-level
// MethodId is called on a category that does not expose the
// (optional) method (Part 4 §5.12.2.4), so it means "not supported".
if (code == StatusCodes.BadNotSupported ||
code == StatusCodes.BadNotImplemented)
code == StatusCodes.BadNotImplemented ||
code == StatusCodes.BadMethodInvalid)
{
return new NotSupportedException(
$"OPC UA Part 17 {operation} is not supported by this " +
Expand Down
59 changes: 55 additions & 4 deletions src/Opc.Ua.Client/AliasNames/AliasNameResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Opc.Ua.Client.AliasNames.Refresh;

namespace Opc.Ua.Client.AliasNames
Expand Down Expand Up @@ -81,6 +82,8 @@ public AliasNameResolver(
Options = (options ?? new AliasNameResolverOptions()).Clone();
m_strategy = Options.RefreshStrategy
?? BuildBuiltInStrategy(Options);
m_logger = client.Session.MessageContext.Telemetry
.CreateLogger<AliasNameResolver>();
}

/// <summary>The wrapped <see cref="AliasNameClient"/>.</summary>
Expand All @@ -104,7 +107,19 @@ public async Task EnsureLoadedAsync(CancellationToken ct = default)
{
return;
}
await EnsureStrategyStartedAsync(ct).ConfigureAwait(false);
try
{
await EnsureStrategyStartedAsync(ct).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not OperationCanceledException ||
!ct.IsCancellationRequested)
{
// The refresh strategy only drives invalidation; a server that
// rejects its subscription (or lacks the optional LastChange)
// must not make alias resolution fail. Run degraded (manual
// refresh) and retry the start on the next load.
m_logger.AliasRefreshStrategyStartFailed(ex, m_strategy.GetType().Name);
}
await RefreshAsync(ct).ConfigureAwait(false);
}

Expand Down Expand Up @@ -391,10 +406,29 @@ private static void PopulateFromNonVerbose(
arr[i] = a.ReferencedNodes[i];
reverse[arr[i]] = key;
}
forward[key] = arr;
forward[key] = Merge(forward, key, arr);
}
}

/// <summary>
/// The cache is keyed by <see cref="QualifiedName.Name"/> only, but
/// FindAlias may legally return the same name more than once (other
/// namespace, or defined in several sub-categories). Append the
/// targets of a repeated name instead of replacing the earlier ones.
/// </summary>
/// <typeparam name="T">The element type of the cached arrays.</typeparam>
private static T[] Merge<T>(Dictionary<string, T[]> map, string key, T[] values)
{
if (!map.TryGetValue(key, out T[]? existing) || existing.Length == 0)
{
return values;
}
var merged = new T[existing.Length + values.Length];
Array.Copy(existing, merged, existing.Length);
Array.Copy(values, 0, merged, existing.Length, values.Length);
return merged;
}

private static void PopulateFromVerbose(
IReadOnlyList<AliasNameVerboseDataType> aliases,
Dictionary<string, ExpandedNodeId[]> forward,
Expand All @@ -413,14 +447,17 @@ private static void PopulateFromVerbose(
uris[i] = i < a.ServerUris.Count ? a.ServerUris[i] : null;
reverse[arr[i]] = key;
}
forward[key] = arr;
serverUris[key] = uris;
// Merge both maps the same way so the ServerUris stay
// parallel to the targets.
forward[key] = Merge(forward, key, arr);
serverUris[key] = Merge(serverUris, key, uris);
}
}

private readonly SemaphoreSlim m_semaphore = new(1, 1);
private readonly SemaphoreSlim m_strategyStartLock = new(1, 1);
private readonly IAliasNameRefreshStrategy m_strategy;
private readonly ILogger m_logger;

private Dictionary<string, ExpandedNodeId[]> m_forward
= new(StringComparer.Ordinal);
Expand All @@ -446,4 +483,18 @@ private Dictionary<string, ExpandedNodeId[]> m_forward
private long m_invalidationGeneration;
private int m_strategyStarted;
}

/// <summary>
/// Source-generated logging for <see cref="AliasNameResolver"/>.
/// </summary>
internal static partial class AliasNameResolverLog
{
[LoggerMessage(EventId = ClientEventIds.AliasNameResolver + 0, Level = LogLevel.Warning,
Message = "Alias refresh strategy {Strategy} failed to start; resolving without " +
"automatic invalidation and retrying the start on the next load.")]
public static partial void AliasRefreshStrategyStartFailed(
this ILogger logger,
Exception exception,
string strategy);
}
}
Loading
Loading