feat(deployment-manager): resolve DeploymentManager-006 — query site deployment state before redeploy and reconcile
Adds DeploymentStateQuery request/response contracts (Commons), a site-side handler (SiteRuntime), a CommunicationService query method (Communication), and reconciliation in DeploymentService: when a prior record is InProgress or Failed-on-timeout, query the site; if it already holds the target revision hash mark the record Success without re-sending; on query failure fall through to a normal deploy (site-side stale-rejection is the safety net).
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScadaLink.Commons.Messages.Deployment;
|
||||
|
||||
namespace ScadaLink.Communication.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-2: Tests for CommunicationService initialization and state.
|
||||
/// </summary>
|
||||
public class CommunicationServiceTests
|
||||
public class CommunicationServiceTests : TestKit
|
||||
{
|
||||
[Fact]
|
||||
public async Task BeforeInitialization_ThrowsOnUsage()
|
||||
@@ -18,7 +21,7 @@ public class CommunicationServiceTests
|
||||
// CommunicationService requires SetCommunicationActor before use
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
service.DeployInstanceAsync("site1",
|
||||
new Commons.Messages.Deployment.DeployInstanceCommand(
|
||||
new DeployInstanceCommand(
|
||||
"dep1", "inst1", "hash1", "{}", "admin", DateTimeOffset.UtcNow)));
|
||||
}
|
||||
|
||||
@@ -30,4 +33,63 @@ public class CommunicationServiceTests
|
||||
Assert.NotNull(method);
|
||||
Assert.Equal(typeof(void), method!.ReturnType);
|
||||
}
|
||||
|
||||
// ── DeploymentManager-006: query-the-site-before-redeploy ──
|
||||
|
||||
[Fact]
|
||||
public async Task QueryDeploymentStateAsync_BeforeInitialization_Throws()
|
||||
{
|
||||
var service = new CommunicationService(
|
||||
Options.Create(new CommunicationOptions()),
|
||||
NullLogger<CommunicationService>.Instance);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
service.QueryDeploymentStateAsync("site1",
|
||||
new DeploymentStateQueryRequest("corr-1", "inst1", DateTimeOffset.UtcNow)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryDeploymentStateAsync_SendsEnvelopeAndReturnsResponse()
|
||||
{
|
||||
// The query must be dispatched as a SiteEnvelope over the existing
|
||||
// command/control transport, exactly like other site-directed commands,
|
||||
// and the typed response returned to the caller.
|
||||
var service = new CommunicationService(
|
||||
Options.Create(new CommunicationOptions()),
|
||||
NullLogger<CommunicationService>.Instance);
|
||||
|
||||
// A probe stands in for CentralCommunicationActor: it asserts the
|
||||
// envelope shape and replies with a typed response.
|
||||
var commActor = Sys.ActorOf(Props.Create(() => new EchoStateQueryActor()));
|
||||
service.SetCommunicationActor(commActor);
|
||||
|
||||
var request = new DeploymentStateQueryRequest("corr-9", "QueriedInst", DateTimeOffset.UtcNow);
|
||||
var response = await service.QueryDeploymentStateAsync("site-a", request);
|
||||
|
||||
Assert.Equal("corr-9", response.CorrelationId);
|
||||
Assert.Equal("QueriedInst", response.InstanceUniqueName);
|
||||
Assert.True(response.IsDeployed);
|
||||
Assert.Equal("sha256:applied", response.AppliedRevisionHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stand-in for CentralCommunicationActor: verifies the message is wrapped
|
||||
/// in a SiteEnvelope targeting the requested site and replies with a typed
|
||||
/// DeploymentStateQueryResponse.
|
||||
/// </summary>
|
||||
private class EchoStateQueryActor : ReceiveActor
|
||||
{
|
||||
public EchoStateQueryActor()
|
||||
{
|
||||
Receive<SiteEnvelope>(env =>
|
||||
{
|
||||
if (env is { SiteId: "site-a", Message: DeploymentStateQueryRequest req })
|
||||
{
|
||||
Sender.Tell(new DeploymentStateQueryResponse(
|
||||
req.CorrelationId, req.InstanceUniqueName, true,
|
||||
"dep-applied", "sha256:applied", DateTimeOffset.UtcNow));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,22 @@ public class SiteCommunicationActorTests : TestKit
|
||||
dmProbe.ExpectMsg<DeleteInstanceCommand>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeploymentStateQuery_ForwardedToDeploymentManager()
|
||||
{
|
||||
// DeploymentManager-006: the site-before-redeploy query travels over the
|
||||
// ClusterClient command/control transport and is routed to the local
|
||||
// Deployment Manager, which owns the deployed-config store.
|
||||
var dmProbe = CreateTestProbe();
|
||||
var siteActor = Sys.ActorOf(Props.Create(() =>
|
||||
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
||||
|
||||
var request = new DeploymentStateQueryRequest("corr-q", "inst1", DateTimeOffset.UtcNow);
|
||||
siteActor.Tell(request);
|
||||
|
||||
dmProbe.ExpectMsg<DeploymentStateQueryRequest>(msg => msg.CorrelationId == "corr-q");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntegrationCall_WithoutHandler_ReturnsFailure()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user