fix(site-runtime): site-side compile failure now rejects the deployment per spec (S3) — synchronous validation gate before actor creation

Validation runs on the actor thread (not off-thread as the plan sketched): piping
the verdict back to self reorders concurrent deploys relative to each other and to
delete/disable, breaking the redeploy-supersede FIFO ordering (SR020/SR029). A deploy
is an infrequent admin command, so a brief synchronous Roslyn compile is acceptable.
This commit is contained in:
Joseph Doherty
2026-07-08 23:33:48 -04:00
parent 2caab79c0a
commit b83d1ed19c
3 changed files with 77 additions and 1 deletions
@@ -35,6 +35,10 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
{
private readonly SiteStorageService _storage;
private readonly ScriptCompilationService _compilationService;
// Pure, off-thread deploy-time compile gate. A site-side compile failure
// must reject the deployment (spec: no partial state applied), so every
// DeployInstanceCommand is validated before any actor/persistence work.
private readonly DeployCompileValidator _deployCompileValidator;
private readonly SharedScriptLibrary _sharedScriptLibrary;
private readonly SiteStreamManager? _streamManager;
private readonly SiteRuntimeOptions _options;
@@ -125,6 +129,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
{
_storage = storage;
_compilationService = compilationService;
_deployCompileValidator = new DeployCompileValidator(compilationService);
_sharedScriptLibrary = sharedScriptLibrary;
_streamManager = streamManager;
_options = options;
@@ -387,6 +392,41 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// apply still replies to the right actor.
/// </param>
private void HandleDeploy(DeployInstanceCommand command, IActorRef replyTo)
{
// Site-side compile gate (S3): a compile failure must reject the
// deployment with NO partial state applied (no Instance Actor, no
// persisted config) — the design spec's contract. Validation runs
// synchronously on the actor thread: it is a pure prefix step of the
// deploy handler, so the existing redeploy-supersede / delete-during-
// redeploy ordering (which depends on strict mailbox FIFO) is preserved
// exactly. It is NOT run off-thread — piping the verdict back to self
// reorders concurrent deploys relative to each other and to
// 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.
var errors = _deployCompileValidator.Validate(command.FlattenedConfigurationJson);
if (errors.Count > 0)
{
var instanceName = command.InstanceUniqueName;
var joined = string.Join(" | ", errors);
LogDeploymentEvent("Error", instanceName,
$"Instance {instanceName} deploy rejected — site compile validation failed (deploymentId={command.DeploymentId})",
joined);
replyTo.Tell(new DeploymentStatusResponse(
command.DeploymentId, instanceName, DeploymentStatus.Failed,
"Script compilation failed on site: " + joined, DateTimeOffset.UtcNow));
return;
}
ProceedWithDeploy(command, replyTo);
}
/// <summary>
/// Core deploy application (redeploy-supersede + fresh apply), reached only
/// after the synchronous compile gate in <see cref="HandleDeploy"/> passes.
/// </summary>
private void ProceedWithDeploy(DeployInstanceCommand command, IActorRef replyTo)
{
var instanceName = command.InstanceUniqueName;
_logger.LogInformation(
@@ -1336,7 +1336,9 @@ public class InstanceActor : ReceiveActor
/// Creates child Script Actors and Alarm Actors from the flattened configuration.
/// Script Actors spawned per script definition.
/// Alarm Actors spawned per alarm definition, as peers to Script Actors.
/// Compilation errors reject entire instance deployment (logged but actor still starts).
/// Compile failures are rejected at deploy time by the Deployment Manager's
/// validation gate; a failure reaching this point (startup load of a legacy
/// config) is logged and the offending script/alarm skipped.
///
/// Each child is seeded from a private point-in-time snapshot
/// of <c>_attributes</c>, NOT the live dictionary. The snapshot is taken here on