c8e2f4da02
Central and each site are SEPARATE Akka clusters, so central cannot act on a
site's membership -- it asks. New TriggerSiteFailover/SiteFailoverAck contract
travels the existing ClusterClient command/control channel (mirroring the
RetryParkedOperation relay); the site's own SiteCommunicationActor performs the
graceful Leave and acks the outcome.
- ClusterFailoverCoordinator moved out of Host into Communication/ClusterState,
beside ActiveNodeEvaluator. Both paths now share ONE oldest-Up implementation;
SiteCommunicationActor cannot reference Host, and the two definitions must not
drift or the node asked to leave stops being the singleton host.
- Site scope is the SITE-SPECIFIC role (site-{SiteId}), not the base Site role --
site singletons are placed on the former, so the base role would move the wrong
node. Pinned by a unit test asserting the role string and by a real-cluster test.
- Site-side guards: refuses a command addressed to another site (a misroute must
never fail over a site the operator did not select), refuses when there is no
peer, and reports a fault as an ack rather than throwing into supervision --
a restart there would drop central's Ask into a bare timeout and lose the reason.
- Ack is sent before the Leave takes effect so it still reaches central.
- UI: the same control now serves both scopes via a SiteId parameter. The site
confirmation deliberately does NOT claim the admin's page will disconnect --
it won't, and crying wolf there devalues the central warning that is real. A
site refusal and an unreachable site surface distinctly.
- Rolling upgrade: a site on an older binary has no handler, so the message
dead-letters and the Ask times out, reported as "site did not respond". That
is the honest outcome; documented on the contract.
Fallout fixed: HealthPageTests now renders the page inside
CascadingAuthenticationState with the real policy set and IAuthorizationService,
because the cards embed an AuthorizeView. That mirrors production, where the
layout supplies the cascading value.
461 lines
19 KiB
C#
461 lines
19 KiB
C#
using Akka.Actor;
|
|
using Akka.Cluster.Tools.Client;
|
|
using Akka.TestKit.Xunit2;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Health;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
|
|
using ZB.MOM.WW.ScadaBridge.Communication.Actors;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Communication.Tests;
|
|
|
|
/// <summary>
|
|
/// WP-4: Tests for SiteCommunicationActor message routing to local actors.
|
|
/// </summary>
|
|
public class SiteCommunicationActorTests : TestKit
|
|
{
|
|
private readonly CommunicationOptions _options = new();
|
|
|
|
public SiteCommunicationActorTests()
|
|
: base(@"akka.loglevel = DEBUG")
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public void RefreshDeploymentCommand_ForwardedToDeploymentManager()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
var command = new RefreshDeploymentCommand(
|
|
"dep1", "inst1", "hash1", "admin", DateTimeOffset.UtcNow,
|
|
"https://central/", "tok1");
|
|
siteActor.Tell(command);
|
|
|
|
dmProbe.ExpectMsg<RefreshDeploymentCommand>(msg => msg.DeploymentId == "dep1");
|
|
}
|
|
|
|
[Fact]
|
|
public void LifecycleCommands_ForwardedToDeploymentManager()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
siteActor.Tell(new DisableInstanceCommand("cmd1", "inst1", DateTimeOffset.UtcNow));
|
|
dmProbe.ExpectMsg<DisableInstanceCommand>();
|
|
|
|
siteActor.Tell(new EnableInstanceCommand("cmd2", "inst1", DateTimeOffset.UtcNow));
|
|
dmProbe.ExpectMsg<EnableInstanceCommand>();
|
|
|
|
siteActor.Tell(new DeleteInstanceCommand("cmd3", "inst1", DateTimeOffset.UtcNow));
|
|
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()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
var request = new IntegrationCallRequest(
|
|
"corr1", "site1", "inst1", "ExtSys1", "GetData",
|
|
new Dictionary<string, object?>(), DateTimeOffset.UtcNow);
|
|
|
|
siteActor.Tell(request);
|
|
|
|
ExpectMsg<IntegrationCallResponse>(msg =>
|
|
!msg.Success && msg.ErrorMessage == "Integration handler not available");
|
|
}
|
|
|
|
[Fact]
|
|
public void IntegrationCall_WithHandler_ForwardedToHandler()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var handlerProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
// Register integration handler
|
|
siteActor.Tell(new RegisterLocalHandler(LocalHandlerType.Integration, handlerProbe.Ref));
|
|
|
|
var request = new IntegrationCallRequest(
|
|
"corr1", "site1", "inst1", "ExtSys1", "GetData",
|
|
new Dictionary<string, object?>(), DateTimeOffset.UtcNow);
|
|
|
|
siteActor.Tell(request);
|
|
handlerProbe.ExpectMsg<IntegrationCallRequest>(msg => msg.CorrelationId == "corr1");
|
|
}
|
|
|
|
[Fact]
|
|
public void NotificationSubmit_WithCentralClient_ForwardedToCentralAndAckRoutedBack()
|
|
{
|
|
// The site forwards a buffered notification to central over the ClusterClient
|
|
// command/control transport; the central ack must route back to the original
|
|
// sender (the S&F forwarder's Ask), not to the SiteCommunicationActor.
|
|
var dmProbe = CreateTestProbe();
|
|
var centralClientProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
siteActor.Tell(new RegisterCentralClient(centralClientProbe.Ref));
|
|
|
|
var submit = new NotificationSubmit(
|
|
"notif-1", "Operators", "Subj", "Body", "site1", "inst1", "alarmScript",
|
|
DateTimeOffset.UtcNow);
|
|
siteActor.Tell(submit);
|
|
|
|
// Central client (acting as ClusterClient) receives a ClusterClient.Send wrapping
|
|
// the NotificationSubmit, addressed to the central communication actor. Fish past
|
|
// any periodic HeartbeatMessage the actor's timer may interleave.
|
|
var send = centralClientProbe.FishForMessage<ClusterClient.Send>(
|
|
s => s.Message is NotificationSubmit);
|
|
Assert.Equal("/user/central-communication", send.Path);
|
|
var forwarded = Assert.IsType<NotificationSubmit>(send.Message);
|
|
Assert.Equal("notif-1", forwarded.NotificationId);
|
|
|
|
// The ack is sent to the ClusterClient.Send's Sender — replying as that probe
|
|
// must land back at the test actor (the original Tell sender).
|
|
centralClientProbe.Reply(new NotificationSubmitAck("notif-1", Accepted: true, Error: null));
|
|
ExpectMsg<NotificationSubmitAck>(ack => ack.NotificationId == "notif-1" && ack.Accepted);
|
|
}
|
|
|
|
[Fact]
|
|
public void NotificationSubmit_WithoutCentralClient_RepliesWithNonAccepted()
|
|
{
|
|
// No ClusterClient registered yet: the submit cannot be forwarded, so the actor
|
|
// replies with a non-accepted ack and the S&F forwarder treats it as transient.
|
|
var dmProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
var submit = new NotificationSubmit(
|
|
"notif-2", "Operators", "Subj", "Body", "site1", null, null,
|
|
DateTimeOffset.UtcNow);
|
|
siteActor.Tell(submit);
|
|
|
|
ExpectMsg<NotificationSubmitAck>(ack => ack.NotificationId == "notif-2" && !ack.Accepted);
|
|
}
|
|
|
|
[Fact]
|
|
public void NotificationStatusQuery_WithCentralClient_ForwardedToCentralAndResponseRoutedBack()
|
|
{
|
|
// Notify.Status(id) issues a NotificationStatusQuery; the site actor forwards it
|
|
// to central over the ClusterClient command/control transport and the central
|
|
// response must route back to the original sender (the helper's Ask).
|
|
var dmProbe = CreateTestProbe();
|
|
var centralClientProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
siteActor.Tell(new RegisterCentralClient(centralClientProbe.Ref));
|
|
|
|
var query = new NotificationStatusQuery("corr-99", "notif-1");
|
|
siteActor.Tell(query);
|
|
|
|
var send = centralClientProbe.FishForMessage<ClusterClient.Send>(
|
|
s => s.Message is NotificationStatusQuery);
|
|
Assert.Equal("/user/central-communication", send.Path);
|
|
var forwarded = Assert.IsType<NotificationStatusQuery>(send.Message);
|
|
Assert.Equal("notif-1", forwarded.NotificationId);
|
|
|
|
// The response is sent to the ClusterClient.Send's Sender — replying as that
|
|
// probe must land back at the test actor (the original Tell sender).
|
|
centralClientProbe.Reply(new NotificationStatusResponse(
|
|
"corr-99", Found: true, Status: "Delivered", RetryCount: 0,
|
|
LastError: null, DeliveredAt: DateTimeOffset.UtcNow));
|
|
ExpectMsg<NotificationStatusResponse>(r => r.CorrelationId == "corr-99" && r.Found);
|
|
}
|
|
|
|
[Fact]
|
|
public void NotificationStatusQuery_WithoutCentralClient_RepliesWithNotFound()
|
|
{
|
|
// No ClusterClient registered yet: the query cannot reach central, so the actor
|
|
// replies Found: false. Notify.Status then falls back to the site S&F buffer.
|
|
var dmProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
siteActor.Tell(new NotificationStatusQuery("corr-100", "notif-2"));
|
|
|
|
ExpectMsg<NotificationStatusResponse>(
|
|
r => r.CorrelationId == "corr-100" && !r.Found);
|
|
}
|
|
|
|
[Fact]
|
|
public void EventLogQuery_WithoutHandler_ReturnsFailure()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
var request = new EventLogQueryRequest(
|
|
"corr1", "site1", null, null, null, null, null, null, null, 25, DateTimeOffset.UtcNow);
|
|
|
|
siteActor.Tell(request);
|
|
|
|
ExpectMsg<EventLogQueryResponse>(msg => !msg.Success);
|
|
}
|
|
|
|
// ── Task 5 (#22): central→site Retry/Discard relay for parked cached calls ──
|
|
|
|
[Fact]
|
|
public void RetryParkedOperation_WithHandler_ForwardedToParkedMessageHandler()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var handlerProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
siteActor.Tell(new RegisterLocalHandler(LocalHandlerType.ParkedMessages, handlerProbe.Ref));
|
|
|
|
var id = Commons.Types.TrackedOperationId.New();
|
|
siteActor.Tell(new RetryParkedOperation("corr-rp", id));
|
|
|
|
handlerProbe.ExpectMsg<RetryParkedOperation>(msg =>
|
|
msg.CorrelationId == "corr-rp" && msg.TrackedOperationId.Equals(id));
|
|
}
|
|
|
|
[Fact]
|
|
public void DiscardParkedOperation_WithHandler_ForwardedToParkedMessageHandler()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var handlerProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
siteActor.Tell(new RegisterLocalHandler(LocalHandlerType.ParkedMessages, handlerProbe.Ref));
|
|
|
|
var id = Commons.Types.TrackedOperationId.New();
|
|
siteActor.Tell(new DiscardParkedOperation("corr-dp", id));
|
|
|
|
handlerProbe.ExpectMsg<DiscardParkedOperation>(msg =>
|
|
msg.CorrelationId == "corr-dp" && msg.TrackedOperationId.Equals(id));
|
|
}
|
|
|
|
[Fact]
|
|
public void RetryParkedOperation_WithoutHandler_RepliesNotAppliedAck()
|
|
{
|
|
// No parked-message handler registered — the relay must get a definitive
|
|
// non-applied ack, not silence (the SiteCallAuditActor's Ask must not
|
|
// hang and then mis-report site-unreachable when the site IS reachable).
|
|
var dmProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
siteActor.Tell(new RetryParkedOperation("corr-no-handler", Commons.Types.TrackedOperationId.New()));
|
|
|
|
var ack = ExpectMsg<ParkedOperationActionAck>();
|
|
Assert.Equal("corr-no-handler", ack.CorrelationId);
|
|
Assert.False(ack.Applied);
|
|
Assert.NotNull(ack.ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void DiscardParkedOperation_WithoutHandler_RepliesNotAppliedAck()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
siteActor.Tell(new DiscardParkedOperation("corr-no-handler", Commons.Types.TrackedOperationId.New()));
|
|
|
|
var ack = ExpectMsg<ParkedOperationActionAck>();
|
|
Assert.False(ack.Applied);
|
|
Assert.NotNull(ack.ErrorMessage);
|
|
}
|
|
|
|
// ── M7 OPC UA cross-cluster routing: Search (T15), WriteTag (T14), Verify (T17) ──
|
|
//
|
|
// Regression guard for the M7 dead-letter defect. These three interactive
|
|
// commands have downstream handlers in DataConnectionManagerActor but were NOT
|
|
// forwarded through SiteCommunicationActor → Deployment Manager, so they
|
|
// dead-lettered and the central Ask timed out in the real cluster. They must
|
|
// forward to the Deployment Manager proxy exactly like BrowseNodeCommand, with
|
|
// the original Ask sender preserved so the result routes straight back.
|
|
|
|
[Fact]
|
|
public void SearchAddressSpaceCommand_ForwardedToDeploymentManager_SenderPreserved()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var senderProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
var command = new SearchAddressSpaceCommand("conn1", "Temp", 5, 100);
|
|
siteActor.Tell(command, senderProbe.Ref);
|
|
|
|
dmProbe.ExpectMsg<SearchAddressSpaceCommand>(msg => msg.ConnectionName == "conn1");
|
|
Assert.Equal(senderProbe.Ref, dmProbe.LastSender);
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteTagRequest_ForwardedToDeploymentManager_SenderPreserved()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var senderProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
var request = new WriteTagRequest(
|
|
"corr-w", "conn1", "Channel1.Device1.Tag1", 42, DateTimeOffset.UtcNow);
|
|
siteActor.Tell(request, senderProbe.Ref);
|
|
|
|
dmProbe.ExpectMsg<WriteTagRequest>(msg => msg.CorrelationId == "corr-w");
|
|
Assert.Equal(senderProbe.Ref, dmProbe.LastSender);
|
|
}
|
|
|
|
[Fact]
|
|
public void VerifyEndpointCommand_ForwardedToDeploymentManager_SenderPreserved()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
var senderProbe = CreateTestProbe();
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
|
|
|
|
var command = new VerifyEndpointCommand("conn1", "OpcUa", "{}");
|
|
siteActor.Tell(command, senderProbe.Ref);
|
|
|
|
dmProbe.ExpectMsg<VerifyEndpointCommand>(msg => msg.ConnectionName == "conn1");
|
|
Assert.Equal(senderProbe.Ref, dmProbe.LastSender);
|
|
}
|
|
|
|
// ── Communication-018: heartbeat IsActive reflects this node's cluster role ──
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void Heartbeat_StampsIsActive_FromInjectedCheck(bool isActive)
|
|
{
|
|
// Communication-018: HeartbeatMessage.IsActive must reflect the actual
|
|
// active/standby role of this node, not a hard-coded `true`. The actor
|
|
// now takes a Func<bool> override (defaulting to a real Akka.Cluster
|
|
// leader check in production); tests inject a stub so they do not need
|
|
// to bring up a full cluster in the TestKit ActorSystem.
|
|
var dmProbe = CreateTestProbe();
|
|
var centralClientProbe = CreateTestProbe();
|
|
var fastHeartbeatOptions = new CommunicationOptions
|
|
{
|
|
TransportHeartbeatInterval = TimeSpan.FromMilliseconds(50)
|
|
};
|
|
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", fastHeartbeatOptions, dmProbe.Ref, () => isActive)));
|
|
|
|
siteActor.Tell(new RegisterCentralClient(centralClientProbe.Ref));
|
|
|
|
var send = centralClientProbe.FishForMessage<ClusterClient.Send>(
|
|
s => s.Message is HeartbeatMessage, TimeSpan.FromSeconds(3));
|
|
var heartbeat = Assert.IsType<HeartbeatMessage>(send.Message);
|
|
Assert.Equal(isActive, heartbeat.IsActive);
|
|
Assert.Equal("site1", heartbeat.SiteId);
|
|
}
|
|
|
|
// ---- Central→site failover relay (Task 10) ----------------------------------
|
|
// The failover action is injected for the same reason isActiveCheck is: performing a
|
|
// real Cluster.Leave would require Akka.Cluster in the TestKit ActorSystem. These
|
|
// tests pin the ROUTING and GUARD logic; the actual Leave against a real two-node
|
|
// site cluster is covered by SiteFailoverRelayTests in the integration suite.
|
|
|
|
[Fact]
|
|
public void TriggerSiteFailover_IssuesTheLeave_AndAcksWithTheTarget()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
string? roleAskedFor = null;
|
|
Func<string, string?> failOver = role =>
|
|
{
|
|
roleAskedFor = role;
|
|
return "akka.tcp://scadabridge@site1-a:8082";
|
|
};
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref, null, failOver)));
|
|
|
|
siteActor.Tell(new TriggerSiteFailover("corr-1", "site1"));
|
|
|
|
var ack = ExpectMsg<SiteFailoverAck>();
|
|
Assert.True(ack.Accepted);
|
|
Assert.Equal("corr-1", ack.CorrelationId);
|
|
Assert.Equal("akka.tcp://scadabridge@site1-a:8082", ack.TargetAddress);
|
|
Assert.Null(ack.ErrorMessage);
|
|
// Site singletons are scoped to the site-specific role, not the base "Site" role;
|
|
// failing over the wrong scope would move the wrong node.
|
|
Assert.Equal("site-site1", roleAskedFor);
|
|
}
|
|
|
|
[Fact]
|
|
public void TriggerSiteFailover_RefusesWhenThereIsNoPeer()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
Func<string, string?> failOver = _ => null;
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref, null, failOver)));
|
|
|
|
siteActor.Tell(new TriggerSiteFailover("corr-2", "site1"));
|
|
|
|
var ack = ExpectMsg<SiteFailoverAck>();
|
|
Assert.False(ack.Accepted);
|
|
Assert.Null(ack.TargetAddress);
|
|
Assert.NotNull(ack.ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void TriggerSiteFailover_RefusesACommandAddressedToAnotherSite()
|
|
{
|
|
// A misrouted command must be refused, not silently acted on — acting would fail
|
|
// over a site the operator never selected.
|
|
var dmProbe = CreateTestProbe();
|
|
var invoked = false;
|
|
Func<string, string?> failOver = _ =>
|
|
{
|
|
invoked = true;
|
|
return "addr";
|
|
};
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref, null, failOver)));
|
|
|
|
siteActor.Tell(new TriggerSiteFailover("corr-3", "site2"));
|
|
|
|
var ack = ExpectMsg<SiteFailoverAck>();
|
|
Assert.False(ack.Accepted);
|
|
Assert.Contains("site2", ack.ErrorMessage);
|
|
Assert.False(invoked);
|
|
}
|
|
|
|
[Fact]
|
|
public void TriggerSiteFailover_FaultInTheLeave_IsReportedNotThrown()
|
|
{
|
|
var dmProbe = CreateTestProbe();
|
|
Func<string, string?> failOver = _ => throw new InvalidOperationException("cluster unavailable");
|
|
var siteActor = Sys.ActorOf(Props.Create(() =>
|
|
new SiteCommunicationActor("site1", _options, dmProbe.Ref, null, failOver)));
|
|
|
|
siteActor.Tell(new TriggerSiteFailover("corr-4", "site1"));
|
|
|
|
var ack = ExpectMsg<SiteFailoverAck>();
|
|
Assert.False(ack.Accepted);
|
|
Assert.Contains("cluster unavailable", ack.ErrorMessage);
|
|
}
|
|
}
|