review(Core.VirtualTags): fix Good-null upstream blocking downstream (Medium)
Re-review at 7286d320. -014 (Medium): AreInputsReady gated on value!=null, so a script
returning null (Good quality) permanently blocked change-triggered dependents at
BadWaitingForInitialData; now gates on the StatusCode Good bit only + test. -015:
TimerTriggerScheduler.Start throws on double-call. -016: fix wrong status-code comment.
This commit is contained in:
@@ -28,6 +28,7 @@ public sealed class TimerTriggerScheduler : IDisposable
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private long _skippedTickCount;
|
||||
private bool _disposed;
|
||||
private bool _started;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="TimerTriggerScheduler"/> class.</summary>
|
||||
/// <param name="engine">The virtual tag engine to trigger evaluations on.</param>
|
||||
@@ -52,9 +53,13 @@ public sealed class TimerTriggerScheduler : IDisposable
|
||||
/// behavior.
|
||||
/// </summary>
|
||||
/// <param name="definitions">The virtual tag definitions to schedule timers for.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when <c>Start</c> is called more than once on the same instance.</exception>
|
||||
public void Start(IReadOnlyList<VirtualTagDefinition> definitions)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(TimerTriggerScheduler));
|
||||
if (_started) throw new InvalidOperationException(
|
||||
"TimerTriggerScheduler.Start has already been called. Create a new instance for each Load cycle.");
|
||||
_started = true;
|
||||
|
||||
var byInterval = definitions
|
||||
.Where(d => d.TimerInterval.HasValue && d.TimerInterval.Value > TimeSpan.Zero)
|
||||
|
||||
@@ -387,17 +387,23 @@ public sealed class VirtualTagEngine : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when every entry in <paramref name="cache"/> has a non-null value
|
||||
/// and a Good/Uncertain-quality <see cref="DataValueSnapshot.StatusCode"/>. Scripts
|
||||
/// cast <c>ctx.GetTag(path).Value</c> unconditionally; short-circuiting before the
|
||||
/// script runs keeps a not-yet-cached upstream from faulting the virtual tag with
|
||||
/// <c>BadInternalError</c>.
|
||||
/// Returns true when every entry in <paramref name="cache"/> has a
|
||||
/// Good/Uncertain-quality <see cref="DataValueSnapshot.StatusCode"/> (bit 31 = 0).
|
||||
/// Readiness is gated on <c>StatusCode</c> alone — a Good-quality snapshot with a
|
||||
/// null <c>Value</c> (e.g. a script that returns <c>null</c>, or a
|
||||
/// <c>ctx.SetVirtualTag</c> call with a null argument) is considered ready and the
|
||||
/// downstream script is allowed to run. The downstream script will observe the null
|
||||
/// value and typically throw an NRE, which the outer evaluation catch maps to
|
||||
/// <c>BadInternalError</c> — the correct sentinel for "script ran but faulted"
|
||||
/// rather than "inputs not yet available". Previously the null-value check
|
||||
/// caused a Good-quality null upstream to permanently block all its dependents at
|
||||
/// <c>BadWaitingForInitialData</c> (Core.VirtualTags-014).
|
||||
/// </summary>
|
||||
private static bool AreInputsReady(IReadOnlyDictionary<string, DataValueSnapshot> cache)
|
||||
{
|
||||
foreach (var kv in cache)
|
||||
{
|
||||
if (kv.Value is null || kv.Value.Value is null) return false;
|
||||
if (kv.Value is null) return false;
|
||||
if ((kv.Value.StatusCode & 0x80000000u) != 0) return false;
|
||||
}
|
||||
return true;
|
||||
@@ -470,8 +476,8 @@ public sealed class VirtualTagEngine : IDisposable
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Caller logs + maps to BadTypeMismatch — we let null propagate so the
|
||||
// outer evaluation path sets the Bad quality.
|
||||
// Caller maps null to BadInternalError (0x80020000) — we let null propagate
|
||||
// so the outer evaluation path sets the Bad quality on the snapshot.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user