feat(adminui): Reconnect/Restart on DriverStatusPanel (DriverOperator-gated)
- RestartDriver / ReconnectDriver messages + AdminOperationsActor handlers (broadcast via driver-control DPS topic; audited via ConfigEdits). - DriverHostActor subscribes to driver-control; locates the matching child DriverInstanceActor and stops+respawns it (Restart) or sends it a ForceReconnect internal message (Reconnect — re-enters Reconnecting state without full stop). DriverInstanceSpec constructor call uses named args to handle the full 6-parameter signature. - New DriverOperator authorization policy mapped to DriverOperator or FleetAdmin role; documented in docs/security.md. Map LDAP group via GroupToRole (e.g. "ot-driver-operator": "DriverOperator"). - DriverStatusPanel renders Reconnect + Restart buttons when the user holds the DriverOperator policy (hidden otherwise). Restart requires an in-page Razor confirm block (no JS confirm, keeps SignalR event loop unblocked). Both buttons show a spinner and are disabled during in-flight; result chip auto-clears after 8s. Username sourced from AuthenticationStateProvider. Reconnect resolves to "ForceReconnect" (re-enter Reconnecting, not full stop+respawn) — transport drops and retries while actor and in-memory state are preserved. All DriverInstanceActor states handle ForceReconnect safely (no-op when already in transition).
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Cluster.Tools.PublishSubscribe;
|
||||
using Akka.Event;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
|
||||
@@ -49,6 +50,8 @@ public sealed class AdminOperationsActor : ReceiveActor
|
||||
|
||||
ReceiveAsync<StartDeployment>(HandleStartDeploymentAsync);
|
||||
ReceiveAsync<TestDriverConnect>(HandleTestDriverConnectAsync);
|
||||
ReceiveAsync<RestartDriver>(HandleRestartDriverAsync);
|
||||
ReceiveAsync<ReconnectDriver>(HandleReconnectDriverAsync);
|
||||
}
|
||||
|
||||
private async Task HandleStartDeploymentAsync(StartDeployment msg)
|
||||
@@ -167,4 +170,65 @@ public sealed class AdminOperationsActor : ReceiveActor
|
||||
msg.CorrelationId));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRestartDriverAsync(RestartDriver msg)
|
||||
{
|
||||
var replyTo = Sender;
|
||||
try
|
||||
{
|
||||
// Broadcast to every DriverHostActor on every node via the driver-control DPS topic.
|
||||
// Only the host that owns the instance will act; others ignore it (id not found in _children).
|
||||
DistributedPubSub.Get(Context.System).Mediator.Tell(new Publish("driver-control", msg));
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
db.ConfigEdits.Add(new ConfigEdit
|
||||
{
|
||||
EntityType = "DriverInstance",
|
||||
EntityId = Guid.TryParse(msg.DriverInstanceId, out var guid) ? guid : Guid.Empty,
|
||||
FieldsJson = $"{{\"op\":\"restart\",\"driverInstanceId\":{System.Text.Json.JsonSerializer.Serialize(msg.DriverInstanceId)}}}",
|
||||
EditedBy = msg.ActorByUserName,
|
||||
SourceNode = Akka.Cluster.Cluster.Get(Context.System).SelfAddress.Host ?? "unknown",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_log.Info("AdminOps: RestartDriver dispatched for {DriverInstanceId} by {User}",
|
||||
msg.DriverInstanceId, msg.ActorByUserName);
|
||||
replyTo.Tell(new RestartDriverResult(true, null, msg.CorrelationId));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(ex, "AdminOps: RestartDriver failed for {DriverInstanceId}", msg.DriverInstanceId);
|
||||
replyTo.Tell(new RestartDriverResult(false, ex.Message, msg.CorrelationId));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleReconnectDriverAsync(ReconnectDriver msg)
|
||||
{
|
||||
var replyTo = Sender;
|
||||
try
|
||||
{
|
||||
// Broadcast to every DriverHostActor; only the one owning the instance reacts.
|
||||
DistributedPubSub.Get(Context.System).Mediator.Tell(new Publish("driver-control", msg));
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
db.ConfigEdits.Add(new ConfigEdit
|
||||
{
|
||||
EntityType = "DriverInstance",
|
||||
EntityId = Guid.TryParse(msg.DriverInstanceId, out var guid) ? guid : Guid.Empty,
|
||||
FieldsJson = $"{{\"op\":\"reconnect\",\"driverInstanceId\":{System.Text.Json.JsonSerializer.Serialize(msg.DriverInstanceId)}}}",
|
||||
EditedBy = msg.ActorByUserName,
|
||||
SourceNode = Akka.Cluster.Cluster.Get(Context.System).SelfAddress.Host ?? "unknown",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_log.Info("AdminOps: ReconnectDriver dispatched for {DriverInstanceId} by {User}",
|
||||
msg.DriverInstanceId, msg.ActorByUserName);
|
||||
replyTo.Tell(new ReconnectDriverResult(true, null, msg.CorrelationId));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(ex, "AdminOps: ReconnectDriver failed for {DriverInstanceId}", msg.DriverInstanceId);
|
||||
replyTo.Tell(new ReconnectDriverResult(false, ex.Message, msg.CorrelationId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user