feat(siteruntime): thread ParentExecutionId into the routed script's ScriptRuntimeContext
This commit is contained in:
@@ -1,7 +1,17 @@
|
|||||||
namespace ScadaLink.Commons.Messages.ScriptExecution;
|
namespace ScadaLink.Commons.Messages.ScriptExecution;
|
||||||
|
|
||||||
|
/// <param name="ParentExecutionId">
|
||||||
|
/// Audit Log #23 (ParentExecutionId): the spawning execution's <c>ExecutionId</c>.
|
||||||
|
/// For an inbound-API-routed call this is the inbound request's per-request
|
||||||
|
/// execution id (carried in from <c>RouteToCallRequest.ParentExecutionId</c>);
|
||||||
|
/// the routed script execution records it as its <c>ParentExecutionId</c> so a
|
||||||
|
/// spawned execution points back at its spawner. Additive trailing member —
|
||||||
|
/// null for normal (tag-change / timer-triggered) runs, nested <c>Script.Call</c>
|
||||||
|
/// invocations, and any request built before the field existed.
|
||||||
|
/// </param>
|
||||||
public record ScriptCallRequest(
|
public record ScriptCallRequest(
|
||||||
string ScriptName,
|
string ScriptName,
|
||||||
IReadOnlyDictionary<string, object?>? Parameters,
|
IReadOnlyDictionary<string, object?>? Parameters,
|
||||||
int CurrentCallDepth,
|
int CurrentCallDepth,
|
||||||
string CorrelationId);
|
string CorrelationId,
|
||||||
|
Guid? ParentExecutionId = null);
|
||||||
|
|||||||
@@ -735,9 +735,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
|||||||
{
|
{
|
||||||
if (_instanceActors.TryGetValue(request.InstanceUniqueName, out var instanceActor))
|
if (_instanceActors.TryGetValue(request.InstanceUniqueName, out var instanceActor))
|
||||||
{
|
{
|
||||||
// Convert to ScriptCallRequest and Ask the Instance Actor
|
// Convert to ScriptCallRequest and Ask the Instance Actor.
|
||||||
|
// Audit Log #23 (ParentExecutionId): carry the inbound request's
|
||||||
|
// ExecutionId down as ParentExecutionId so the routed script
|
||||||
|
// execution can record its spawner.
|
||||||
var scriptCall = new ScriptCallRequest(
|
var scriptCall = new ScriptCallRequest(
|
||||||
request.ScriptName, request.Parameters, 0, request.CorrelationId);
|
request.ScriptName, request.Parameters, 0, request.CorrelationId,
|
||||||
|
ParentExecutionId: request.ParentExecutionId);
|
||||||
var sender = Sender;
|
var sender = Sender;
|
||||||
instanceActor.Ask<ScriptCallResult>(scriptCall, TimeSpan.FromSeconds(30))
|
instanceActor.Ask<ScriptCallResult>(scriptCall, TimeSpan.FromSeconds(30))
|
||||||
.ContinueWith(t =>
|
.ContinueWith(t =>
|
||||||
|
|||||||
@@ -320,7 +320,10 @@ public class InstanceActor : ReceiveActor
|
|||||||
{
|
{
|
||||||
if (_scriptActors.TryGetValue(request.ScriptName, out var scriptActor))
|
if (_scriptActors.TryGetValue(request.ScriptName, out var scriptActor))
|
||||||
{
|
{
|
||||||
// Forward the request to the Script Actor, preserving the original sender
|
// Forward the request to the Script Actor, preserving the original
|
||||||
|
// sender. The whole record is forwarded unchanged, so any
|
||||||
|
// ParentExecutionId (Audit Log #23) set by an inbound-API-routed
|
||||||
|
// call is carried through to the Script Actor verbatim.
|
||||||
scriptActor.Forward(request);
|
scriptActor.Forward(request);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -184,7 +184,13 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SpawnExecution(request.Parameters, request.CurrentCallDepth, Sender, request.CorrelationId);
|
// Audit Log #23 (ParentExecutionId): carry any inbound-routed
|
||||||
|
// ParentExecutionId through to the ScriptExecutionActor so the routed
|
||||||
|
// script's ScriptRuntimeContext can record its spawner. Null for normal
|
||||||
|
// (tag-change / timer) runs and nested Script.Call invocations.
|
||||||
|
SpawnExecution(
|
||||||
|
request.Parameters, request.CurrentCallDepth, Sender, request.CorrelationId,
|
||||||
|
request.ParentExecutionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -379,7 +385,8 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
|||||||
IReadOnlyDictionary<string, object?>? parameters,
|
IReadOnlyDictionary<string, object?>? parameters,
|
||||||
int callDepth,
|
int callDepth,
|
||||||
IActorRef replyTo,
|
IActorRef replyTo,
|
||||||
string correlationId)
|
string correlationId,
|
||||||
|
Guid? parentExecutionId = null)
|
||||||
{
|
{
|
||||||
var executionId = $"{_scriptName}-exec-{_executionCounter++}";
|
var executionId = $"{_scriptName}-exec-{_executionCounter++}";
|
||||||
|
|
||||||
@@ -401,7 +408,10 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
|||||||
_logger,
|
_logger,
|
||||||
_scope,
|
_scope,
|
||||||
_healthCollector,
|
_healthCollector,
|
||||||
_serviceProvider));
|
_serviceProvider,
|
||||||
|
// Audit Log #23 (ParentExecutionId): null for trigger-driven runs;
|
||||||
|
// an inbound-API-routed call supplies the inbound request's id.
|
||||||
|
parentExecutionId));
|
||||||
|
|
||||||
Context.ActorOf(props, executionId);
|
Context.ActorOf(props, executionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ public class ScriptExecutionActor : ReceiveActor
|
|||||||
ILogger logger,
|
ILogger logger,
|
||||||
Commons.Types.Scripts.ScriptScope scope,
|
Commons.Types.Scripts.ScriptScope scope,
|
||||||
ISiteHealthCollector? healthCollector = null,
|
ISiteHealthCollector? healthCollector = null,
|
||||||
IServiceProvider? serviceProvider = null)
|
IServiceProvider? serviceProvider = null,
|
||||||
|
// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||||
|
// ExecutionId for an inbound-API-routed call. Null for normal
|
||||||
|
// (tag-change / timer) runs and nested Script.Call invocations.
|
||||||
|
Guid? parentExecutionId = null)
|
||||||
{
|
{
|
||||||
// Immediately begin execution
|
// Immediately begin execution
|
||||||
var self = Self;
|
var self = Self;
|
||||||
@@ -52,7 +56,8 @@ public class ScriptExecutionActor : ReceiveActor
|
|||||||
ExecuteScript(
|
ExecuteScript(
|
||||||
scriptName, instanceName, compiledScript, parameters, callDepth,
|
scriptName, instanceName, compiledScript, parameters, callDepth,
|
||||||
instanceActor, sharedScriptLibrary, options, replyTo, correlationId,
|
instanceActor, sharedScriptLibrary, options, replyTo, correlationId,
|
||||||
self, parent, logger, scope, healthCollector, serviceProvider);
|
self, parent, logger, scope, healthCollector, serviceProvider,
|
||||||
|
parentExecutionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ExecuteScript(
|
private static void ExecuteScript(
|
||||||
@@ -71,7 +76,8 @@ public class ScriptExecutionActor : ReceiveActor
|
|||||||
ILogger logger,
|
ILogger logger,
|
||||||
Commons.Types.Scripts.ScriptScope scope,
|
Commons.Types.Scripts.ScriptScope scope,
|
||||||
ISiteHealthCollector? healthCollector,
|
ISiteHealthCollector? healthCollector,
|
||||||
IServiceProvider? serviceProvider)
|
IServiceProvider? serviceProvider,
|
||||||
|
Guid? parentExecutionId)
|
||||||
{
|
{
|
||||||
var timeout = TimeSpan.FromSeconds(options.ScriptExecutionTimeoutSeconds);
|
var timeout = TimeSpan.FromSeconds(options.ScriptExecutionTimeoutSeconds);
|
||||||
|
|
||||||
@@ -164,7 +170,12 @@ public class ScriptExecutionActor : ReceiveActor
|
|||||||
// emission. Best-effort: null degrades the helpers to a
|
// emission. Best-effort: null degrades the helpers to a
|
||||||
// no-emission path; the S&F handoff and TrackedOperationId
|
// no-emission path; the S&F handoff and TrackedOperationId
|
||||||
// return are unaffected.
|
// return are unaffected.
|
||||||
cachedForwarder: cachedForwarder);
|
cachedForwarder: cachedForwarder,
|
||||||
|
// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||||
|
// id for an inbound-API-routed call. The routed script still
|
||||||
|
// mints its own fresh ExecutionId — this records the spawner.
|
||||||
|
// Null for normal (tag-change / timer) runs.
|
||||||
|
parentExecutionId: parentExecutionId);
|
||||||
|
|
||||||
var globals = new ScriptGlobals
|
var globals = new ScriptGlobals
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -116,6 +116,19 @@ public class ScriptRuntimeContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Guid _executionId;
|
private readonly Guid _executionId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||||
|
/// <see cref="_executionId"/> when this script run was spawned by another
|
||||||
|
/// execution — for an inbound-API-routed call this is the inbound request's
|
||||||
|
/// per-request execution id. <c>null</c> for normal (tag-change /
|
||||||
|
/// timer-triggered) runs and nested <c>CallScript</c> invocations. The
|
||||||
|
/// routed script still mints its OWN fresh <see cref="_executionId"/>; this
|
||||||
|
/// field records the spawner so a spawned execution's audit rows can point
|
||||||
|
/// back at the execution that spawned it. (Task 5 wires the emitter that
|
||||||
|
/// stamps this onto <c>AuditEvent.ParentExecutionId</c>.)
|
||||||
|
/// </summary>
|
||||||
|
private readonly Guid? _parentExecutionId;
|
||||||
|
|
||||||
/// <param name="executionId">
|
/// <param name="executionId">
|
||||||
/// Audit Log #23: the per-execution id for this script run. When omitted
|
/// Audit Log #23: the per-execution id for this script run. When omitted
|
||||||
/// (tag-change / timer-triggered executions) a fresh id is generated; an
|
/// (tag-change / timer-triggered executions) a fresh id is generated; an
|
||||||
@@ -123,6 +136,13 @@ public class ScriptRuntimeContext
|
|||||||
/// request. Stamped into <c>AuditEvent.ExecutionId</c> on every
|
/// request. Stamped into <c>AuditEvent.ExecutionId</c> on every
|
||||||
/// trust-boundary audit row this execution emits.
|
/// trust-boundary audit row this execution emits.
|
||||||
/// </param>
|
/// </param>
|
||||||
|
/// <param name="parentExecutionId">
|
||||||
|
/// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||||
|
/// <c>ExecutionId</c> — supplied for an inbound-API-routed call (the
|
||||||
|
/// inbound request's per-request id), <c>null</c> for normal (tag-change /
|
||||||
|
/// timer-triggered) runs. The routed script still generates its own fresh
|
||||||
|
/// <paramref name="executionId"/>; this only records the spawner.
|
||||||
|
/// </param>
|
||||||
public ScriptRuntimeContext(
|
public ScriptRuntimeContext(
|
||||||
IActorRef instanceActor,
|
IActorRef instanceActor,
|
||||||
IActorRef self,
|
IActorRef self,
|
||||||
@@ -141,7 +161,8 @@ public class ScriptRuntimeContext
|
|||||||
IAuditWriter? auditWriter = null,
|
IAuditWriter? auditWriter = null,
|
||||||
IOperationTrackingStore? operationTrackingStore = null,
|
IOperationTrackingStore? operationTrackingStore = null,
|
||||||
ICachedCallTelemetryForwarder? cachedForwarder = null,
|
ICachedCallTelemetryForwarder? cachedForwarder = null,
|
||||||
Guid? executionId = null)
|
Guid? executionId = null,
|
||||||
|
Guid? parentExecutionId = null)
|
||||||
{
|
{
|
||||||
_instanceActor = instanceActor;
|
_instanceActor = instanceActor;
|
||||||
_self = self;
|
_self = self;
|
||||||
@@ -161,6 +182,9 @@ public class ScriptRuntimeContext
|
|||||||
_operationTrackingStore = operationTrackingStore;
|
_operationTrackingStore = operationTrackingStore;
|
||||||
_cachedForwarder = cachedForwarder;
|
_cachedForwarder = cachedForwarder;
|
||||||
_executionId = executionId ?? Guid.NewGuid();
|
_executionId = executionId ?? Guid.NewGuid();
|
||||||
|
// Audit Log #23 (ParentExecutionId): stored verbatim — no `?? NewGuid()`
|
||||||
|
// fallback. A non-routed run legitimately has no parent and stays null.
|
||||||
|
_parentExecutionId = parentExecutionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -264,7 +288,10 @@ public class ScriptRuntimeContext
|
|||||||
_externalSystemClient, _instanceName, _logger, _executionId, _auditWriter, _siteId, _sourceScript,
|
_externalSystemClient, _instanceName, _logger, _executionId, _auditWriter, _siteId, _sourceScript,
|
||||||
// Audit Log #23 (M3 Bundle E — Task E3): emit CachedSubmit telemetry
|
// Audit Log #23 (M3 Bundle E — Task E3): emit CachedSubmit telemetry
|
||||||
// on every ExternalSystem.CachedCall enqueue.
|
// on every ExternalSystem.CachedCall enqueue.
|
||||||
_cachedForwarder);
|
_cachedForwarder,
|
||||||
|
// Audit Log #23 (ParentExecutionId): the spawning execution's id,
|
||||||
|
// threaded alongside _executionId. Null for non-routed runs.
|
||||||
|
_parentExecutionId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WP-13: Provides access to database operations.
|
/// WP-13: Provides access to database operations.
|
||||||
@@ -285,7 +312,10 @@ public class ScriptRuntimeContext
|
|||||||
_sourceScript,
|
_sourceScript,
|
||||||
// Audit Log #23 (M3 Bundle E — Task E6): emit CachedSubmit telemetry on
|
// Audit Log #23 (M3 Bundle E — Task E6): emit CachedSubmit telemetry on
|
||||||
// every Database.CachedWrite enqueue.
|
// every Database.CachedWrite enqueue.
|
||||||
_cachedForwarder);
|
_cachedForwarder,
|
||||||
|
// Audit Log #23 (ParentExecutionId): the spawning execution's id,
|
||||||
|
// threaded alongside _executionId. Null for non-routed runs.
|
||||||
|
_parentExecutionId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides access to the Notification Outbox API.
|
/// Provides access to the Notification Outbox API.
|
||||||
@@ -302,7 +332,10 @@ public class ScriptRuntimeContext
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public NotifyHelper Notify => new(
|
public NotifyHelper Notify => new(
|
||||||
_storeAndForward, _siteCommunicationActor, _siteId, _instanceName, _sourceScript, _askTimeout, _logger,
|
_storeAndForward, _siteCommunicationActor, _siteId, _instanceName, _sourceScript, _askTimeout, _logger,
|
||||||
_executionId, _auditWriter);
|
_executionId, _auditWriter,
|
||||||
|
// Audit Log #23 (ParentExecutionId): the spawning execution's id,
|
||||||
|
// threaded alongside _executionId. Null for non-routed runs.
|
||||||
|
_parentExecutionId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audit Log #23 (M3): site-local tracking-status API for cached operations.
|
/// Audit Log #23 (M3): site-local tracking-status API for cached operations.
|
||||||
@@ -384,6 +417,15 @@ public class ScriptRuntimeContext
|
|||||||
private readonly string _instanceName;
|
private readonly string _instanceName;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly Guid _executionId;
|
private readonly Guid _executionId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23 (ParentExecutionId): the spawning execution's id when
|
||||||
|
/// this run was inbound-API-routed; <c>null</c> for non-routed runs.
|
||||||
|
/// Threaded alongside <see cref="_executionId"/> ready for the Task 5
|
||||||
|
/// emitter — no audit row carries it yet.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Guid? _parentExecutionId;
|
||||||
|
|
||||||
private readonly IAuditWriter? _auditWriter;
|
private readonly IAuditWriter? _auditWriter;
|
||||||
private readonly string _siteId;
|
private readonly string _siteId;
|
||||||
private readonly string? _sourceScript;
|
private readonly string? _sourceScript;
|
||||||
@@ -398,7 +440,9 @@ public class ScriptRuntimeContext
|
|||||||
// DatabaseHelper, AuditingDbConnection, AuditingDbCommand) — a required
|
// DatabaseHelper, AuditingDbConnection, AuditingDbCommand) — a required
|
||||||
// Guid cannot follow the optional provenance params without a
|
// Guid cannot follow the optional provenance params without a
|
||||||
// required-after-optional compile error, so the post-logger slot is the
|
// required-after-optional compile error, so the post-logger slot is the
|
||||||
// one consistent position that compiles cleanly everywhere.
|
// one consistent position that compiles cleanly everywhere. The nullable
|
||||||
|
// parentExecutionId is a trailing optional param so existing positional
|
||||||
|
// callers stay source-compatible.
|
||||||
internal ExternalSystemHelper(
|
internal ExternalSystemHelper(
|
||||||
IExternalSystemClient? client,
|
IExternalSystemClient? client,
|
||||||
string instanceName,
|
string instanceName,
|
||||||
@@ -407,7 +451,8 @@ public class ScriptRuntimeContext
|
|||||||
IAuditWriter? auditWriter = null,
|
IAuditWriter? auditWriter = null,
|
||||||
string siteId = "",
|
string siteId = "",
|
||||||
string? sourceScript = null,
|
string? sourceScript = null,
|
||||||
ICachedCallTelemetryForwarder? cachedForwarder = null)
|
ICachedCallTelemetryForwarder? cachedForwarder = null,
|
||||||
|
Guid? parentExecutionId = null)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_instanceName = instanceName;
|
_instanceName = instanceName;
|
||||||
@@ -417,6 +462,7 @@ public class ScriptRuntimeContext
|
|||||||
_siteId = siteId;
|
_siteId = siteId;
|
||||||
_sourceScript = sourceScript;
|
_sourceScript = sourceScript;
|
||||||
_cachedForwarder = cachedForwarder;
|
_cachedForwarder = cachedForwarder;
|
||||||
|
_parentExecutionId = parentExecutionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ExternalCallResult> Call(
|
public async Task<ExternalCallResult> Call(
|
||||||
@@ -1001,6 +1047,15 @@ public class ScriptRuntimeContext
|
|||||||
private readonly string _instanceName;
|
private readonly string _instanceName;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly Guid _executionId;
|
private readonly Guid _executionId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23 (ParentExecutionId): the spawning execution's id when
|
||||||
|
/// this run was inbound-API-routed; <c>null</c> for non-routed runs.
|
||||||
|
/// Threaded alongside <see cref="_executionId"/> ready for the Task 5
|
||||||
|
/// emitter — no audit row carries it yet.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Guid? _parentExecutionId;
|
||||||
|
|
||||||
private readonly string _siteId;
|
private readonly string _siteId;
|
||||||
private readonly string? _sourceScript;
|
private readonly string? _sourceScript;
|
||||||
private readonly ICachedCallTelemetryForwarder? _cachedForwarder;
|
private readonly ICachedCallTelemetryForwarder? _cachedForwarder;
|
||||||
@@ -1020,7 +1075,7 @@ public class ScriptRuntimeContext
|
|||||||
// Parameter ordering: executionId sits immediately after the
|
// Parameter ordering: executionId sits immediately after the
|
||||||
// ILogger — see the note on ExternalSystemHelper's ctor for why the
|
// ILogger — see the note on ExternalSystemHelper's ctor for why the
|
||||||
// post-logger slot is the one consistent position across all four
|
// post-logger slot is the one consistent position across all four
|
||||||
// audit-threaded ctors.
|
// audit-threaded ctors. parentExecutionId is a trailing optional param.
|
||||||
internal DatabaseHelper(
|
internal DatabaseHelper(
|
||||||
IDatabaseGateway? gateway,
|
IDatabaseGateway? gateway,
|
||||||
string instanceName,
|
string instanceName,
|
||||||
@@ -1029,7 +1084,8 @@ public class ScriptRuntimeContext
|
|||||||
IAuditWriter? auditWriter = null,
|
IAuditWriter? auditWriter = null,
|
||||||
string siteId = "",
|
string siteId = "",
|
||||||
string? sourceScript = null,
|
string? sourceScript = null,
|
||||||
ICachedCallTelemetryForwarder? cachedForwarder = null)
|
ICachedCallTelemetryForwarder? cachedForwarder = null,
|
||||||
|
Guid? parentExecutionId = null)
|
||||||
{
|
{
|
||||||
_gateway = gateway;
|
_gateway = gateway;
|
||||||
_instanceName = instanceName;
|
_instanceName = instanceName;
|
||||||
@@ -1039,6 +1095,7 @@ public class ScriptRuntimeContext
|
|||||||
_siteId = siteId;
|
_siteId = siteId;
|
||||||
_sourceScript = sourceScript;
|
_sourceScript = sourceScript;
|
||||||
_cachedForwarder = cachedForwarder;
|
_cachedForwarder = cachedForwarder;
|
||||||
|
_parentExecutionId = parentExecutionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<System.Data.Common.DbConnection> Connection(
|
public async Task<System.Data.Common.DbConnection> Connection(
|
||||||
@@ -1213,6 +1270,14 @@ public class ScriptRuntimeContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Guid _executionId;
|
private readonly Guid _executionId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23 (ParentExecutionId): the spawning execution's id when
|
||||||
|
/// this run was inbound-API-routed; <c>null</c> for non-routed runs.
|
||||||
|
/// Threaded alongside <see cref="_executionId"/> ready for the Task 5
|
||||||
|
/// emitter — no audit row carries it yet.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Guid? _parentExecutionId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
||||||
/// <c>Notification</c>/<c>NotifySend</c> row produced when the script
|
/// <c>Notification</c>/<c>NotifySend</c> row produced when the script
|
||||||
@@ -1224,7 +1289,8 @@ public class ScriptRuntimeContext
|
|||||||
private readonly IAuditWriter? _auditWriter;
|
private readonly IAuditWriter? _auditWriter;
|
||||||
|
|
||||||
// Parameter ordering: executionId sits immediately after the ILogger,
|
// Parameter ordering: executionId sits immediately after the ILogger,
|
||||||
// consistent with the other audit-threaded ctors.
|
// consistent with the other audit-threaded ctors. parentExecutionId is
|
||||||
|
// a trailing optional param.
|
||||||
internal NotifyHelper(
|
internal NotifyHelper(
|
||||||
StoreAndForwardService? storeAndForward,
|
StoreAndForwardService? storeAndForward,
|
||||||
ICanTell? siteCommunicationActor,
|
ICanTell? siteCommunicationActor,
|
||||||
@@ -1234,7 +1300,8 @@ public class ScriptRuntimeContext
|
|||||||
TimeSpan askTimeout,
|
TimeSpan askTimeout,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
Guid executionId,
|
Guid executionId,
|
||||||
IAuditWriter? auditWriter = null)
|
IAuditWriter? auditWriter = null,
|
||||||
|
Guid? parentExecutionId = null)
|
||||||
{
|
{
|
||||||
_storeAndForward = storeAndForward;
|
_storeAndForward = storeAndForward;
|
||||||
_siteCommunicationActor = siteCommunicationActor;
|
_siteCommunicationActor = siteCommunicationActor;
|
||||||
@@ -1245,6 +1312,7 @@ public class ScriptRuntimeContext
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_executionId = executionId;
|
_executionId = executionId;
|
||||||
_auditWriter = auditWriter;
|
_auditWriter = auditWriter;
|
||||||
|
_parentExecutionId = parentExecutionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1259,7 +1327,10 @@ public class ScriptRuntimeContext
|
|||||||
_executionId,
|
_executionId,
|
||||||
// Audit Log #23 (M4 Bundle C): forward the writer so Send()
|
// Audit Log #23 (M4 Bundle C): forward the writer so Send()
|
||||||
// can emit one NotifySend(Submitted) row per accepted submission.
|
// can emit one NotifySend(Submitted) row per accepted submission.
|
||||||
_auditWriter);
|
_auditWriter,
|
||||||
|
// Audit Log #23 (ParentExecutionId): the spawning execution's
|
||||||
|
// id, threaded alongside _executionId. Null for non-routed runs.
|
||||||
|
_parentExecutionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1340,6 +1411,14 @@ public class ScriptRuntimeContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Guid _executionId;
|
private readonly Guid _executionId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23 (ParentExecutionId): the spawning execution's id when
|
||||||
|
/// this run was inbound-API-routed; <c>null</c> for non-routed runs.
|
||||||
|
/// Threaded alongside <see cref="_executionId"/> ready for the Task 5
|
||||||
|
/// emitter — no audit row carries it yet.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Guid? _parentExecutionId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
||||||
/// <c>Notification</c>/<c>NotifySend</c> row written immediately after
|
/// <c>Notification</c>/<c>NotifySend</c> row written immediately after
|
||||||
@@ -1356,7 +1435,8 @@ public class ScriptRuntimeContext
|
|||||||
string? sourceScript,
|
string? sourceScript,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
Guid executionId,
|
Guid executionId,
|
||||||
IAuditWriter? auditWriter = null)
|
IAuditWriter? auditWriter = null,
|
||||||
|
Guid? parentExecutionId = null)
|
||||||
{
|
{
|
||||||
_listName = listName;
|
_listName = listName;
|
||||||
_storeAndForward = storeAndForward;
|
_storeAndForward = storeAndForward;
|
||||||
@@ -1366,6 +1446,7 @@ public class ScriptRuntimeContext
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_executionId = executionId;
|
_executionId = executionId;
|
||||||
_auditWriter = auditWriter;
|
_auditWriter = auditWriter;
|
||||||
|
_parentExecutionId = parentExecutionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Akka.TestKit.Xunit2;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ScadaLink.Commons.Messages.Deployment;
|
using ScadaLink.Commons.Messages.Deployment;
|
||||||
|
using ScadaLink.Commons.Messages.InboundApi;
|
||||||
using ScadaLink.Commons.Messages.Lifecycle;
|
using ScadaLink.Commons.Messages.Lifecycle;
|
||||||
using ScadaLink.Commons.Types.Enums;
|
using ScadaLink.Commons.Types.Enums;
|
||||||
using ScadaLink.Commons.Types.Flattening;
|
using ScadaLink.Commons.Types.Flattening;
|
||||||
@@ -68,6 +69,28 @@ public class DeploymentManagerActorTests : TestKit, IDisposable
|
|||||||
return JsonSerializer.Serialize(config);
|
return JsonSerializer.Serialize(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a config carrying a single callable (no-trigger) script that
|
||||||
|
/// returns a constant — enough for an inbound <see cref="RouteToCallRequest"/>
|
||||||
|
/// to be routed end-to-end through the Instance/Script/ScriptExecution actors.
|
||||||
|
/// </summary>
|
||||||
|
private static string MakeConfigWithScriptJson(string instanceName, string scriptName)
|
||||||
|
{
|
||||||
|
var config = new FlattenedConfiguration
|
||||||
|
{
|
||||||
|
InstanceUniqueName = instanceName,
|
||||||
|
Attributes =
|
||||||
|
[
|
||||||
|
new ResolvedAttribute { CanonicalName = "TestAttr", Value = "42", DataType = "Int32" }
|
||||||
|
],
|
||||||
|
Scripts =
|
||||||
|
[
|
||||||
|
new ResolvedScript { CanonicalName = scriptName, Code = "return 7;" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
return JsonSerializer.Serialize(config);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DeploymentManager_CreatesInstanceActors_FromStoredConfigs()
|
public async Task DeploymentManager_CreatesInstanceActors_FromStoredConfigs()
|
||||||
{
|
{
|
||||||
@@ -240,4 +263,57 @@ public class DeploymentManagerActorTests : TestKit, IDisposable
|
|||||||
var response = ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(5));
|
var response = ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(5));
|
||||||
Assert.Equal(DeploymentStatus.Success, response.Status);
|
Assert.Equal(DeploymentStatus.Success, response.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Audit Log #23 (ParentExecutionId, Task 4): inbound-API routing ──
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RouteInboundApiCall_WithParentExecutionId_RoutesToScriptSuccessfully()
|
||||||
|
{
|
||||||
|
// A RouteToCallRequest carrying a ParentExecutionId (the inbound
|
||||||
|
// request's ExecutionId) must be mapped to a ScriptCallRequest and
|
||||||
|
// routed end-to-end through the Instance/Script/ScriptExecution actors.
|
||||||
|
// The additive ParentExecutionId field must not break that routing.
|
||||||
|
var actor = CreateDeploymentManager();
|
||||||
|
await Task.Delay(500); // empty startup
|
||||||
|
|
||||||
|
actor.Tell(new DeployInstanceCommand(
|
||||||
|
"dep-route", "RoutedPump", "sha256:route",
|
||||||
|
MakeConfigWithScriptJson("RoutedPump", "DoWork"), "admin", DateTimeOffset.UtcNow));
|
||||||
|
ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(5));
|
||||||
|
await Task.Delay(1000); // let the InstanceActor + ScriptActor spin up
|
||||||
|
|
||||||
|
var parentExecutionId = Guid.NewGuid();
|
||||||
|
actor.Tell(new RouteToCallRequest(
|
||||||
|
"route-corr-1", "RoutedPump", "DoWork",
|
||||||
|
Parameters: null, DateTimeOffset.UtcNow, ParentExecutionId: parentExecutionId));
|
||||||
|
|
||||||
|
var response = ExpectMsg<RouteToCallResponse>(TimeSpan.FromSeconds(10));
|
||||||
|
Assert.Equal("route-corr-1", response.CorrelationId);
|
||||||
|
Assert.True(response.Success, $"Routed call failed: {response.ErrorMessage}");
|
||||||
|
Assert.Equal(7, Convert.ToInt32(response.ReturnValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RouteInboundApiCall_WithoutParentExecutionId_StillRoutes()
|
||||||
|
{
|
||||||
|
// A routed call with no ParentExecutionId (e.g. the Central UI sandbox)
|
||||||
|
// is the additive-default path — it must route exactly as before.
|
||||||
|
var actor = CreateDeploymentManager();
|
||||||
|
await Task.Delay(500);
|
||||||
|
|
||||||
|
actor.Tell(new DeployInstanceCommand(
|
||||||
|
"dep-route2", "RoutedPump2", "sha256:route2",
|
||||||
|
MakeConfigWithScriptJson("RoutedPump2", "DoWork"), "admin", DateTimeOffset.UtcNow));
|
||||||
|
ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(5));
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
// No ParentExecutionId argument — exercises the additive `= null` default.
|
||||||
|
actor.Tell(new RouteToCallRequest(
|
||||||
|
"route-corr-2", "RoutedPump2", "DoWork",
|
||||||
|
Parameters: null, DateTimeOffset.UtcNow));
|
||||||
|
|
||||||
|
var response = ExpectMsg<RouteToCallResponse>(TimeSpan.FromSeconds(10));
|
||||||
|
Assert.Equal("route-corr-2", response.CorrelationId);
|
||||||
|
Assert.True(response.Success, $"Routed call failed: {response.ErrorMessage}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ public class ExecutionCorrelationContextTests
|
|||||||
IExternalSystemClient? externalSystemClient,
|
IExternalSystemClient? externalSystemClient,
|
||||||
IDatabaseGateway? databaseGateway,
|
IDatabaseGateway? databaseGateway,
|
||||||
IAuditWriter? auditWriter,
|
IAuditWriter? auditWriter,
|
||||||
Guid? executionId = null)
|
Guid? executionId = null,
|
||||||
|
Guid? parentExecutionId = null)
|
||||||
{
|
{
|
||||||
var compilationService = new ScriptCompilationService(
|
var compilationService = new ScriptCompilationService(
|
||||||
NullLogger<ScriptCompilationService>.Instance);
|
NullLogger<ScriptCompilationService>.Instance);
|
||||||
@@ -87,7 +88,24 @@ public class ExecutionCorrelationContextTests
|
|||||||
auditWriter: auditWriter,
|
auditWriter: auditWriter,
|
||||||
operationTrackingStore: null,
|
operationTrackingStore: null,
|
||||||
cachedForwarder: null,
|
cachedForwarder: null,
|
||||||
executionId: executionId);
|
executionId: executionId,
|
||||||
|
parentExecutionId: parentExecutionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a private <see cref="Guid"/>/<see cref="Nullable{Guid}"/> field off a
|
||||||
|
/// <see cref="ScriptRuntimeContext"/>. The ParentExecutionId plumbing (Audit
|
||||||
|
/// Log #23, Task 4) only stores the value on the context — no emitter stamps
|
||||||
|
/// it onto an audit row yet (that is Task 5) — so the field is inspected
|
||||||
|
/// directly rather than through an emitted row.
|
||||||
|
/// </summary>
|
||||||
|
private static object? ReadPrivateField(ScriptRuntimeContext context, string fieldName)
|
||||||
|
{
|
||||||
|
var field = typeof(ScriptRuntimeContext).GetField(
|
||||||
|
fieldName,
|
||||||
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||||
|
Assert.NotNull(field);
|
||||||
|
return field!.GetValue(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -183,4 +201,54 @@ public class ExecutionCorrelationContextTests
|
|||||||
Assert.Null(apiRow.CorrelationId);
|
Assert.Null(apiRow.CorrelationId);
|
||||||
Assert.Null(dbRow.CorrelationId);
|
Assert.Null(dbRow.CorrelationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParentExecutionIdSupplied_StoredVerbatim_AndOwnExecutionIdIsFreshAndDistinct()
|
||||||
|
{
|
||||||
|
// Audit Log #23 (ParentExecutionId, Task 4): an inbound-API-routed call
|
||||||
|
// supplies the spawning execution's ExecutionId as the routed script's
|
||||||
|
// ParentExecutionId. The context must store that value verbatim AND
|
||||||
|
// still mint its OWN fresh ExecutionId — the routed script is a new
|
||||||
|
// execution, it does not inherit the parent's id.
|
||||||
|
var parentExecutionId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var context = CreateContext(
|
||||||
|
externalSystemClient: null,
|
||||||
|
databaseGateway: null,
|
||||||
|
auditWriter: null,
|
||||||
|
// executionId omitted — the ctor's `?? Guid.NewGuid()` fallback runs.
|
||||||
|
parentExecutionId: parentExecutionId);
|
||||||
|
|
||||||
|
var storedParent = ReadPrivateField(context, "_parentExecutionId");
|
||||||
|
var ownExecutionId = ReadPrivateField(context, "_executionId");
|
||||||
|
|
||||||
|
// The parent id is carried through untouched.
|
||||||
|
Assert.Equal(parentExecutionId, storedParent);
|
||||||
|
|
||||||
|
// The routed script's own ExecutionId is freshly generated, non-empty,
|
||||||
|
// and NOT the parent id — they are separate correlation values.
|
||||||
|
Assert.NotNull(ownExecutionId);
|
||||||
|
var ownId = Assert.IsType<Guid>(ownExecutionId);
|
||||||
|
Assert.NotEqual(Guid.Empty, ownId);
|
||||||
|
Assert.NotEqual(parentExecutionId, ownId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoParentExecutionIdSupplied_NonRoutedRun_ParentStaysNull()
|
||||||
|
{
|
||||||
|
// A normal (tag-change / timer) script run is not inbound-API-routed —
|
||||||
|
// no ParentExecutionId is supplied, so _parentExecutionId stays null
|
||||||
|
// while the run still gets its own fresh ExecutionId.
|
||||||
|
var context = CreateContext(
|
||||||
|
externalSystemClient: null,
|
||||||
|
databaseGateway: null,
|
||||||
|
auditWriter: null);
|
||||||
|
|
||||||
|
var storedParent = ReadPrivateField(context, "_parentExecutionId");
|
||||||
|
var ownExecutionId = ReadPrivateField(context, "_executionId");
|
||||||
|
|
||||||
|
Assert.Null(storedParent);
|
||||||
|
var ownId = Assert.IsType<Guid>(ownExecutionId);
|
||||||
|
Assert.NotEqual(Guid.Empty, ownId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user