merge(r2): r2-plan03

This commit is contained in:
Joseph Doherty
2026-07-13 11:08:27 -04:00
15 changed files with 446 additions and 13 deletions
@@ -186,6 +186,10 @@ public class AlarmActor : ReceiveActor
// Handle the off-dispatcher trigger-expression evaluation result (P1).
Receive<ExpressionEvalResult>(HandleExpressionEvalResult);
// Handle a faulted off-dispatcher evaluation task (N2) — clears in-flight
// instead of stranding the alarm trigger with an unhandled Status.Failure.
Receive<ExpressionEvalFailed>(HandleExpressionEvalFailed);
}
/// <inheritdoc />
@@ -551,7 +555,8 @@ public class AlarmActor : ReceiveActor
}
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach,
ScriptExecutionScheduler.Shared(_options)).Unwrap().PipeTo(self,
success: r => new ExpressionEvalResult(r));
success: r => new ExpressionEvalResult(r),
failure: ex => new ExpressionEvalFailed(ex));
}
/// <summary>
@@ -576,6 +581,22 @@ public class AlarmActor : ReceiveActor
if (_evalPending) { _evalPending = false; StartExpressionEvaluation(); }
}
/// <summary>
/// Applies a faulted off-dispatcher evaluation task (N2): logs the fault, then
/// delegates to the false result (per the class contract — a throwing expression
/// is treated as false so the state machine still runs, an Active alarm clears)
/// which clears in-flight and drains any pending evaluation. Without this the
/// alarm trigger would be permanently parked with _evalInFlight stuck true.
/// </summary>
private void HandleExpressionEvalFailed(ExpressionEvalFailed msg)
{
_healthCollector?.IncrementAlarmError();
_logger.LogError(msg.Cause,
"Alarm {Alarm} trigger-expression evaluation task faulted on {Instance}; treated as false",
_alarmName, _instanceName);
HandleExpressionEvalResult(new ExpressionEvalResult(false));
}
/// <summary>
/// HiLo level evaluator: returns the most-severe matching band for the
/// given value. Severity order checked from highest to lowest so that a
@@ -795,6 +816,15 @@ public class AlarmActor : ReceiveActor
/// actor thread.
/// </summary>
private sealed record ExpressionEvalResult(bool Result);
/// <summary>
/// Piped back to self when the off-dispatcher evaluation TASK itself faults
/// (e.g. ObjectDisposedException from a disposed ScriptExecutionScheduler
/// during shutdown) — the inner body's catch never sees that. Without this
/// mapping the actor receives an unhandled Status.Failure and _evalInFlight
/// stays true forever, permanently parking the expression trigger (N2).
/// </summary>
internal sealed record ExpressionEvalFailed(Exception Cause);
}
internal enum RateOfChangeDirection { Either, Rising, Falling }
@@ -518,7 +518,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
// delete/disable commands, breaking that ordering. A deploy is an
// infrequent admin command, so briefly holding the singleton for a pure
// Roslyn compile is acceptable; the central deployer already Asks and
// waits for the DeploymentStatusResponse.
// waits for the DeploymentStatusResponse. Redeploys and multi-instance
// deploys of unchanged scripts hit the process-wide compile cache
// (SiteScriptCompileCache), so the synchronous gate recompiles only
// genuinely new code and the Instance Actor's PreStart reuses the gate's
// compile (N4).
var errors = _deployCompileValidator.Validate(command.FlattenedConfigurationJson);
if (errors.Count > 0)
{
@@ -159,6 +159,10 @@ public class ScriptActor : ReceiveActor, IWithTimers
// Handle the off-dispatcher trigger-expression evaluation result (P1).
Receive<ExpressionEvalResult>(HandleExpressionEvalResult);
// Handle a faulted off-dispatcher evaluation task (N2) — clears in-flight
// instead of stranding the trigger with an unhandled Status.Failure.
Receive<ExpressionEvalFailed>(HandleExpressionEvalFailed);
}
/// <inheritdoc />
@@ -310,7 +314,8 @@ public class ScriptActor : ReceiveActor, IWithTimers
}
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach,
ScriptExecutionScheduler.Shared(_options)).Unwrap().PipeTo(self,
success: r => new ExpressionEvalResult(r));
success: r => new ExpressionEvalResult(r),
failure: ex => new ExpressionEvalFailed(ex));
}
/// <summary>
@@ -333,6 +338,18 @@ public class ScriptActor : ReceiveActor, IWithTimers
if (_evalPending) { _evalPending = false; StartExpressionEvaluation(); }
}
/// <summary>
/// Applies a faulted off-dispatcher evaluation task (N2): logs the fault via the
/// existing error path (which also increments the health counter), then reuses the
/// false-result path — clearing in-flight, applying the false edge, and draining any
/// pending evaluation — so a transient scheduler fault cannot permanently park the trigger.
/// </summary>
private void HandleExpressionEvalFailed(ExpressionEvalFailed msg)
{
LogExpressionError(msg.Cause);
HandleExpressionEvalResult(new ExpressionEvalResult(false));
}
/// <summary>
/// Applies a WhileTrue trigger's condition-state transition: on the
/// false→true edge, fire once and start the re-fire timer; on the
@@ -621,6 +638,15 @@ public class ScriptActor : ReceiveActor, IWithTimers
/// the actor thread.
/// </summary>
private sealed record ExpressionEvalResult(bool Result);
/// <summary>
/// Piped back to self when the off-dispatcher evaluation TASK itself faults
/// (e.g. ObjectDisposedException from a disposed ScriptExecutionScheduler
/// during shutdown) — the inner body's catch never sees that. Without this
/// mapping the actor receives an unhandled Status.Failure and _evalInFlight
/// stays true forever, permanently parking the expression trigger (N2).
/// </summary>
internal sealed record ExpressionEvalFailed(Exception Cause);
}
// ── Trigger config types ──