merge(r2): r2-plan06
# Conflicts: # CLAUDE.md
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 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**.
|
||||
**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.
|
||||
|
||||
@@ -45,14 +45,12 @@ public interface IScriptArtifactChangeBus
|
||||
- `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
|
||||
## Handoff to plan 06 — dispositions
|
||||
|
||||
Plan 06 implements the consumer side and the remaining producers:
|
||||
|
||||
| Item | Responsibility |
|
||||
| Item | Disposition |
|
||||
|---|---|
|
||||
| **(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. |
|
||||
| **(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. |
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -118,13 +118,13 @@ The `sbk_<keyId>_<secret>` design already supports zero-downtime rotation — a
|
||||
|--------|-------------|---------|
|
||||
| `UNAUTHORIZED` | 401 | Missing/invalid API key (every auth-stage failure maps here — no stage leak). |
|
||||
| `NOT_APPROVED` | 403 | Method not found **or** key not in scope — indistinguishable by design. |
|
||||
| `UNSUPPORTED_MEDIA_TYPE` | 415 | Request carried a body with a non-JSON `Content-Type`. A body with **no** `Content-Type` is still parsed leniently as JSON. |
|
||||
| `UNSUPPORTED_MEDIA_TYPE` | 415 | Request carried a body — declared by `Content-Length` or `Transfer-Encoding` (chunked) — with a non-JSON `Content-Type`. A body with **no** `Content-Type` is still parsed leniently as JSON, and a chunked (no `Content-Length`) body is treated identically to a fixed-length one. |
|
||||
| `INVALID_JSON` | 400 | Request body was not valid JSON. |
|
||||
| `VALIDATION_FAILED` | 400 | Parameter validation failed (missing required field, wrong type, undeclared field). |
|
||||
| `BODY_TOO_LARGE` | 413 | Request body exceeded `MaxRequestBodyBytes`. |
|
||||
| `STANDBY_NODE` | 503 | Request hit a standby central node; the inbound API serves only the active node. |
|
||||
| `TIMEOUT` | 500 | The method script exceeded its execution timeout. |
|
||||
| `SITE_UNREACHABLE` | 500 | A routed `Route.To(...).Call(...)` could not reach the target site. |
|
||||
| `SITE_UNREACHABLE` | 500 | The target site did not respond to the routed request within the routing timeout (no contact / down / partitioned) — typed from the routed Ask expiring (`AskTimeoutException`), never inferred from an error-message substring. A `Success=false` response means the site answered and is classified `SCRIPT_ERROR`. |
|
||||
| `SCRIPT_COMPILE_FAILED` | 500 | The method's script failed to compile (returned consistently on every node until a compiling version is saved). |
|
||||
| `SCRIPT_ERROR` | 500 | Catch-all for any other script-execution failure (details are logged centrally, never leaked to the caller). |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user