Skip to content

Activation seeds from durable truth — the write-loss family fixed at its root (#590 #648 #667) - #923

Merged
rbuergi merged 1 commit into
mainfrom
fix/648-activation-durable-first-seed
Aug 8, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/648-activation-durable-first-seed

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #590, #648, #667 — one mechanism behind all three, plus the live prod sighting recorded on #919.

Root cause — the activation seed raced durable truth and lost

MeshNodeTypeSource.Initialize merged the durable storage read with the cache-backed routing legs (Observable.Merge), and GetInitialValueAsync takes the first accepted emission then disposes the subscription. On any async backend (PG — the prod portals) the synchronously-replaying stale routing snapshot reliably won the seed and the late durable row was discarded unobserved. Every subsequent write minted below durable truth → MonotonicWriteGuard refused each save → AdoptDurableTruth rebased the owner and clobbered the acked write in RAM. That is #590 (updates stop persisting after re-activation), #648 (acked-success loss on a stale second activation) and #667/#919 (deploy-window discards) — and why it never reproduced on in-memory (synchronous reads).

Fixes

Verification

  • Acceptance (unmodified): StaleActivationSeedRollbackTest 15/15 consecutive on the final build. Honest note: the claimed 8/15 red on pristine main did not reproduce on this machine (in-memory is structurally green) — the deterministic pin below covers the mechanism instead.
  • New deterministic repros with a gated-read adapter (async-backend stand-in): StaleActivationDurableFirstSeedTest — fail-without confirmed (the acked write demonstrably destroyed by the refusal→rebase cycle), pass-with green; SupersededActivationPatchNackTest (never-Success across all reachable NACK shapes); an all-keys-refused ⇒ Conflict pin; an activation-is-a-read pin.
  • Suites: Hosting.Monolith ×3 consecutive 482/0 · Data 303 · NodeOperations 85 · Graph 820 · Query 361 · Orleans TwoSiloRecycleConvergenceTest 1/1. Independently re-verified 7/7 across the new + acceptance + overlay classes after merging current main. -c Release -warnaserror clean per project.

Residual (documented)

Quiescing-NACK branch not directly red/greened locally (Monolith passes Quiescing in <20 ms; matters most on Orleans). Activation now holds for the durable read — on PG a boot storm serializes on the pg:{adapter} pool; that is the requested trade. Take(1) seed semantics otherwise unchanged.

Closes #590
Closes #648
Closes #667

🤖 Generated with Claude Code

…e prod sighting recorded on #919.

## Root cause — the activation seed raced durable truth and lost

`MeshNodeTypeSource.Initialize` merged the durable storage read with the cache-backed routing legs (`Observable.Merge`), and `GetInitialValueAsync` takes the **first** accepted emission then disposes the subscription. On any async backend (PG — the prod portals) the synchronously-replaying stale routing snapshot reliably won the seed and the late durable row was **discarded unobserved**. Every subsequent write minted below durable truth → `MonotonicWriteGuard` refused each save → `AdoptDurableTruth` rebased the owner and **clobbered the acked write in RAM**. That is #590 (updates stop persisting after re-activation), #648 (acked-success loss on a stale second activation) and #667/#919 (deploy-window discards) — and why it never reproduced on in-memory (synchronous reads).

## Fixes

- **Seed ordering**: `Merge` → **`Concat`** — the durable read settles first (row / null / fault-degrade), the routing leg follows; activation *holds* for one storage read (#667's "wait, don't Not-found"); version floor unchanged.
- **Enrichment graft**: the durable row (authoritative for version/content) is grafted with the routing node's non-persistable `HubConfiguration` delegate — without it, durable-first seeding stripped the delegate and fired spurious activation compiles (caught as 2 suite regressions, both healed).
- **InMemory delegate strip**: a durable store cannot hold an in-process delegate (FS/PG strip via serialization; InMemory retained the instance and latched stale overlay configs forever — deterministically reproduced and fixed via `OverlaySelfHealInstanceRecycleTest`).
- **#648 invariant 1**: an all-keys-refused three-way merge NACKs **`Conflict`** — never acks Success.
- **#648 invariant 2**: a disposing activation NACKs retryable (`OwnerDisposing`) at handler entry; direct-path NACKs now re-enqueue like the late path.
- **#667**: new wire code **`OwnerNotReady`** — a cold store's defer-timeout is retryable and provably-not-applied, never a false `NotFound` (which also poisoned the stream cache's negative cache).

## Verification

- Acceptance (unmodified): `StaleActivationSeedRollbackTest` **15/15 consecutive** on the final build. Honest note: the claimed 8/15 red on pristine main did **not** reproduce on this machine (in-memory is structurally green) — the deterministic pin below covers the mechanism instead.
- New deterministic repros with a gated-read adapter (async-backend stand-in): `StaleActivationDurableFirstSeedTest` — **fail-without confirmed** (the acked write demonstrably destroyed by the refusal→rebase cycle), pass-with green; `SupersededActivationPatchNackTest` (never-Success across all reachable NACK shapes); an all-keys-refused ⇒ Conflict pin; an activation-is-a-read pin.
- Suites: Hosting.Monolith **×3 consecutive 482/0** · Data 303 · NodeOperations 85 · Graph 820 · Query 361 · Orleans `TwoSiloRecycleConvergenceTest` 1/1. Independently re-verified **7/7** across the new + acceptance + overlay classes after merging current main. `-c Release -warnaserror` clean per project.

## Residual (documented)

Quiescing-NACK branch not directly red/greened locally (Monolith passes Quiescing in <20 ms; matters most on Orleans). Activation now holds for the durable read — on PG a boot storm serializes on the `pg:{adapter}` pool; that is the requested trade. `Take(1)` seed semantics otherwise unchanged.

Closes #590
Closes #648
Closes #667

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Copilot AI lite review requested due to automatic review settings August 7, 2026 23:51
@rbuergi
rbuergi enabled auto-merge August 7, 2026 23:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a root-cause mechanism behind multiple silent write-loss issues (#590/#648/#667) by ensuring per-node-hub activation seeds from durable storage truth before any cache/routing replays, and by tightening owner-side ACK/NACK semantics so “provably not applied” patches are retried rather than falsely acknowledged.

Changes:

  • Fix activation seeding by ordering durable reads ahead of routing/cache emissions (Merge → Concat) and grafting routing-only, non-persistable enrichment (HubConfiguration) onto the durable seed.
  • Strengthen cross-hub patch handling: superseded activations NACK early, and all-keys-refused three-way merges NACK Conflict instead of ACKing success.
  • Add deterministic test coverage with a gated async-like storage adapter and new repro/pin tests for the activation/ACK invariants; align InMemory persistence behavior with serializing backends by stripping HubConfiguration delegates on write.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/MeshWeaver.Hosting.Monolith.Test/SupersededActivationPatchNackTest.cs New deterministic pins for “superseded activation must NACK” and “all-keys-refused must not ACK success”.
test/MeshWeaver.Hosting.Monolith.Test/StaleActivationDurableFirstSeedTest.cs New deterministic repro for durable-first activation seeding using a gated async-like read adapter.
test/MeshWeaver.Hosting.Monolith.Test/GatedReadStorageAdapter.cs Test-only storage adapter decorator to force genuinely asynchronous/held reads per path.
src/MeshWeaver.Mesh.Contract/MeshNodeStreamExtensions.cs Client-side re-enqueue logic extended to treat the new retryable owner NACK as safe to retry.
src/MeshWeaver.Hosting/Persistence/InMemoryStorageAdapter.cs Strips non-serializable HubConfiguration delegate on write to match durable backends.
src/MeshWeaver.Graph/MeshNodeTypeSource.cs Durable-first activation seeding (Concat) plus routing enrichment graft to preserve in-process delegate behavior.
src/MeshWeaver.Data/DataExtensions.cs Owner-side patch handling: superseded activation early NACK, refused-merge Conflict NACK, and “store not initialized” NACK changed to retryable.
src/MeshWeaver.Data.Contract/MeshNodeError.cs Introduces OwnerNotReady error code intended to represent “provably not applied; safe to retry”.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +63 to +74
OwnerDisposing = 7,
/// <summary>
/// The owning activation is up but has NOT finished loading its durable state
/// (activation seed in flight — a slow storage read, a NodeType assembly reload
/// window) — the patch was provably NEVER applied. 🚨 Deliberately distinct from
/// <see cref="NotFound"/>: answering "not found" for a node that exists poisons
/// every existence check built on the read (and the stream cache's negative
/// cache) — the #667 deploy-window write loss. Same retry contract as
/// <see cref="OwnerDisposing"/>: the write never happened, so the mirror may
/// safely re-enqueue the SAME update once the activation has loaded.
/// </summary>
OwnerNotReady = 8
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

672 tests   672 ✅  5m 0s ⏱️
 10 suites    0 💤
 10 files      0 ❌

Results for commit af7836c.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 676 tests   1 485 ✅  6m 1s ⏱️
   11 suites    191 💤
   11 files        0 ❌

Results for commit af7836c.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 207 tests   1 202 ✅  5m 33s ⏱️
   11 suites      5 💤
   11 files        0 ❌

Results for commit af7836c.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 771 tests   1 769 ✅  5m 45s ⏱️
   11 suites      2 💤
   11 files        0 ❌

Results for commit af7836c.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files     11 suites   5m 59s ⏱️
1 763 tests 1 655 ✅ 108 💤 0 ❌
1 857 runs  1 749 ✅ 108 💤 0 ❌

Results for commit af7836c.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 185 tests   1 185 ✅  8m 30s ⏱️
   11 suites      0 💤
   11 files        0 ❌

Results for commit af7836c.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results

   65 files     65 suites   36m 50s ⏱️
8 274 tests 7 968 ✅ 306 💤 0 ❌
8 368 runs  8 062 ✅ 306 💤 0 ❌

Results for commit af7836c.

@rbuergi
rbuergi merged commit 23d62b9 into main Aug 8, 2026
17 checks passed
@rbuergi
rbuergi deleted the fix/648-activation-durable-first-seed branch August 19, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants