fix(host): retry VT evaluation once on ObjectDisposedException from the apply-boundary cache clear (02/S12)
This commit is contained in:
@@ -54,7 +54,7 @@
|
|||||||
{
|
{
|
||||||
"id": "R2-03-T8",
|
"id": "R2-03-T8",
|
||||||
"subject": "S12 fix: retry-once on ObjectDisposedException in RoslynVirtualTagEvaluator.Evaluate (re-fetch via GetOrCompile; no retry when _disposed or second hit); update xmldoc (turns T7 green)",
|
"subject": "S12 fix: retry-once on ObjectDisposedException in RoslynVirtualTagEvaluator.Evaluate (re-fetch via GetOrCompile; no retry when _disposed or second hit); update xmldoc (turns T7 green)",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": [
|
"blockedBy": [
|
||||||
"R2-03-T6",
|
"R2-03-T6",
|
||||||
"R2-03-T7"
|
"R2-03-T7"
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ namespace ZB.MOM.WW.OtOpcUa.Host.Engines;
|
|||||||
/// fan-out between actors is owned by <c>DependencyMuxActor</c>, not by the eval engine.
|
/// fan-out between actors is owned by <c>DependencyMuxActor</c>, not by the eval engine.
|
||||||
/// Cycle detection + cascade ordering live in <see cref="VirtualTagEngine"/>; this adapter
|
/// Cycle detection + cascade ordering live in <see cref="VirtualTagEngine"/>; this adapter
|
||||||
/// stays single-tag scoped to keep <see cref="VirtualTagActor"/>'s message loop simple.
|
/// stays single-tag scoped to keep <see cref="VirtualTagActor"/>'s message loop simple.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// 02/S12 — the apply-boundary <see cref="ClearCompiledScripts"/> (driven by the host actor per
|
||||||
|
/// deploy generation) disposes cached evaluators while unchanged children may still be evaluating.
|
||||||
|
/// A clear landing between this adapter's <c>GetOrCompile</c> and the run's disposed guard would
|
||||||
|
/// otherwise surface as a spurious <c>Failure</c>. The compiled path is therefore retry-safe: an
|
||||||
|
/// <see cref="ObjectDisposedException"/> from an in-flight evaluation re-fetches (recompiling the
|
||||||
|
/// same source into the current cache generation) and retries once. A second disposal mid-retry
|
||||||
|
/// (two applies inside one evaluation — not a realistic cadence) falls through to the failure path.
|
||||||
|
/// Residual (accepted, bounded to one generation): a stopping child can recompile its stale source
|
||||||
|
/// into the fresh cache for one generation; P7's expression-set gate means no-op redeploys no
|
||||||
|
/// longer open this window at all.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IScriptCacheOwner, IDisposable
|
public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IScriptCacheOwner, IDisposable
|
||||||
{
|
{
|
||||||
@@ -63,6 +76,26 @@ public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IScriptCac
|
|||||||
: VirtualTagEvalResult.Ok(null);
|
: VirtualTagEvalResult.Ok(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var readCache = BuildReadCache(dependencies);
|
||||||
|
// Per-evaluation script logger: bind both ScriptId and VirtualTagId from the virtual-tag id
|
||||||
|
// (in the live path the script id equals the virtual-tag id) so the Script-log page can
|
||||||
|
// attribute each line. EquipmentId stays unbound for now.
|
||||||
|
var scriptLog = _scriptRoot
|
||||||
|
.ForContext(ScriptLoggerFactory.ScriptIdProperty, virtualTagId)
|
||||||
|
.ForContext(ScriptLoggerFactory.VirtualTagIdProperty, virtualTagId);
|
||||||
|
var context = new VirtualTagContext(
|
||||||
|
readCache,
|
||||||
|
setVirtualTag: (path, _) =>
|
||||||
|
_logger.LogDebug("VirtualTag {Id}: cross-tag write to {Path} dropped (single-tag adapter)",
|
||||||
|
virtualTagId, path),
|
||||||
|
logger: scriptLog);
|
||||||
|
|
||||||
|
// 02/S12: fetch + run inside a bounded loop so an ObjectDisposedException caused by a concurrent
|
||||||
|
// apply-boundary ClearCompiledScripts (which disposed the evaluator between GetOrCompile and the
|
||||||
|
// run's disposed guard) re-fetches and retries exactly once. The loop runs at most twice — the
|
||||||
|
// retry catch only matches attempt == 0.
|
||||||
|
for (var attempt = 0; ; attempt++)
|
||||||
|
{
|
||||||
ScriptEvaluator<VirtualTagContext, object?> evaluator;
|
ScriptEvaluator<VirtualTagContext, object?> evaluator;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -78,26 +111,18 @@ public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IScriptCac
|
|||||||
_logger.LogWarning(ex, "VirtualTag {Id}: sandbox violation", virtualTagId);
|
_logger.LogWarning(ex, "VirtualTag {Id}: sandbox violation", virtualTagId);
|
||||||
return VirtualTagEvalResult.Failure($"sandbox violation: {ex.Message}");
|
return VirtualTagEvalResult.Failure($"sandbox violation: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// The cache itself is being torn down (this evaluator is disposing) — a fetch-time
|
||||||
|
// disposal is a shutdown signal, not the apply-boundary race, so never retry.
|
||||||
|
return VirtualTagEvalResult.Failure("evaluator disposed");
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "VirtualTag {Id}: compile threw", virtualTagId);
|
_logger.LogWarning(ex, "VirtualTag {Id}: compile threw", virtualTagId);
|
||||||
return VirtualTagEvalResult.Failure($"compile failure: {ex.Message}");
|
return VirtualTagEvalResult.Failure($"compile failure: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var readCache = BuildReadCache(dependencies);
|
|
||||||
// Per-evaluation script logger: bind both ScriptId and VirtualTagId from the virtual-tag id
|
|
||||||
// (in the live path the script id equals the virtual-tag id) so the Script-log page can
|
|
||||||
// attribute each line. EquipmentId stays unbound for now.
|
|
||||||
var scriptLog = _scriptRoot
|
|
||||||
.ForContext(ScriptLoggerFactory.ScriptIdProperty, virtualTagId)
|
|
||||||
.ForContext(ScriptLoggerFactory.VirtualTagIdProperty, virtualTagId);
|
|
||||||
var context = new VirtualTagContext(
|
|
||||||
readCache,
|
|
||||||
setVirtualTag: (path, _) =>
|
|
||||||
_logger.LogDebug("VirtualTag {Id}: cross-tag write to {Path} dropped (single-tag adapter)",
|
|
||||||
virtualTagId, path),
|
|
||||||
logger: scriptLog);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Route through TimedScriptEvaluator (Task.Run + WaitAsync): a raw CancellationToken can't
|
// Route through TimedScriptEvaluator (Task.Run + WaitAsync): a raw CancellationToken can't
|
||||||
@@ -113,12 +138,25 @@ public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IScriptCac
|
|||||||
{
|
{
|
||||||
return VirtualTagEvalResult.Failure($"script timed out after {_runTimeout.TotalSeconds:F1}s");
|
return VirtualTagEvalResult.Failure($"script timed out after {_runTimeout.TotalSeconds:F1}s");
|
||||||
}
|
}
|
||||||
|
catch (ObjectDisposedException) when (!_disposed && attempt == 0)
|
||||||
|
{
|
||||||
|
// S12: the apply-boundary ClearCompiledScripts disposed the evaluator between our
|
||||||
|
// GetOrCompile and the run's disposed-guard. Re-fetch — GetOrCompile recompiles the same
|
||||||
|
// source into the CURRENT cache generation — and retry once. A second disposal mid-retry
|
||||||
|
// (two applies inside one evaluation) falls through to the failure path below.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
return VirtualTagEvalResult.Failure("evaluator disposed during evaluation");
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "VirtualTag {Id}: script execution threw", virtualTagId);
|
_logger.LogWarning(ex, "VirtualTag {Id}: script execution threw", virtualTagId);
|
||||||
return VirtualTagEvalResult.Failure($"script threw: {ex.Message}");
|
return VirtualTagEvalResult.Failure($"script threw: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void ClearCompiledScripts() => _cache.Clear();
|
public void ClearCompiledScripts() => _cache.Clear();
|
||||||
|
|||||||
Reference in New Issue
Block a user