Skip to content

fix(resilience): every HttpClient names its own pipeline instance — '-standard//' named nobody (#4528) - #5664

Merged
rbuergi merged 1 commit into
mainfrom
fix/4528-defaults-pipeline-per-client
Sep 25, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/4528-defaults-pipeline-per-client

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Refs #4528

What Source: '-standard//…' actually was

Not "a caller resolving an unnamed HttpClient". ConfigureHttpClientDefaults hands out a builder with no name, and AddStandardResilienceHandler names its pipeline {builder.Name}-standard (PipelineNameHelper.GetName, Microsoft.Extensions.Http.Resilience 10.10.0). So the defaults pipeline is -standard for every client that does not re-register itself — named or not — and with no selector the registry keys it HttpKey("-standard", ""): ONE pipeline, ONE circuit breaker, for self-update-handover, deployment-report, module-published-broadcast, OpenGraphPreview, the OCI tag listers and every Plugins client registered by name.

What it named in production (read-only Logs actions on memex.meshweaver.cloud)

  • Ops/logs-memexcloud-20260924-timeout-stack2 / …-2x9rq-stack: every TimeoutRejectedException under the -standard lines ends in SelfUpdateHandover.<>c__DisplayClass37_0.<<Post… — the named self-update-handover client posting to https://memex.systemorph.com/api/hooks/Hosting/PlatformBuilds.
  • Ops/logs-memexcloud-20260924-totaltimeouts-1440 (157 lines) against Ops/logs-memex-20260924-boots-600: the large bursts coincide with the control instance's replicas (re)starting (06:29, 07:07/07:22, 08:22, 09:21/09:32, 10:03–10:08, 12:23/12:47, 13:43 UTC); the 13:43 attempts answered 503 in 1.3 ms (ingress, no ready backend).

The change

ServiceDefaults.AddHttpClientResilienceDefaults (new, called by AddServiceDefaults; behaviour otherwise unchanged):

  1. an outermost DelegatingHandler on every client stamps HttpMessageHandlerBuilder.Name into request.Options;
  2. the defaults' standard handler gets .SelectPipelineBy(_ => ClientNameOf) → the pipeline instance is the client's name ((unnamed) for the default client).

Lines now read -standard/self-update-handover/Standard-AttemptTimeout; each client has its own breaker. Keyed by name, never by authority (the defaults serve user-supplied URLs; the registry never evicts). The two plugin-registry clients keep their own pipelines (pinned by a test).

This does NOT change any budget and does not make the control inbox answer — that is the control instance restarting (documented with the evidence in the new doc page). Service discovery is still registered after resilience, so the handler order is unchanged.

Verification

  • EveryClientNamesItsOwnResiliencePipelineTest (Memex.Portal.Shared.Test) builds exactly what production builds and reads the identity off Polly's own log line: 3/3 green. Negative control: with the SelectPipelineBy line removed it fails with the production form Execution attempt. Source: '-standard//Standard-Re….
  • NonexistentHostIsNotRetriedTest (the Memex portal outbound calls fail DNS resolution for boss-software.ch hosts (Polly retries exhausted) #4613 carve-out on the same pipeline) still green.
  • dotnet build -c Release -warnaserror: Memex.Portal.ServiceDefaults, Memex.Portal.Shared.Test, MeshWeaver.Documentation — 0 warnings, 0 errors each.

Docs

New Doc/Architecture/EveryHttpClientNamesItsPipeline (finding, evidence, fix, what it does not fix), linked from the Architecture topic map; LogWatchTriage corrected ("empty client name" → the shared defaults pipeline).

Pairs-with: none — nothing is removed; one public method is added.

Goes live on a roll of the portals; no recycle needed (the resilience registration is process-start DI).

🤖 Generated with Claude Code

…-standard//' named nobody (#4528)

ConfigureHttpClientDefaults hands out a builder with no name, so AddStandardResilienceHandler
named the defaults pipeline '-standard' for EVERY client that does not re-register itself, and
the registry keyed it by an empty instance: one pipeline, one circuit breaker, one anonymous
'Source: -standard//…' for self-update-handover, deployment-report, the OCI listers, the web
fetcher and every other named client. It was read as "some caller uses an unnamed HttpClient".

Measured on memex-cloud (read-only Logs actions): every TimeoutRejectedException stack under
those lines ends in SelfUpdateHandover.Post — the named self-update-handover client posting to
the control inbox — and the bursts coincide with the control instance's replicas (re)starting.

An outermost handler now stamps HttpMessageHandlerBuilder.Name onto the request and the
defaults' standard handler selects its pipeline instance by it (SelectPipelineBy), so every line
reads '-standard/<client>/…' and each client trips its own breaker. Keyed by client name, never
by authority: the defaults serve user-supplied URLs and the registry never evicts.

Refs #4528

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ±0    1 suites  ±0   3m 14s ⏱️ +10s
342 tests ±0  342 ✅ ±0  0 💤 ±0  0 ❌ ±0 
346 runs  ±0  346 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit f4e102f. ± Comparison against base commit 423b863.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

443 tests  ±0   443 ✅ ±0   1m 2s ⏱️ -1s
  3 suites ±0     0 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit f4e102f. ± Comparison against base commit 423b863.

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.

Copilot review overview

🟡 Changes recommended

Unresolved compile errors and a broader-than-intended public helper contract must be addressed.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)
What changed in this PR

Updates HTTP resilience so each client uses its own named pipeline, with regression tests and architecture documentation.

Changes:

  • Adds per-client pipeline selection and request-name stamping.
  • Adds pipeline identity tests.
  • Documents the diagnosis, fix, and operational guidance.
File Reviewed changes
test/​Memex.Portal.Shared.Test/​EveryClientNamesItsOwnResiliencePipelineTest.cs Adds pipeline identity coverage; contains compile and async-path issues requiring changes.
src/​MeshWeaver.Documentation/​Data/​Architecture/​LogWatchTriage.md Updates timeout attribution guidance.
src/​MeshWeaver.Documentation/​Data/​Architecture/​EveryHttpClientNamesItsPipeline.md Documents the incident and fix; contains link and wording nits.
src/​MeshWeaver.Documentation/​Data/​Architecture.md Adds the architecture page to the index.
memex/​aspire/​Memex.Portal.ServiceDefaults/​ServiceDefaults.cs Adds per-client resilience selection; contains a compile issue and broader helper side effects.

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

http.AddServiceDiscovery();
})
// 🚨 ONE PIPELINE INSTANCE PER CLIENT NAME (#4528) — see ClientNameOf.
.SelectPipelineBy(static _ => ClientNameOf);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not changed — this compiles and runs as written. The overload is HttpStandardResiliencePipelineBuilderExtensions.SelectPipelineBy(this IHttpStandardResiliencePipelineBuilder, Func<IServiceProvider, Func<HttpRequestMessage, string>> selectorFactory) (read from the Microsoft.Extensions.Http.Resilience 10.10.0 assembly, line 87 of the decompiled class): it takes a FACTORY that returns the selector. So static _ => ClientNameOf returning the method group is correct. Evidence: dotnet build -c Release -warnaserror on Memex.Portal.ServiceDefaults gives 0 warnings and 0 errors, and EveryClientNamesItsOwnResiliencePipelineTest passes 3/3. With the call removed, that test fails with the production form '-standard//Standard-Retry', which shows the selector really is in effect.


Send(factory.CreateClient(Handover));
Send(factory.CreateClient(Report));
Send(factory.CreateClient());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not changed — this compiles and runs as written. The parameterless CreateClient() is the extension method HttpClientFactoryExtensions.CreateClient(this IHttpClientFactory) in Microsoft.Extensions.Http, and it calls CreateClient(Options.DefaultName). That is exactly the "unnamed client" case the assertion -standard/(unnamed)/ pins. Evidence: the test project builds with -c Release -warnaserror (0 errors) and the test passes 3/3.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 648 tests  ±0   1 648 ✅ ±0   3m 11s ⏱️ -2s
    2 suites ±0       0 💤 ±0 
    2 files   ±0       0 ❌ ±0 

Results for commit f4e102f. ± Comparison against base commit 423b863.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    3 files  ±0      3 suites  ±0   6m 20s ⏱️ +3s
2 012 tests ±0  2 012 ✅ ±0  0 💤 ±0  0 ❌ ±0 
2 013 runs  ±0  2 013 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit f4e102f. ± Comparison against base commit 423b863.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

743 tests  ±0   551 ✅ ±0   7m 8s ⏱️ -2s
  3 suites ±0   192 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit f4e102f. ± Comparison against base commit 423b863.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    5 files  ±0      5 suites  ±0   15m 42s ⏱️ +9s
4 081 tests +3  4 079 ✅ +3  2 💤 ±0  0 ❌ ±0 
4 085 runs  +3  4 083 ✅ +3  2 💤 ±0  0 ❌ ±0 

Results for commit f4e102f. ± Comparison against base commit 423b863.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   17 files  ±0     17 suites  ±0   36m 39s ⏱️ +16s
9 269 tests +3  9 075 ✅ +3  194 💤 ±0  0 ❌ ±0 
9 278 runs  +3  9 084 ✅ +3  194 💤 ±0  0 ❌ ±0 

Results for commit f4e102f. ± Comparison against base commit 423b863.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 24, 2026
@meshweaver-cloud

Copy link
Copy Markdown
Contributor

⛔ Merge-queue steward: left dequeued — the group build failed on an assertion the flake catalogue does not know.
Group build: https://github.com/Systemorph/MeshWeaver/actions/runs/36019287652

  • MeshWeaver.Graph.Test.NackReachesTheWaiterDuringTeardownTest.OwnerDisposingUnderMeshTeardown_StillAnswersTheWaitingCaller — MeshWeaver.Reactive.Assertions.AssertionException : Did not expect collection {"LW1lBiz2VEaR2HYBc7KHdQ"} to contain "LW1lBiz2VEaR2HYBc7KHdQ" because the owner minted an OwnerDisposing NACK for this patch, and a caller was armed and waiting

Not a catalogued flake. Fix the failure, or — with run URLs, an issue and an assertion-message pattern — add it to .github/known-flakes.json (see Doc/Architecture/MergeQueue). Re-queue with gh pr merge <n> --auto once the head is green; label queue-rejected marks this PR as needing a person.

@rbuergi

rbuergi commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Both of this PR's queue ejections are now fixed. It is ready for the maintainer to re-queue with gh pr merge 5664 --auto.

  1. Run 36019287652, shard 4. NackReachesTheWaiterDuringTeardownTest.OwnerDisposingUnderMeshTeardown_StillAnswersTheWaitingCaller failed on the test's own fence, and test: the owner-disposing NACK test fences on the owner ACCEPTING the patch #5688 (merged) fixes it.
  2. Run 36016772968, shard 5. HeapStepNamesItsAllocatorTest.ConcurrentRecordAndDrain_LoseNoSample came from feat(liveness): a heap step names what was allocated in it — [HEAPSTEP] (Refs #5555) #5665, which sat ahead of this PR in the group. It is fixed on feat(liveness): a heap step names what was allocated in it — [HEAPSTEP] (Refs #5555) #5665's own branch (973af80): the race is measured on the whole window instead of the Top-N summary. feat(liveness): a heap step names what was allocated in it — [HEAPSTEP] (Refs #5555) #5665 is not merged yet and is also waiting for a maintainer re-queue.

@rbuergi rbuergi removed the queue-rejected The merge queue rejected this PR on an uncatalogued failure; a person owns it now label Sep 25, 2026
@rbuergi
rbuergi added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 92006e8 Sep 25, 2026
53 of 55 checks passed
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