5.4 KiB
Script-Artifact Invalidation Contract (ScriptArtifactsChanged)
Status: Seam + first producer shipped in PLAN-05 (T14). Consumer (a) shipped in PLAN-R2-06 T1 (ScriptArtifactChangeSubscriber). (b) Management producers remain unwired by design — delete calls InvalidateMethod directly; edits self-heal via the revision check. (c) is satisfied by the per-request revision check (PLAN-06 T1).
Problem. When a script-bearing artifact's definition changes at runtime — an Inbound API method's script, a shared script, or a template's scripts — any node that has already compiled and cached a handler/delegate for it keeps serving the stale version until a process restart. This is the root of two recorded project-memory gotchas (a DB-direct edit to a broken inbound method keeps returning "Script compilation failed" from _knownBadMethods; a non-compiling update leaves the old delegate serving). A bundle import is a third mutation path with the same hazard.
The fix is a small, explicit contract for "a script artifact changed → invalidate anything compiled from it," decoupled so producers (importer, management edits, failover) and consumers (compiled-handler caches) never reference each other.
The seam (Commons)
// Commons/Messages/Scripting/ScriptArtifactsChanged.cs
public sealed record ScriptArtifactsChanged(
string ArtifactKind, // ScriptArtifactKinds.ApiMethod | SharedScript | Template
IReadOnlyList<string> Names, // post-resolution (renamed) names
string Source, // "BundleImport" | "Management" | "FailoverActivation"
DateTimeOffset OccurredAtUtc);
public static class ScriptArtifactKinds
{
public const string ApiMethod = "ApiMethod";
public const string SharedScript = "SharedScript";
public const string Template = "Template";
}
// Commons/Interfaces/Scripting/IScriptArtifactChangeBus.cs
public interface IScriptArtifactChangeBus
{
void Publish(ScriptArtifactsChanged notification);
IDisposable Subscribe(Action<ScriptArtifactsChanged> handler);
}
Semantics (normative)
- Advisory, at-least-once. A notification is a hint to invalidate, not a command with delivery guarantees. Duplicates are allowed; over-notification (e.g. an
Overwriteresolution that actually fell through toAdd) is allowed and harmless. - Published only AFTER commit. A producer publishes after its mutating transaction commits — never inside it. A notification for a rolled-back write is worse than a missed one.
- Never faults the producer. The bus swallows-and-logs any subscriber-handler exception; the producer additionally guards its own publish call. A misbehaving consumer can never turn a committed write into a reported failure.
- In-process, per node.
InProcessScriptArtifactChangeBus(Host) is node-local: a lock-free copy-on-write subscriber list. It does not cross the cluster. Cross-node and failover freshness are the consumer's responsibility — every consumer MUST also self-heal without a notification (e.g. a content-hash / revision check on next use), because a notification published on node A does not reach node B, and a node that was down during the change never hears it. - Names are post-resolution.
Namescarries the artifact's persisted name after import name-resolution (aRenamecarriesRenameTo).
What plan 05 ships
- The Commons seam (record + kinds + interface).
InProcessScriptArtifactChangeBusin the Host, registered as a central-role singleton.- The Transport bundle importer as the first producer:
BundleImporter.ApplyAsynccollects the Overwrite/Rename resolutions forApiMethod/SharedScript/Templateand publishes oneScriptArtifactsChangedper kind aftertx.CommitAsync. The importer's dependency on the bus is optional (IScriptArtifactChangeBus?) so Transport-only hosts (and unit tests) run without it.
Handoff to plan 06 — dispositions
| Item | Disposition |
|---|---|
| (a) Inbound API consumer | Shipped (PLAN-R2-06 T1). ScriptArtifactChangeSubscriber (an InboundAPI IHostedService registered by AddInboundAPI) subscribes to the bus and, on an ApiMethod notification, calls InboundScriptExecutor.InvalidateMethod per name — evicting the named entries from _scriptHandlers and _knownBadMethods, closing the "DB-direct edit keeps returning compile error" and "non-compiling update keeps old handler" gotchas. |
| (b) Management producers | Unwired by design. The ManagementActor delete path calls InboundScriptExecutor.InvalidateMethod directly; the update/edit paths self-heal via the per-request revision check, so no notification is needed for correctness. Left unwired deliberately rather than publishing Source = "Management" notifications. |
| (c) Cross-node / failover freshness | Satisfied by the per-request revision check (PLAN-06 T1). The compiled handler is revision-checked against the freshly-fetched ApiMethod.Script on every request, so a node that missed the in-process notification still self-heals on next use, and a newly-active failover node never serves a stale compile. |
Consumer (a) is now wired, so plan 05's ApiMethod publisher reaches a real subscriber. SharedScript/Template notifications still publish with no consumer — that is safe: nothing at central caches compiled artifacts of those kinds, so there is nothing to invalidate. The seam is stable and the producer is correct.