Repository navigation
fix(create): the post-creation verdict names what the leg waits on, and is the only verdict - #6455
Conversation
…nd is the only verdict The boot-time stalls read "<path> was written, but its post-creation handlers had not finished within 30s" for creates no registered handler applies to. For such a node the leg only resolves the handler registrations and asks each one's Matches, and a handler a module registered is a forwarding proxy whose every call first resolves the module's instance from the module's own container. - The leg reports its stage as it enters each step: resolving, matching <handler> (i/n), <handler> (i/n), <handler> (i/n): additional nodes. On the verdict, the log line and the fate trail. - A module's proxy is named without resolving it (ModuleServiceProxy.Label): registered type or interface, module, registration index. The validators stage uses the same label. - The deadline claims the answer before it writes the line, so 'outcome unknown' is never logged for a create a finishing handler chain answered Ok. - The chain's own deadline is no verdict once the node was emitted. The leg runs inside the chain's OnNext, so a leg that returned late was followed by a second Error line, "stalled at stage write", for a create already written and answered. Repro: a real module in its own container, one singleton's construction parked, a create of an unrelated type. Fails on the code before (the line names nothing, the second line is written). Control: nothing parked, the first create succeeds. Refs #6391 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Additional-node discovery can still be reported under the wrong stage, and central verdict behavior lacks accurate documentation and race coverage.
4 open findings
What changed in this PR
Improves stalled post-creation diagnostics and ensures each create produces one consistent verdict.
Changes:
- Tracks the exact post-creation stage and labels module proxies without resolving them.
- Prevents duplicate or contradictory deadline verdicts.
- Adds integration coverage, localized messages, and operational documentation.
| File | Description |
|---|---|
src/MeshWeaver.Mesh.Contract/MeshExtensions.cs |
Refines stages and deadline verdict handling. |
src/MeshWeaver.Mesh.Contract/ModuleServiceForwarders.cs |
Adds non-resolving proxy labels. |
src/MeshWeaver.Messaging.Hub/Localization/strings.en.json |
Adds stage detail to English verdict. |
src/MeshWeaver.Messaging.Hub/Localization/strings.de.json |
Adds stage detail to German verdict. |
test/MeshWeaver.Hosting.Test/LifecycleRequestsAlwaysReachAVerdictTest.cs |
Tests stalled post-creation handlers. |
test/MeshWeaver.Compiler.Pipeline.Test/ACreateWaitsOnAModuleContainerUnderConstructionTest.cs |
Reproduces module-container stalls. |
src/MeshWeaver.Documentation/Data/Architecture/WriteVerdictTotality.md |
Documents stages and verdict semantics. |
src/MeshWeaver.Documentation/Data/Architecture/ReadingASiloEviction.md |
Updates operational diagnosis guidance. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // 🚨 CLAIMED FIRST, then said. This used to READ the gate, write the Error | ||
| // line and only then claim through Respond — so a handler chain finishing | ||
| // in between answered `Ok` under a line that says 'outcome unknown'. The | ||
| // line is the instrument this failure is read through, so it is written | ||
| // only for a verdict that IS the create's answer. | ||
| if (!TryClaimResponse()) |
There was a problem hiding this comment.
Agreed, the first push did not pin the ordering on purpose. Added PostCreationVerdictIsSaidOnceTest (MeshWeaver.Hosting.Test) in 5825364. It parks the leg inside a handler, releases it from INSIDE the write of the Error line (the point between the old gate read and the old claim) and gives it 3 s to finish and answer before the write returns. The reply must be the line's verdict.
Measured on main's src/ with the test in place: it fails with LINE: … answering 'outcome unknown' and REPLY: success=True. With the claim made first it passes: the reply is Unavailable and carries the same stage as the line.
…e; pin the claim-first order Review round on the post-creation verdict: - GetAdditionalNodes ran, and its result was enumerated, while the stage still named the handler's Handle. It is now `<handler> (i/n): discovering additional nodes`, the result is materialised once inside the try, and the stage returns to the handler when Handle is subscribed. - PostCreationVerdictIsSaidOnceTest makes the claim-before-log window deterministic: it releases a parked leg from inside the write of the Error line and gives it time to answer. The reply is the line's verdict. On main's order it is Ok under a line saying 'outcome unknown' (measured). - Docs: after the release the parked create carries the same Unavailable verdict and only the next create is Ok; `resolving` names no module container, only `matching … [module …]` does. Refs #6391 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n-stage-names-handler
…y's label follows a live swap Review round 2: - The deadline builds its verdict, claims, POSTS, and only then writes the trail and the Error line. Once the gate is claimed every later Respond is refused, so nothing may stand between the claim and the post. - ModuleServiceProxy's label is read from the module's CURRENT generation's registration on every call instead of being remembered at boot: the proxy outlives a live swap, which keeps route, service type and lifetime but may change the implementing class. The Create overload added for the remembered name is gone again. AProxysLabel_NamesTheCurrentGenerationsImplementation_AfterALiveSwap pins it; on the remembered name it reads "Greeter" after a swap to "RenamedGreeter" (measured). Refs #6391 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n-stage-names-handler
| // "reached no verdict — stalled at stage write": a second Error line, for a | ||
| // create that was written and already answered, naming a stage that had | ||
| // finished. Nothing is owed here: the row is written and the answer was given. | ||
| if (ex is LifecycleVerdictStalledException && System.Threading.Volatile.Read(ref emitted)) |
There was a problem hiding this comment.
Not changing this, because in the case described the answer is still owned, and gating on responded would bring back the false verdict.
The branch does not cancel anything. Once emitted is set, the same OnNext arms the post-creation deadline (Observable.Timer(deadline).Subscribe(...)) BEFORE it subscribes the handler leg, and that timer is disposed in exactly one place: the leg's Finally, i.e. when the leg has terminated. Each terminal of the leg answers: completion posts Ok, a fault runs the compensation and posts its failure. So when the outer fallback is delivered after the callback returns with responded == 0, one of two things is true:
- the leg is still pending asynchronously, and the post-creation timer is armed. Its due time is the same absolute deadline, already past, so it fires as soon as its scheduler runs it, and it posts the verdict that names the step;
- or the leg terminated inside the callback, in which case it already claimed the gate.
There is no path where this fallback is the sole remaining answer. If it answered whenever responded is still 0, it would win exactly that race and say reached no verdict — stalled at stage write for a row that is written, which is the second, wrong verdict this branch exists to remove (ACreateWaitsOnAModuleContainerUnderConstructionTest asserts one Error line for the create). A delayed timer delays the correct verdict by the same delay; it does not lose it.
Test Results 20 files - 4 20 suites - 4 54m 30s ⏱️ + 1m 11s Results for commit f921766. ± Comparison against base commit 5467df1. This pull request removes 335 and adds 22 tests. Note that renamed tests count towards both. |



Refs #6391
What the stall is
With #6421 live, a reading on 2026-10-10 found 18 stalls on memex-cloud in 14 h, all in the first minutes of a pod's first process, all reading
<path> was written, but its post-creation handlers had not finished within 30s. The targets are System-written creates:Admin/PlatformVersion/_Activity/startup-…, thenAdmin/_Notification/…about every 70 s.Read for one of those pods (
memex-portal-deployment-5499f76b76-ltlwc, governed read-onlyLogsactions on control, listed below):CREATE_STAGE write (+311ms) → CREATE_CHAIN_EMITTED (+365ms) → CREATE_POST_HANDLERS_START (+365ms)and nothing after it. The node-operation hub's pump is idle.Application started. The replacing one logged it 40 ms after its boot create.No registered handler applies to these node types: the four in
src/are forUser,Space, the sync config and non-static partition roots, and the three MeshWeaver.AI registers are all forUser. For a node no handler applies to, the post-creation leg does two things only. It resolves everyINodePostCreationHandlerregistration, and it asks each one'sMatches. A handler a module registered is a forwarding proxy (ModuleServiceProxy), and every call on it,Matchesincluded, first resolves the module's instance from the module's own container. So every create in the mesh, whatever its type, waits on every module's container until that module's handler exists.The repro below produces the production line byte for byte on the code before this change.
What changes
post-creation-handlers: resolving,… matching <handler> (i/n),… <handler> (i/n),… <handler> (i/n): additional nodes.ModuleServiceProxy.Label):<registered type or interface> [module <name>, registration <index>]. Naming it by resolving it would wait on the same container. The validators stage (fix(create): a stalled validators stage names the validator it waits on (#6391) #6421) uses the same label, so a validator a module contributes is no longer named by a generated proxy class.Okunder a line saying 'outcome unknown'.OnNext, and Rx'sTimeoutholds an expiry back while anOnNextis in flight. A leg that returned late was therefore followed by a second Error line,reached no verdict — stalled at stage write, for a create that was written and already answered. It is now a trail entry (CREATE_CHAIN_DEADLINE_AFTER_EMIT).The 30 s budget is unchanged. No timeout, retry or bound was touched.
What this does not establish
What holds a module's container during those boots. No log line carries a thread's stack, and the
AnalyzeDumpaction that reads the dump those processes wrote is not on the control instance yet (its content schema refusesdumpPod). The same pods are the ones described in "a silo wedged from boot" (see #6432).That the production wait is the resolve-or-match step is an inference by elimination: no handler applies, so nothing asynchronous is left to wait for. The refined stage on the next occurrence confirms or refutes it in one line and names the module. A change that lets the create answer without the module's handler would decide
Matcheson the handler's behalf, so none is made here.Tests
ACreateWaitsOnAModuleContainerUnderConstructionTest(MeshWeaver.Compiler.Pipeline.Test). A real module compiled and installed into its own container. The construction of one of its singletons is parked, then a node of an unrelated type is created as System. The row is written, the line readspost-creation-handlers: matching INodePostCreationHandler [module MeshWeaver.Test.HeldContainer, registration 0] (4/4), the module's handler was never built, exactly one Error line is written for the create, and after the release the reply carries the same verdict and the next create is answeredOk.src/: the test fails. The line reads… was written, but its post-creation handlers had not finished within 6s — answering 'outcome unknown', andreached no verdict within 6s — stalled at stage writefollows it.LifecycleRequestsAlwaysReachAVerdictTest(MeshWeaver.Hosting.Test): a handler that never completes is named in the verdict (post-creation-handlers: SilentPostCreationHandler (), and the same handler armed for another path lets the create through.Run locally on the final tree: Hosting.Test 14 of 14 for the four verdict classes, Graph.Test 10 of 10 for the post-creation classes,
LocalizationTest66 of 66, MeshWeaver.Documentation.Test 815 of 815. Release-warnaserroris clean for Mesh.Contract, Messaging.Hub, Documentation, Graph and the four test projects, one project per invocation.ModuleRootServicesSwapTest.AModuleWithRootServices_SwapsLive_AndEveryConsumerFollowsTheNewGenerationfails in a local Debug run with and without this change (1 context(s) RETAINED), measured on the unchangedsrc/in the same run as the negative control.Docs
Doc/Architecture/WriteVerdictTotalitycarries the readings, the stage table, the two corrections and what is not established.Doc/Architecture/ReadingASiloEvictionadds two readings to the boot fingerprint: the missingApplication started, and what the create line says from this image on.Deploy
Reaches a portal with its image. No address needs a recycle. The log line keeps its opening words, so
post-creation handlers had not finishedstill finds it. After the roll, the next boot-time occurrence on memex-cloud is read for itswaiting atclause.Mirror-sync: MeshWeaver.Plugins runs
npm run sync:i18n -- --ref <merge sha>after this lands — one re-worded value,activity.node.create.postCreationOutcomeUnknowngains{stage}in en and de; no key is added.Governed reads behind the readings (control, read-only
Logs)Ops/Actions/logs-memexcloud-20261010-w14b-boot-ltlwcand…-ltlwc-2through…-ltlwc-7.🤖 Generated with Claude Code