refactor(deploy): retire cross-cluster DeployInstanceCommand wire path (config now fetched)
This commit is contained in:
@@ -92,11 +92,6 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
|
|||||||
Receive<RegisterLocalHandler>(HandleRegisterLocalHandler);
|
Receive<RegisterLocalHandler>(HandleRegisterLocalHandler);
|
||||||
|
|
||||||
// Pattern 1: Instance Deployment — forward to Deployment Manager
|
// Pattern 1: Instance Deployment — forward to Deployment Manager
|
||||||
Receive<DeployInstanceCommand>(msg =>
|
|
||||||
{
|
|
||||||
_log.Debug("Routing DeployInstanceCommand for {0} to DeploymentManager", msg.InstanceUniqueName);
|
|
||||||
_deploymentManagerProxy.Forward(msg);
|
|
||||||
});
|
|
||||||
Receive<RefreshDeploymentCommand>(msg =>
|
Receive<RefreshDeploymentCommand>(msg =>
|
||||||
{
|
{
|
||||||
_log.Debug("Routing RefreshDeploymentCommand for {0} to DeploymentManager", msg.InstanceUniqueName);
|
_log.Debug("Routing RefreshDeploymentCommand for {0} to DeploymentManager", msg.InstanceUniqueName);
|
||||||
|
|||||||
@@ -116,29 +116,9 @@ public class CommunicationService
|
|||||||
// ── Pattern 1: Instance Deployment ──
|
// ── Pattern 1: Instance Deployment ──
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a deployment command for an instance to a site.
|
/// Sends a small "refresh deployment" notify to a site (notify-and-fetch):
|
||||||
/// </summary>
|
/// the site fetches the config over HTTP rather than receiving it inline.
|
||||||
/// <param name="siteId">The target site identifier.</param>
|
/// Reply is the existing DeploymentStatusResponse, bounded by the deployment timeout.
|
||||||
/// <param name="command">The deployment command.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
|
||||||
/// <returns>The deployment status response.</returns>
|
|
||||||
public async Task<DeploymentStatusResponse> DeployInstanceAsync(
|
|
||||||
string siteId, DeployInstanceCommand command, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(
|
|
||||||
"Sending DeployInstanceCommand to site {SiteId}, instance={Instance}, correlationId={DeploymentId}",
|
|
||||||
siteId, command.InstanceUniqueName, command.DeploymentId);
|
|
||||||
|
|
||||||
var envelope = new SiteEnvelope(siteId, command);
|
|
||||||
return await GetActor().Ask<DeploymentStatusResponse>(
|
|
||||||
envelope, _options.DeploymentTimeout, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a small "refresh deployment" notify to a site (notify-and-fetch).
|
|
||||||
/// Replaces <see cref="DeployInstanceAsync"/> on the wire: the site fetches the
|
|
||||||
/// config over HTTP rather than receiving it inline. Reply is the existing
|
|
||||||
/// DeploymentStatusResponse, bounded by the deployment timeout.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="siteId">The target site identifier.</param>
|
/// <param name="siteId">The target site identifier.</param>
|
||||||
/// <param name="command">The refresh-deployment notify.</param>
|
/// <param name="command">The refresh-deployment notify.</param>
|
||||||
|
|||||||
@@ -154,9 +154,9 @@ public class SiteReplicationActor : ReceiveActor
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(msg.CentralFetchBaseUrl))
|
if (string.IsNullOrEmpty(msg.CentralFetchBaseUrl))
|
||||||
{
|
{
|
||||||
// The still-present direct DeployInstanceCommand wire path (retired in Task 14)
|
// The direct DeployInstanceCommand cross-cluster wire path was retired (Task 14).
|
||||||
// replicates with empty coords; there is nothing to fetch. Skip quietly rather
|
// This guard is a defensive fallback: skip quietly rather than calling FetchAsync("")
|
||||||
// than calling FetchAsync("") and logging an error — T18 reconciliation backstops.
|
// and logging a spurious error. T18 reconciliation backstops any missed writes.
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"No fetch coords for {Instance} (deployment {DeploymentId}) — skipping replicated fetch; T18 reconciliation is the backstop",
|
"No fetch coords for {Instance} (deployment {DeploymentId}) — skipping replicated fetch; T18 reconciliation is the backstop",
|
||||||
msg.InstanceName, msg.DeploymentId);
|
msg.InstanceName, msg.DeploymentId);
|
||||||
|
|||||||
@@ -81,26 +81,6 @@ public class CentralCommunicationActorTests : TestKit
|
|||||||
private Site CreateSite(string identifier, string? nodeAAddress, string? nodeBAddress = null) =>
|
private Site CreateSite(string identifier, string? nodeAAddress, string? nodeBAddress = null) =>
|
||||||
new("Test Site", identifier) { NodeAAddress = nodeAAddress, NodeBAddress = nodeBAddress };
|
new("Test Site", identifier) { NodeAAddress = nodeAAddress, NodeBAddress = nodeBAddress };
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ClusterClientRouting_RoutesToConfiguredSite()
|
|
||||||
{
|
|
||||||
var site = CreateSite("site1", "akka.tcp://scadabridge@host:8082");
|
|
||||||
var (actor, _, siteProbes) = CreateActorWithMockRepo(new[] { site });
|
|
||||||
|
|
||||||
// Wait for auto-refresh (PreStart schedules with TimeSpan.Zero initial delay)
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
|
|
||||||
var command = new DeployInstanceCommand(
|
|
||||||
"dep1", "inst1", "hash1", "{}", "admin", DateTimeOffset.UtcNow);
|
|
||||||
actor.Tell(new SiteEnvelope("site1", command));
|
|
||||||
|
|
||||||
// The site1 probe (acting as ClusterClient) should receive a ClusterClient.Send
|
|
||||||
var msg = siteProbes["site1"].ExpectMsg<ClusterClient.Send>();
|
|
||||||
Assert.Equal("/user/site-communication", msg.Path);
|
|
||||||
Assert.IsType<DeployInstanceCommand>(msg.Message);
|
|
||||||
Assert.Equal("dep1", ((DeployInstanceCommand)msg.Message).DeploymentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ClusterClientRouting_RefreshDeploymentCommand_RoutesToSite()
|
public void ClusterClientRouting_RefreshDeploymentCommand_RoutesToSite()
|
||||||
{
|
{
|
||||||
@@ -128,8 +108,9 @@ public class CentralCommunicationActorTests : TestKit
|
|||||||
// Wait for auto-refresh
|
// Wait for auto-refresh
|
||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
|
|
||||||
var command = new DeployInstanceCommand(
|
var command = new RefreshDeploymentCommand(
|
||||||
"dep1", "inst1", "hash1", "{}", "admin", DateTimeOffset.UtcNow);
|
"dep1", "inst1", "hash1", "admin", DateTimeOffset.UtcNow,
|
||||||
|
"https://central:9000", "tok1");
|
||||||
actor.Tell(new SiteEnvelope("unknown-site", command));
|
actor.Tell(new SiteEnvelope("unknown-site", command));
|
||||||
|
|
||||||
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||||
@@ -177,11 +158,12 @@ public class CentralCommunicationActorTests : TestKit
|
|||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
|
|
||||||
// Verify routing to site1 works
|
// Verify routing to site1 works
|
||||||
var cmd1 = new DeployInstanceCommand(
|
var cmd1 = new RefreshDeploymentCommand(
|
||||||
"dep1", "inst1", "hash1", "{}", "admin", DateTimeOffset.UtcNow);
|
"dep1", "inst1", "hash1", "admin", DateTimeOffset.UtcNow,
|
||||||
|
"https://central:9000", "tok1");
|
||||||
actor.Tell(new SiteEnvelope("site1", cmd1));
|
actor.Tell(new SiteEnvelope("site1", cmd1));
|
||||||
var msg1 = siteProbes["site1"].ExpectMsg<ClusterClient.Send>();
|
var msg1 = siteProbes["site1"].ExpectMsg<ClusterClient.Send>();
|
||||||
Assert.Equal("dep1", ((DeployInstanceCommand)msg1.Message).DeploymentId);
|
Assert.Equal("dep1", ((RefreshDeploymentCommand)msg1.Message).DeploymentId);
|
||||||
|
|
||||||
// Update mock repo to return both sites
|
// Update mock repo to return both sites
|
||||||
var site2 = CreateSite("site2", "akka.tcp://scadabridge@host2:8082");
|
var site2 = CreateSite("site2", "akka.tcp://scadabridge@host2:8082");
|
||||||
@@ -193,11 +175,12 @@ public class CentralCommunicationActorTests : TestKit
|
|||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
|
|
||||||
// Verify routing to site2 now works
|
// Verify routing to site2 now works
|
||||||
var cmd2 = new DeployInstanceCommand(
|
var cmd2 = new RefreshDeploymentCommand(
|
||||||
"dep2", "inst2", "hash2", "{}", "admin", DateTimeOffset.UtcNow);
|
"dep2", "inst2", "hash2", "admin", DateTimeOffset.UtcNow,
|
||||||
|
"https://central:9000", "tok2");
|
||||||
actor.Tell(new SiteEnvelope("site2", cmd2));
|
actor.Tell(new SiteEnvelope("site2", cmd2));
|
||||||
var msg2 = siteProbes["site2"].ExpectMsg<ClusterClient.Send>();
|
var msg2 = siteProbes["site2"].ExpectMsg<ClusterClient.Send>();
|
||||||
Assert.Equal("dep2", ((DeployInstanceCommand)msg2.Message).DeploymentId);
|
Assert.Equal("dep2", ((RefreshDeploymentCommand)msg2.Message).DeploymentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -244,14 +227,15 @@ public class CentralCommunicationActorTests : TestKit
|
|||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
|
|
||||||
// good-site must still be registered and routable despite bad-site failing to parse.
|
// good-site must still be registered and routable despite bad-site failing to parse.
|
||||||
var cmd = new DeployInstanceCommand(
|
var cmd = new RefreshDeploymentCommand(
|
||||||
"dep1", "inst1", "hash1", "{}", "admin", DateTimeOffset.UtcNow);
|
"dep1", "inst1", "hash1", "admin", DateTimeOffset.UtcNow,
|
||||||
|
"https://central:9000", "tok1");
|
||||||
actor.Tell(new SiteEnvelope("good-site", cmd));
|
actor.Tell(new SiteEnvelope("good-site", cmd));
|
||||||
|
|
||||||
Assert.True(siteProbes.ContainsKey("good-site"),
|
Assert.True(siteProbes.ContainsKey("good-site"),
|
||||||
"good-site should have a ClusterClient even though bad-site's address is malformed");
|
"good-site should have a ClusterClient even though bad-site's address is malformed");
|
||||||
var msg = siteProbes["good-site"].ExpectMsg<ClusterClient.Send>();
|
var msg = siteProbes["good-site"].ExpectMsg<ClusterClient.Send>();
|
||||||
Assert.Equal("dep1", ((DeployInstanceCommand)msg.Message).DeploymentId);
|
Assert.Equal("dep1", ((RefreshDeploymentCommand)msg.Message).DeploymentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private NotificationSubmit CreateSubmit(string id = "notif1") =>
|
private NotificationSubmit CreateSubmit(string id = "notif1") =>
|
||||||
|
|||||||
@@ -15,20 +15,6 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Tests;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommunicationServiceTests : TestKit
|
public class CommunicationServiceTests : TestKit
|
||||||
{
|
{
|
||||||
[Fact]
|
|
||||||
public async Task BeforeInitialization_ThrowsOnUsage()
|
|
||||||
{
|
|
||||||
var options = Options.Create(new CommunicationOptions());
|
|
||||||
var logger = NullLogger<CommunicationService>.Instance;
|
|
||||||
var service = new CommunicationService(options, logger);
|
|
||||||
|
|
||||||
// CommunicationService requires SetCommunicationActor before use
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
||||||
service.DeployInstanceAsync("site1",
|
|
||||||
new DeployInstanceCommand(
|
|
||||||
"dep1", "inst1", "hash1", "{}", "admin", DateTimeOffset.UtcNow)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UnsubscribeDebugView_IsTellNotAsk()
|
public void UnsubscribeDebugView_IsTellNotAsk()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,20 +25,6 @@ public class SiteCommunicationActorTests : TestKit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeployCommand_ForwardedToDeploymentManager()
|
|
||||||
{
|
|
||||||
var dmProbe = CreateTestProbe();
|
|
||||||
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
||||||
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
||||||
|
|
||||||
var command = new DeployInstanceCommand(
|
|
||||||
"dep1", "inst1", "hash1", "{}", "admin", DateTimeOffset.UtcNow);
|
|
||||||
siteActor.Tell(command);
|
|
||||||
|
|
||||||
dmProbe.ExpectMsg<DeployInstanceCommand>(msg => msg.DeploymentId == "dep1");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RefreshDeploymentCommand_ForwardedToDeploymentManager()
|
public void RefreshDeploymentCommand_ForwardedToDeploymentManager()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -142,8 +142,9 @@ akka {
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ApplyConfigDeploy_EmptyFetchCoords_SkipsFetchAndWrite()
|
public async Task ApplyConfigDeploy_EmptyFetchCoords_SkipsFetchAndWrite()
|
||||||
{
|
{
|
||||||
// The direct DeployInstanceCommand wire path (retired in Task 14) replicates with
|
// The direct DeployInstanceCommand cross-cluster wire path was retired in Task 14.
|
||||||
// empty coords; the guard must skip quietly — no FetchAsync("") call, no write.
|
// This tests the defensive guard: if empty coords arrive, the actor must skip quietly
|
||||||
|
// — no FetchAsync("") call, no write — rather than erroring.
|
||||||
var fetcher = new FakeConfigFetcher(_ => Task.FromResult("never"));
|
var fetcher = new FakeConfigFetcher(_ => Task.FromResult("never"));
|
||||||
var actor = CreateReplicationActor(fetcher);
|
var actor = CreateReplicationActor(fetcher);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user