feat: wire ExternalSystem, Database, and Notify APIs into script runtime

IServiceProvider now flows through the actor chain (DeploymentManagerActor
→ InstanceActor → ScriptActor → ScriptExecutionActor) so scripts can
resolve IExternalSystemClient, IDatabaseGateway, and
INotificationDeliveryService from DI. ScriptGlobals exposes ExternalSystem,
Database, Notify, and Scripts as top-level properties so scripts can use
them without the Instance. prefix.
This commit is contained in:
Joseph Doherty
2026-03-18 02:41:18 -04:00
parent 8095c8efbe
commit 899dec6b6f
12 changed files with 767 additions and 100 deletions

View File

@@ -32,6 +32,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
private readonly ILogger<DeploymentManagerActor> _logger;
private readonly IActorRef? _dclManager;
private readonly ISiteHealthCollector? _healthCollector;
private readonly IServiceProvider? _serviceProvider;
private readonly Dictionary<string, IActorRef> _instanceActors = new();
private int _totalDeployedCount;
@@ -45,7 +46,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
SiteRuntimeOptions options,
ILogger<DeploymentManagerActor> logger,
IActorRef? dclManager = null,
ISiteHealthCollector? healthCollector = null)
ISiteHealthCollector? healthCollector = null,
IServiceProvider? serviceProvider = null)
{
_storage = storage;
_compilationService = compilationService;
@@ -54,6 +56,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
_options = options;
_dclManager = dclManager;
_healthCollector = healthCollector;
_serviceProvider = serviceProvider;
_logger = logger;
// Lifecycle commands
@@ -561,7 +564,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
_options,
loggerFactory.CreateLogger<InstanceActor>(),
_dclManager,
_healthCollector));
_healthCollector,
_serviceProvider));
var actorRef = Context.ActorOf(props, instanceName);
_instanceActors[instanceName] = actorRef;

View File

@@ -39,6 +39,7 @@ public class InstanceActor : ReceiveActor
private readonly SiteRuntimeOptions _options;
private readonly ILogger _logger;
private readonly ISiteHealthCollector? _healthCollector;
private readonly IServiceProvider? _serviceProvider;
private readonly Dictionary<string, object?> _attributes = new();
private readonly Dictionary<string, string> _attributeQualities = new();
private readonly Dictionary<string, AlarmState> _alarmStates = new();
@@ -64,7 +65,8 @@ public class InstanceActor : ReceiveActor
SiteRuntimeOptions options,
ILogger logger,
IActorRef? dclManager = null,
ISiteHealthCollector? healthCollector = null)
ISiteHealthCollector? healthCollector = null,
IServiceProvider? serviceProvider = null)
{
_instanceUniqueName = instanceUniqueName;
_storage = storage;
@@ -75,6 +77,7 @@ public class InstanceActor : ReceiveActor
_logger = logger;
_dclManager = dclManager;
_healthCollector = healthCollector;
_serviceProvider = serviceProvider;
// Deserialize the flattened configuration
_configuration = JsonSerializer.Deserialize<FlattenedConfiguration>(configJson);
@@ -479,7 +482,8 @@ public class InstanceActor : ReceiveActor
_sharedScriptLibrary,
_options,
_logger,
_healthCollector));
_healthCollector,
_serviceProvider));
var actorRef = Context.ActorOf(props, $"script-{script.CanonicalName}");
_scriptActors[script.CanonicalName] = actorRef;

View File

@@ -31,6 +31,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
private readonly SiteRuntimeOptions _options;
private readonly ILogger _logger;
private readonly ISiteHealthCollector? _healthCollector;
private readonly IServiceProvider? _serviceProvider;
private Script<object?>? _compiledScript;
private ScriptTriggerConfig? _triggerConfig;
@@ -49,7 +50,8 @@ public class ScriptActor : ReceiveActor, IWithTimers
SharedScriptLibrary sharedScriptLibrary,
SiteRuntimeOptions options,
ILogger logger,
ISiteHealthCollector? healthCollector = null)
ISiteHealthCollector? healthCollector = null,
IServiceProvider? serviceProvider = null)
{
_scriptName = scriptName;
_instanceName = instanceName;
@@ -59,6 +61,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
_options = options;
_logger = logger;
_healthCollector = healthCollector;
_serviceProvider = serviceProvider;
_minTimeBetweenRuns = scriptConfig.MinTimeBetweenRuns;
// Parse trigger configuration
@@ -212,7 +215,8 @@ public class ScriptActor : ReceiveActor, IWithTimers
replyTo,
correlationId,
_logger,
_healthCollector));
_healthCollector,
_serviceProvider));
Context.ActorOf(props, executionId);
}

View File

@@ -1,6 +1,8 @@
using Akka.Actor;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Messages.ScriptExecution;
using ScadaLink.HealthMonitoring;
using ScadaLink.SiteRuntime.Scripts;
@@ -30,7 +32,8 @@ public class ScriptExecutionActor : ReceiveActor
IActorRef replyTo,
string correlationId,
ILogger logger,
ISiteHealthCollector? healthCollector = null)
ISiteHealthCollector? healthCollector = null,
IServiceProvider? serviceProvider = null)
{
// Immediately begin execution
var self = Self;
@@ -39,7 +42,7 @@ public class ScriptExecutionActor : ReceiveActor
ExecuteScript(
scriptName, instanceName, compiledScript, parameters, callDepth,
instanceActor, sharedScriptLibrary, options, replyTo, correlationId,
self, parent, logger, healthCollector);
self, parent, logger, healthCollector, serviceProvider);
}
private static void ExecuteScript(
@@ -56,16 +59,31 @@ public class ScriptExecutionActor : ReceiveActor
IActorRef self,
IActorRef parent,
ILogger logger,
ISiteHealthCollector? healthCollector)
ISiteHealthCollector? healthCollector,
IServiceProvider? serviceProvider)
{
var timeout = TimeSpan.FromSeconds(options.ScriptExecutionTimeoutSeconds);
// CTS must be created inside the async lambda so it outlives this method
_ = Task.Run(async () =>
{
IServiceScope? serviceScope = null;
using var cts = new CancellationTokenSource(timeout);
try
{
// Resolve integration services from DI (scoped lifetime)
IExternalSystemClient? externalSystemClient = null;
IDatabaseGateway? databaseGateway = null;
INotificationDeliveryService? notificationService = null;
if (serviceProvider != null)
{
serviceScope = serviceProvider.CreateScope();
externalSystemClient = serviceScope.ServiceProvider.GetService<IExternalSystemClient>();
databaseGateway = serviceScope.ServiceProvider.GetService<IDatabaseGateway>();
notificationService = serviceScope.ServiceProvider.GetService<INotificationDeliveryService>();
}
var context = new ScriptRuntimeContext(
instanceActor,
self,
@@ -74,7 +92,10 @@ public class ScriptExecutionActor : ReceiveActor
options.MaxScriptCallDepth,
timeout,
instanceName,
logger);
logger,
externalSystemClient,
databaseGateway,
notificationService);
var globals = new ScriptGlobals
{
@@ -123,6 +144,8 @@ public class ScriptExecutionActor : ReceiveActor
}
finally
{
// Dispose the DI scope (and scoped services) after script execution completes
serviceScope?.Dispose();
// Stop self after execution completes
self.Tell(PoisonPill.Instance);
}

View File

@@ -178,4 +178,28 @@ public class ScriptGlobals
public IReadOnlyDictionary<string, object?> Parameters { get; set; } =
new Dictionary<string, object?>();
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Top-level ExternalSystem access for scripts (delegates to Instance.ExternalSystem).
/// Usage: ExternalSystem.Call("systemName", "methodName", params)
/// </summary>
public ScriptRuntimeContext.ExternalSystemHelper ExternalSystem => Instance.ExternalSystem;
/// <summary>
/// Top-level Database access for scripts (delegates to Instance.Database).
/// Usage: Database.Connection("name") or Database.CachedWrite("name", "sql", params)
/// </summary>
public ScriptRuntimeContext.DatabaseHelper Database => Instance.Database;
/// <summary>
/// Top-level Notify access for scripts (delegates to Instance.Notify).
/// Usage: Notify.To("listName").Send("subject", "message")
/// </summary>
public ScriptRuntimeContext.NotifyHelper Notify => Instance.Notify;
/// <summary>
/// Top-level Scripts access for shared script calls (delegates to Instance.Scripts).
/// Usage: Scripts.CallShared("scriptName", params)
/// </summary>
public ScriptRuntimeContext.ScriptCallHelper Scripts => Instance.Scripts;
}