fix(scriptanalysis): warn on scripts blocking on a Task (.Result/.Wait()/GetAwaiter().GetResult()) (review 2026-10-21 01/SR-U2)
Advisory, not a trust-policy violation: every policy entry blocks deploy, and an error here would fail the next redeploy of already-deployed scripts. ValidationService surfaces it as a non-blocking ScriptCompilation warning.
This commit is contained in:
@@ -99,4 +99,40 @@ public class ScriptCompiler
|
||||
? Result<bool>.Success(true)
|
||||
: Result<bool>.Failure($"Script '{scriptName}' {error}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Non-blocking advisories for a script that already compiles — today, synchronous
|
||||
/// blocking on a <see cref="System.Threading.Tasks.Task"/> (<c>.Result</c>,
|
||||
/// <c>.Wait()</c>, <c>.GetAwaiter().GetResult()</c>), which can starve the site's
|
||||
/// script-execution pool (review 2026-10-21 01/SR-U2). Memoised process-wide like
|
||||
/// the compile verdict, under its own surface key. A faulting analysis yields no
|
||||
/// advisories (and is not cached) — an advisory must never fail validation.
|
||||
/// </summary>
|
||||
/// <param name="code">The C# script code.</param>
|
||||
/// <returns>Advisory messages; empty when there is nothing to flag.</returns>
|
||||
public IReadOnlyList<string> FindAdvisories(string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
return [];
|
||||
|
||||
try
|
||||
{
|
||||
var (_, joined) = ScriptCompileVerdictCache.GetOrAdd(AdvisorySurface, code, () =>
|
||||
{
|
||||
var advisories = BlockingWaitAnalyzer.Find(code, typeof(ScriptCompileSurface));
|
||||
return (true, advisories.Count == 0 ? null : string.Join(AdvisorySeparator, advisories));
|
||||
});
|
||||
return joined == null ? [] : joined.Split(AdvisorySeparator);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cache surface key for the advisory pass (distinct from the compile verdict).</summary>
|
||||
private const string AdvisorySurface = nameof(ScriptCompileSurface) + "/advisories";
|
||||
|
||||
/// <summary>Separator for the advisories packed into one cached string.</summary>
|
||||
private const string AdvisorySeparator = "\n";
|
||||
}
|
||||
|
||||
@@ -269,7 +269,8 @@ public class ValidationService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that all scripts compile successfully using the ScriptCompiler.
|
||||
/// Validates that all scripts compile successfully using the ScriptCompiler, and adds
|
||||
/// non-blocking warnings for compiled scripts that block synchronously on a Task.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration to validate.</param>
|
||||
/// <returns>A <see cref="ValidationResult"/> with errors for each script that fails compilation.</returns>
|
||||
@@ -286,6 +287,16 @@ public class ValidationService
|
||||
errors.Add(ValidationEntry.Error(ValidationCategory.ScriptCompilation,
|
||||
$"Script '{script.CanonicalName}' failed compilation: {result.Error}",
|
||||
script.CanonicalName));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Advisories never gate deploy (review 2026-10-21 01/SR-U2): a script that
|
||||
// blocks on a Task still deploys, but the operator sees why it is risky.
|
||||
foreach (var advisory in _scriptCompiler.FindAdvisories(script.Code))
|
||||
{
|
||||
warnings.Add(ValidationEntry.Warning(ValidationCategory.ScriptCompilation,
|
||||
$"Script '{script.CanonicalName}' {advisory}",
|
||||
script.CanonicalName));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user