feat(adminops): IDriverProbe + TestDriverConnect actor handler
- IDriverProbe abstraction in Core.Abstractions; one impl per driver type, resolved by DriverType string. Phase 7.3 + 7.4 add concrete probes for the 9 supported driver types. - TestDriverConnect / TestDriverConnectResult messages. - AdminOperationsActor.HandleTestDriverConnectAsync looks up the probe by DriverType, runs it with a [1,60]s clamped timeout, and returns success/latency or failure/message. Probes that throw or time out surface as soft failures.
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AdminUI → AdminOperationsActor request: probe one driver type's connection using
|
||||||
|
/// the supplied JSON config. Routed through <c>IAdminOperationsClient</c>; reply is
|
||||||
|
/// <see cref="TestDriverConnectResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="DriverType">Must match an installed <c>IDriverProbe.DriverType</c>.</param>
|
||||||
|
/// <param name="ConfigJson">Driver config as JSON (same shape as <c>DriverInstance.DriverConfig</c>).</param>
|
||||||
|
/// <param name="TimeoutSeconds">Per-probe timeout; server clamps to [1, 60].</param>
|
||||||
|
/// <param name="CorrelationId">Round-trip correlation token.</param>
|
||||||
|
public sealed record TestDriverConnect(
|
||||||
|
string DriverType,
|
||||||
|
string ConfigJson,
|
||||||
|
int TimeoutSeconds,
|
||||||
|
Guid CorrelationId);
|
||||||
|
|
||||||
|
/// <summary>Reply for <see cref="TestDriverConnect"/>.</summary>
|
||||||
|
/// <param name="Ok">True iff the probe succeeded.</param>
|
||||||
|
/// <param name="Message">Failure reason; null on success.</param>
|
||||||
|
/// <param name="LatencyMs">Round-trip latency in milliseconds; null on failure or timeout.</param>
|
||||||
|
/// <param name="CorrelationId">Echoes the request's correlation token.</param>
|
||||||
|
public sealed record TestDriverConnectResult(
|
||||||
|
bool Ok,
|
||||||
|
string? Message,
|
||||||
|
double? LatencyMs,
|
||||||
|
Guid CorrelationId);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test-connect probe for one driver type. Implementations deserialize a driver-config
|
||||||
|
/// JSON, attempt a cheap connection (TCP open, OPC UA session, gRPC ping — whatever the
|
||||||
|
/// driver's native protocol supports), and report success/failure with latency. Probes
|
||||||
|
/// MUST NOT mutate any persistent state; the AdminUI invokes them against transient
|
||||||
|
/// config from the typed form, NOT against the persisted DriverInstance row.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDriverProbe
|
||||||
|
{
|
||||||
|
/// <summary>DriverInstance.DriverType string this probe handles. Used for DI lookup.</summary>
|
||||||
|
string DriverType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run the probe with the supplied config + timeout. Honour <paramref name="ct"/> for
|
||||||
|
/// timeout cancellation. Never throw on connection failure; instead return a result
|
||||||
|
/// with <c>Ok = false</c> + a message.
|
||||||
|
/// </summary>
|
||||||
|
Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Outcome of a single <see cref="IDriverProbe.ProbeAsync"/> call.</summary>
|
||||||
|
/// <param name="Ok">True iff the probe reached its target and the handshake succeeded.</param>
|
||||||
|
/// <param name="Message">Human-readable status; null on success.</param>
|
||||||
|
/// <param name="Latency">Wall-clock duration of the successful probe; null on failure.</param>
|
||||||
|
public sealed record DriverProbeResult(bool Ok, string? Message, TimeSpan? Latency);
|
||||||
@@ -7,6 +7,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|||||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
||||||
|
|
||||||
@@ -19,28 +20,35 @@ public sealed class AdminOperationsActor : ReceiveActor
|
|||||||
{
|
{
|
||||||
private readonly IDbContextFactory<OtOpcUaConfigDbContext> _dbFactory;
|
private readonly IDbContextFactory<OtOpcUaConfigDbContext> _dbFactory;
|
||||||
private readonly IActorRef _coordinator;
|
private readonly IActorRef _coordinator;
|
||||||
|
private readonly IReadOnlyDictionary<string, IDriverProbe> _probesByType;
|
||||||
private readonly ILoggingAdapter _log = Context.GetLogger();
|
private readonly ILoggingAdapter _log = Context.GetLogger();
|
||||||
|
|
||||||
/// <summary>Creates actor props for the admin operations actor.</summary>
|
/// <summary>Creates actor props for the admin operations actor.</summary>
|
||||||
/// <param name="dbFactory">Factory for creating config database contexts.</param>
|
/// <param name="dbFactory">Factory for creating config database contexts.</param>
|
||||||
/// <param name="coordinator">Reference to the deployment coordinator actor.</param>
|
/// <param name="coordinator">Reference to the deployment coordinator actor.</param>
|
||||||
|
/// <param name="probes">Driver probes registered in DI; keyed by DriverType (case-insensitive).</param>
|
||||||
/// <returns>Props configured to create an AdminOperationsActor.</returns>
|
/// <returns>Props configured to create an AdminOperationsActor.</returns>
|
||||||
public static Props Props(
|
public static Props Props(
|
||||||
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
||||||
IActorRef coordinator) =>
|
IActorRef coordinator,
|
||||||
Akka.Actor.Props.Create(() => new AdminOperationsActor(dbFactory, coordinator));
|
IEnumerable<IDriverProbe> probes) =>
|
||||||
|
Akka.Actor.Props.Create(() => new AdminOperationsActor(dbFactory, coordinator, probes));
|
||||||
|
|
||||||
/// <summary>Initializes a new instance of the AdminOperationsActor.</summary>
|
/// <summary>Initializes a new instance of the AdminOperationsActor.</summary>
|
||||||
/// <param name="dbFactory">Factory for creating config database contexts.</param>
|
/// <param name="dbFactory">Factory for creating config database contexts.</param>
|
||||||
/// <param name="coordinator">Reference to the deployment coordinator actor.</param>
|
/// <param name="coordinator">Reference to the deployment coordinator actor.</param>
|
||||||
|
/// <param name="probes">Driver probes registered in DI; keyed by DriverType (case-insensitive).</param>
|
||||||
public AdminOperationsActor(
|
public AdminOperationsActor(
|
||||||
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
IDbContextFactory<OtOpcUaConfigDbContext> dbFactory,
|
||||||
IActorRef coordinator)
|
IActorRef coordinator,
|
||||||
|
IEnumerable<IDriverProbe> probes)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_coordinator = coordinator;
|
_coordinator = coordinator;
|
||||||
|
_probesByType = probes.ToDictionary(p => p.DriverType, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
ReceiveAsync<StartDeployment>(HandleStartDeploymentAsync);
|
ReceiveAsync<StartDeployment>(HandleStartDeploymentAsync);
|
||||||
|
ReceiveAsync<TestDriverConnect>(HandleTestDriverConnectAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleStartDeploymentAsync(StartDeployment msg)
|
private async Task HandleStartDeploymentAsync(StartDeployment msg)
|
||||||
@@ -112,4 +120,51 @@ public sealed class AdminOperationsActor : ReceiveActor
|
|||||||
msg.CorrelationId));
|
msg.CorrelationId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleTestDriverConnectAsync(TestDriverConnect msg)
|
||||||
|
{
|
||||||
|
var replyTo = Sender;
|
||||||
|
if (!_probesByType.TryGetValue(msg.DriverType, out var probe))
|
||||||
|
{
|
||||||
|
replyTo.Tell(new TestDriverConnectResult(
|
||||||
|
false,
|
||||||
|
$"No probe registered for driver type '{msg.DriverType}'.",
|
||||||
|
null,
|
||||||
|
msg.CorrelationId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clampedSec = Math.Clamp(msg.TimeoutSeconds, 1, 60);
|
||||||
|
var timeout = TimeSpan.FromSeconds(clampedSec);
|
||||||
|
using var cts = new CancellationTokenSource(timeout);
|
||||||
|
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await probe.ProbeAsync(msg.ConfigJson, timeout, cts.Token);
|
||||||
|
sw.Stop();
|
||||||
|
replyTo.Tell(new TestDriverConnectResult(
|
||||||
|
result.Ok,
|
||||||
|
result.Message,
|
||||||
|
result.Ok ? sw.Elapsed.TotalMilliseconds : (double?)null,
|
||||||
|
msg.CorrelationId));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
replyTo.Tell(new TestDriverConnectResult(
|
||||||
|
false,
|
||||||
|
$"Probe timed out after {clampedSec}s.",
|
||||||
|
null,
|
||||||
|
msg.CorrelationId));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Error(ex, "Probe for {DriverType} threw", msg.DriverType);
|
||||||
|
replyTo.Tell(new TestDriverConnectResult(
|
||||||
|
false,
|
||||||
|
ex.Message,
|
||||||
|
null,
|
||||||
|
msg.CorrelationId));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using ZB.MOM.WW.OtOpcUa.ControlPlane.Audit;
|
|||||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.Coordinators;
|
using ZB.MOM.WW.OtOpcUa.ControlPlane.Coordinators;
|
||||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.Fleet;
|
using ZB.MOM.WW.OtOpcUa.ControlPlane.Fleet;
|
||||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.Redundancy;
|
using ZB.MOM.WW.OtOpcUa.ControlPlane.Redundancy;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.ControlPlane;
|
namespace ZB.MOM.WW.OtOpcUa.ControlPlane;
|
||||||
|
|
||||||
@@ -59,7 +60,8 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
|
var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
|
||||||
var coordinator = registry.Get<ConfigPublishCoordinatorKey>();
|
var coordinator = registry.Get<ConfigPublishCoordinatorKey>();
|
||||||
return AdminOperationsActor.Props(dbFactory, coordinator);
|
var probes = resolver.GetService<IEnumerable<IDriverProbe>>() ?? Enumerable.Empty<IDriverProbe>();
|
||||||
|
return AdminOperationsActor.Props(dbFactory, coordinator, probes);
|
||||||
},
|
},
|
||||||
singletonOptions);
|
singletonOptions);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj"/>
|
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj"/>
|
||||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Cluster\ZB.MOM.WW.OtOpcUa.Cluster.csproj"/>
|
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Cluster\ZB.MOM.WW.OtOpcUa.Cluster.csproj"/>
|
||||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Configuration\ZB.MOM.WW.OtOpcUa.Configuration.csproj"/>
|
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Configuration\ZB.MOM.WW.OtOpcUa.Configuration.csproj"/>
|
||||||
|
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|||||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
||||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;
|
using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests;
|
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
|
|||||||
{
|
{
|
||||||
var dbFactory = NewInMemoryDbFactory();
|
var dbFactory = NewInMemoryDbFactory();
|
||||||
var coordinator = CreateTestProbe("coord");
|
var coordinator = CreateTestProbe("coord");
|
||||||
var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref));
|
var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>()));
|
||||||
|
|
||||||
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
|
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var coordinator = CreateTestProbe("coord");
|
var coordinator = CreateTestProbe("coord");
|
||||||
var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref));
|
var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty<IDriverProbe>()));
|
||||||
|
|
||||||
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
|
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user