fix(deploy): persist which nodes never acked on a deploy timeout (lmxopcua#784)
In the normal timeout branch (some acks landed) the deployment row was set to
TimedOut with a null FailureReason, so GET /api/deployments/{id} could not say
which node to fix; only the central log named it. The branch now persists a
reason naming the silent nodes ("Timed out waiting for acks (n/m landed). No ack
from: ..."), bounded to 20 named nodes plus a count of the rest, and still
backstopped by the 2048-char Truncate.
Tests: the reason names the silent node and not the acked one; on a 25-node
silence it names 20, counts 5 and fits the column. Mutation-checked: dropping
the SetFailureReason fails both, dropping the bound fails the second.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
+68
@@ -111,6 +111,74 @@ public sealed class ConfigPublishCoordinatorTimeoutTests : ControlPlaneActorTest
|
||||
}, duration: TimeSpan.FromSeconds(3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// lmxopcua#784 — a deployment that timed out with SOME acks landed must persist WHY: the status API
|
||||
/// returned TimedOut with a null reason, and only the central log named the silent node. The reason
|
||||
/// names the node(s) that never acked and not the one that did.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Timeout_with_some_acks_landed_persists_a_reason_naming_the_silent_nodes()
|
||||
{
|
||||
var dbFactory = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedDispatchingDeployment(dbFactory, status: DeploymentStatus.AwaitingApplyAcks);
|
||||
SeedNodeRows(dbFactory, deploymentId, applied: new[] { "site-a-1:4053" }, applying: new[] { "site-b-1:4053" });
|
||||
|
||||
var actor = Sys.ActorOf(ConfigPublishCoordinator.Props(dbFactory, TimeSpan.FromMinutes(5)));
|
||||
actor.Tell(new ConfigPublishCoordinator.DeadlineElapsed(deploymentId));
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
using var db = dbFactory.CreateDbContext();
|
||||
var row = db.Deployments.Single();
|
||||
row.Status.ShouldBe(DeploymentStatus.TimedOut);
|
||||
row.FailureReason.ShouldNotBeNull();
|
||||
row.FailureReason.ShouldContain("No ack from: site-b-1:4053.");
|
||||
row.FailureReason.ShouldContain("(1/2 landed)");
|
||||
row.FailureReason.ShouldNotContain("site-a-1");
|
||||
}, duration: TimeSpan.FromSeconds(3));
|
||||
}
|
||||
|
||||
/// <summary>lmxopcua#784 — the timeout reason stays bounded on a large fleet: it names 20 nodes and counts the rest.</summary>
|
||||
[Fact]
|
||||
public void Timeout_reason_is_bounded_when_many_nodes_are_silent()
|
||||
{
|
||||
var dbFactory = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedDispatchingDeployment(dbFactory, status: DeploymentStatus.AwaitingApplyAcks);
|
||||
var silent = Enumerable.Range(0, 25).Select(i => $"node-{i:D2}-with-a-fairly-long-host-name:4053").ToArray();
|
||||
SeedNodeRows(dbFactory, deploymentId, applied: new[] { "acked-node:4053" }, applying: silent);
|
||||
|
||||
var actor = Sys.ActorOf(ConfigPublishCoordinator.Props(dbFactory, TimeSpan.FromMinutes(5)));
|
||||
actor.Tell(new ConfigPublishCoordinator.DeadlineElapsed(deploymentId));
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
using var db = dbFactory.CreateDbContext();
|
||||
var reason = db.Deployments.Single().FailureReason;
|
||||
reason.ShouldNotBeNull();
|
||||
reason.ShouldContain("node-19-with-a-fairly-long-host-name:4053");
|
||||
reason.ShouldNotContain("node-20-");
|
||||
reason.ShouldContain("(and 5 more)");
|
||||
reason.Length.ShouldBeLessThanOrEqualTo(2048);
|
||||
}, duration: TimeSpan.FromSeconds(3));
|
||||
}
|
||||
|
||||
private static void SeedNodeRows(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory, DeploymentId deploymentId, string[] applied, string[] applying)
|
||||
{
|
||||
using var db = dbFactory.CreateDbContext();
|
||||
foreach (var n in applied)
|
||||
db.NodeDeploymentStates.Add(new Configuration.Entities.NodeDeploymentState
|
||||
{
|
||||
NodeId = n, DeploymentId = deploymentId.Value, Status = NodeDeploymentStatus.Applied,
|
||||
});
|
||||
foreach (var n in applying)
|
||||
db.NodeDeploymentStates.Add(new Configuration.Entities.NodeDeploymentState
|
||||
{
|
||||
NodeId = n, DeploymentId = deploymentId.Value, Status = NodeDeploymentStatus.Applying,
|
||||
});
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static DeploymentId SeedDispatchingDeployment(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
||||
DeploymentStatus status = DeploymentStatus.Dispatching)
|
||||
|
||||
Reference in New Issue
Block a user