Skip to content

The starvation stand-in was starving the SECURITY reads too, and hanging the create - #1427

Merged
rbuergi merged 3 commits into
mainfrom
investigate/starved-flake
Aug 13, 2026
Merged

rbuergi merged 3 commits into
mainfrom
investigate/starved-flake

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Shard 1 of #1418 failed SourceDiscoveryUnavailableTest and I re-ran it green, calling it a flake. Following up: it is not a flake, and the same root cause was silently costing this test ~26 seconds on every run.

What actually happened

The failure was at line 153 — the CreateNode of the healthy source node — with the starve exception and "emitted nothing at all". The stand-in's own comment claims that cannot happen:

🚨 Matched on the namespace: FORM, not on the bare path, so only source DISCOVERY starves. Node creation and single-node reads under the same subtree use path:-shaped queries and must keep working

That is false. The security layer reads its policy and grants with namespace:-shaped queries over the same subtree:

namespace:…/StarvedSourceType/Source id:_Policy       nodeType:PartitionAccessPolicy
namespace:…/StarvedSourceType/Source/_Access          nodeType:AccessAssignment

so the marker starved those too. The CI log shows both evicted as faulted synced queries, and the hub snapshot shows the consequence:

Hub portal/nodeops-… Executing(CreateNodeRequest, 33014ms)

The create sat 33 seconds waiting on security reads that could never answer, and the 30 s assertion gave up first. It passed whenever those queries were already cached and failed when they were cold — which is exactly why it reproduced on a CI shard and never locally (10/10 green on main before this change).

The fix

CodeQueryResolver ANDs nodeType:Code into every source query it emits, and the security queries carry their own node types — so that clause separates them exactly. The starve now requires it.

Proof it does not MASK the thing under test

  • Narrowed: the test passes and the starved type still reaches Unavailable.
  • Starve disabled entirely: the test fails — the type compiles Ok — so the source-discovery leg is provably still starved.

It also stops the test paying for a hang it never meant to cause: 30.2 s (the CI soft-deadline warning, and ~30 s locally) → a stable 4 s across 5 runs.

🚩 Worth a separate look

The product behaviour underneath is that a starved security-policy read leaves CreateNodeRequest executing for 33 s rather than failing fast. In a real cross-silo starvation — the very scenario this suite exists for (#1218) — node creation into the affected namespace would hang the same way. The faulted-query eviction machinery already recovers the query; the in-flight caller still waits. That is a robustness gap of its own and I have not touched it here.

🤖 Generated with Claude Code

…ing the create

Shard 1, 2026-08-13: SourceDiscoveryUnavailableTest failed at line 153 — the
CreateNode of the healthy source node — with the starve exception and
'emitted nothing at all'. It was called a flake. It is not.

The stand-in's own comment claims:

  🚨 Matched on the namespace: FORM, not on the bare path, so only source
  DISCOVERY starves. Node creation and single-node reads under the same
  subtree use path:-shaped queries and must keep working

That is false. The SECURITY layer reads its policy and grants with
namespace:-shaped queries over the SAME subtree:

  namespace:…/StarvedSourceType/Source id:_Policy nodeType:PartitionAccessPolicy
  namespace:…/StarvedSourceType/Source/_Access nodeType:AccessAssignment

so the marker starved those too. The CI log shows both being evicted as faulted
synced queries, and the hub snapshot shows the consequence:

  Hub portal/nodeops-… Executing(CreateNodeRequest, 33014ms)

The create sat for 33 s waiting on security reads that could never answer, and
the 30 s assertion gave up first. It passed whenever those queries were already
cached and failed when they were cold — which is exactly why it reproduced on a
CI shard and never locally (10/10 green on main before this change).

CodeQueryResolver ANDs nodeType:Code into every source query it emits, and the
security queries carry their own node types, so that clause separates them
exactly. The starve now requires it.

Not a masking change — the starve still bites:

- narrowed: the test passes and the starved type still reaches Unavailable;
- starve disabled entirely: the test FAILS, because the type compiles Ok — so
  the source-discovery leg is provably still starved.

It also stops the test paying for the hang it never meant to cause: 30.2 s
(soft-deadline warning on CI, and ~30 s locally) → a stable 4 s over 5 runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 13, 2026 13:49

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 stabilizes SourceDiscoveryUnavailableTest by narrowing the starvation stand-in so it only faults source discovery queries (nodeType:Code) and no longer starves security policy/grant reads under the same namespace (which could cause CreateNode to hang until the assertion timeout).

Changes:

  • Require nodeType:Code in the query-matching predicate that triggers starvation in StarvedSourceQueryProvider.
  • Add a dedicated SourceDiscoveryMarker constant and expand the inline documentation to explain why the discriminator is necessary (security reads also use namespace: queries).

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

Comment on lines +278 to +279
/// <c>CodeQueryResolver</c> ANDs <c>nodeType:Code</c> into every source query it emits, and the
/// security queries carry their own node types — so that clause separates them exactly.</para>
@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   5m 23s ⏱️ +5s
1 833 tests ±0  1 730 ✅ ±0  103 💤 ±0  0 ❌ ±0 
2 152 runs  +1  2 049 ✅ +1  103 💤 ±0  0 ❌ ±0 

Results for commit 53e8216. ± Comparison against base commit a83f5fd.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 909 tests  ±0   1 717 ✅ ±0   5m 11s ⏱️ -56s
   11 suites ±0     192 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 53e8216. ± Comparison against base commit a83f5fd.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

723 tests  ±0   723 ✅ ±0   10m 8s ⏱️ -12s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 53e8216. ± Comparison against base commit a83f5fd.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 924 tests  +4   1 914 ✅ +4   8m 20s ⏱️ +55s
   11 suites ±0      10 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 53e8216. ± Comparison against base commit a83f5fd.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

2 454 tests   - 122   2 453 ✅  - 122   6m 8s ⏱️ -36s
   12 suites ±  0       1 💤 ±  0 
   12 files   ±  0       0 ❌ ±  0 

Results for commit 53e8216. ± Comparison against base commit a83f5fd.

This pull request removes 335 and adds 213 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ A_stored_slide_image_is_embedded_as_a_data_uri
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ A_traversal_reference_is_refused_rather_than_read
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ An_unresolvable_reference_is_reported_rather_than_silently_blank
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ Every_reference_shape_resolves_against_a_real_collection
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ The_export_embeds_the_stored_image_or_refuses_clearly
MeshWeaver.Hosting.Monolith.Test.DeckLayoutAreaTest ‑ CustomQuery_SelectsMatchingSlides_OrderedByNodeOrder
MeshWeaver.Hosting.Monolith.Test.DeckLayoutAreaTest ‑ DeckContent_RoundTrips_WithShortTypeDiscriminator
MeshWeaver.Hosting.Monolith.Test.DeckLayoutAreaTest ‑ DeckPresent_NoManifest_DefaultsToSubtreeSlideQuery_InOrder
MeshWeaver.Hosting.Monolith.Test.DeckLayoutAreaTest ‑ DeckPresent_WalksReferencedSlidePaths_LivingOutsideTheDeck_InManifestOrder
MeshWeaver.Hosting.Monolith.Test.DeckLayoutAreaTest ‑ EmptyManifest_DefaultsToSubtree_OrderedByNodeOrder
…
MeshWeaver.Hosting.Monolith.Test.CrossHubWritePersistenceTest ‑ StreamUpdate_CrossHub_NoPriorSubscription_Persists
MeshWeaver.Hosting.Monolith.Test.DeckExportScriptRelayTest ‑ DeckExport_RendersOnePagePerSlide_ContentFaithful_Landscape
MeshWeaver.Hosting.Monolith.Test.DeckPixelExportScriptRelayTest ‑ DeckExport_PixelFidelity_RendersInABrowser_OrRefusesClearly
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Leaf_Delete_SucceedsAndRemovesNode
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Missing_Node_Fails_NotFound
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ NoDeletePermission_OnRoot_Fails_Unauthorized_AndLogsPath
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ NonRecursive_WithChildren_Fails_HasChildren_NothingDeleted
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Recursive_Delete_Log_ListsAllAffectedPathsAndSucceeded
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Recursive_Delete_RemovesEntireSubtree
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Validator_RejectsDescendant_BlocksWholeSubtree_AllPathsListed
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 498 tests  +123   1 498 ✅ +123   7m 30s ⏱️ +16s
   11 suites ±  0       0 💤 ±  0 
   11 files   ±  0       0 ❌ ±  0 

Results for commit 53e8216. ± Comparison against base commit a83f5fd.

This pull request removes 213 and adds 336 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.CrossHubWritePersistenceTest ‑ StreamUpdate_CrossHub_NoPriorSubscription_Persists
MeshWeaver.Hosting.Monolith.Test.DeckExportScriptRelayTest ‑ DeckExport_RendersOnePagePerSlide_ContentFaithful_Landscape
MeshWeaver.Hosting.Monolith.Test.DeckPixelExportScriptRelayTest ‑ DeckExport_PixelFidelity_RendersInABrowser_OrRefusesClearly
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Leaf_Delete_SucceedsAndRemovesNode
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Missing_Node_Fails_NotFound
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ NoDeletePermission_OnRoot_Fails_Unauthorized_AndLogsPath
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ NonRecursive_WithChildren_Fails_HasChildren_NothingDeleted
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Recursive_Delete_Log_ListsAllAffectedPathsAndSucceeded
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Recursive_Delete_RemovesEntireSubtree
MeshWeaver.Hosting.Monolith.Test.DeleteNodeBehaviorTest ‑ Validator_RejectsDescendant_BlocksWholeSubtree_AllPathsListed
…
MeshWeaver.Hosting.Monolith.Test.CrossClusterBuildClaimTest ‑ TwoClustersClaimingConcurrently_ExactlyOneBuilderProceeds
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ A_stored_slide_image_is_embedded_as_a_data_uri
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ A_traversal_reference_is_refused_rather_than_read
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ An_unresolvable_reference_is_reported_rather_than_silently_blank
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ Every_reference_shape_resolves_against_a_real_collection
MeshWeaver.Hosting.Monolith.Test.DeckAssetInliningTest ‑ The_export_embeds_the_stored_image_or_refuses_clearly
MeshWeaver.Hosting.Monolith.Test.DeckLayoutAreaTest ‑ CustomQuery_SelectsMatchingSlides_OrderedByNodeOrder
MeshWeaver.Hosting.Monolith.Test.DeckLayoutAreaTest ‑ DeckContent_RoundTrips_WithShortTypeDiscriminator
MeshWeaver.Hosting.Monolith.Test.DeckLayoutAreaTest ‑ DeckPresent_NoManifest_DefaultsToSubtreeSlideQuery_InOrder
MeshWeaver.Hosting.Monolith.Test.DeckLayoutAreaTest ‑ DeckPresent_WalksReferencedSlidePaths_LivingOutsideTheDeck_InManifestOrder
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    66 files  ±0      66 suites  ±0   42m 42s ⏱️ -29s
10 341 tests +5  10 035 ✅ +5  306 💤 ±0  0 ❌ ±0 
10 660 runs  +6  10 354 ✅ +6  306 💤 ±0  0 ❌ ±0 

Results for commit 53e8216. ± Comparison against base commit a83f5fd.

♻️ This comment has been updated with latest results.

rbuergi and others added 2 commits August 13, 2026 16:46
Copilot is right, and it caught a claim I had copied rather than checked:
CodeQueryResolver appends nodeType:Code only when the query does not already
name a node type (line 145) — so 'ANDs it into every source query it emits' is
an overstatement.

The starve marker is still correct HERE, and the test proves it rather than
asserting it: the starved type declares no Sources, so its discovery runs the
DEFAULT query and does carry the clause. The note now says exactly that, and
names the case it would not cover — a declared source query that names some
other node type.

Also corrected CodeQueryResolver's own summary, which is where the overstated
claim came from and where the next person would copy it from too.

Comments only — SourceDiscoveryUnavailableTest still green in 4s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi

rbuergi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Right, and thank you — I had copied that claim rather than checked it. CodeQueryResolver line 145 appends nodeType:Code only when the query does not already name a node type, so "ANDs it into every source query it emits" is an overstatement.

The marker is still correct here, and the test proves that rather than asserting it: the starved type declares no Sources, so its discovery runs the default query (namespace:{path}/Source scope:subtree) and does carry the clause. The note now says exactly that, and names the case it would not cover — a declared source query naming some other node type.

I also corrected CodeQueryResolver's own summary, which is where the overstatement came from and where the next person would copy it from, as I did.

Comments only; SourceDiscoveryUnavailableTest still green in 4s.

@rbuergi
rbuergi merged commit d4d866d into main Aug 13, 2026
19 checks passed
@rbuergi
rbuergi deleted the investigate/starved-flake branch August 19, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants