fix: lazy-compile API method scripts and prefix composed alarm trigger attributes
- InboundScriptExecutor lazy-compiles scripts on first request, solving the multi-node problem where methods created via CLI/UI were only compiled on the ManagementActor's node, not the node handling the HTTP request. - ManagementActor hot-registers API method scripts on create/update/delete for the local node. - FlatteningService prefixes the "attribute" field in composed alarm trigger configs with the composition instance name so alarms evaluate against the correct path-qualified attribute (e.g. CoolingTank.Level not Level).
This commit is contained in:
@@ -15,9 +15,12 @@ public class InboundScriptExecutor
|
||||
private readonly ILogger<InboundScriptExecutor> _logger;
|
||||
private readonly Dictionary<string, Func<InboundScriptContext, Task<object?>>> _scriptHandlers = new();
|
||||
|
||||
public InboundScriptExecutor(ILogger<InboundScriptExecutor> logger)
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public InboundScriptExecutor(ILogger<InboundScriptExecutor> logger, IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -28,6 +31,14 @@ public class InboundScriptExecutor
|
||||
_scriptHandlers[methodName] = handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a compiled script handler for a method name.
|
||||
/// </summary>
|
||||
public void RemoveHandler(string methodName)
|
||||
{
|
||||
_scriptHandlers.Remove(methodName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles and registers a single API method script.
|
||||
/// </summary>
|
||||
@@ -107,16 +118,14 @@ public class InboundScriptExecutor
|
||||
var context = new InboundScriptContext(parameters, route, cts.Token);
|
||||
|
||||
object? result;
|
||||
if (_scriptHandlers.TryGetValue(method.Name, out var handler))
|
||||
if (!_scriptHandlers.TryGetValue(method.Name, out var handler))
|
||||
{
|
||||
result = await handler(context).WaitAsync(cts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No compiled handler — this means the script hasn't been registered.
|
||||
// In production, we'd compile the method.Script and cache it.
|
||||
return new InboundScriptResult(false, null, "Script not compiled or registered for this method");
|
||||
// Lazy compile on first request (handles methods created after startup)
|
||||
if (!CompileAndRegister(method))
|
||||
return new InboundScriptResult(false, null, "Script compilation failed for this method");
|
||||
handler = _scriptHandlers[method.Name];
|
||||
}
|
||||
result = await handler(context).WaitAsync(cts.Token);
|
||||
|
||||
var resultJson = result != null
|
||||
? JsonSerializer.Serialize(result)
|
||||
|
||||
@@ -1059,6 +1059,10 @@ public class ManagementActor : ReceiveActor
|
||||
};
|
||||
await repo.AddApiMethodAsync(method);
|
||||
await repo.SaveChangesAsync();
|
||||
|
||||
// Hot-register the compiled script so it's immediately available
|
||||
sp.GetService<InboundAPI.InboundScriptExecutor>()?.CompileAndRegister(method);
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
@@ -1073,14 +1077,24 @@ public class ManagementActor : ReceiveActor
|
||||
method.ReturnDefinition = cmd.ReturnDefinition;
|
||||
await repo.UpdateApiMethodAsync(method);
|
||||
await repo.SaveChangesAsync();
|
||||
|
||||
// Re-compile and register the updated script
|
||||
sp.GetService<InboundAPI.InboundScriptExecutor>()?.CompileAndRegister(method);
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteApiMethod(IServiceProvider sp, DeleteApiMethodCommand cmd)
|
||||
{
|
||||
var repo = sp.GetRequiredService<IInboundApiRepository>();
|
||||
var method = await repo.GetApiMethodByIdAsync(cmd.ApiMethodId);
|
||||
await repo.DeleteApiMethodAsync(cmd.ApiMethodId);
|
||||
await repo.SaveChangesAsync();
|
||||
|
||||
// Remove the compiled script handler
|
||||
if (method != null)
|
||||
sp.GetService<InboundAPI.InboundScriptExecutor>()?.RemoveHandler(method.Name);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
<ProjectReference Include="../ScadaLink.Communication/ScadaLink.Communication.csproj" />
|
||||
<ProjectReference Include="../ScadaLink.Security/ScadaLink.Security.csproj" />
|
||||
<ProjectReference Include="../ScadaLink.HealthMonitoring/ScadaLink.HealthMonitoring.csproj" />
|
||||
<ProjectReference Include="..\ScadaLink.InboundAPI\ScadaLink.InboundAPI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -320,6 +320,7 @@ public class FlatteningService
|
||||
alarms[canonicalName] = alarm with
|
||||
{
|
||||
CanonicalName = canonicalName,
|
||||
TriggerConfiguration = PrefixTriggerAttribute(alarm.TriggerConfiguration, prefix),
|
||||
Source = "Composed"
|
||||
};
|
||||
}
|
||||
@@ -396,6 +397,51 @@ public class FlatteningService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefixes the "attribute" (or "attributeName") field in alarm trigger configuration JSON
|
||||
/// with the composition instance name, so composed alarms monitor the path-qualified attribute.
|
||||
/// </summary>
|
||||
private static string? PrefixTriggerAttribute(string? triggerConfigJson, string prefix)
|
||||
{
|
||||
if (string.IsNullOrEmpty(triggerConfigJson)) return triggerConfigJson;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(triggerConfigJson);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Find the attribute key name used
|
||||
string? attrKey = null;
|
||||
if (root.TryGetProperty("attribute", out _)) attrKey = "attribute";
|
||||
else if (root.TryGetProperty("attributeName", out _)) attrKey = "attributeName";
|
||||
|
||||
if (attrKey == null) return triggerConfigJson;
|
||||
|
||||
var attrValue = root.GetProperty(attrKey).GetString();
|
||||
if (string.IsNullOrEmpty(attrValue)) return triggerConfigJson;
|
||||
|
||||
// Rebuild JSON with prefixed attribute name
|
||||
using var ms = new System.IO.MemoryStream();
|
||||
using (var writer = new System.Text.Json.Utf8JsonWriter(ms))
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
foreach (var prop in root.EnumerateObject())
|
||||
{
|
||||
if (prop.Name == attrKey)
|
||||
writer.WriteString(attrKey, $"{prefix}.{attrValue}");
|
||||
else
|
||||
prop.WriteTo(writer);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
return System.Text.Encoding.UTF8.GetString(ms.ToArray());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return triggerConfigJson;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves alarm on-trigger script references from script IDs to canonical names.
|
||||
/// This is done by finding the script in the template chain whose ID matches the alarm's OnTriggerScriptId,
|
||||
|
||||
Reference in New Issue
Block a user