feat(runtime): per-script execution timeout overriding the global default (#9)
Spec promised a per-script timeout but only the global ScriptExecutionTimeoutSeconds existed. Add nullable TemplateScript.ExecutionTimeoutSeconds threaded through EF + flattening (ResolvedScript) to ScriptExecutionActor/AlarmExecutionActor, which use perScript ?? global for the execution CTS. Includes the EF migration for the new column.
This commit is contained in:
@@ -72,6 +72,15 @@ public class AlarmActor : ReceiveActor
|
||||
private readonly string? _onTriggerScriptName;
|
||||
private readonly Script<object?>? _onTriggerCompiledScript;
|
||||
|
||||
/// <summary>
|
||||
/// M2.5 (#9): the on-trigger script's per-script execution timeout in seconds,
|
||||
/// or null to use the global default. Forwarded to each spawned
|
||||
/// <see cref="AlarmExecutionActor"/>, which applies <c>perScript ?? global</c>
|
||||
/// (treating ≤ 0 as "use global"). The value comes from the referenced
|
||||
/// on-trigger script's <see cref="ResolvedScript.ExecutionTimeoutSeconds"/>.
|
||||
/// </summary>
|
||||
private readonly int? _onTriggerExecutionTimeoutSeconds;
|
||||
|
||||
// Expression trigger: compiled expression + the attribute snapshot it
|
||||
// evaluates against. This field is the single home for the compiled
|
||||
// expression on the hot path.
|
||||
@@ -107,6 +116,9 @@ public class AlarmActor : ReceiveActor
|
||||
/// <param name="serviceProvider">Optional DI service provider used to resolve the optional
|
||||
/// <see cref="ISiteEventLogger"/> for M1.5 <c>alarm</c> operational events. Fire-and-forget;
|
||||
/// a logging failure never affects alarm evaluation.</param>
|
||||
/// <param name="onTriggerExecutionTimeoutSeconds">M2.5 (#9): the on-trigger script's per-script
|
||||
/// execution timeout in seconds (from its <see cref="ResolvedScript.ExecutionTimeoutSeconds"/>),
|
||||
/// or null/non-positive to use the global default.</param>
|
||||
public AlarmActor(
|
||||
string alarmName,
|
||||
string instanceName,
|
||||
@@ -119,7 +131,9 @@ public class AlarmActor : ReceiveActor
|
||||
Script<object?>? compiledTriggerExpression = null,
|
||||
IReadOnlyDictionary<string, object?>? initialAttributes = null,
|
||||
ISiteHealthCollector? healthCollector = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
IServiceProvider? serviceProvider = null,
|
||||
// M2.5 (#9): per-script timeout for the on-trigger script (null = global).
|
||||
int? onTriggerExecutionTimeoutSeconds = null)
|
||||
{
|
||||
_alarmName = alarmName;
|
||||
_instanceName = instanceName;
|
||||
@@ -135,6 +149,7 @@ public class AlarmActor : ReceiveActor
|
||||
_priority = alarmConfig.PriorityLevel;
|
||||
_onTriggerScriptName = alarmConfig.OnTriggerScriptCanonicalName;
|
||||
_onTriggerCompiledScript = onTriggerCompiledScript;
|
||||
_onTriggerExecutionTimeoutSeconds = onTriggerExecutionTimeoutSeconds;
|
||||
_compiledTriggerExpression = compiledTriggerExpression;
|
||||
|
||||
// Seed the trigger-expression attribute snapshot from the instance's
|
||||
@@ -574,7 +589,9 @@ public class AlarmActor : ReceiveActor
|
||||
_instanceActor,
|
||||
_sharedScriptLibrary,
|
||||
_options,
|
||||
_logger));
|
||||
_logger,
|
||||
// M2.5 (#9): per-script timeout from the on-trigger script (null = global).
|
||||
_onTriggerExecutionTimeoutSeconds));
|
||||
|
||||
Context.ActorOf(props, executionId);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public class AlarmExecutionActor : ReceiveActor
|
||||
/// <param name="sharedScriptLibrary">Shared script library providing common utilities.</param>
|
||||
/// <param name="options">Site runtime configuration options, including the execution timeout.</param>
|
||||
/// <param name="logger">Logger for execution diagnostics.</param>
|
||||
/// <param name="executionTimeoutSeconds">M2.5 (#9): the on-trigger script's per-script execution timeout in seconds. Null or non-positive falls back to the global <see cref="SiteRuntimeOptions.ScriptExecutionTimeoutSeconds"/>.</param>
|
||||
public AlarmExecutionActor(
|
||||
string alarmName,
|
||||
string instanceName,
|
||||
@@ -38,7 +39,10 @@ public class AlarmExecutionActor : ReceiveActor
|
||||
IActorRef instanceActor,
|
||||
SharedScriptLibrary sharedScriptLibrary,
|
||||
SiteRuntimeOptions options,
|
||||
ILogger logger)
|
||||
ILogger logger,
|
||||
// M2.5 (#9): per-script execution timeout override (seconds) for the
|
||||
// alarm on-trigger script. Null or non-positive falls back to the global.
|
||||
int? executionTimeoutSeconds = null)
|
||||
{
|
||||
var self = Self;
|
||||
var parent = Context.Parent;
|
||||
@@ -46,7 +50,8 @@ public class AlarmExecutionActor : ReceiveActor
|
||||
ExecuteAlarmScript(
|
||||
alarmName, instanceName, level, priority, message,
|
||||
compiledScript, instanceActor,
|
||||
sharedScriptLibrary, options, self, parent, logger);
|
||||
sharedScriptLibrary, options, self, parent, logger,
|
||||
executionTimeoutSeconds);
|
||||
}
|
||||
|
||||
private static void ExecuteAlarmScript(
|
||||
@@ -61,9 +66,15 @@ public class AlarmExecutionActor : ReceiveActor
|
||||
SiteRuntimeOptions options,
|
||||
IActorRef self,
|
||||
IActorRef parent,
|
||||
ILogger logger)
|
||||
ILogger logger,
|
||||
int? executionTimeoutSeconds)
|
||||
{
|
||||
var timeout = TimeSpan.FromSeconds(options.ScriptExecutionTimeoutSeconds);
|
||||
// M2.5 (#9): per-script timeout overrides the global default. A null or
|
||||
// non-positive per-script value (≤ 0) falls back to the global.
|
||||
var timeout = TimeSpan.FromSeconds(
|
||||
executionTimeoutSeconds is { } perScript && perScript > 0
|
||||
? perScript
|
||||
: options.ScriptExecutionTimeoutSeconds);
|
||||
|
||||
// SiteRuntime-009: run the alarm on-trigger body on the dedicated
|
||||
// script-execution scheduler, not the shared .NET thread pool.
|
||||
|
||||
@@ -754,6 +754,10 @@ public class InstanceActor : ReceiveActor
|
||||
foreach (var alarm in _configuration.Alarms)
|
||||
{
|
||||
Script<object?>? onTriggerScript = null;
|
||||
// M2.5 (#9): the on-trigger script's per-script execution timeout,
|
||||
// captured from its ResolvedScript so the AlarmExecutionActor can
|
||||
// apply perScript ?? global. Null when there is no on-trigger script.
|
||||
int? onTriggerTimeoutSeconds = null;
|
||||
|
||||
// Compile on-trigger script if defined
|
||||
if (!string.IsNullOrEmpty(alarm.OnTriggerScriptCanonicalName))
|
||||
@@ -763,6 +767,7 @@ public class InstanceActor : ReceiveActor
|
||||
|
||||
if (triggerScriptDef != null)
|
||||
{
|
||||
onTriggerTimeoutSeconds = triggerScriptDef.ExecutionTimeoutSeconds;
|
||||
var result = _compilationService.Compile(
|
||||
$"alarm-trigger-{alarm.CanonicalName}", triggerScriptDef.Code);
|
||||
if (result.IsSuccess)
|
||||
@@ -794,7 +799,9 @@ public class InstanceActor : ReceiveActor
|
||||
triggerExpression,
|
||||
attributeSnapshot,
|
||||
_healthCollector,
|
||||
_serviceProvider));
|
||||
_serviceProvider,
|
||||
// M2.5 (#9): per-script timeout for the alarm on-trigger script.
|
||||
onTriggerTimeoutSeconds));
|
||||
|
||||
var actorRef = Context.ActorOf(props, $"alarm-{alarm.CanonicalName}");
|
||||
_alarmActors[alarm.CanonicalName] = actorRef;
|
||||
|
||||
@@ -43,6 +43,13 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
private Script<object?>? _compiledScript;
|
||||
private ScriptTriggerConfig? _triggerConfig;
|
||||
private TimeSpan? _minTimeBetweenRuns;
|
||||
|
||||
/// <summary>
|
||||
/// M2.5 (#9): the per-script execution timeout in seconds, or null to use the
|
||||
/// global default. Threaded down to each spawned <see cref="ScriptExecutionActor"/>,
|
||||
/// which applies <c>perScript ?? global</c> (and treats ≤ 0 as "use global").
|
||||
/// </summary>
|
||||
private readonly int? _executionTimeoutSeconds;
|
||||
private DateTimeOffset _lastExecutionTime = DateTimeOffset.MinValue;
|
||||
private int _executionCounter;
|
||||
private readonly Commons.Types.Scripts.ScriptScope _scope;
|
||||
@@ -112,6 +119,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
_healthCollector = healthCollector;
|
||||
_serviceProvider = serviceProvider;
|
||||
_minTimeBetweenRuns = scriptConfig.MinTimeBetweenRuns;
|
||||
_executionTimeoutSeconds = scriptConfig.ExecutionTimeoutSeconds;
|
||||
_scope = scriptConfig.Scope;
|
||||
_compiledTriggerExpression = compiledTriggerExpression;
|
||||
|
||||
@@ -426,7 +434,9 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
_serviceProvider,
|
||||
// Audit Log #23 (ParentExecutionId): null for trigger-driven runs;
|
||||
// an inbound-API-routed call supplies the inbound request's id.
|
||||
parentExecutionId));
|
||||
parentExecutionId,
|
||||
// M2.5 (#9): per-script timeout override (null = use global).
|
||||
_executionTimeoutSeconds));
|
||||
|
||||
Context.ActorOf(props, executionId);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
/// <param name="healthCollector">Optional health collector for recording execution metrics.</param>
|
||||
/// <param name="serviceProvider">Optional DI service provider for script execution services.</param>
|
||||
/// <param name="parentExecutionId">ExecutionId of the spawning inbound-API execution for audit correlation; null for normal runs.</param>
|
||||
/// <param name="executionTimeoutSeconds">M2.5 (#9): per-script execution timeout in seconds. Null or non-positive falls back to the global <see cref="SiteRuntimeOptions.ScriptExecutionTimeoutSeconds"/>.</param>
|
||||
public ScriptExecutionActor(
|
||||
string scriptName,
|
||||
string instanceName,
|
||||
@@ -65,7 +66,10 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
// 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)
|
||||
Guid? parentExecutionId = null,
|
||||
// M2.5 (#9): per-script execution timeout override (seconds). Null or
|
||||
// non-positive falls back to the global ScriptExecutionTimeoutSeconds.
|
||||
int? executionTimeoutSeconds = null)
|
||||
{
|
||||
// Immediately begin execution
|
||||
var self = Self;
|
||||
@@ -75,7 +79,7 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
scriptName, instanceName, compiledScript, parameters, callDepth,
|
||||
instanceActor, sharedScriptLibrary, options, replyTo, correlationId,
|
||||
self, parent, logger, scope, healthCollector, serviceProvider,
|
||||
parentExecutionId);
|
||||
parentExecutionId, executionTimeoutSeconds);
|
||||
}
|
||||
|
||||
private static void ExecuteScript(
|
||||
@@ -95,9 +99,15 @@ public class ScriptExecutionActor : ReceiveActor
|
||||
Commons.Types.Scripts.ScriptScope scope,
|
||||
ISiteHealthCollector? healthCollector,
|
||||
IServiceProvider? serviceProvider,
|
||||
Guid? parentExecutionId)
|
||||
Guid? parentExecutionId,
|
||||
int? executionTimeoutSeconds)
|
||||
{
|
||||
var timeout = TimeSpan.FromSeconds(options.ScriptExecutionTimeoutSeconds);
|
||||
// M2.5 (#9): per-script timeout overrides the global default. A null or
|
||||
// non-positive per-script value (≤ 0) falls back to the global.
|
||||
var timeout = TimeSpan.FromSeconds(
|
||||
executionTimeoutSeconds is { } perScript && perScript > 0
|
||||
? perScript
|
||||
: options.ScriptExecutionTimeoutSeconds);
|
||||
|
||||
// SiteRuntime-009: run the script body on the dedicated script-execution
|
||||
// scheduler, not the shared .NET thread pool, so blocking script I/O cannot
|
||||
|
||||
Reference in New Issue
Block a user