fix(site-runtime): trace arm is cleared when the traced DCL write fails (arch-review F6)
The Flow Debugger trace arm was recorded when a traced write was ISSUED, not when the DCL acknowledged it. A write the device rejected (or that timed out) left the arm standing for the whole TraceArmWindowSeconds (30 s), so the next unrelated subscription update on that attribute was stamped with the debugger's execution id and spawned a traced, parented run — the debugger drew a "device echo" of a write that never landed. The write's continuation runs off the actor's dispatcher, so it cannot touch _traceArms directly; it Tells self a new DisarmTrace message instead, handled in a Receive<>. The disarm is keyed on the writer's execution id, so a late failure cannot steal an arm a newer traced write has since taken. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -179,6 +179,8 @@ public class InstanceActor : ReceiveActor
|
||||
/// DCL value change for that attribute is published stamped with the writer's id and
|
||||
/// <c>Trace</c>, and the arm is consumed. Bounded by the attribute count (one entry per
|
||||
/// attribute, newest wins); no sweep — an unconsumed arm is overwritten or expires in place.
|
||||
/// Armed at ISSUE, disarmed on a failed/timed-out write (arch-review F6, <see cref="DisarmTrace"/>);
|
||||
/// a successful write's echo consumes it.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, (Guid SourceExecutionId, DateTimeOffset ExpiresAt)> _traceArms = new();
|
||||
// Maps each tag path to every attribute canonical name that references it.
|
||||
@@ -339,6 +341,17 @@ public class InstanceActor : ReceiveActor
|
||||
Receive<TagValueUpdate>(HandleTagValueUpdate);
|
||||
Receive<SubscribeTagsResponse>(HandleSubscribeTagsResponse); // S4/UA6: real retry-aware handler
|
||||
Receive<RetryTagSubscribe>(msg => SendTagSubscribe(msg.ConnectionName));
|
||||
|
||||
// Arch-review F6: a traced write the DCL rejected (or that timed out) drops its own arm,
|
||||
// so an unrelated later update is not stamped as that write's device echo.
|
||||
Receive<DisarmTrace>(msg =>
|
||||
{
|
||||
if (_traceArms.TryGetValue(msg.AttributeName, out var arm)
|
||||
&& arm.SourceExecutionId == msg.SourceExecutionId)
|
||||
{
|
||||
_traceArms.Remove(msg.AttributeName);
|
||||
}
|
||||
});
|
||||
Receive<ConnectionQualityChanged>(HandleConnectionQualityChanged);
|
||||
|
||||
// Handle alarm state changes from Alarm Actors (Tell pattern)
|
||||
@@ -756,17 +769,29 @@ public class InstanceActor : ReceiveActor
|
||||
|
||||
// Flow Debugger trace arm (design §5.3): only a TRACED write with a known writer arms;
|
||||
// the device echo that follows is stamped in HandleTagValueUpdate. Untraced writes
|
||||
// never touch the map, so production behaviour is unchanged.
|
||||
// never touch the map, so production behaviour is unchanged. The arm is taken at ISSUE
|
||||
// and dropped again on a failed/timed-out write (arch-review F6, via the DisarmTrace
|
||||
// self-message below); a successful write's echo consumes it.
|
||||
Guid? armed = null;
|
||||
if (command.Trace && command.SourceExecutionId is { } traceSource)
|
||||
{
|
||||
_traceArms[attributeName] = (traceSource, DateTimeOffset.UtcNow.AddSeconds(_options.TraceArmWindowSeconds));
|
||||
armed = traceSource;
|
||||
}
|
||||
|
||||
// Captured for the continuation: it runs OFF the actor's dispatcher, so it must never
|
||||
// touch _traceArms directly — it Tells self instead.
|
||||
var self = Self;
|
||||
|
||||
// Ask the DCL and pipe the result back to the original caller. The DCL
|
||||
// returns the failure synchronously so the script can handle it.
|
||||
_dclManager.Ask<WriteTagResponse>(writeRequest, TimeSpan.FromSeconds(30))
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
var succeeded = t.IsCompletedSuccessfully && t.Result.Success;
|
||||
if (!succeeded && armed is { } armedSource)
|
||||
self.Tell(new DisarmTrace(attributeName, armedSource));
|
||||
|
||||
if (t.IsCompletedSuccessfully)
|
||||
return new SetStaticAttributeResponse(
|
||||
correlationId, instanceName, attributeName,
|
||||
@@ -2112,6 +2137,16 @@ public class InstanceActor : ReceiveActor
|
||||
/// </summary>
|
||||
private sealed record RetryTagSubscribe(string ConnectionName);
|
||||
|
||||
/// <summary>
|
||||
/// Self-message (arch-review F6): a traced data write FAILED, so drop its trace arm — but only
|
||||
/// if it is still the armed one. The write's continuation runs off the dispatcher and must not
|
||||
/// touch <see cref="_traceArms"/> itself; matching on <paramref name="SourceExecutionId"/> keeps
|
||||
/// a late failure from stealing an arm a newer traced write has since taken.
|
||||
/// </summary>
|
||||
/// <param name="AttributeName">Canonical name of the attribute whose arm is to be dropped.</param>
|
||||
/// <param name="SourceExecutionId">Execution id of the writer that took the arm.</param>
|
||||
private sealed record DisarmTrace(string AttributeName, Guid SourceExecutionId);
|
||||
|
||||
/// <summary>Self-tell that fires the coalesced static-override persist flush (P4, arch-review WP1.5).</summary>
|
||||
private sealed class FlushStaticOverrides
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user