feat(dcl): extend CreateConnectionCommand with backup config and failover retry count

Update CreateConnectionCommand to carry PrimaryConnectionDetails,
BackupConnectionDetails, and FailoverRetryCount. Update all callers:
DataConnectionManagerActor, DataConnectionActor, DeploymentManagerActor,
FlatteningService, and ConnectionConfig. The actor stores both configs
but continues using primary only — failover logic comes in Task 3.
This commit is contained in:
Joseph Doherty
2026-03-22 08:24:39 -04:00
parent 04af03980e
commit 46304678da
7 changed files with 42 additions and 10 deletions

View File

@@ -7,4 +7,6 @@ namespace ScadaLink.Commons.Messages.DataConnection;
public record CreateConnectionCommand(
string ConnectionName,
string ProtocolType,
IDictionary<string, string> ConnectionDetails);
IDictionary<string, string> PrimaryConnectionDetails,
IDictionary<string, string>? BackupConnectionDetails = null,
int FailoverRetryCount = 3);

View File

@@ -33,6 +33,8 @@ public sealed record ConnectionConfig
{
public string Protocol { get; init; } = string.Empty;
public string? ConfigurationJson { get; init; }
public string? BackupConfigurationJson { get; init; }
public int FailoverRetryCount { get; init; } = 3;
}
/// <summary>

View File

@@ -61,6 +61,9 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
private int _resolvedTags;
private readonly IDictionary<string, string> _connectionDetails;
private readonly IDictionary<string, string> _primaryConfig;
private readonly IDictionary<string, string>? _backupConfig;
private readonly int _failoverRetryCount;
/// <summary>
/// Captured Self reference for use from non-actor threads (event handlers, callbacks).
@@ -73,13 +76,18 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
IDataConnection adapter,
DataConnectionOptions options,
ISiteHealthCollector healthCollector,
IDictionary<string, string>? connectionDetails = null)
IDictionary<string, string>? primaryConfig = null,
IDictionary<string, string>? backupConfig = null,
int failoverRetryCount = 3)
{
_connectionName = connectionName;
_adapter = adapter;
_options = options;
_healthCollector = healthCollector;
_connectionDetails = connectionDetails ?? new Dictionary<string, string>();
_primaryConfig = primaryConfig ?? new Dictionary<string, string>();
_backupConfig = backupConfig;
_failoverRetryCount = failoverRetryCount;
_connectionDetails = _primaryConfig;
}
protected override void PreStart()

View File

@@ -45,10 +45,13 @@ public class DataConnectionManagerActor : ReceiveActor
}
// WP-34: Factory creates the correct adapter based on protocol type
var adapter = _factory.Create(command.ProtocolType, command.ConnectionDetails);
var adapter = _factory.Create(command.ProtocolType, command.PrimaryConnectionDetails);
var props = Props.Create(() => new DataConnectionActor(
command.ConnectionName, adapter, _options, _healthCollector, command.ConnectionDetails));
command.ConnectionName, adapter, _options, _healthCollector,
command.PrimaryConnectionDetails,
command.BackupConnectionDetails,
command.FailoverRetryCount));
// Sanitize name for Akka actor path (replace spaces and invalid chars)
var actorName = new string(command.ConnectionName

View File

@@ -422,7 +422,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
if (_createdConnections.Contains(name))
continue;
var connectionDetails = new Dictionary<string, string>();
var primaryDetails = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(connConfig.ConfigurationJson))
{
try
@@ -431,14 +431,29 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
using var doc = System.Text.Json.JsonDocument.Parse(connConfig.ConfigurationJson);
foreach (var prop in doc.RootElement.EnumerateObject())
{
connectionDetails[prop.Name] = prop.Value.ToString();
primaryDetails[prop.Name] = prop.Value.ToString();
}
}
catch { /* Ignore parse errors */ }
}
Dictionary<string, string>? backupDetails = null;
if (!string.IsNullOrEmpty(connConfig.BackupConfigurationJson))
{
try
{
backupDetails = new Dictionary<string, string>();
using var doc = System.Text.Json.JsonDocument.Parse(connConfig.BackupConfigurationJson);
foreach (var prop in doc.RootElement.EnumerateObject())
{
backupDetails[prop.Name] = prop.Value.ToString();
}
}
catch { backupDetails = null; /* Ignore parse errors */ }
}
_dclManager.Tell(new Commons.Messages.DataConnection.CreateConnectionCommand(
name, connConfig.Protocol, connectionDetails));
name, connConfig.Protocol, primaryDetails, backupDetails, connConfig.FailoverRetryCount));
_createdConnections.Add(name);
_logger.LogInformation(

View File

@@ -90,7 +90,9 @@ public class FlatteningService
connections[attr.BoundDataConnectionName] = new ConnectionConfig
{
Protocol = conn.Protocol,
ConfigurationJson = conn.PrimaryConfiguration
ConfigurationJson = conn.PrimaryConfiguration,
BackupConfigurationJson = conn.BackupConfiguration,
FailoverRetryCount = conn.FailoverRetryCount
};
}
}

View File

@@ -70,7 +70,7 @@ public class DataConnectionManagerActorTests : TestKit
new DataConnectionManagerActor(_mockFactory, _options, _mockHealthCollector)));
manager.Tell(new CreateConnectionCommand(
"conn1", "OpcUa", new Dictionary<string, string>()));
"conn1", "OpcUa", new Dictionary<string, string>(), null, 3));
// Factory should have been called
AwaitCondition(() =>