feat(deploy): additive degraded flag on ApplyAck + publish-actor apply outcome reply (#601)
ApplyAck gains trailing Degraded/DegradedReason (defaulted, so an old node's DPS JSON or gRPC ack decodes exactly as before). mesh_transport.proto adds ApplyAckMessage.degraded = 6 / optional degraded_reason = 7. OpcUaPublishActor replies RebuildCompleted(Degraded, Reason) on every handler path when the RebuildAddressSpace carries ReportOutcome. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -7,9 +7,24 @@ public enum ApplyAckOutcome { Applied, Failed }
|
||||
/// <summary>
|
||||
/// Per-node acknowledgment returned by <c>DriverHostActor</c> to the dispatching coordinator.
|
||||
/// </summary>
|
||||
/// <param name="DeploymentId">The deployment being acknowledged.</param>
|
||||
/// <param name="NodeId">The acknowledging node.</param>
|
||||
/// <param name="Outcome">Whether the node applied the deployment.</param>
|
||||
/// <param name="FailureReason">Why the apply failed; null on <see cref="ApplyAckOutcome.Applied"/>.</param>
|
||||
/// <param name="CorrelationId">The dispatch's correlation id.</param>
|
||||
/// <param name="Degraded">
|
||||
/// Gitea #601: <see langword="true"/> when the node applied the deployment (the revision advanced) but
|
||||
/// its OPC UA address-space apply was DEGRADED — swallowed per-node sink faults, a failed rebuild, or no
|
||||
/// outcome reported in time — so the served address space may be partial. Only meaningful with
|
||||
/// <see cref="ApplyAckOutcome.Applied"/>. Trailing and defaulted so an ack from a node built before
|
||||
/// this field existed (DPS JSON or gRPC without field 6) decodes exactly as it always did: clean.
|
||||
/// </param>
|
||||
/// <param name="DegradedReason">A short, operator-readable description of the degradation, or null.</param>
|
||||
public sealed record ApplyAck(
|
||||
DeploymentId DeploymentId,
|
||||
NodeId NodeId,
|
||||
ApplyAckOutcome Outcome,
|
||||
string? FailureReason,
|
||||
CorrelationId CorrelationId);
|
||||
CorrelationId CorrelationId,
|
||||
bool Degraded = false,
|
||||
string? DegradedReason = null);
|
||||
|
||||
@@ -216,6 +216,11 @@ public static class MeshTransportDtoMapper
|
||||
if (message.FailureReason is not null)
|
||||
proto.FailureReason = message.FailureReason;
|
||||
|
||||
// #601 — additive fields; a clean ack leaves both unset, byte-identical to a pre-#601 encode.
|
||||
proto.Degraded = message.Degraded;
|
||||
if (message.DegradedReason is not null)
|
||||
proto.DegradedReason = message.DegradedReason;
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
@@ -249,7 +254,10 @@ public static class MeshTransportDtoMapper
|
||||
NodeId.Parse(proto.NodeId),
|
||||
outcome,
|
||||
proto.HasFailureReason ? proto.FailureReason : null,
|
||||
CorrelationId.Parse(proto.CorrelationId));
|
||||
CorrelationId.Parse(proto.CorrelationId),
|
||||
// #601 — absent on an old node's ack, which decodes as false/null: today's clean ack.
|
||||
Degraded: proto.Degraded,
|
||||
DegradedReason: proto.HasDegradedReason ? proto.DegradedReason : null);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Envelope
|
||||
|
||||
@@ -105,6 +105,11 @@ message ApplyAckMessage {
|
||||
ApplyOutcome outcome = 3;
|
||||
optional string failure_reason = 4; // nullable in the record — presence distinguishes null from ""
|
||||
string correlation_id = 5; // CorrelationId (Guid) in its "N" hex form
|
||||
// Gitea #601 — ADDITIVE. True when the node applied the deployment but its address-space apply was
|
||||
// degraded (sink faults, failed rebuild, or no outcome in time). An old node never sets it, so it decodes
|
||||
// as false: exactly today's clean ack. Only meaningful with APPLY_OUTCOME_APPLIED.
|
||||
bool degraded = 6;
|
||||
optional string degraded_reason = 7; // short operator-readable reason; presence = set
|
||||
}
|
||||
|
||||
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy.ApplyAckOutcome, shifted by one so 0 stays
|
||||
|
||||
@@ -100,8 +100,31 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
/// that cannot succeed, leaving the rebuild a no-op and clients browsing an empty server. Null
|
||||
/// on the normal path, where loading from the ConfigDb by id is correct.
|
||||
/// </param>
|
||||
/// <param name="ReportOutcome">
|
||||
/// Gitea #601 — when <see langword="true"/>, the actor replies to the sender with a
|
||||
/// <see cref="RebuildCompleted"/> once the apply outcome is known (clean or degraded), on EVERY path
|
||||
/// through the handler. <c>DriverHostActor</c> sets it on the deploy path and sends the deployment's
|
||||
/// ack only after that reply, so a degraded apply can no longer seal a deployment green. Off for the
|
||||
/// boot/restore paths, which re-ack nothing.
|
||||
/// </param>
|
||||
public sealed record RebuildAddressSpace(
|
||||
CorrelationId Correlation, DeploymentId? DeploymentId = null, byte[]? Artifact = null);
|
||||
CorrelationId Correlation, DeploymentId? DeploymentId = null, byte[]? Artifact = null,
|
||||
bool ReportOutcome = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #601 — the reply to a <see cref="RebuildAddressSpace"/> carrying
|
||||
/// <see cref="RebuildAddressSpace.ReportOutcome"/>: whether the address-space apply was clean.
|
||||
/// </summary>
|
||||
/// <param name="Correlation">The rebuild's correlation id, echoed.</param>
|
||||
/// <param name="DeploymentId">The rebuild's deployment id, echoed (null for a legacy caller).</param>
|
||||
/// <param name="Degraded">
|
||||
/// <see langword="true"/> when the apply swallowed per-node sink faults, its rebuild failed, the
|
||||
/// pipeline threw, or there was no usable artifact — i.e. the served address space may be partial or
|
||||
/// stale. An empty plan (nothing changed) is clean.
|
||||
/// </param>
|
||||
/// <param name="DegradedReason">A short operator-readable reason; null when clean.</param>
|
||||
public sealed record RebuildCompleted(
|
||||
CorrelationId Correlation, DeploymentId? DeploymentId, bool Degraded, string? DegradedReason);
|
||||
|
||||
public sealed record ServiceLevelChanged(byte ServiceLevel);
|
||||
|
||||
@@ -437,6 +460,17 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
private void HandleRebuild(RebuildAddressSpace msg)
|
||||
{
|
||||
// Captured up front: the pipeline below is synchronous on this actor's thread today, but the reply
|
||||
// must reach whoever asked even if a future edit introduces an await into it.
|
||||
var requester = Sender;
|
||||
var (degraded, reason) = HandleRebuildCore(msg);
|
||||
if (msg.ReportOutcome)
|
||||
requester.Tell(new RebuildCompleted(msg.Correlation, msg.DeploymentId, degraded, reason));
|
||||
}
|
||||
|
||||
/// <summary>The rebuild pipeline. Returns whether the apply was degraded, and why (Gitea #601).</summary>
|
||||
private (bool Degraded, string? Reason) HandleRebuildCore(RebuildAddressSpace msg)
|
||||
{
|
||||
using var span = OtOpcUaTelemetry.StartAddressSpaceRebuildSpan();
|
||||
span?.SetTag("otopcua.correlation_id", msg.Correlation.ToString());
|
||||
@@ -460,8 +494,9 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
_log.Error(ex, "OpcUaPublish: sink.RebuildAddressSpace threw (correlation={Correlation})",
|
||||
msg.Correlation);
|
||||
return (true, $"address-space rebuild threw: {ex.Message}");
|
||||
}
|
||||
return;
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -499,7 +534,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
_log.Debug(
|
||||
"OpcUaPublish: no artifact to materialise yet (deployment={Id}, correlation={Correlation})",
|
||||
msg.DeploymentId, msg.Correlation);
|
||||
return;
|
||||
return (true, "no usable artifact reached the address-space apply; the previously served address space was kept");
|
||||
}
|
||||
|
||||
// Parse the artifact ONCE for this rebuild. Every consumer below (data-plane authority, then the
|
||||
@@ -532,7 +567,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
_log.Debug("OpcUaPublish: rebuild requested but plan is empty (correlation={Correlation})",
|
||||
msg.Correlation);
|
||||
return;
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
var outcome = _applier.Apply(plan);
|
||||
@@ -608,16 +643,19 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
"OpcUaPublish: address-space apply DEGRADED (correlation={Correlation}, rebuildFailed={RebuildFailed}, failedNodes={FailedNodes}, added={Added}, removed={Removed}, changed={Changed}) — the running address space may be stale or partial",
|
||||
msg.Correlation, outcome.RebuildFailed, failedNodes,
|
||||
outcome.AddedNodes, outcome.RemovedNodes, outcome.ChangedNodes);
|
||||
return (true, outcome.RebuildFailed
|
||||
? $"address-space rebuild failed (failedNodes={failedNodes})"
|
||||
: $"address-space apply degraded (failedNodes={failedNodes}); not recorded as applied, the next deploy of this revision re-diffs and repairs it");
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.Info("OpcUaPublish: applied rebuild (correlation={Correlation}, added={Added}, removed={Removed}, changed={Changed}, rebuild={Rebuild})",
|
||||
msg.Correlation, outcome.AddedNodes, outcome.RemovedNodes, outcome.ChangedNodes, outcome.RebuildCalled);
|
||||
}
|
||||
|
||||
_log.Info("OpcUaPublish: applied rebuild (correlation={Correlation}, added={Added}, removed={Removed}, changed={Changed}, rebuild={Rebuild})",
|
||||
msg.Correlation, outcome.AddedNodes, outcome.RemovedNodes, outcome.ChangedNodes, outcome.RebuildCalled);
|
||||
return (false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(ex, "OpcUaPublish: rebuild pipeline threw (correlation={Correlation})", msg.Correlation);
|
||||
return (true, $"address-space apply threw: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user