feat(commons/transport): ScriptArtifactsChanged invalidation contract + post-commit publish from bundle import (consumer lands in plan 06)
New Commons seam: ScriptArtifactsChanged record + ScriptArtifactKinds + the IScriptArtifactChangeBus pub/sub interface. Host ships InProcessScriptArtifactChangeBus (lock-free copy-on-write, swallow-and-log per subscriber) registered as a central-role singleton. BundleImporter gains an optional IScriptArtifactChangeBus? and, AFTER tx.CommitAsync, publishes one notification per script-bearing kind (ApiMethod/ SharedScript/Template) whose resolution was Overwrite/Rename, using post-resolution names — fully guarded so a bad subscriber can't fail a committed import. Contract + plan-06 handoff table documented. No consumer yet (additive; publisher is correct). Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# Script-Artifact Invalidation Contract (`ScriptArtifactsChanged`)
|
||||
|
||||
**Status:** Seam + first producer shipped in PLAN-05 (arch-review 05, Task 14). Consumer + additional producers land in **plan 06**.
|
||||
|
||||
**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)
|
||||
|
||||
```csharp
|
||||
// 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)
|
||||
|
||||
1. **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 `Overwrite` resolution that actually fell through to `Add`) is allowed and harmless.
|
||||
2. **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.
|
||||
3. **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.
|
||||
4. **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.
|
||||
5. **Names are post-resolution.** `Names` carries the artifact's persisted name after import name-resolution (a `Rename` carries `RenameTo`).
|
||||
|
||||
## What plan 05 ships
|
||||
|
||||
- The Commons seam (record + kinds + interface).
|
||||
- `InProcessScriptArtifactChangeBus` in the Host, registered as a central-role singleton.
|
||||
- The **Transport bundle importer as the first producer**: `BundleImporter.ApplyAsync` collects the Overwrite/Rename resolutions for `ApiMethod` / `SharedScript` / `Template` and publishes one `ScriptArtifactsChanged` per kind **after** `tx.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
|
||||
|
||||
Plan 06 implements the consumer side and the remaining producers:
|
||||
|
||||
| Item | Responsibility |
|
||||
|---|---|
|
||||
| **(a) Inbound API consumer** | `InboundScriptExecutor` subscribes to the bus and, on an `ApiMethod` notification, evicts 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** | Publish `ScriptArtifactsChanged` from the `ManagementActor` `UpdateApiMethod` / shared-script / template-script edit paths (`Source = "Management"`), so a UI/CLI edit invalidates the same caches a bundle import does. |
|
||||
| **(c) Cross-node / failover freshness** | A revision-keyed handler cache (compile keyed by code hash — see PLAN-05 Task 15's `ScriptCompileVerdictCache`) 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. |
|
||||
|
||||
Until (a)–(c) land, plan 05's publisher is a no-op in practice (no subscribers), but the seam is stable and the producer is correct, so plan 06 is purely additive.
|
||||
Reference in New Issue
Block a user