feat: wire SQLite replication between site nodes and fix ConfigurationDatabase tests

Add SiteReplicationActor (runs on every site node) to replicate deployed
configs and store-and-forward buffer operations to the standby peer via
cluster member discovery and fire-and-forget Tell. Wire ReplicationService
handler and pass replication actor to DeploymentManagerActor singleton.

Fix 5 pre-existing ConfigurationDatabase test failures: RowVersion NOT NULL
on SQLite, stale migration name assertion, and seed data count mismatch.
This commit is contained in:
Joseph Doherty
2026-03-18 08:28:02 -04:00
parent f063fb1ca3
commit eb8ead58d2
23 changed files with 707 additions and 33 deletions

View File

@@ -6,6 +6,7 @@ using ScadaLink.Commons.Messages.Deployment;
using ScadaLink.Commons.Messages.Lifecycle;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.HealthMonitoring;
using ScadaLink.SiteRuntime.Messages;
using ScadaLink.SiteRuntime.Persistence;
using ScadaLink.SiteRuntime.Scripts;
using ScadaLink.SiteRuntime.Streaming;
@@ -31,6 +32,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
private readonly SiteRuntimeOptions _options;
private readonly ILogger<DeploymentManagerActor> _logger;
private readonly IActorRef? _dclManager;
private readonly IActorRef? _replicationActor;
private readonly ISiteHealthCollector? _healthCollector;
private readonly IServiceProvider? _serviceProvider;
private readonly Dictionary<string, IActorRef> _instanceActors = new();
@@ -46,6 +48,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
SiteRuntimeOptions options,
ILogger<DeploymentManagerActor> logger,
IActorRef? dclManager = null,
IActorRef? replicationActor = null,
ISiteHealthCollector? healthCollector = null,
IServiceProvider? serviceProvider = null)
{
@@ -55,6 +58,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
_streamManager = streamManager;
_options = options;
_dclManager = dclManager;
_replicationActor = replicationActor;
_healthCollector = healthCollector;
_serviceProvider = serviceProvider;
_logger = logger;
@@ -238,6 +242,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
// Static overrides are reset on redeployment per design decision
await _storage.ClearStaticOverridesAsync(instanceName);
// Replicate to standby node
_replicationActor?.Tell(new ReplicateConfigDeploy(
instanceName, command.FlattenedConfigurationJson,
command.DeploymentId, command.RevisionHash, true));
return new DeployPersistenceResult(command.DeploymentId, instanceName, true, null, sender);
}).ContinueWith(t =>
{
@@ -285,6 +294,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
var sender = Sender;
_storage.SetInstanceEnabledAsync(instanceName, false).ContinueWith(t =>
{
if (t.IsCompletedSuccessfully)
_replicationActor?.Tell(new ReplicateConfigSetEnabled(instanceName, false));
return new InstanceLifecycleResponse(
command.CommandId,
instanceName,
@@ -308,6 +320,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
Task.Run(async () =>
{
await _storage.SetInstanceEnabledAsync(instanceName, true);
_replicationActor?.Tell(new ReplicateConfigSetEnabled(instanceName, true));
var configs = await _storage.GetAllDeployedConfigsAsync();
var config = configs.FirstOrDefault(c => c.InstanceUniqueName == instanceName);
return new EnableResult(command, config, null, sender);
@@ -365,6 +378,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
var sender = Sender;
_storage.RemoveDeployedConfigAsync(instanceName).ContinueWith(t =>
{
if (t.IsCompletedSuccessfully)
_replicationActor?.Tell(new ReplicateConfigRemove(instanceName));
return new InstanceLifecycleResponse(
command.CommandId,
instanceName,
@@ -548,6 +564,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
}
// Replicate artifacts to standby node
_replicationActor?.Tell(new ReplicateArtifacts(command));
return new ArtifactDeploymentResponse(
command.DeploymentId, "", true, null, DateTimeOffset.UtcNow);
}