Repository navigation
fix(self-update): establishing the policy is not the same as re-establishing it - #2935
Conversation
…lishing it memex-cloud served memex.meshweaver.cloud from rc8.ci.6829 while ACR had reached rc9.ci.7231 — about 400 builds behind, its pinned module set advanced past the image (modules 'contributing nothing'), memory at 89%, boot churn, and satellite CI gates failing on its bundle endpoints. Every trigger — startup, build completion, policy change, AND the safety net that exists to bound a dead event channel — is gated on the first emission of the live Admin/UpdatePolicy read. That gate is right: #2731/#2797 removed the synthetic default emission precisely because deciding under a guessed policy rolled pinned installs on every pod start. The cadence behind the gate was wrong. A faulted first read was paced by RetryInterval — six hours, documented for RE-establishing a watch that has already produced. Before the first read nothing is retained and no check can run, so that value does not pace anything: it takes the whole feature out, and the safety net cannot bound it because the safety net is behind the same gate. On a heavy instance whose boot-time SubscribeRequest exceeds its minute, that is six hours of an install that looks healthy and has stopped asking. Two pods of the SAME build proved the mechanism rather than suggesting it: the one logging 'policy stream faulted; re-establishing in 06:00:00' had run ZERO checks; its sibling, whose read succeeded, had run two. So the two states get two cadences, chosen in one place (SelfUpdateOptions.PolicyRetryDelay): PolicyEstablishRetryInterval (30 s) while no policy has ever been read, RetryInterval once one has — where the value is retained, checks continue, and a fault costs freshness rather than the feature. The warning now names which state it is in. 🚨 The pre-existing first-read test could not see this: it set RetryInterval itself to 500 ms, so it proved recovery HAPPENS while saying nothing about recovery happening usefully — the config-shaped guard that cannot observe the outcome. It now sets the establishing interval and leaves RetryInterval at its production default, and a second test pins the same fault shape with the real six-hour value, so the only thing that can make it pass is the fix. Mutation-proven: with PolicyRetryDelay reverted to RetryInterval behind a runtime-false condition, 3 tests fail (both new ones and the corrected pre-existing one); restored, 31/31 green. Verified: Release -warnaserror clean on MeshWeaver.Hosting and MeshWeaver.Hosting.Monolith.Test; SelfUpdate suite 31/31. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a self-update resilience gap where a faulted first Admin/UpdatePolicy read could effectively disable all self-update checks for the full RetryInterval (6 hours by default), by introducing a distinct, faster retry cadence specifically for the “policy establishing” state.
Changes:
- Added
SelfUpdateOptions.PolicyEstablishRetryIntervalplus a single decision point (PolicyRetryDelay) to select the correct retry delay based on whether a policy has ever been successfully read. - Updated
SelfUpdateHostedServiceto track when the first policy value has been read and to log/pace retries differently before vs. after establishment. - Strengthened tests to cover production cadence behavior (6h retry) and to pin the retry-delay decision logic, and added a “What’s New” entry for the user-visible impact.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/MeshWeaver.Hosting.Monolith.Test/SelfUpdatePollerResilienceTest.cs | Updates the first-read resilience test to avoid masking the defect and adds a production-cadence regression test plus a pure unit test for the delay decision. |
| src/MeshWeaver.Hosting/SelfUpdate/SelfUpdateOptions.cs | Introduces PolicyEstablishRetryInterval and PolicyRetryDelay(...) to centralize the two-cadence behavior. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-01-an-install-that-cannot-read-its-update-policy-no-longer-stops-updating.md | Documents the fix as a user-noticeable reliability improvement, including the new configuration knob. |
| memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateHostedService.cs | Tracks “policy established” state and uses it to choose retry delay and improve warning clarity during faults. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 0)793 tests 789 ✅ 3m 20s ⏱️ Results for commit 792acf0. |
Test Results (shard 1)1 138 tests 1 138 ✅ 3m 53s ⏱️ Results for commit 792acf0. |
Test Results (shard 5) 8 files 8 suites 4m 13s ⏱️ Results for commit 792acf0. |
Test Results (shard 4)1 165 tests 1 165 ✅ 4m 35s ⏱️ Results for commit 792acf0. |
Test Results (shard 3)1 152 tests 1 152 ✅ 7m 59s ⏱️ Results for commit 792acf0. |
Test Results (shard 2)3 059 tests 2 867 ✅ 6m 31s ⏱️ Results for commit 792acf0. |
Test Results 44 files 44 suites 30m 33s ⏱️ Results for commit 792acf0. |
The incident. memex-cloud served
memex.meshweaver.cloudfrommemex-portal-ai:3.0.0-rc8.ci.6829while ACR had reachedrc9.ci.7231— roughly 400 builds behind. Its pinned module set had advanced past the image, so modules reported "CONTRIBUTING NOTHING", new pods failedrequired_modulesat the startup probe and crash-looped (140 restarts), memory sat at 89%, and other repos' CI gates failed against its bundle endpoints. Systemorph/Memex#154. The environment is rolled and healthy again; this is the reason it got there.The defect. Every self-update trigger — startup, build completion, policy change, and the safety net that exists to bound a dead event channel — is gated on the first emission of the live
Admin/UpdatePolicyread. That gate is correct and deliberate (#2731/#2797 removed the synthetic default emission because deciding under a guessed policy rolled pinned installs on every pod start).The cadence behind it was wrong. A faulted first read was paced by
RetryInterval— six hours, documented for re-establishing a watch that has already produced. Before the first read nothing is retained and no check can run, so that value does not pace anything; it removes the feature, and the safety net cannot bound it because the safety net sits behind the same gate. On a heavy instance whose boot-timeSubscribeRequestexceeds its minute, that is six hours of an install that looks healthy and has silently stopped asking.Measured, not inferred. Two pods of the same build, same instance:
…-q5zcmpolicy stream faulted; re-establishing in 06:00:00…-7sqbkThe fix. Two states, two cadences, chosen in one place (
SelfUpdateOptions.PolicyRetryDelay):PolicyEstablishRetryInterval(30 s) while no policy has ever been read;RetryIntervalonce one has — where the value is retained, checks continue, and a fault costs freshness rather than the feature. The warning now names which state it is in.🚨 Why the existing test could not catch this: it set
RetryIntervalitself to 500 ms, so it proved recovery happens while saying nothing about recovery happening usefully — a guard whose knob hides the outcome it is meant to observe. It now sets the establishing interval and leavesRetryIntervalat its production default; a second test pins the same fault shape with the real six-hour value, so only the fix can make it pass.Mutation-proven:
PolicyRetryDelayreverted toRetryIntervalbehind a runtime-false condition fails 3 tests (both new, plus the corrected pre-existing one); restored, 31/31. Release-warnaserrorclean on MeshWeaver.Hosting and MeshWeaver.Hosting.Monolith.Test.🤖 Generated with Claude Code