Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
175 changes: 175 additions & 0 deletions src/AR.Iec61850/Mms/MmsClientSession.ConfiguredStaticFastSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
namespace AR.Iec61850.Mms;

/// <summary>
/// Hot-path RCB state capture for configured-static activation.
///
/// Normal discovery/diagnostic probing intentionally reads many attributes. Activation must
/// not repeat that broad scan. This path attempts one complete RCB structure read first and
/// falls back only to the exact leaf evidence still required for the imminent mutation.
/// There are no sleeps and no unrelated RCB/DataSet scans.
/// </summary>
public sealed partial class MmsClientSession
{
private async Task<MmsReportRcbSnapshot> CaptureConfiguredStaticActivationSnapshotAsync(
MmsReportControlCandidate source,
string stage,
bool includeReservationEvidence,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(source);

var clone = CloneReportControlCandidate(source);
clone.EnabledState = string.Empty;
clone.ReservationState = string.Empty;
clone.ReservationTimeSeconds = string.Empty;
clone.Owner = string.Empty;
clone.DataSetReference = string.Empty;
clone.DataSetProbeState = MmsRcbDataSetProbeState.NotAttempted;
clone.DataSetProbeMessage = string.Empty;
clone.ProbeDiagnostics.Clear();

var logicalReads = 0;
var structureRead = false;
var leafFallbacks = 0;

try
{
var baseReference = MmsObjectReference.Parse(
source.Reference,
source.FunctionalConstraint);
var baseRead = await ReadSingleVariableAsync(
baseReference,
cancellationToken).ConfigureAwait(false);
logicalReads++;

clone.ProbeDiagnostics.Add(
$"Activation RCB base {baseReference.Item}: {(baseRead.IsSuccess ? "OK" : baseRead.Message)}");

if (baseRead.IsSuccess &&
baseRead.Value is { Kind: MmsDataKind.Structure } structure &&
structure.Children.Count > 0)
{
ApplyReportControlStructure(clone, structure);
structureRead = true;

// DatSet is element 2 for BRCB and element 3 for URCB in the standard RCB
// structure layouts used by ApplyReportControlStructure.
var dataSetIndex = source.Buffered ? 2 : 3;
if (structure.Children.Count > dataSetIndex)
{
clone.DataSetProbeState = MmsRcbDataSetProbeState.ReadSucceeded;
clone.DataSetProbeMessage = "DatSet observed in complete RCB structure read.";
}
}

if (MmsRcbAvailabilityEvaluator.ParseBool(clone.EnabledState) is null)
{
leafFallbacks++;
logicalReads++;
await ProbeReportControlAttributeAsync(
clone,
"RptEna",
value => clone.EnabledState = NormalizeReportAttributeText(value),
cancellationToken).ConfigureAwait(false);
}

// A positive live DatSet contradiction is safety-relevant. If the complete
// structure did not provide the field, read only this one leaf.
if (clone.DataSetProbeState != MmsRcbDataSetProbeState.ReadSucceeded)
{
leafFallbacks++;
logicalReads++;
var beforeDiagnostics = clone.ProbeDiagnostics.Count;
await ProbeReportControlAttributeAsync(
clone,
"DatSet",
value =>
{
var text = NormalizeReportAttributeText(value);
clone.DataSetReference = string.IsNullOrWhiteSpace(text)
? string.Empty
: NormalizeReportedDataSetReference(clone.Domain, text);
},
cancellationToken).ConfigureAwait(false);

var datSetDiagnostic = clone.ProbeDiagnostics
.Skip(beforeDiagnostics)
.LastOrDefault(line => line.StartsWith("DatSet", StringComparison.OrdinalIgnoreCase));
if (datSetDiagnostic?.Contains(": OK ", StringComparison.OrdinalIgnoreCase) == true)
{
clone.DataSetProbeState = MmsRcbDataSetProbeState.ReadSucceeded;
clone.DataSetProbeMessage = datSetDiagnostic;
}
else
{
clone.DataSetProbeState = MmsRcbDataSetProbeState.ReadFailed;
clone.DataSetProbeMessage = datSetDiagnostic ?? "DatSet activation leaf read did not produce exact evidence.";
}
}

if (includeReservationEvidence)
{
if (source.Buffered)
{
if (MmsRcbAvailabilityEvaluator.ParseUnsigned(clone.ReservationTimeSeconds) is null &&
source.Attributes.Contains("ResvTms", StringComparer.OrdinalIgnoreCase))
{
leafFallbacks++;
logicalReads++;
await ProbeReportControlAttributeAsync(
clone,
"ResvTms",
value => clone.ReservationTimeSeconds = NormalizeReportAttributeText(value),
cancellationToken).ConfigureAwait(false);
}
}
else if (MmsRcbAvailabilityEvaluator.ParseBool(clone.ReservationState) is null &&
source.Attributes.Contains("Resv", StringComparer.OrdinalIgnoreCase))
{
leafFallbacks++;
logicalReads++;
await ProbeReportControlAttributeAsync(
clone,
"Resv",
value => clone.ReservationState = NormalizeReportAttributeText(value),
cancellationToken).ConfigureAwait(false);
}

// Owner is a separate optional leaf on many IEDs. Avoid paying this RTT
// when explicit reservation evidence is already free. Probe it only when
// reservation evidence remains ambiguous or earlier evidence was positive.
var explicitFree = source.Buffered
? MmsRcbAvailabilityEvaluator.ParseUnsigned(clone.ReservationTimeSeconds) == 0
: MmsRcbAvailabilityEvaluator.ParseBool(clone.ReservationState) == false;
var earlierPositiveOwner = MmsRcbAvailabilityEvaluator.HasOwner(source.Owner);
if (source.Attributes.Contains("Owner", StringComparer.OrdinalIgnoreCase) &&
(!explicitFree || earlierPositiveOwner))
{
leafFallbacks++;
logicalReads++;
await ProbeOwnerReadOnlyAsync(clone, cancellationToken).ConfigureAwait(false);
}
}

var hasExactEnable = MmsRcbAvailabilityEvaluator.ParseBool(clone.EnabledState).HasValue;
var message =
$"Configured-static JIT snapshot: exactRptEna={hasExactEnable.ToString().ToLowerInvariant()}, " +
$"structureRead={structureRead.ToString().ToLowerInvariant()}, logicalReads={logicalReads}, " +
$"leafFallbacks={leafFallbacks}, dataSetProbe={clone.DataSetProbeState}.";

return MmsReportRcbSnapshot.FromCandidate(
stage,
clone,
success: hasExactEnable,
message);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
return MmsReportRcbSnapshot.FromCandidate(
stage,
clone,
success: false,
$"Configured-static JIT snapshot failed after logicalReads={logicalReads}: {ex.GetType().Name}: {ex.Message}");
}
}
}
91 changes: 81 additions & 10 deletions src/AR.Iec61850/Mms/MmsClientSession.SclStaticReport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,44 @@ public async Task<MmsPersistentReportMonitorStartResult> StartConfiguredStaticRe

try
{
snapshots.Add(await CaptureReportControlSnapshotAsync(
var beforeSnapshot = await CaptureConfiguredStaticActivationSnapshotAsync(
rcb,
"scl-static-before-enable",
cancellationToken).ConfigureAwait(false));
"configured-static-before-enable",
includeReservationEvidence: true,
cancellationToken).ConfigureAwait(false);
snapshots.Add(beforeSnapshot);

var beforeProof = MmsConfiguredStaticActivationVerifier.VerifyBeforeWrite(
beforeSnapshot,
plan.DataSetReference);
if (!beforeProof.IsProven)
{
return new MmsPersistentReportMonitorStartResult
{
IsSuccess = false,
WriteSteps = writes,
Warnings = warnings,
RcbSnapshots = snapshots,
Message = $"Configured static activation failed JIT pre-write proof: {beforeProof.Kind}: {beforeProof.Message}"
};
}

var normalPathWrites = new List<MmsReportControlSemanticWrite>
{
MmsReportControlSemanticWrite.ReportEnable(true)
};
if (!rcb.Buffered &&
rcb.Attributes.Contains("Resv", StringComparer.OrdinalIgnoreCase))
{
normalPathWrites.Add(MmsReportControlSemanticWrite.Reservation(true));
}
if (triggerGeneralInterrogation)
normalPathWrites.Add(MmsReportControlSemanticWrite.GeneralInterrogation(true));

await PrimeReportSemanticTypeEvidenceFromStructureAsync(
rcb,
normalPathWrites,
cancellationToken).ConfigureAwait(false);

monitor = new MmsPersistentReportMonitorSession(
plan,
Expand Down Expand Up @@ -185,14 +219,51 @@ public async Task<MmsPersistentReportMonitorStartResult> StartConfiguredStaticRe

monitor.EnabledByThisClient = true;

snapshots.Add(await CaptureReportControlSnapshotAsync(
var afterEnable = await CaptureConfiguredStaticActivationSnapshotAsync(
rcb,
"scl-static-after-enable-1",
cancellationToken).ConfigureAwait(false));
snapshots.Add(await CaptureReportControlSnapshotAsync(
rcb,
"scl-static-after-enable-2",
cancellationToken).ConfigureAwait(false));
"configured-static-after-enable",
includeReservationEvidence: false,
cancellationToken).ConfigureAwait(false);
snapshots.Add(afterEnable);

var enableProof = MmsConfiguredStaticActivationVerifier.VerifyAfterEnable(
afterEnable,
plan.DataSetReference);
if (!enableProof.IsProven)
{
UnregisterPersistentReportMonitor(monitor);

writes.Add(await TryWriteReportSemanticAttributeForCleanupAsync(
rcb,
MmsReportControlSemanticWrite.ReportEnable(false),
CancellationToken.None).ConfigureAwait(false));
enabled = false;
monitor.EnabledByThisClient = false;

if (reservationTouched)
{
writes.Add(rcb.Buffered
? await TryWriteReportSemanticAttributeForCleanupAsync(
rcb,
MmsReportControlSemanticWrite.ReservationTime(0),
CancellationToken.None).ConfigureAwait(false)
: await TryWriteReportSemanticAttributeForCleanupAsync(
rcb,
MmsReportControlSemanticWrite.Reservation(false),
CancellationToken.None).ConfigureAwait(false));
reservationTouched = false;
monitor.ReservationTouched = false;
}

return new MmsPersistentReportMonitorStartResult
{
IsSuccess = false,
WriteSteps = writes,
Warnings = warnings,
RcbSnapshots = snapshots,
Message = $"Configured static activation failed post-enable proof: {enableProof.Kind}: {enableProof.Message}"
};
}

if (triggerGeneralInterrogation)
{
Expand Down
Loading