feat(siteruntime): Tracking.Status(id) script API (#23 M3)

This commit is contained in:
Joseph Doherty
2026-05-20 13:56:59 -04:00
parent b86d7c61ab
commit 0f28d13da7
3 changed files with 151 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ using Akka.Actor;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Interfaces;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Messages.ScriptExecution;
using ScadaLink.Commons.Types;
@@ -105,6 +106,11 @@ public class ScriptExecutionActor : ReceiveActor
// composes the SQLite hot-path + drop-oldest ring); null in tests / hosts
// that haven't called AddAuditLog, which the helper handles as a no-op.
IAuditWriter? auditWriter = null;
// Audit Log #23 (M3 Bundle A — Task A3): site-local tracking store
// backing Tracking.Status(id). Singleton; null in tests / hosts
// that haven't wired the store, which the helper handles by
// throwing on access.
IOperationTrackingStore? operationTrackingStore = null;
if (serviceProvider != null)
{
@@ -115,6 +121,7 @@ public class ScriptExecutionActor : ReceiveActor
siteId = serviceScope.ServiceProvider.GetService<ISiteIdentityProvider>()?.SiteId
?? string.Empty;
auditWriter = serviceScope.ServiceProvider.GetService<IAuditWriter>();
operationTrackingStore = serviceScope.ServiceProvider.GetService<IOperationTrackingStore>();
}
var context = new ScriptRuntimeContext(
@@ -138,7 +145,11 @@ public class ScriptExecutionActor : ReceiveActor
// ExternalSystem.Call. Writer is best-effort; failures are logged
// and swallowed inside the helper so the script's call path is
// never aborted by an audit failure.
auditWriter: auditWriter);
auditWriter: auditWriter,
// Audit Log #23 (M3 Bundle A — Task A3): site-local tracking store
// backing Tracking.Status(id). Authoritative source of truth for
// cached-call status — read directly by the script API.
operationTrackingStore: operationTrackingStore);
var globals = new ScriptGlobals
{

View File

@@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using Akka.Actor;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Interfaces;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Messages.Instance;
using ScadaLink.Commons.Messages.Notification;
@@ -85,6 +86,14 @@ public class ScriptRuntimeContext
/// </summary>
private readonly IAuditWriter? _auditWriter;
/// <summary>
/// Audit Log #23 (M3): site-local tracking store consulted by
/// <c>Tracking.Status(TrackedOperationId)</c>. Optional — when null the
/// helper throws on access, mirroring the existing
/// "service-not-wired" behaviour of the other integration helpers.
/// </summary>
private readonly IOperationTrackingStore? _operationTrackingStore;
public ScriptRuntimeContext(
IActorRef instanceActor,
IActorRef self,
@@ -100,7 +109,8 @@ public class ScriptRuntimeContext
ICanTell? siteCommunicationActor = null,
string siteId = "",
string? sourceScript = null,
IAuditWriter? auditWriter = null)
IAuditWriter? auditWriter = null,
IOperationTrackingStore? operationTrackingStore = null)
{
_instanceActor = instanceActor;
_self = self;
@@ -117,6 +127,7 @@ public class ScriptRuntimeContext
_siteId = siteId;
_sourceScript = sourceScript;
_auditWriter = auditWriter;
_operationTrackingStore = operationTrackingStore;
}
/// <summary>
@@ -235,6 +246,15 @@ public class ScriptRuntimeContext
public NotifyHelper Notify => new(
_storeAndForward, _siteCommunicationActor, _siteId, _instanceName, _sourceScript, _askTimeout, _logger);
/// <summary>
/// Audit Log #23 (M3): site-local tracking-status API for cached operations.
/// <c>Tracking.Status(trackedOperationId)</c> reads the site SQLite tracking row
/// directly (authoritative source of truth — no central round-trip) and
/// returns a <see cref="TrackingStatusSnapshot"/>, or <c>null</c> when the
/// id is unknown / has already been purged.
/// </summary>
public TrackingHelper Tracking => new(_operationTrackingStore, _logger);
/// <summary>
/// Helper class for Scripts.CallShared() syntax.
/// </summary>
@@ -746,4 +766,46 @@ public class ScriptRuntimeContext
return notificationId;
}
}
/// <summary>
/// Audit Log #23 (M3): script-side accessor for cached-operation tracking.
/// <c>Tracking.Status(trackedOperationId)</c> reads the site-local SQLite
/// row directly via <see cref="IOperationTrackingStore.GetStatusAsync"/> —
/// the site is the single source of truth for cached-call status, so no
/// central round-trip is needed and the call is answered authoritatively.
/// </summary>
public class TrackingHelper
{
private readonly IOperationTrackingStore? _store;
private readonly ILogger _logger;
internal TrackingHelper(IOperationTrackingStore? store, ILogger logger)
{
_store = store;
_logger = logger;
}
/// <summary>
/// Returns the latest tracking snapshot for the supplied id, or
/// <c>null</c> when the id is unknown (never recorded, or purged after
/// the retention window).
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when the script runtime was constructed without an
/// <see cref="IOperationTrackingStore"/> — mirrors the
/// "service-not-wired" failure mode of the other integration helpers.
/// </exception>
public Task<TrackingStatusSnapshot?> Status(
TrackedOperationId trackedOperationId,
CancellationToken cancellationToken = default)
{
if (_store == null)
{
throw new InvalidOperationException(
"Operation tracking store not available");
}
return _store.GetStatusAsync(trackedOperationId, cancellationToken);
}
}
}