Skip to content

Self-update check runs on every build completion on every replica under UpdatePolicy=None, and the replicas fight over Admin/UpdatePolicy.lastCheckedAt (158 checks, 14 refused writes, 2 queue failures in 5 h) #3790

Closed

Description

@rbuergi

What happened

memex.systemorph.com, 2026-09-09 01:30–06:36Z (Loki, both replicas, rbuergi/Script/loki-memex-bundles on memex-cloud):

  • [SelfUpdate] check (BuildCompletion): updates are disabled on this install (Admin/UpdatePolicy = None); the registry was not listed. — 158 times in 5 h, i.e. every platform-build completion event wakes the self-update check on EVERY replica, which then only discovers the policy is None and writes its lastCheckedAt.
  • That write goes cross-hub from each replica's cache hub to the owner of Admin/UpdatePolicy, and the two replicas race on the same leaf: [MergeGuard] Admin/UpdatePolicy: refused stale/reordered cross-hub write to 'lastCheckedAt' ×14, [UpdateRemote] OWNER_NACK_REENQUEUE … code=Conflict ×10, STALE_MIRROR … rebuilding the patch ×5, [UpdateQueue] ADVANCE_WITHOUT_HANDOFF … bound=5000ms ×3, and twice [UpdateQueue] FAILED path=Admin/UpdatePolicy seq=576 elapsedMs=10015 with a MeshNodeStreamException: MeshNode Conflict … PARTIALLY refused.

Nothing user-visible breaks — the refused field is a timestamp — but it is a steady conflict/retry storm on an Admin node, 10 s stalls in the update queue of the cache hub that also carries user writes, and log volume that buries the lines that matter (the same window's real signal, #3768, was 4 lines in 212).

Defects

  1. A check that cannot act should not run per event. With Admin/UpdatePolicy = None the BuildCompletion trigger has nothing to decide; subscribing to build completions (and running the SafetyNet timer) while the policy is None is wasted work on every replica. Read the policy first; a None policy leaves the trigger unsubscribed until the policy node changes.
  2. lastCheckedAt is a per-replica observation written into a shared node by every replica. Two replicas writing one leaf on the same event is the conflict by construction. Either the leader (one replica — the self-updater already elects who may patch, canPatch) writes it, or it is not a node field at all (a health/metrics reading per pod).

Where

memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateHostedService.cs (the check + the lastCheckedAt write), Admin/UpdatePolicy owner. Measured on 3.0.0-ci.8131 (image 3853374), 2 replicas.

Activity

  1. rbuergi commented on Sep 9, 2026

    @rbuergi
    ContributorAuthor

    An independent measurement of the cumulative cost: the node is at version 62,671

    Read live 2026-09-09T07:40Z via get @Admin/UpdatePolicy (a plain node read, no Loki):

    version:          62671
    lastModified:     2026-09-09T07:40:12.966Z
    lastModifiedBy:   system-security
    lastCheckedAt:    2026-09-09T07:39:42.187Z
    lastCheckTrigger: BuildCompletion
    lastCheckVerdict: "updates are disabled on this install (Admin/UpdatePolicy = None);
                       the registry was not listed."
    

    Three things this adds to the 5-hour Loki window already recorded here:

    1. The write count is enormous and cumulative. Every check writes lastCheckedAt, so version: 62671 is a lower bound on how many times this one node has been rewritten. The 158-checks-in-5-h rate measured above is not a burst — it is the steady state, and it has been running long enough to put five figures of versions on a node whose content decision has not changed since heldAt: 2026-09-07T22:27:17Z.

    2. The trigger and verdict are still exactly as reported, ~1.5 h after the window this issue measured: lastCheckTrigger: BuildCompletion, and the verdict is the "updates are disabled" line. So the loop is live now, not a past episode.

    3. The work is provably pointless in this state. lastCheckedAt (07:39:42Z) has moved thousands of times since checkedAt (2026-09-07T22:26:44Z) and heldAt (22:27:17Z) — the two fields that record an actual decision — have not moved at all. Every one of those writes re-derived the same answer: policy is None, nothing to do.

    🚨 Worth noting for whoever fixes it: version is also the thing a reader would use to spot this from outside, without Loki. A policy node whose version climbs by thousands while its heldAt/checkedAt stand still is the signature, and it is one cheap read.

  2. rbuergi commented on Sep 9, 2026

    @rbuergi
    ContributorAuthor

    Fixed by #3795, merged to main.

    Under Admin/UpdatePolicy = None the two fleet-paced triggers (BuildCompletion, ModuleSetProposed) are no longer decision points, so they run no check — while Startup, PolicyChange and the safety net still do. On the measured window that takes a pinned install from ~158 checks in five hours to about five, and with it the [MergeGuard] refusals, the OWNER_NACK_REENQUEUEs and the ten-second [UpdateQueue] FAILED stalls, which were all products of the rate.

    Defect 2 as filed — "lastCheckedAt is a per-replica observation written into a shared node by every replica" — is deliberately not changed, and the PR says so: the field describes "this install checked", not "this pod checked", so the shared field is the right model, and a merge guard refusing the older of two concurrent stamps is the framework being right. What made it a storm was the rate.

    Pinned by SelfUpdateChecksOnlyAtDecisionPointsTest — a 15-case truth table over every trigger × every policy, plus the same build-completion event pushed through the same seam under two policies (not a check under None, a check under Continuous). Falsified: removing the filter fails the acceptance test in 298 ms while the other 18 cases still pass. Documented in Doc/Architecture/SelfUpdateTargetSelection §5 and a What's New entry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions