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:
@@ -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
|
||||
|
||||
@@ -232,6 +232,40 @@ public class DeploymentManagerActorTests : TestKit, IDisposable
|
||||
Assert.Equal("NewPump", response.InstanceUniqueName);
|
||||
}
|
||||
|
||||
// ── S3: site-side compile failure rejects the deployment (no partial state) ──
|
||||
|
||||
private static string MakeConfigWithBrokenScriptJson(string instanceName) =>
|
||||
JsonSerializer.Serialize(new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = instanceName,
|
||||
Attributes = [new ResolvedAttribute { CanonicalName = "TestAttr", Value = "42", DataType = "Int32" }],
|
||||
Scripts = [new ResolvedScript { CanonicalName = "S1", Code = "this is not C# ~~~" }]
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public async Task Deploy_WithNonCompilingScript_ReportsFailed_AndCreatesNoInstanceActor()
|
||||
{
|
||||
var actor = CreateDeploymentManager();
|
||||
await Task.Delay(500); // wait for empty startup
|
||||
|
||||
actor.Tell(new DeployInstanceCommand(
|
||||
"dep-bad", "bad-instance", "r1",
|
||||
MakeConfigWithBrokenScriptJson("bad-instance"), "admin", DateTimeOffset.UtcNow));
|
||||
|
||||
var resp = ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(10));
|
||||
Assert.Equal(DeploymentStatus.Failed, resp.Status);
|
||||
Assert.NotNull(resp.ErrorMessage);
|
||||
Assert.Contains("compil", resp.ErrorMessage!, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// No partial state applied: instance actor must not exist …
|
||||
Sys.ActorSelection(actor.Path / "bad-instance").Tell(new Identify(1));
|
||||
Assert.Null(ExpectMsg<ActorIdentity>().Subject);
|
||||
|
||||
// … and no config must have been persisted.
|
||||
var configs = await _storage.GetAllDeployedConfigsAsync();
|
||||
Assert.DoesNotContain(configs, c => c.InstanceUniqueName == "bad-instance");
|
||||
}
|
||||
|
||||
// ── M1.6: site event log `deployment` category ─────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user