Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0504654
test(rcb): reproduce 66/300 candidate global guard and require exact-…
masarray Oct 19, 2026
dbf1189
feat(rcb): exact-target indexed read admission with stable bounded co…
masarray Oct 19, 2026
b2d3519
fix(rcb): replace global 64/128 admission abort with exact dataset sc…
masarray Oct 19, 2026
dc31e0f
fix(rcb): fence continuation cursor to accepted MMS association lifetime
masarray Oct 19, 2026
f2765f5
fix(rcb): fence continuation cursor to accepted MMS association lifetime
masarray Oct 19, 2026
f9f0189
fix(rcb): fence continuation cursor to accepted MMS association lifetime
masarray Oct 19, 2026
7f96daf
fix(rcb): fence continuation cursor to accepted MMS association lifetime
masarray Oct 19, 2026
adffc51
test(rcb): prove occupied matching RCB blocked while exact free sibli…
masarray Oct 19, 2026
37cd183
perf(rcb): prioritize indexed configured RCB instances for read-only …
masarray Oct 19, 2026
c79ee7d
perf(rcb): hash-index prioritized candidate exclusion rather than rep…
masarray Oct 19, 2026
954e48f
perf(rcb): stop paged reads after each exact DataSet has strong non-b…
masarray Oct 19, 2026
3d4599f
test(rcb): prioritize two indexed instances in 302 catalog without fa…
masarray Oct 19, 2026
6391244
test(rcb): correct 300 unrelated plus two relevant catalog cardinality
masarray Oct 19, 2026
1a3d82b
test(rcb): reject wrong live DatSet even when configured RCB referenc…
masarray Oct 19, 2026
edf9294
fix(rcb): refuse exact-name fallback when live DatSet binding contrad…
masarray Oct 19, 2026
59ecf1b
feat(rcb): bounded resumable exact-target availability pager without …
masarray Oct 19, 2026
15f9d55
test(rcb): replace preflight global target budget rejection with 66/3…
masarray Oct 19, 2026
e8b6be6
fix(rcb): page exact-target availability and stop on safe eligible ca…
masarray Oct 19, 2026
d9e7952
fix(rcb): count deferred exact targets by actual probe admission, not…
masarray Oct 19, 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
126 changes: 93 additions & 33 deletions src/AR.Iec61850/Mms/MmsCanonicalStaticAcquisitionPreflight.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ public sealed class MmsCanonicalStaticAcquisitionProbePlan

public sealed class MmsCanonicalStaticAcquisitionProbeOptions
{
/// <summary>Maximum exact RCBs inspected per page, not a global catalog cap.</summary>
public int MaxExactTargets { get; init; } = 64;
public int MaxExactTargetPagesPerInvocation { get; init; } = 8;
public string? ExactTargetContinuationToken { get; init; }
public bool ReadDataSetDirectories { get; init; } = true;
public IReadOnlySet<string> CallerOwnedRcbReferences { get; init; }
= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Expand All @@ -54,6 +57,9 @@ public sealed class MmsCanonicalStaticAcquisitionProbeResult
public MmsCanonicalStaticAcquisitionProbePlan Plan { get; init; } = new();
public MmsCanonicalStaticAcquisitionProbeStatus Status { get; init; }
public bool NetworkProbePerformed { get; init; }
public int ExactTargetPageCount { get; init; }
public int DeferredExactTargetCount { get; init; }
public string? ExactTargetContinuationToken { get; init; }
public MmsRcbAvailabilityResult? Availability { get; init; }
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();

Expand Down Expand Up @@ -247,47 +253,101 @@ public async Task<MmsCanonicalStaticAcquisitionProbeResult> ProbeCanonicalStatic
};
}

var maxTargets = Math.Clamp(options.MaxExactTargets, 1, 512);
if (plan.ExactTargetReportControlReferences.Count > maxTargets)
var maxPerPage = Math.Clamp(options.MaxExactTargets, 1, 512);
var maxPages = Math.Clamp(options.MaxExactTargetPagesPerInvocation, 1, 32);
EnsureMmsReady();

var snapshots = new List<MmsRcbAvailabilitySnapshot>();
var probeWarnings = new List<string>();
var continuation = options.ExactTargetContinuationToken;
var totalExact = 0;
var pages = 0;
var targetedProbes = 0;
var stateReads = 0;
var directoryReads = 0;
var directoryHits = 0;

while (pages < maxPages && IsMmsInitiated)
{
return new MmsCanonicalStaticAcquisitionProbeResult
{
Plan = plan,
Status = MmsCanonicalStaticAcquisitionProbeStatus.TargetBudgetExceeded,
NetworkProbePerformed = false,
Warnings = plan.Warnings
.Concat(new[]
{
$"Exact static RCB target count {plan.ExactTargetReportControlReferences.Count} exceeds the configured hot-path budget {maxTargets}; no partial probe was started."
})
.ToArray()
};
}
cancellationToken.ThrowIfCancellationRequested();
var page = MmsCanonicalStaticExactTargetPager.Select(
plan, liveInventory, maxPerPage, continuation,
_canonicalStaticRcbAssociationKey);
totalExact = page.TotalExactTargets;
continuation = page.ContinuationToken;
if (page.References.Count == 0)
break;

EnsureMmsReady();
pages++;
targetedProbes += page.References.Count;
// Exact target list, never a broad RCB availability probe.
var batch = await CheckReportControlAvailabilityAsync(
liveInventory,
directory,
new MmsRcbAvailabilityOptions
{
MaxReportControls = maxPerPage,
ReadDataSetDirectories = options.ReadDataSetDirectories,
TargetReportControlReferences = page.References,
CallerOwnedRcbReferences = options.CallerOwnedRcbReferences
},
cancellationToken).ConfigureAwait(false);
snapshots.AddRange(batch.ReportControls);
probeWarnings.AddRange(batch.Warnings);
stateReads += batch.TargetedRcbStateLogicalReadCount;
directoryReads += batch.DataSetDirectoryNetworkReadCount;
directoryHits += batch.DataSetDirectoryCacheHitCount;

var availability = await CheckReportControlAvailabilityAsync(
liveInventory,
directory,
new MmsRcbAvailabilityOptions
{
MaxReportControls = maxTargets,
ReadDataSetDirectories = options.ReadDataSetDirectories,
TargetReportControlReferences = plan.ExactTargetReportControlReferences,
CallerOwnedRcbReferences = options.CallerOwnedRcbReferences
},
cancellationToken).ConfigureAwait(false);
// Once each resolved DataSet segment has an independently eligible
// exact RCB, additional pages provide no benefit to this one-shot
// selected acquisition. Availability is READ-ONLY; the consumer
// still verifies ordered members and user authorization before writes.
var resolvedSegments = plan.TargetResolution.Segments
.Where(segment => segment.ExactLiveReportControlReferences.Count > 0)
.ToArray();
var allHaveEligible = resolvedSegments.Length > 0 &&
resolvedSegments.All(segment =>
snapshots.Any(snapshot =>
segment.ExactLiveReportControlReferences.Any(reference =>
string.Equals(
MmsRcbAvailabilityEvaluator.NormalizeReference(reference),
MmsRcbAvailabilityEvaluator.NormalizeReference(snapshot.Reference),
StringComparison.OrdinalIgnoreCase)) &&
MmsConfiguredStaticRcbEligibilityPolicy.Evaluate(
snapshot, allowCallerOwned: true,
allowReducedMissingReservationEvidence: true).IsEligible));
if (allHaveEligible || continuation is null)
break;
}

var status = plan.UnresolvedSegmentCount > 0
? MmsCanonicalStaticAcquisitionProbeStatus.PartialAvailabilityChecked
: MmsCanonicalStaticAcquisitionProbeStatus.AvailabilityChecked;
var deferred = Math.Max(0, totalExact - targetedProbes);
if (continuation is not null)
probeWarnings.Add(
$"Exact target availability deferred {deferred} RCB(s) after {pages} bounded page(s); resume using ExactTargetContinuationToken.");
var availability = new MmsRcbAvailabilityResult
{
ReportControls = snapshots,
Warnings = probeWarnings.Distinct(StringComparer.Ordinal).ToArray(),
InventoryReportControlCount = liveInventory.ReportControls.Count,
TargetFilterApplied = true,
RequestedTargetReportControlCount = plan.ExactTargetReportControlReferences.Count,
MatchedTargetReportControlCount = snapshots.Count,
TargetedRcbStateLogicalReadCount = stateReads,
DataSetDirectoryNetworkReadCount = directoryReads,
DataSetDirectoryCacheHitCount = directoryHits
};

return new MmsCanonicalStaticAcquisitionProbeResult
{
Plan = plan,
Status = status,
NetworkProbePerformed = true,
Availability = availability,
Status = !IsMmsInitiated || pages == 0 || continuation is not null || plan.UnresolvedSegmentCount > 0
? MmsCanonicalStaticAcquisitionProbeStatus.PartialAvailabilityChecked
: MmsCanonicalStaticAcquisitionProbeStatus.AvailabilityChecked,
NetworkProbePerformed = pages > 0,
Availability = pages > 0 ? availability : null,
ExactTargetPageCount = pages,
DeferredExactTargetCount = deferred,
ExactTargetContinuationToken = continuation,
Warnings = plan.Warnings
.Concat(availability.Warnings)
.Distinct(StringComparer.Ordinal)
Expand Down
72 changes: 72 additions & 0 deletions src/AR.Iec61850/Mms/MmsCanonicalStaticExactTargetPager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Security.Cryptography;
using System.Text;

namespace AR.Iec61850.Mms;

public sealed class MmsCanonicalStaticExactTargetPage
{
public IReadOnlySet<string> References { get; init; } =
new HashSet<string>(StringComparer.Ordinal);
public int TotalExactTargets { get; init; }
public string? ContinuationToken { get; init; }
public bool HasMore => ContinuationToken is not null;
}

/// <summary>
/// Resumable exact-target availability admission. Unlike a global 64/128 RCB
/// abort, this pure selector constrains work per page. It cannot add targets
/// missing from the verified live inventory or invent indexed RCB siblings.
/// </summary>
public static class MmsCanonicalStaticExactTargetPager
{
public static MmsCanonicalStaticExactTargetPage Select(
MmsCanonicalStaticAcquisitionProbePlan plan,
MmsReportInventory inventory,
int maxPerPage,
string? continuationToken = null,
string? associationKey = null)
{
ArgumentNullException.ThrowIfNull(plan);
ArgumentNullException.ThrowIfNull(inventory);
if (maxPerPage <= 0)
throw new ArgumentOutOfRangeException(nameof(maxPerPage));
if (inventory.Authority != MmsReportInventoryAuthority.LiveMmsObserved)
throw new ArgumentException("Exact target paging requires live MMS inventory.", nameof(inventory));

var liveReferences = inventory.ReportControls
.Select(candidate => MmsRcbAvailabilityEvaluator.NormalizeReference(candidate.Reference))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var targets = plan.ExactTargetReportControlReferences
.Select(MmsRcbAvailabilityEvaluator.NormalizeReference)
.Where(reference => reference.Length > 0 && liveReferences.Contains(reference))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(reference => reference, StringComparer.Ordinal)
.ToArray();
var input = new StringBuilder().Append("association=")
.Append(associationKey ?? "detached").Append('\n');
foreach (var reference in targets)
input.Append(reference).Append('\n');
var fingerprint = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(input.ToString())));

var start = 0;
if (continuationToken is not null)
{
var parts = continuationToken.Split(':');
if (parts.Length != 2 ||
!string.Equals(parts[0], fingerprint, StringComparison.Ordinal) ||
!int.TryParse(parts[1], out start) || start < 0 || start >= targets.Length)
throw new ArgumentException(
"Exact RCB target continuation does not match current association/catalog.",
nameof(continuationToken));
}
var next = Math.Min(targets.Length, start + maxPerPage);
return new MmsCanonicalStaticExactTargetPage
{
TotalExactTargets = targets.Length,
References = targets.Skip(start).Take(next - start)
.ToHashSet(StringComparer.OrdinalIgnoreCase),
ContinuationToken = next < targets.Length ? $"{fingerprint}:{next}" : null
};
}
}
Loading