docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
This commit is contained in:
Joseph Doherty
2026-05-28 08:10:17 -04:00
parent f9fc7dd2e1
commit 64e3fbe035
756 changed files with 9876 additions and 96 deletions
@@ -15,12 +15,15 @@ public sealed record AuthorizationDecision(
AuthorizationVerdict Verdict,
IReadOnlyList<MatchedGrant> Provenance)
{
/// <summary>Gets a value indicating whether the authorization decision allows the operation.</summary>
public bool IsAllowed => Verdict == AuthorizationVerdict.Allow;
/// <summary>Convenience constructor for the common "no grants matched" outcome.</summary>
public static AuthorizationDecision NotGranted() => new(AuthorizationVerdict.NotGranted, []);
/// <summary>Allow with the list of grants that matched.</summary>
/// <param name="provenance">The list of grants that matched to allow the operation.</param>
/// <returns>An authorization decision with Allow verdict.</returns>
public static AuthorizationDecision Allowed(IReadOnlyList<MatchedGrant> provenance)
=> new(AuthorizationVerdict.Allow, provenance);
}
@@ -19,5 +19,8 @@ public interface IPermissionEvaluator
/// failure to <c>BadUserAccessDenied</c> per OPC UA Part 4 when the result is not
/// <see cref="AuthorizationVerdict.Allow"/>.
/// </summary>
/// <param name="session">The user session containing resolved LDAP groups and roles.</param>
/// <param name="operation">The OPC UA operation being requested.</param>
/// <param name="scope">The node address scope being accessed.</param>
AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope);
}
@@ -31,6 +31,8 @@ public sealed class PermissionTrie
/// session's <paramref name="ldapGroups"/>. Returns the matched-grant list; the caller
/// OR-s the flag bits to decide whether the requested permission is carried.
/// </summary>
/// <param name="scope">The node scope to match permissions for.</param>
/// <param name="ldapGroups">The user's LDAP group memberships.</param>
public IReadOnlyList<MatchedGrant> CollectMatches(NodeScope scope, IEnumerable<string> ldapGroups)
{
ArgumentNullException.ThrowIfNull(scope);
@@ -23,6 +23,7 @@ public sealed class PermissionTrieCache
new(StringComparer.OrdinalIgnoreCase);
/// <summary>Install a trie for a cluster + make it the current generation.</summary>
/// <param name="trie">The permission trie to install.</param>
public void Install(PermissionTrie trie)
{
ArgumentNullException.ThrowIfNull(trie);
@@ -32,6 +33,7 @@ public sealed class PermissionTrieCache
}
/// <summary>Get the current-generation trie for a cluster; null when nothing installed.</summary>
/// <param name="clusterId">The cluster identifier.</param>
public PermissionTrie? GetTrie(string clusterId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
@@ -39,6 +41,8 @@ public sealed class PermissionTrieCache
}
/// <summary>Get a specific (cluster, generation) trie; null if that pair isn't cached.</summary>
/// <param name="clusterId">The cluster identifier.</param>
/// <param name="generationId">The generation identifier.</param>
public PermissionTrie? GetTrie(string clusterId, long generationId)
{
if (!_byCluster.TryGetValue(clusterId, out var entry)) return null;
@@ -46,10 +50,12 @@ public sealed class PermissionTrieCache
}
/// <summary>The generation id the <see cref="GetTrie(string)"/> shortcut currently serves for a cluster.</summary>
/// <param name="clusterId">The cluster identifier.</param>
public long? CurrentGenerationId(string clusterId)
=> _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current.GenerationId : null;
/// <summary>Drop every cached trie for one cluster.</summary>
/// <param name="clusterId">The cluster identifier.</param>
public void Invalidate(string clusterId) => _byCluster.TryRemove(clusterId, out _);
/// <summary>
@@ -59,6 +65,8 @@ public sealed class PermissionTrieCache
/// class-typed entry) so a concurrent <see cref="Install"/> on the same cluster is never
/// silently overwritten.
/// </summary>
/// <param name="clusterId">The cluster identifier.</param>
/// <param name="keepLatest">The number of most recent generations to retain.</param>
public void Prune(string clusterId, int keepLatest = 3)
{
if (keepLatest < 1) throw new ArgumentOutOfRangeException(nameof(keepLatest), keepLatest, "keepLatest must be >= 1");
@@ -96,12 +104,18 @@ public sealed class PermissionTrieCache
// Class (not record) so TryUpdate in Prune uses reference equality for the CAS comparison.
private sealed class ClusterEntry(PermissionTrie current, IReadOnlyDictionary<long, PermissionTrie> tries)
{
/// <summary>Gets the current (latest) permission trie for the cluster.</summary>
public PermissionTrie Current { get; } = current;
/// <summary>Gets all cached tries for the cluster keyed by generation ID.</summary>
public IReadOnlyDictionary<long, PermissionTrie> Tries { get; } = tries;
/// <summary>Creates a cluster entry from a single trie.</summary>
/// <param name="trie">The permission trie to create the entry from.</param>
public static ClusterEntry FromSingle(PermissionTrie trie) =>
new(trie, new Dictionary<long, PermissionTrie> { [trie.GenerationId] = trie });
/// <summary>Creates a new entry with an additional trie, updating current if it's newer.</summary>
/// <param name="trie">The new permission trie to add.</param>
public ClusterEntry WithAdditional(PermissionTrie trie)
{
var next = new Dictionary<long, PermissionTrie>(Tries) { [trie.GenerationId] = trie };
@@ -14,6 +14,9 @@ public sealed class TriePermissionEvaluator : IPermissionEvaluator
private readonly PermissionTrieCache _cache;
private readonly TimeProvider _timeProvider;
/// <summary>Initializes a new instance of the TriePermissionEvaluator class.</summary>
/// <param name="cache">The permission trie cache.</param>
/// <param name="timeProvider">The time provider for checking authorization state staleness; if null, uses system time.</param>
public TriePermissionEvaluator(PermissionTrieCache cache, TimeProvider? timeProvider = null)
{
ArgumentNullException.ThrowIfNull(cache);
@@ -21,6 +24,11 @@ public sealed class TriePermissionEvaluator : IPermissionEvaluator
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <summary>Authorizes an operation against the user's session and node scope.</summary>
/// <param name="session">The user's authorization session.</param>
/// <param name="operation">The OPC UA operation to authorize.</param>
/// <param name="scope">The target node scope.</param>
/// <returns>An authorization decision indicating whether the operation is allowed.</returns>
public AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope)
{
ArgumentNullException.ThrowIfNull(session);
@@ -65,6 +73,8 @@ public sealed class TriePermissionEvaluator : IPermissionEvaluator
}
/// <summary>Maps each <see cref="OpcUaOperation"/> to the <see cref="NodePermissions"/> bit required to grant it.</summary>
/// <param name="op">The OPC UA operation.</param>
/// <returns>The required node permission for the operation.</returns>
public static NodePermissions MapOperationToPermission(OpcUaOperation op) => op switch
{
OpcUaOperation.Browse => NodePermissions.Browse,
@@ -63,6 +63,7 @@ public sealed record UserAuthorizationState
/// <see cref="AuthCacheMaxStaleness"/>. The evaluator short-circuits to NotGranted
/// whenever this is true.
/// </summary>
/// <param name="utcNow">The current UTC time.</param>
public bool IsStale(DateTime utcNow) => utcNow - MembershipResolvedUtc > AuthCacheMaxStaleness;
/// <summary>
@@ -70,6 +71,7 @@ public sealed record UserAuthorizationState
/// ceiling — a signal to the caller to kick off an async refresh, while the current
/// call still evaluates against the cached memberships.
/// </summary>
/// <param name="utcNow">The current UTC time.</param>
public bool NeedsRefresh(DateTime utcNow) =>
!IsStale(utcNow) && utcNow - MembershipResolvedUtc > MembershipFreshnessInterval;
}
@@ -62,6 +62,7 @@ public sealed class DriverFactoryRegistry
/// if no driver assembly registered one — bootstrapper logs + skips so a
/// missing-assembly deployment doesn't take down the whole server.
/// </summary>
/// <param name="driverType">The driver type to look up.</param>
public Func<string, string, IDriver>? TryGet(string driverType)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
@@ -73,12 +74,14 @@ public sealed class DriverFactoryRegistry
/// for unknown driver types — a missing registration is already a skipped-bootstrap
/// case upstream; we don't double-surface that failure here.
/// </summary>
/// <param name="driverType">The driver type to look up.</param>
public DriverTier GetTier(string driverType)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
lock (_lock) return _tiers.GetValueOrDefault(driverType, DriverTier.A);
}
/// <summary>Gets the collection of registered driver type names.</summary>
public IReadOnlyCollection<string> RegisteredTypes
{
get { lock (_lock) return [.. _factories.Keys]; }
@@ -12,17 +12,24 @@ public sealed class DriverFactoryRegistryAdapter : IDriverFactory
{
private readonly DriverFactoryRegistry _registry;
/// <summary>Initializes the adapter with the underlying registry.</summary>
/// <param name="registry">The driver factory registry to adapt.</param>
public DriverFactoryRegistryAdapter(DriverFactoryRegistry registry)
{
ArgumentNullException.ThrowIfNull(registry);
_registry = registry;
}
/// <summary>Attempts to create a driver instance by type and configuration.</summary>
/// <param name="driverType">The driver type name.</param>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="driverConfigJson">The driver configuration as a JSON string.</param>
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)
{
var factory = _registry.TryGet(driverType);
return factory?.Invoke(driverInstanceId, driverConfigJson);
}
/// <summary>Gets the collection of supported driver type names.</summary>
public IReadOnlyCollection<string> SupportedTypes => _registry.RegisteredTypes;
}
@@ -13,11 +13,14 @@ public sealed class DriverHost : IAsyncDisposable
private readonly Dictionary<string, IDriver> _drivers = new();
private readonly object _lock = new();
/// <summary>Gets the collection of registered driver instance identifiers.</summary>
public IReadOnlyCollection<string> RegisteredDriverIds
{
get { lock (_lock) return [.. _drivers.Keys]; }
}
/// <summary>Gets the health status of a registered driver.</summary>
/// <param name="driverInstanceId">The driver instance identifier to query.</param>
public DriverHealth? GetHealth(string driverInstanceId)
{
lock (_lock)
@@ -29,6 +32,7 @@ public sealed class DriverHost : IAsyncDisposable
/// (<c>OtOpcUaServer</c>) to instantiate one <c>DriverNodeManager</c> per driver at
/// startup. Returns null when the driver is not registered.
/// </summary>
/// <param name="driverInstanceId">The driver instance identifier to look up.</param>
public IDriver? GetDriver(string driverInstanceId)
{
lock (_lock)
@@ -40,6 +44,9 @@ public sealed class DriverHost : IAsyncDisposable
/// throws, the driver is kept in the registry so the operator can retry; quality on its
/// nodes will reflect <see cref="DriverState.Faulted"/> until <c>Reinitialize</c> succeeds.
/// </summary>
/// <param name="driver">The driver instance to register.</param>
/// <param name="driverConfigJson">The configuration JSON for the driver.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public async Task RegisterAsync(IDriver driver, string driverConfigJson, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(driver);
@@ -60,6 +67,9 @@ public sealed class DriverHost : IAsyncDisposable
}
}
/// <summary>Unregisters a driver and calls shutdown.</summary>
/// <param name="driverInstanceId">The driver instance identifier to unregister.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public async Task UnregisterAsync(string driverInstanceId, CancellationToken ct)
{
IDriver? driver;
@@ -73,6 +83,7 @@ public sealed class DriverHost : IAsyncDisposable
catch { /* shutdown is best-effort; logs elsewhere */ }
}
/// <summary>Disposes the driver host and all registered drivers.</summary>
public async ValueTask DisposeAsync()
{
List<IDriver> snapshot;
@@ -26,6 +26,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Observability;
public static class DriverHealthReport
{
/// <summary>Compute the fleet-wide readiness verdict from per-driver states.</summary>
/// <param name="drivers">The list of per-driver health snapshots to aggregate.</param>
public static ReadinessVerdict Aggregate(IReadOnlyList<DriverHealthSnapshot> drivers)
{
ArgumentNullException.ThrowIfNull(drivers);
@@ -52,6 +53,7 @@ public static class DriverHealthReport
/// Map a <see cref="ReadinessVerdict"/> to the HTTP status the /readyz endpoint should
/// return per the Stream C.1 state matrix.
/// </summary>
/// <param name="verdict">The readiness verdict to map to HTTP status.</param>
public static int HttpStatus(ReadinessVerdict verdict) => verdict switch
{
ReadinessVerdict.Healthy => 200,
@@ -18,6 +18,10 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Observability;
public static class LogContextEnricher
{
/// <summary>Attach the capability-call property set. Dispose the returned scope to pop.</summary>
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
/// <param name="driverType">The driver type name.</param>
/// <param name="capability">The driver capability being invoked.</param>
/// <param name="correlationId">The correlation ID for tracing the call.</param>
public static IDisposable Push(string driverInstanceId, string driverType, DriverCapability capability, string correlationId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
@@ -41,8 +45,12 @@ public static class LogContextEnricher
private sealed class CompositeScope : IDisposable
{
private readonly IDisposable[] _inner;
/// <summary>Initializes a new instance of the CompositeScope with the given inner scopes.</summary>
/// <param name="inner">The inner scopes to composite and dispose together.</param>
public CompositeScope(params IDisposable[] inner) => _inner = inner;
/// <summary>Disposes all inner scopes in reverse order.</summary>
public void Dispose()
{
// Reverse-order disposal matches Serilog's stack semantics.
@@ -182,6 +182,7 @@ public static class EquipmentNodeWalker
/// any legacy row that slipped past the check constraint or any future driver that
/// wants an opaque non-JSON reference.
/// </remarks>
/// <param name="tagConfig">The tag configuration JSON or string.</param>
internal static string ExtractFullName(string tagConfig)
{
if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig;
@@ -19,8 +19,10 @@ namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa;
/// </remarks>
public class GenericDriverNodeManager(IDriver driver) : IDisposable
{
/// <summary>Gets the underlying driver instance.</summary>
protected IDriver Driver { get; } = driver ?? throw new ArgumentNullException(nameof(driver));
/// <summary>Gets the driver instance identifier.</summary>
public string DriverInstanceId => Driver.DriverInstanceId;
// Source tag (DriverAttributeInfo.FullName) → alarm-condition sink. Populated during
@@ -45,6 +47,8 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
/// project's <c>OpcUaApplicationHost.PopulateAddressSpaces</c> wraps this call in a per-driver
/// try/catch that logs + leaves the driver's subtree empty until a Reinitialize succeeds.
/// </summary>
/// <param name="builder">The address space builder to populate.</param>
/// <param name="ct">The cancellation token.</param>
public async Task BuildAddressSpaceAsync(IAddressSpaceBuilder builder, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(builder);
@@ -79,6 +83,7 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
}
}
/// <summary>Disposes the node manager and cleans up alarm subscriptions.</summary>
public void Dispose()
{
if (_disposed) return;
@@ -106,12 +111,23 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
IAddressSpaceBuilder inner,
ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IAddressSpaceBuilder
{
/// <summary>Adds a folder to the address space.</summary>
/// <param name="browseName">The browse name of the folder node.</param>
/// <param name="displayName">The display name of the folder node.</param>
public IAddressSpaceBuilder Folder(string browseName, string displayName)
=> new CapturingBuilder(inner.Folder(browseName, displayName), sinks);
/// <summary>Adds a variable to the address space.</summary>
/// <param name="browseName">The browse name of the variable node.</param>
/// <param name="displayName">The display name of the variable node.</param>
/// <param name="attributeInfo">Metadata describing the variable's data type and properties.</param>
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
=> new CapturingHandle(inner.Variable(browseName, displayName, attributeInfo), sinks);
/// <summary>Adds a property to the address space.</summary>
/// <param name="browseName">The browse name of the property node.</param>
/// <param name="dataType">The OPC UA data type of the property.</param>
/// <param name="value">The initial value of the property, or null.</param>
public void AddProperty(string browseName, DriverDataType dataType, object? value)
=> inner.AddProperty(browseName, dataType, value);
}
@@ -120,8 +136,11 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
IVariableHandle inner,
ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IVariableHandle
{
/// <summary>Gets the full reference for the variable.</summary>
public string FullReference => inner.FullReference;
/// <summary>Marks the variable as an alarm condition and registers its sink.</summary>
/// <param name="info">Configuration for the alarm condition.</param>
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
{
var sink = inner.MarkAsAlarmCondition(info);
@@ -42,7 +42,9 @@ public static class IdentificationFolderBuilder
"ManufacturerUri", "DeviceManualUri",
};
/// <summary>True when the equipment row has at least one non-null Identification field.</summary>
/// <summary>Checks whether the equipment row has at least one non-null Identification field.</summary>
/// <param name="equipment">The equipment entity.</param>
/// <returns>True if any Identification field is non-null; otherwise false.</returns>
public static bool HasAnyFields(Equipment equipment)
{
ArgumentNullException.ThrowIfNull(equipment);
@@ -58,10 +60,12 @@ public static class IdentificationFolderBuilder
}
/// <summary>
/// Build the Identification sub-folder under <paramref name="equipmentBuilder"/>. No-op
/// when every field is null. Returns the sub-folder builder (or null when no-op) so
/// callers can attach additional nodes underneath if needed.
/// Builds the Identification sub-folder under the given equipment builder. Returns the sub-folder builder
/// (or null when no fields are present) so callers can attach additional nodes if needed.
/// </summary>
/// <param name="equipmentBuilder">The equipment address space builder.</param>
/// <param name="equipment">The equipment entity with identification fields.</param>
/// <returns>The sub-folder builder, or null if no fields are present.</returns>
public static IAddressSpaceBuilder? Build(IAddressSpaceBuilder equipmentBuilder, Equipment equipment)
{
ArgumentNullException.ThrowIfNull(equipmentBuilder);
@@ -29,6 +29,11 @@ public sealed class AlarmSurfaceInvoker
private readonly IPerCallHostResolver? _hostResolver;
private readonly string _defaultHost;
/// <summary>Initializes a new instance of the AlarmSurfaceInvoker class.</summary>
/// <param name="invoker">The capability invoker for resilience pipeline.</param>
/// <param name="alarmSource">The alarm source to invoke.</param>
/// <param name="defaultHost">The default host name for single-host scenarios.</param>
/// <param name="hostResolver">Optional per-call host resolver for multi-host dispatch.</param>
public AlarmSurfaceInvoker(
CapabilityInvoker invoker,
IAlarmSource alarmSource,
@@ -52,6 +57,8 @@ public sealed class AlarmSurfaceInvoker
/// the driver's opaque handle together with its resolved host so <see cref="UnsubscribeAsync"/>
/// routes through the same host's pipeline that the subscription was created on.
/// </summary>
/// <param name="sourceNodeIds">The source node IDs to subscribe to.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<IReadOnlyList<IAlarmSubscriptionHandle>> SubscribeAsync(
IReadOnlyList<string> sourceNodeIds,
CancellationToken cancellationToken)
@@ -80,6 +87,8 @@ public sealed class AlarmSurfaceInvoker
/// handles not created by this invoker so the method remains safe to call on any
/// <see cref="IAlarmSubscriptionHandle"/> implementation.
/// </summary>
/// <param name="handle">The subscription handle to unsubscribe.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public ValueTask UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(handle);
@@ -99,6 +108,8 @@ public sealed class AlarmSurfaceInvoker
/// AlarmAcknowledge pipeline (no-retry per decision #143 — an alarm-ack is not idempotent
/// at the plant-floor acknowledgement level even if the OPC UA spec permits re-issue).
/// </summary>
/// <param name="acknowledgements">The alarm acknowledgement requests.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
CancellationToken cancellationToken)
@@ -139,6 +150,11 @@ public sealed class AlarmSurfaceInvoker
return result;
}
/// <summary>
/// Wraps an <see cref="IAlarmSubscriptionHandle"/> returned by the driver with the
/// resolved host name used when the subscription was created. <see cref="UnsubscribeAsync"/>
/// unwraps this to route the unsubscribe through the same host's resilience pipeline.
/// </summary>
/// <summary>
/// Wraps an <see cref="IAlarmSubscriptionHandle"/> returned by the driver with the
/// resolved host name used when the subscription was created. <see cref="UnsubscribeAsync"/>
@@ -146,8 +162,11 @@ public sealed class AlarmSurfaceInvoker
/// </summary>
private sealed class HostBoundHandle(IAlarmSubscriptionHandle inner, string host) : IAlarmSubscriptionHandle
{
/// <summary>Gets the inner subscription handle.</summary>
public IAlarmSubscriptionHandle Inner { get; } = inner;
/// <summary>Gets the resolved host name.</summary>
public string Host { get; } = host;
/// <summary>Gets the diagnostic ID from the inner handle.</summary>
public string DiagnosticId => Inner.DiagnosticId;
}
}
@@ -54,6 +54,10 @@ public sealed class CapabilityInvoker
/// <summary>Execute a capability call returning a value, honoring the per-capability pipeline.</summary>
/// <typeparam name="TResult">Return type of the underlying driver call.</typeparam>
/// <param name="capability">The driver capability being executed.</param>
/// <param name="hostName">The host name for logging and status tracking.</param>
/// <param name="callSite">The async function to execute.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public async ValueTask<TResult> ExecuteAsync<TResult>(
DriverCapability capability,
string hostName,
@@ -78,6 +82,10 @@ public sealed class CapabilityInvoker
}
/// <summary>Execute a void-returning capability call, honoring the per-capability pipeline.</summary>
/// <param name="capability">The driver capability being executed.</param>
/// <param name="hostName">The host name for logging and status tracking.</param>
/// <param name="callSite">The async function to execute.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public async ValueTask ExecuteAsync(
DriverCapability capability,
string hostName,
@@ -108,6 +116,11 @@ public sealed class CapabilityInvoker
/// decisions #44-45). If <c>true</c>, the call runs through the capability's pipeline which may
/// retry when the tier configuration permits.
/// </summary>
/// <typeparam name="TResult">Return type of the underlying driver call.</typeparam>
/// <param name="hostName">The host name for logging and status tracking.</param>
/// <param name="isIdempotent">Whether the write operation is idempotent.</param>
/// <param name="callSite">The async function to execute.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public async ValueTask<TResult> ExecuteWriteAsync<TResult>(
string hostName,
bool isIdempotent,
@@ -42,6 +42,8 @@ public sealed record DriverResilienceOptions
/// Look up the effective policy for a capability, falling back to tier defaults when no
/// override is configured. Never returns null.
/// </summary>
/// <param name="capability">The driver capability to resolve the policy for.</param>
/// <returns>The effective CapabilityPolicy for the specified capability.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown when neither the override map nor the tier defaults carry an entry for the
/// requested capability. The <c>TierDefaults_Cover_EveryCapability</c> invariant test
@@ -70,6 +72,8 @@ public sealed record DriverResilienceOptions
/// Stream A.2 specification. Retries skipped on <see cref="DriverCapability.Write"/> and
/// <see cref="DriverCapability.AlarmAcknowledge"/> regardless of tier.
/// </summary>
/// <param name="tier">The driver tier to get defaults for.</param>
/// <returns>The default policy dictionary for the specified tier.</returns>
public static IReadOnlyDictionary<DriverCapability, CapabilityPolicy> GetTierDefaults(DriverTier tier) =>
tier switch
{
@@ -47,6 +47,9 @@ public static class DriverResilienceOptionsParser
/// human-readable error message when the JSON was malformed (options still returned
/// = tier defaults).
/// </summary>
/// <param name="tier">The driver tier for default resilience options.</param>
/// <param name="resilienceConfigJson">The optional JSON configuration string to parse.</param>
/// <param name="parseDiagnostic">An out parameter containing diagnostic information if parsing fails.</param>
public static DriverResilienceOptions ParseOrDefaults(
DriverTier tier,
string? resilienceConfigJson,
@@ -117,16 +120,23 @@ public static class DriverResilienceOptionsParser
private sealed class ResilienceConfigShape
{
/// <summary>Gets or sets the maximum concurrent bulkhead requests.</summary>
public int? BulkheadMaxConcurrent { get; set; }
/// <summary>Gets or sets the maximum bulkhead queue size.</summary>
public int? BulkheadMaxQueue { get; set; }
/// <summary>Gets or sets the scheduled recycle interval in seconds.</summary>
public int? RecycleIntervalSeconds { get; set; }
/// <summary>Gets or sets the per-capability resilience policies.</summary>
public Dictionary<string, CapabilityPolicyShape>? CapabilityPolicies { get; set; }
}
private sealed class CapabilityPolicyShape
{
/// <summary>Gets or sets the operation timeout in seconds.</summary>
public int? TimeoutSeconds { get; set; }
/// <summary>Gets or sets the number of retry attempts.</summary>
public int? RetryCount { get; set; }
/// <summary>Gets or sets the failure count threshold before the circuit breaker opens.</summary>
public int? BreakerFailureThreshold { get; set; }
}
}
@@ -70,6 +70,8 @@ public sealed class DriverResiliencePipelineBuilder
}
/// <summary>Drop cached pipelines for one driver instance (e.g. on ResilienceConfig change). Test + Admin-reload use.</summary>
/// <param name="driverInstanceId">The driver instance ID whose pipelines should be invalidated.</param>
/// <returns>The number of pipelines removed.</returns>
public int Invalidate(string driverInstanceId)
{
var removed = 0;
@@ -19,6 +19,9 @@ public sealed class DriverResilienceStatusTracker
private readonly ConcurrentDictionary<StatusKey, ResilienceStatusSnapshot> _status = new();
/// <summary>Record a Polly pipeline failure for <paramref name="hostName"/>.</summary>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="hostName">The host name.</param>
/// <param name="utcNow">The UTC timestamp of the failure event.</param>
public void RecordFailure(string driverInstanceId, string hostName, DateTime utcNow)
{
var key = new StatusKey(driverInstanceId, hostName);
@@ -32,6 +35,9 @@ public sealed class DriverResilienceStatusTracker
}
/// <summary>Reset the consecutive-failure count on a successful pipeline execution.</summary>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="hostName">The host name.</param>
/// <param name="utcNow">The UTC timestamp of the success event.</param>
public void RecordSuccess(string driverInstanceId, string hostName, DateTime utcNow)
{
var key = new StatusKey(driverInstanceId, hostName);
@@ -45,6 +51,9 @@ public sealed class DriverResilienceStatusTracker
}
/// <summary>Record a circuit-breaker open event.</summary>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="hostName">The host name.</param>
/// <param name="utcNow">The UTC timestamp of the breaker open event.</param>
public void RecordBreakerOpen(string driverInstanceId, string hostName, DateTime utcNow)
{
var key = new StatusKey(driverInstanceId, hostName);
@@ -54,6 +63,9 @@ public sealed class DriverResilienceStatusTracker
}
/// <summary>Record a process recycle event (Tier C only).</summary>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="hostName">The host name.</param>
/// <param name="utcNow">The UTC timestamp of the recycle event.</param>
public void RecordRecycle(string driverInstanceId, string hostName, DateTime utcNow)
{
var key = new StatusKey(driverInstanceId, hostName);
@@ -63,6 +75,11 @@ public sealed class DriverResilienceStatusTracker
}
/// <summary>Capture / update the MemoryTracking-supplied baseline + current footprint.</summary>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="hostName">The host name.</param>
/// <param name="baselineBytes">The baseline footprint in bytes.</param>
/// <param name="currentBytes">The current footprint in bytes.</param>
/// <param name="utcNow">The UTC timestamp of the footprint update.</param>
public void RecordFootprint(string driverInstanceId, string hostName, long baselineBytes, long currentBytes, DateTime utcNow)
{
var key = new StatusKey(driverInstanceId, hostName);
@@ -87,6 +104,8 @@ public sealed class DriverResilienceStatusTracker
/// surface (a cheap stand-in for Polly bulkhead depth). Paired with
/// <see cref="RecordCallComplete"/>; callers use try/finally.
/// </summary>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="hostName">The host name.</param>
public void RecordCallStart(string driverInstanceId, string hostName)
{
var key = new StatusKey(driverInstanceId, hostName);
@@ -96,6 +115,8 @@ public sealed class DriverResilienceStatusTracker
}
/// <summary>Paired with <see cref="RecordCallStart"/> — decrements the in-flight counter.</summary>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="hostName">The host name.</param>
public void RecordCallComplete(string driverInstanceId, string hostName)
{
var key = new StatusKey(driverInstanceId, hostName);
@@ -105,6 +126,8 @@ public sealed class DriverResilienceStatusTracker
}
/// <summary>Snapshot of a specific (instance, host) pair; null if no counters recorded yet.</summary>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="hostName">The host name.</param>
public ResilienceStatusSnapshot? TryGet(string driverInstanceId, string hostName) =>
_status.TryGetValue(new StatusKey(driverInstanceId, hostName), out var snapshot) ? snapshot : null;
@@ -118,11 +141,17 @@ public sealed class DriverResilienceStatusTracker
/// <summary>Snapshot of the resilience counters for one <c>(DriverInstanceId, HostName)</c> pair.</summary>
public sealed record ResilienceStatusSnapshot
{
/// <summary>Gets the number of consecutive pipeline failures.</summary>
public int ConsecutiveFailures { get; init; }
/// <summary>Gets the UTC timestamp of the last circuit-breaker open event, if any.</summary>
public DateTime? LastBreakerOpenUtc { get; init; }
/// <summary>Gets the UTC timestamp of the last recycle event, if any.</summary>
public DateTime? LastRecycleUtc { get; init; }
/// <summary>Gets the baseline process footprint in bytes.</summary>
public long BaselineFootprintBytes { get; init; }
/// <summary>Gets the current process footprint in bytes.</summary>
public long CurrentFootprintBytes { get; init; }
/// <summary>Gets the UTC timestamp when the counters were last updated.</summary>
public DateTime LastSampledUtc { get; init; }
/// <summary>
@@ -20,6 +20,10 @@ public sealed class MemoryRecycle
private readonly IDriverSupervisor? _supervisor;
private readonly ILogger<MemoryRecycle> _logger;
/// <summary>Initializes a new instance of the <see cref="MemoryRecycle"/> class.</summary>
/// <param name="tier">The driver tier.</param>
/// <param name="supervisor">Optional supervisor for process recycling.</param>
/// <param name="logger">Logger for recycling events.</param>
public MemoryRecycle(DriverTier tier, IDriverSupervisor? supervisor, ILogger<MemoryRecycle> logger)
{
_tier = tier;
@@ -33,6 +37,9 @@ public sealed class MemoryRecycle
/// All other combinations are no-ops with respect to process state (soft breaches + Tier A/B
/// hard breaches just log).
/// </summary>
/// <param name="action">The memory tracking action.</param>
/// <param name="footprintBytes">The current process footprint in bytes.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>True when a recycle was requested; false otherwise.</returns>
public async Task<bool> HandleAsync(MemoryTrackingAction action, long footprintBytes, CancellationToken cancellationToken)
{
@@ -32,6 +32,7 @@ public sealed class MemoryTracking
private DateTime? _warmupStartUtc;
/// <summary>Tier-default multiplier/floor constants per decision #146.</summary>
/// <param name="tier">The driver tier.</param>
public static (int Multiplier, long FloorBytes) GetTierConstants(DriverTier tier) => tier switch
{
DriverTier.A => (Multiplier: 3, FloorBytes: 50L * 1024 * 1024),
@@ -55,6 +56,9 @@ public sealed class MemoryTracking
/// <summary>Effective hard threshold = 2 × soft (zero while warming up).</summary>
public long HardThresholdBytes => _baselineBytes == 0 ? 0 : ComputeSoft(_tier, _baselineBytes) * 2;
/// <summary>Initializes a new instance of the <see cref="MemoryTracking"/> class.</summary>
/// <param name="tier">The driver tier for threshold constants.</param>
/// <param name="baselineWindow">Optional custom baseline window duration (default 5 minutes).</param>
public MemoryTracking(DriverTier tier, TimeSpan? baselineWindow = null)
{
_tier = tier;
@@ -67,6 +71,8 @@ public sealed class MemoryTracking
/// samples; once the window elapses the first steady-phase sample triggers baseline capture
/// (median of warmup samples).
/// </summary>
/// <param name="footprintBytes">The current memory footprint in bytes.</param>
/// <param name="utcNow">The current UTC time.</param>
public MemoryTrackingAction Sample(long footprintBytes, DateTime utcNow)
{
if (_phase == TrackingPhase.WarmingUp)
@@ -66,6 +66,9 @@ public sealed class ScheduledRecycleScheduler
/// <see cref="NextRecycleUtc"/>, requests a recycle from the supervisor and advances
/// <see cref="NextRecycleUtc"/> by exactly one interval. Returns true when a recycle fired.
/// </summary>
/// <param name="utcNow">The current UTC time.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>True if a recycle was triggered; false otherwise.</returns>
public async Task<bool> TickAsync(DateTime utcNow, CancellationToken cancellationToken)
{
if (utcNow < _nextRecycleUtc)
@@ -81,6 +84,9 @@ public sealed class ScheduledRecycleScheduler
}
/// <summary>Request an immediate recycle outside the schedule (e.g. MemoryRecycle hard-breach escalation).</summary>
/// <param name="reason">The reason for requesting an immediate recycle.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous recycle operation.</returns>
public Task RequestRecycleNowAsync(string reason, CancellationToken cancellationToken) =>
_supervisor.RecycleAsync(reason, cancellationToken);
}
@@ -41,6 +41,10 @@ public sealed class WedgeDetector
/// Classify the current state against the demand signal. Does not retain state across
/// calls — each call is self-contained; the caller owns the <c>LastProgressUtc</c> clock.
/// </summary>
/// <param name="state">The current driver state.</param>
/// <param name="demand">The current demand signal snapshot.</param>
/// <param name="utcNow">The current UTC time.</param>
/// <returns>The wedge verdict for the given state and demand.</returns>
public WedgeVerdict Classify(DriverState state, DemandSignal demand, DateTime utcNow)
{
if (state != DriverState.Healthy)