Phase 3B: Site I/O & Observability — Communication, DCL, Script/Alarm actors, Health, Event Logging
Communication Layer (WP-1–5): - 8 message patterns with correlation IDs, per-pattern timeouts - Central/Site communication actors, transport heartbeat config - Connection failure handling (no central buffering, debug streams killed) Data Connection Layer (WP-6–14, WP-34): - Connection actor with Become/Stash lifecycle (Connecting/Connected/Reconnecting) - OPC UA + LmxProxy adapters behind IDataConnection - Auto-reconnect, bad quality propagation, transparent re-subscribe - Write-back, tag path resolution with retry, health reporting - Protocol extensibility via DataConnectionFactory Site Runtime (WP-15–25, WP-32–33): - ScriptActor/ScriptExecutionActor (triggers, concurrent execution, blocking I/O dispatcher) - AlarmActor/AlarmExecutionActor (ValueMatch/RangeViolation/RateOfChange, in-memory state) - SharedScriptLibrary (inline execution), ScriptRuntimeContext (API) - ScriptCompilationService (Roslyn, forbidden API enforcement, execution timeout) - Recursion limit (default 10), call direction enforcement - SiteStreamManager (per-subscriber bounded buffers, fire-and-forget) - Debug view backend (snapshot + stream), concurrency serialization - Local artifact storage (4 SQLite tables) Health Monitoring (WP-26–28): - SiteHealthCollector (thread-safe counters, connection state) - HealthReportSender (30s interval, monotonic sequence numbers) - CentralHealthAggregator (offline detection 60s, online recovery) Site Event Logging (WP-29–31): - SiteEventLogger (SQLite, 6 event categories, ISO 8601 UTC) - EventLogPurgeService (30-day retention, 1GB cap) - EventLogQueryService (filters, keyword search, keyset pagination) 541 tests pass, zero warnings.
This commit is contained in:
172
src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs
Normal file
172
src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using Akka.Actor;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Messages.Instance;
|
||||
using ScadaLink.Commons.Messages.ScriptExecution;
|
||||
|
||||
namespace ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// WP-18: Script Runtime API — injected into Script/Alarm Execution Actors.
|
||||
/// Provides the API surface that user scripts interact with:
|
||||
/// Instance.GetAttribute("name")
|
||||
/// Instance.SetAttribute("name", value)
|
||||
/// Instance.CallScript("scriptName", params)
|
||||
/// Scripts.CallShared("scriptName", params)
|
||||
///
|
||||
/// WP-20: Recursion Limit — call depth tracked and enforced.
|
||||
/// </summary>
|
||||
public class ScriptRuntimeContext
|
||||
{
|
||||
private readonly IActorRef _instanceActor;
|
||||
private readonly IActorRef _self;
|
||||
private readonly SharedScriptLibrary _sharedScriptLibrary;
|
||||
private readonly int _currentCallDepth;
|
||||
private readonly int _maxCallDepth;
|
||||
private readonly TimeSpan _askTimeout;
|
||||
private readonly ILogger _logger;
|
||||
private readonly string _instanceName;
|
||||
|
||||
public ScriptRuntimeContext(
|
||||
IActorRef instanceActor,
|
||||
IActorRef self,
|
||||
SharedScriptLibrary sharedScriptLibrary,
|
||||
int currentCallDepth,
|
||||
int maxCallDepth,
|
||||
TimeSpan askTimeout,
|
||||
string instanceName,
|
||||
ILogger logger)
|
||||
{
|
||||
_instanceActor = instanceActor;
|
||||
_self = self;
|
||||
_sharedScriptLibrary = sharedScriptLibrary;
|
||||
_currentCallDepth = currentCallDepth;
|
||||
_maxCallDepth = maxCallDepth;
|
||||
_askTimeout = askTimeout;
|
||||
_instanceName = instanceName;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of an attribute from the Instance Actor.
|
||||
/// Uses Ask pattern (system boundary between script execution and instance state).
|
||||
/// </summary>
|
||||
public async Task<object?> GetAttribute(string attributeName)
|
||||
{
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var request = new GetAttributeRequest(
|
||||
correlationId, _instanceName, attributeName, DateTimeOffset.UtcNow);
|
||||
|
||||
var response = await _instanceActor.Ask<GetAttributeResponse>(request, _askTimeout);
|
||||
|
||||
if (!response.Found)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"GetAttribute: attribute '{Attribute}' not found on instance '{Instance}'",
|
||||
attributeName, _instanceName);
|
||||
}
|
||||
|
||||
return response.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets an attribute value. For data-connected attributes, forwards to DCL via Instance Actor.
|
||||
/// For static attributes, updates in-memory and persists to SQLite via Instance Actor.
|
||||
/// All mutations serialized through the Instance Actor mailbox.
|
||||
/// </summary>
|
||||
public void SetAttribute(string attributeName, string value)
|
||||
{
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var command = new SetStaticAttributeCommand(
|
||||
correlationId, _instanceName, attributeName, value, DateTimeOffset.UtcNow);
|
||||
|
||||
// Tell (fire-and-forget) — mutation serialized through Instance Actor
|
||||
_instanceActor.Tell(command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls a sibling script on the same instance by name (Ask pattern).
|
||||
/// WP-20: Enforces recursion limit.
|
||||
/// WP-22: Uses Ask pattern for CallScript.
|
||||
/// </summary>
|
||||
public async Task<object?> CallScript(string scriptName, IReadOnlyDictionary<string, object?>? parameters = null)
|
||||
{
|
||||
var nextDepth = _currentCallDepth + 1;
|
||||
if (nextDepth > _maxCallDepth)
|
||||
{
|
||||
var msg = $"Script call depth exceeded maximum of {_maxCallDepth}. " +
|
||||
$"CallScript('{scriptName}') rejected at depth {nextDepth}.";
|
||||
_logger.LogError(msg);
|
||||
throw new InvalidOperationException(msg);
|
||||
}
|
||||
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var request = new ScriptCallRequest(
|
||||
scriptName,
|
||||
parameters,
|
||||
nextDepth,
|
||||
correlationId);
|
||||
|
||||
// Ask the Instance Actor, which routes to the appropriate Script Actor
|
||||
var result = await _instanceActor.Ask<ScriptCallResult>(request, _askTimeout);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"CallScript('{scriptName}') failed: {result.ErrorMessage}");
|
||||
}
|
||||
|
||||
return result.ReturnValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to shared script execution via the Scripts property.
|
||||
/// </summary>
|
||||
public ScriptCallHelper Scripts => new(_sharedScriptLibrary, this, _currentCallDepth, _maxCallDepth, _logger);
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for Scripts.CallShared() syntax.
|
||||
/// </summary>
|
||||
public class ScriptCallHelper
|
||||
{
|
||||
private readonly SharedScriptLibrary _library;
|
||||
private readonly ScriptRuntimeContext _context;
|
||||
private readonly int _currentCallDepth;
|
||||
private readonly int _maxCallDepth;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
internal ScriptCallHelper(
|
||||
SharedScriptLibrary library,
|
||||
ScriptRuntimeContext context,
|
||||
int currentCallDepth,
|
||||
int maxCallDepth,
|
||||
ILogger logger)
|
||||
{
|
||||
_library = library;
|
||||
_context = context;
|
||||
_currentCallDepth = currentCallDepth;
|
||||
_maxCallDepth = maxCallDepth;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP-17: Executes a shared script inline (direct method call, not actor message).
|
||||
/// WP-20: Enforces recursion limit.
|
||||
/// </summary>
|
||||
public async Task<object?> CallShared(
|
||||
string scriptName,
|
||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var nextDepth = _currentCallDepth + 1;
|
||||
if (nextDepth > _maxCallDepth)
|
||||
{
|
||||
var msg = $"Script call depth exceeded maximum of {_maxCallDepth}. " +
|
||||
$"CallShared('{scriptName}') rejected at depth {nextDepth}.";
|
||||
_logger.LogError(msg);
|
||||
throw new InvalidOperationException(msg);
|
||||
}
|
||||
|
||||
return await _library.ExecuteAsync(scriptName, _context, parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user