From 92d1df88f4c8c2fe502063502727ea5c40f3af87 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 7 Jun 2026 15:40:06 -0400 Subject: [PATCH] fix(deploy): guardrail estimate is best-effort, never blocks a valid deploy Wrap the script compile-cost guardrail block in its own inner try/catch so a transient SQL failure on ToListAsync cannot fall through to the outer catch and produce a Rejected reply for an otherwise-valid deploy. advisory is declared in the outer scope so the Accepted StartDeploymentResult Message is unaffected on the happy path; the inner catch logs a Warning and leaves advisory null. --- .../AdminOperations/AdminOperationsActor.cs | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs index 786fbc4f..8365739a 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs @@ -112,20 +112,32 @@ public sealed class AdminOperationsActor : ReceiveActor // sources are counted (the compile cache keys on source, so duplicates collapse to one unit). // This only surfaces the estimate to the operator — the DraftValidator gate above is the hard // reject; this advisory rides in the Accepted Message so the UI can show it. - const double PerScriptMiB = 1.66; - var scriptSources = await db.Scripts.AsNoTracking().Select(s => s.SourceCode).ToListAsync(); - var compiled = scriptSources - .Where(src => !PassthroughScript.TryMatch(src, out _)) - .Distinct(StringComparer.Ordinal) - .Count(); + // advisory is declared outside the inner try so the Accepted reply below can still read it even + // when the guardrail query throws (e.g. transient SQL). A failed estimate must NEVER block a + // valid deploy — the outer catch is reserved for genuine seal/save failures. string? advisory = null; - if (compiled > 0) + try { - var estMiB = compiled * PerScriptMiB; - _log.Warning( - "StartDeployment: {Compiled} script(s) will compile (~{EstMiB:F0} MiB RSS per node); ensure node mem_limit covers it", - compiled, estMiB); - advisory = $"{compiled} script(s) will compile (~{estMiB:F0} MiB/node)"; + const double PerScriptMiB = 1.66; // measured post-A0 per-script RSS (design doc 2026-06-07) + var scriptSources = await db.Scripts.AsNoTracking().Select(s => s.SourceCode).ToListAsync(); + var compiled = scriptSources + .Where(src => !PassthroughScript.TryMatch(src, out _)) + .Distinct(StringComparer.Ordinal) + .Count(); + if (compiled > 0) + { + var estMiB = compiled * PerScriptMiB; + _log.Warning( + "StartDeployment: {Compiled} script(s) will compile (~{EstMiB:F0} MiB RSS per node); ensure node mem_limit covers it", + compiled, estMiB); + advisory = $"{compiled} script(s) will compile (~{estMiB:F0} MiB/node)"; + } + } + catch (Exception ex) + { + // Guardrail is advisory-only — a failed estimate must never block a valid deploy. + _log.Warning(ex, "StartDeployment: script compile-cost estimate failed; advisory skipped"); + advisory = null; } var artifact = await ConfigComposer.SnapshotAndFlattenAsync(db);