Repository navigation
fix(hosting): a boot read of a built-but-unregistered type is typed, never untyped (Plugins#2799) - #6411
Conversation
…d is typed, never untyped (Plugins#2799) Boot-time readers (the process's own hosted work: Posts query, Ops/Status watches, install records) read dynamic content before the registration-only pass may run, and got untyped JsonElements (~130 per boot). The stream cache's GetStream/GetQuery seams now wait for ContentTypeOnDemandRegistration, which runs the pass's own non-compiling, non-writing route for that one type (coalesced per type), then type the emission. Amends policy dynamic-content-type-registration-pass accordingly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Exact-path query staleness can cache an incorrect verdict, while concurrent registration can exhaust the filesystem pool.
2 open findings
What changed in this PR
Ensures boot-time reads can register baked dynamic content types on demand and return typed content immediately.
Changes:
- Adds process-scoped on-demand type registration.
- Integrates registration into stream and query read seams.
- Adds integration tests and updates architecture policy/documentation.
| File | Description |
|---|---|
test/MeshWeaver.Hosting.Test/AReadAtBootIsTypedNotUntypedTest.cs |
Tests typed boot reads and no-build behavior. |
src/MeshWeaver.Hosting/MeshNodeStreamCache.cs |
Adds on-demand typing to streams and queries. |
src/MeshWeaver.Hosting/DynamicTypePreWarmerHostedService.cs |
Registers the new service. |
src/MeshWeaver.Hosting/DynamicContentTypeRegistrar.cs |
Exposes single-type registration. |
src/MeshWeaver.Hosting/ContentTypeOnDemandRegistration.cs |
Implements coalesced registration attempts. |
src/MeshWeaver.Hosting/ContentDegradationRegistry.cs |
Updates degradation guidance. |
src/MeshWeaver.Graph/ContentTypeRegistrationSweep.cs |
Documents permitted read-seam registration. |
src/MeshWeaver.Documentation/Data/Architecture/PolicyNotProse.md |
Amends registration policy. |
src/MeshWeaver.Documentation/Data/Architecture/DynamicContentTypeRegistration.md |
Documents behavior and verification. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| () => meshService | ||
| .Query<MeshNode>(MeshQueryRequest.FromQuery($"path:{path}")) | ||
| .Take(1) | ||
| .Timeout(RecordReadBudget)) |
There was a problem hiding this comment.
Agreed, fixed in 0b84fbc. The record is now read from the STORE (IStorageAdapter.Read), which is the authority, never from a query, so a stale or incomplete snapshot can no longer be cached as a NotBaked verdict. It is also never read through the NodeType's own stream, whose first touch can drive the compiling enrichment path. A null from the store means the record genuinely does not exist, and that is a verdict. A fault or an expired RecordReadBudget is Faulted, which is released, so the next read retries.
| return awaited.IsEmpty | ||
| ? Observable.Return(Final()) | ||
| : awaited.Select(nodeType => ensureRegistered(nodeType).Take(1)) | ||
| .Merge() | ||
| .ToList() | ||
| .Select(_ => Final()); |
There was a problem hiding this comment.
Agreed, fixed in 0b84fbc. The distinct unresolved NodeTypes of one result set are now registered one at a time, in ordinal order (OrderBy + Concat), so a broad query never fans out concurrent assembly loads on the FileSystem pool. The emission still waits for the whole set, and each type's attempt stays coalesced across readers by the PromiseCache.
…ype at a time (review on #6411) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Test Results 14 files - 10 14 suites - 10 41m 32s ⏱️ - 7m 47s Results for commit 0b84fbc. ± Comparison against base commit 9809c5e. This pull request removes 1501 and adds 10 tests. Note that renamed tests count towards both. |


Refs Systemorph/MeshWeaver.Plugins#2799 · follows #6405
Root cause
At pod boot, reads of dynamic content happen before those types are registered on the replica. On memex-cloud pod
884964bb7-6gv59, 132 of 134stayed an untyped JsonElementlines were written at 19:48:37.3. The pre-warmer only started at 19:48:37.8, and the registration-only pass finished at 19:52:50. The readers are the process's own boot work, not traffic: aPostsquery, the standing watches onOps/Status/*andOps/Watch/fleet-watch, and, on the current 3.1.10365 generation, the*/_Install/*install records (Ops/Actions/sevm-20261010-2799-bootorder-cloudon control). Holding readiness back would not reach these readers, because they run beforeApplicationStarted. Moving the pass ahead of the barrier would bring back the 414-type boot cost that #1660 removed. #6405 only deferred the warning. A query read in that window stayed untyped until the next change to it, because a query has no late re-type.Fix
MeshNodeStreamCache.GetStream(path, options)andGetQuery(id, options, …)now handle content that stays untyped and names a NodeType differently: the emission waits forContentTypeOnDemandRegistration.EnsureRegistered(nodeType)and is typed after that.EnsureRegisteredreads the type's record as system, with the same 30 s budget the pass uses for its enumeration. It then runsDynamicContentTypeRegistrar.RegisterType, which is the pass's own route for one type: the record's claimed build, identity-checked existing bytes (refetched from the shipped bundle when they are missing), a configuration load from the existing assembly, and one transient probe. It never compiles and never writes the record. A type with no usable build answersNotBakedat once and the read degrades as before.PromiseCache, on the FileSystem IO pool. Only aFaultedattempt is released for retry. Order is preserved (Concat). Content that types cleanly is answered at once.AddDynamicTypePreWarming. Hosts without it, including test hosts, keep the old behaviour. Boot untyped-content warnings settle with the registration pass; a closed type set fetches no bundle #6405's deferred warning stays as the diagnostic for anything still untyped.dynamic-content-type-registration-pass, which said "Never from a read seam". The register row now says: never the enrichment (compiling) path from a read seam; the registration-only route may run on demand for the one type being read. This was the maintainer's directive for ci: Build and Test fires on merge_group, so a merge queue can be enabled (#2412) #2799, option (c). Flagged here so it can be vetoed.Tests
AReadAtBootIsTypedNotUntypedTest(MeshWeaver.Hosting.Test). Setup: the type is baked without hub activation, and the instance is written straight to the store, so nothing registers the type as a side effect.JsonElement.NotBakedand nothing gets compiled.Local runs, Release:
-warnaserrorbuilds of Hosting, Graph, Hosting.Test and Documentation.Test are clean.Deploy
No recycle needed: this takes effect at process start. To verify after the next roll of memex-cloud and memex, count
|~ "(?i)stayed an untyped JsonElement"in each replica's boot window. Before this change it was about 130 per boot; it should now be about 0. Each type a boot reader needed should log oneContentTypeOnDemandRegistration: <type> → Registeredline.Pairs-with: none — adds a public type and internal members only; removes nothing public.
Implementers: none — no interface member added.
Mirror-sync: none — no i18n catalog change.
🤖 Generated with Claude Code