feat(siteeventlog): emit deployment + instance_lifecycle events (M1.6)
DeploymentManagerActor now fire-and-forgets a 'deployment' site operational event on deploy/enable/disable/delete outcomes (Info on success, Error on failure), source 'DeploymentManagerActor'. The disable/delete events are emitted from the existing PipeTo continuations (safe: reads only the immutable _serviceProvider and fire-and-forgets). InstanceActor now emits an 'instance_lifecycle' Info event in PreStart (started) and a new PostStop (stopped) — covering start/stop/enable/disable/redeploy/ failover transitions from the instance's own vantage point. Both actors already hold _serviceProvider; no ctor change. Resolution is optional and LogEventAsync is fire-and-forget so a logging failure never affects the deployment pipeline or instance lifecycle.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Akka.Actor;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
|
||||
@@ -10,6 +11,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.ScriptExecution;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Messages;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
@@ -456,6 +458,10 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
if (result.Success)
|
||||
{
|
||||
// M1.6: operational `deployment` event — deploy succeeded.
|
||||
LogDeploymentEvent("Info", result.InstanceName,
|
||||
$"Instance {result.InstanceName} deployed (deploymentId={result.DeploymentId})");
|
||||
|
||||
result.OriginalSender.Tell(new DeploymentStatusResponse(
|
||||
result.DeploymentId,
|
||||
result.InstanceName,
|
||||
@@ -469,6 +475,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
"Failed to persist deployment {DeploymentId} for {Instance}: {Error}",
|
||||
result.DeploymentId, result.InstanceName, result.Error);
|
||||
|
||||
// M1.6: operational `deployment` event — deploy failed.
|
||||
LogDeploymentEvent("Error", result.InstanceName,
|
||||
$"Instance {result.InstanceName} deploy failed (deploymentId={result.DeploymentId})",
|
||||
result.Error);
|
||||
|
||||
// Persistence failed — undo the optimistic actor creation and counter bump so
|
||||
// the site does not advertise an instance it cannot durably recover.
|
||||
if (_instanceActors.Remove(result.InstanceName, out var orphan))
|
||||
@@ -504,7 +515,17 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
_storage.SetInstanceEnabledAsync(instanceName, false).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully)
|
||||
{
|
||||
_replicationActor?.Tell(new ReplicateConfigSetEnabled(instanceName, false));
|
||||
// M1.6: operational `deployment` event — disable succeeded.
|
||||
LogDeploymentEvent("Info", instanceName, $"Instance {instanceName} disabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogDeploymentEvent("Error", instanceName,
|
||||
$"Instance {instanceName} disable failed",
|
||||
t.Exception?.GetBaseException().Message);
|
||||
}
|
||||
|
||||
return new InstanceLifecycleResponse(
|
||||
command.CommandId,
|
||||
@@ -551,6 +572,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
if (result.Error != null || result.Config == null)
|
||||
{
|
||||
var error = result.Error ?? $"No deployed config found for {instanceName}";
|
||||
// M1.6: operational `deployment` event — enable failed.
|
||||
LogDeploymentEvent("Error", instanceName,
|
||||
$"Instance {instanceName} enable failed", error);
|
||||
result.OriginalSender.Tell(new InstanceLifecycleResponse(
|
||||
result.Command.CommandId, instanceName, false, error, DateTimeOffset.UtcNow));
|
||||
return;
|
||||
@@ -562,6 +586,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
UpdateInstanceCounts();
|
||||
|
||||
// M1.6: operational `deployment` event — enable succeeded.
|
||||
LogDeploymentEvent("Info", instanceName, $"Instance {instanceName} enabled");
|
||||
|
||||
result.OriginalSender.Tell(new InstanceLifecycleResponse(
|
||||
result.Command.CommandId, instanceName, true, null, DateTimeOffset.UtcNow));
|
||||
|
||||
@@ -588,7 +615,17 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
_storage.RemoveDeployedConfigAsync(instanceName).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully)
|
||||
{
|
||||
_replicationActor?.Tell(new ReplicateConfigRemove(instanceName));
|
||||
// M1.6: operational `deployment` event — delete succeeded.
|
||||
LogDeploymentEvent("Info", instanceName, $"Instance {instanceName} deleted");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogDeploymentEvent("Error", instanceName,
|
||||
$"Instance {instanceName} delete failed",
|
||||
t.Exception?.GetBaseException().Message);
|
||||
}
|
||||
|
||||
return new InstanceLifecycleResponse(
|
||||
command.CommandId,
|
||||
@@ -601,6 +638,21 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
_logger.LogInformation("Instance {Instance} deleted", instanceName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M1.6: fire-and-forget a <c>deployment</c> operational event to the optional
|
||||
/// <see cref="ISiteEventLogger"/> on a deploy/enable/disable/delete outcome.
|
||||
/// Resolved optionally and never awaited so a logging failure cannot affect the
|
||||
/// deployment pipeline (matching the established ScriptActor/ScriptExecutionActor
|
||||
/// pattern). Only reads the immutable <c>_serviceProvider</c> field, so it is
|
||||
/// safe to call from the PipeTo continuations that report disable/delete
|
||||
/// outcomes off the actor thread.
|
||||
/// </summary>
|
||||
private void LogDeploymentEvent(string severity, string instanceName, string message, string? details = null)
|
||||
{
|
||||
_ = _serviceProvider?.GetService<ISiteEventLogger>()?.LogEventAsync(
|
||||
"deployment", severity, instanceName, "DeploymentManagerActor", message, details);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DeploymentManager-006: answers a central query for the instance's
|
||||
/// currently-applied deployment identity. The site's deployed-config store
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Akka.Actor;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
|
||||
@@ -9,6 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming;
|
||||
@@ -164,6 +166,11 @@ public class InstanceActor : ReceiveActor
|
||||
base.PreStart();
|
||||
_logger.LogInformation("InstanceActor started for {Instance}", _instanceUniqueName);
|
||||
|
||||
// M1.6: operational `instance_lifecycle` event — instance started.
|
||||
// An instance starts on deploy, on enable (DeploymentManager re-creates
|
||||
// the actor), and on failover/restart; this single point covers them all.
|
||||
LogLifecycleEvent($"Instance {_instanceUniqueName} started");
|
||||
|
||||
// Asynchronously load static overrides from SQLite and pipe to self
|
||||
var self = Self;
|
||||
_storage.GetStaticOverridesAsync(_instanceUniqueName).ContinueWith(t =>
|
||||
@@ -180,6 +187,29 @@ public class InstanceActor : ReceiveActor
|
||||
SubscribeToDcl();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void PostStop()
|
||||
{
|
||||
// M1.6: operational `instance_lifecycle` event — instance stopped. An
|
||||
// instance stops on disable, delete, redeployment, and graceful shutdown;
|
||||
// this single point covers them all.
|
||||
LogLifecycleEvent($"Instance {_instanceUniqueName} stopped");
|
||||
base.PostStop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M1.6: fire-and-forget an <c>instance_lifecycle</c> operational event to the
|
||||
/// optional <see cref="ISiteEventLogger"/>. Resolved optionally and never
|
||||
/// awaited so a logging failure cannot affect the instance lifecycle
|
||||
/// (matching the established ScriptActor/ScriptExecutionActor pattern).
|
||||
/// </summary>
|
||||
private void LogLifecycleEvent(string message)
|
||||
{
|
||||
_ = _serviceProvider?.GetService<ISiteEventLogger>()?.LogEventAsync(
|
||||
"instance_lifecycle", "Info", _instanceUniqueName,
|
||||
$"InstanceActor:{_instanceUniqueName}", message);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override SupervisorStrategy SupervisorStrategy()
|
||||
{
|
||||
|
||||
+70
-2
@@ -10,6 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.TestSupport;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
||||
@@ -44,7 +45,8 @@ public class DeploymentManagerActorTests : TestKit, IDisposable
|
||||
try { File.Delete(_dbFile); } catch { /* cleanup */ }
|
||||
}
|
||||
|
||||
private IActorRef CreateDeploymentManager(SiteRuntimeOptions? options = null)
|
||||
private IActorRef CreateDeploymentManager(
|
||||
SiteRuntimeOptions? options = null, IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
options ??= new SiteRuntimeOptions();
|
||||
return ActorOf(Props.Create(() => new DeploymentManagerActor(
|
||||
@@ -53,7 +55,12 @@ public class DeploymentManagerActorTests : TestKit, IDisposable
|
||||
_sharedScriptLibrary,
|
||||
null, // no stream manager in tests
|
||||
options,
|
||||
NullLogger<DeploymentManagerActor>.Instance)));
|
||||
NullLogger<DeploymentManagerActor>.Instance,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
serviceProvider,
|
||||
null)));
|
||||
}
|
||||
|
||||
private static string MakeConfigJson(string instanceName)
|
||||
@@ -171,6 +178,67 @@ public class DeploymentManagerActorTests : TestKit, IDisposable
|
||||
Assert.Equal("NewPump", response.InstanceUniqueName);
|
||||
}
|
||||
|
||||
// ── M1.6: site event log `deployment` category ─────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task DeploymentManager_DeploySuccess_EmitsDeploymentSiteEvent()
|
||||
{
|
||||
var siteLog = new FakeSiteEventLogger();
|
||||
var actor = CreateDeploymentManager(serviceProvider: new SingleServiceProvider(siteLog));
|
||||
|
||||
await Task.Delay(500); // wait for empty startup
|
||||
|
||||
actor.Tell(new DeployInstanceCommand(
|
||||
"dep-evt-1", "AuditedPump", "sha256:xyz",
|
||||
MakeConfigJson("AuditedPump"), "admin", DateTimeOffset.UtcNow));
|
||||
var response = ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(DeploymentStatus.Success, response.Status);
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
var rows = siteLog.OfType("deployment");
|
||||
Assert.Contains(rows, r =>
|
||||
r.Severity == "Info" &&
|
||||
r.InstanceId == "AuditedPump" &&
|
||||
r.Source == "DeploymentManagerActor" &&
|
||||
r.Message.Contains("deploy", StringComparison.OrdinalIgnoreCase));
|
||||
}, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeploymentManager_DisableEnableDelete_EmitDeploymentSiteEvents()
|
||||
{
|
||||
var siteLog = new FakeSiteEventLogger();
|
||||
var actor = CreateDeploymentManager(serviceProvider: new SingleServiceProvider(siteLog));
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
actor.Tell(new DeployInstanceCommand(
|
||||
"dep-evt-2", "EvtPump", "sha256:abc",
|
||||
MakeConfigJson("EvtPump"), "admin", DateTimeOffset.UtcNow));
|
||||
ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(5));
|
||||
await Task.Delay(1000);
|
||||
|
||||
actor.Tell(new DisableInstanceCommand("cmd-de1", "EvtPump", DateTimeOffset.UtcNow));
|
||||
Assert.True(ExpectMsg<InstanceLifecycleResponse>(TimeSpan.FromSeconds(5)).Success);
|
||||
await Task.Delay(300);
|
||||
|
||||
actor.Tell(new EnableInstanceCommand("cmd-en1", "EvtPump", DateTimeOffset.UtcNow));
|
||||
Assert.True(ExpectMsg<InstanceLifecycleResponse>(TimeSpan.FromSeconds(5)).Success);
|
||||
await Task.Delay(300);
|
||||
|
||||
actor.Tell(new DeleteInstanceCommand("cmd-del-evt", "EvtPump", DateTimeOffset.UtcNow));
|
||||
Assert.True(ExpectMsg<InstanceLifecycleResponse>(TimeSpan.FromSeconds(5)).Success);
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
var rows = siteLog.OfType("deployment");
|
||||
Assert.Contains(rows, r => r.Message.Contains("disabled", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(rows, r => r.Message.Contains("enabled", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(rows, r => r.Message.Contains("deleted", StringComparison.OrdinalIgnoreCase));
|
||||
}, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeploymentManager_Lifecycle_DisableEnableDelete()
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.TestSupport;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
||||
@@ -58,6 +59,82 @@ public class InstanceActorTests : TestKit, IDisposable
|
||||
try { File.Delete(_dbFile); } catch { /* cleanup */ }
|
||||
}
|
||||
|
||||
// ── M1.6: site event log `instance_lifecycle` category ──────────────────
|
||||
|
||||
[Fact]
|
||||
public void InstanceActor_Start_EmitsInstanceLifecycleSiteEvent()
|
||||
{
|
||||
var siteLog = new FakeSiteEventLogger();
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "LifecyclePump",
|
||||
Attributes = [new ResolvedAttribute { CanonicalName = "T", Value = "1", DataType = "Int32" }]
|
||||
};
|
||||
|
||||
ActorOf(Props.Create(() => new InstanceActor(
|
||||
"LifecyclePump",
|
||||
JsonSerializer.Serialize(config),
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
null,
|
||||
_options,
|
||||
NullLogger<InstanceActor>.Instance,
|
||||
null,
|
||||
null,
|
||||
new SingleServiceProvider(siteLog))));
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
var rows = siteLog.OfType("instance_lifecycle");
|
||||
Assert.Contains(rows, r =>
|
||||
r.Severity == "Info" &&
|
||||
r.InstanceId == "LifecyclePump" &&
|
||||
r.Source == "InstanceActor:LifecyclePump" &&
|
||||
r.Message.Contains("started", StringComparison.OrdinalIgnoreCase));
|
||||
}, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstanceActor_Stop_EmitsInstanceLifecycleSiteEvent()
|
||||
{
|
||||
var siteLog = new FakeSiteEventLogger();
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "StoppedPump",
|
||||
Attributes = [new ResolvedAttribute { CanonicalName = "T", Value = "1", DataType = "Int32" }]
|
||||
};
|
||||
|
||||
var actor = ActorOf(Props.Create(() => new InstanceActor(
|
||||
"StoppedPump",
|
||||
JsonSerializer.Serialize(config),
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
null,
|
||||
_options,
|
||||
NullLogger<InstanceActor>.Instance,
|
||||
null,
|
||||
null,
|
||||
new SingleServiceProvider(siteLog))));
|
||||
|
||||
// Let PreStart land its started event, then stop the actor.
|
||||
AwaitAssert(() => Assert.NotEmpty(siteLog.OfType("instance_lifecycle")),
|
||||
TimeSpan.FromSeconds(2));
|
||||
Watch(actor);
|
||||
actor.Tell(PoisonPill.Instance);
|
||||
ExpectTerminated(actor, TimeSpan.FromSeconds(5));
|
||||
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
var rows = siteLog.OfType("instance_lifecycle");
|
||||
Assert.Contains(rows, r =>
|
||||
r.Severity == "Info" &&
|
||||
r.InstanceId == "StoppedPump" &&
|
||||
r.Message.Contains("stopped", StringComparison.OrdinalIgnoreCase));
|
||||
}, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstanceActor_LoadsAttributesFromConfig()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user