fix(management): compile an API method once per save — CompileForPublish replaces the validate pre-pass (review 2026-10-21 05/M1)
This commit is contained in:
@@ -194,6 +194,31 @@ public class InboundScriptExecutor
|
||||
/// <returns>True if successfully compiled and registered, false otherwise.</returns>
|
||||
public bool CompileAndRegister(ApiMethod method, out IReadOnlyList<string> errors)
|
||||
{
|
||||
var compiled = CompileForPublish(method, out errors, out var publish);
|
||||
publish();
|
||||
return compiled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles an API method script ONCE and returns the diagnostics now, together with a
|
||||
/// <paramref name="publish"/> action that applies the outcome to the handler cache later
|
||||
/// without recompiling (review 2026-10-21 05/M1). A transactional caller (the management
|
||||
/// Create/Update path) derives its "saved but did not compile" warning before commit and
|
||||
/// queues <paramref name="publish"/> as a post-commit action; each compile pins a
|
||||
/// non-collectible script assembly, so compiling separately for the warning and for the
|
||||
/// registration doubled that cost per save. On success <paramref name="publish"/>
|
||||
/// registers the handler for the compiled script text and clears any known-bad record;
|
||||
/// on failure it records the known-bad method and leaves any previously registered
|
||||
/// handler in place (see <see cref="CompileAndRegister(ApiMethod, out IReadOnlyList{string})"/>).
|
||||
/// </summary>
|
||||
/// <param name="method">The API method to compile.</param>
|
||||
/// <param name="errors">The compilation/trust-model error messages; empty on success.</param>
|
||||
/// <param name="publish">Applies the compile outcome to the handler cache; idempotent.</param>
|
||||
/// <returns><c>true</c> when the script compiled; otherwise <c>false</c>.</returns>
|
||||
public bool CompileForPublish(ApiMethod method, out IReadOnlyList<string> errors, out Action publish)
|
||||
{
|
||||
var name = method.Name;
|
||||
var script = method.Script;
|
||||
var (handler, compileErrors) = CompileSingleFlight(method);
|
||||
if (handler == null)
|
||||
{
|
||||
@@ -202,15 +227,19 @@ public class InboundScriptExecutor
|
||||
// through the capped TryRecordBadMethod helper so the cache
|
||||
// cannot grow without bound under a flood of unique method names.
|
||||
errors = compileErrors;
|
||||
TryRecordBadMethod(method.Name, method.Script ?? string.Empty);
|
||||
publish = () => TryRecordBadMethod(name, script ?? string.Empty);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The method definition was (re)compiled successfully — drop any stale
|
||||
// failure record so a fixed script is no longer treated as bad.
|
||||
errors = Array.Empty<string>();
|
||||
_knownBadMethods.TryRemove(method.Name, out _);
|
||||
return Register(method, handler);
|
||||
publish = () =>
|
||||
{
|
||||
_knownBadMethods.TryRemove(name, out _);
|
||||
_scriptHandlers[name] = new CachedHandler(script, handler);
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -235,12 +264,6 @@ public class InboundScriptExecutor
|
||||
}
|
||||
}
|
||||
|
||||
private bool Register(ApiMethod method, Func<InboundScriptContext, Task<object?>> handler)
|
||||
{
|
||||
_scriptHandlers[method.Name] = new CachedHandler(method.Script, handler);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles a single API method script into an executable handler. Returns
|
||||
/// <c>null</c> when the script is missing, fails to compile, or violates the
|
||||
|
||||
@@ -3667,12 +3667,16 @@ public class ManagementActor : ReceiveActor
|
||||
var executor = sp.GetService<InboundAPI.InboundScriptExecutor>();
|
||||
if (executor is null)
|
||||
return null;
|
||||
var compiled = executor.ValidateCompilation(method, out var errors);
|
||||
// ONE compile (review 2026-10-21 05/M1): each compile pins a non-collectible script
|
||||
// assembly, so the diagnostics for the warning and the handler registration come
|
||||
// from the same compile. Registration still waits for the commit when the command
|
||||
// runs inside a configuration transaction.
|
||||
var compiled = executor.CompileForPublish(method, out var errors, out var publish);
|
||||
var actions = sp.GetService<IConfigurationCommitActions>();
|
||||
if (actions is not null)
|
||||
actions.Add(() => { executor.CompileAndRegister(method); return Task.CompletedTask; });
|
||||
actions.Add(() => { publish(); return Task.CompletedTask; });
|
||||
else
|
||||
executor.CompileAndRegister(method);
|
||||
publish();
|
||||
if (compiled)
|
||||
return null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user