Skip to content

Reduce session-affinity async allocations - #3042

Closed
artl93 wants to merge 2 commits into
dotnet:mainfrom
artl93:artl93-reduce-affinity-allocations
Closed

Reduce session-affinity async allocations#3042
artl93 wants to merge 2 commits into
dotnet:mainfrom
artl93:artl93-reduce-affinity-allocations

Conversation

@artl93

@artl93 artl93 commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

SessionAffinityMiddleware currently keeps its own async state machine alive while awaiting the downstream proxy pipeline. Built-in affinity policies usually complete synchronously, while forwarding normally suspends.

This change handles synchronously completed affinity results inline and returns the downstream task directly. It avoids one middleware async-frame allocation without changing the public policy interfaces.

Scope: this only benefits affinity-enabled requests where the affinity/failure policy completes synchronously and downstream completes asynchronously. Affinity-disabled requests, immediate failures that do not call downstream, and genuinely asynchronous custom policies do not receive the same reduction.

This PR remains draft.

BenchmarkDotNet results

EgorBot does not appear to be enabled for dotnet/yarp: there is no repository workflow, configuration, or prior invocation in YARP history. As a cross-platform substitute, I ran BenchmarkDotNet 0.15.8 locally with separate exact-parent and exact-final jobs:

  • Parent: 49b23da6d8b0e9df47abf45d7755173f84670941
  • Product change: 69457515f0f0a9e5b99d83d9384360b1889a0366
  • PR head: d3627b95676a7f28c812616e4bb7ca652e2916a6 (test-only follow-up; product source is unchanged)
  • BDN environment: macOS Tahoe 26.5.2 (25F84), Darwin 25.5.0, Apple M5 Pro arm64, .NET SDK 11.0.100-preview.5.26227.104, runtime .NET 9.0.2
  • Comparison: BDN job baseline/current DLL references, apples-to-apples invocation counts, MemoryDiagnoser

Component pipeline

Scenario Parent B/request Final B/request Delta
Disabled, synchronous downstream 1,552 1,552 0
Disabled, forced-async downstream 1,968 1,968 0
Cookie hit, synchronous downstream 2,288 2,288 0
Cookie hit, forced-async downstream 2,904 2,704 -200
Cookie miss, forced-async downstream 2,248 2,048 -200
Stale cookie, forced-async downstream 2,640 2,440 -200
Async header-policy hit control 2,464 2,472 +8
Async header-policy miss control 2,368 2,376 +8

The allocation reduction is limited to the synchronous-policy/async-downstream path. Synchronous downstream and disabled controls are unchanged. Genuinely asynchronous custom-policy controls show a small +8 B/request tradeoff.

Real Kestrel proxy path

The BDN method sends a real loopback request through Kestrel, MapReverseProxy, and a Kestrel backend.

Scenario Parent B/request Final B/request Delta
HTTP/1.1 disabled 4,344 4,341 -3
HTTP/2 disabled 5,971 5,967 -4
HTTP/1.1 cookie hit 5,438 5,229 -209
HTTP/2 cookie hit 7,045 6,804 -241
HTTP/1.1 cookie miss 5,296 5,096 -200
HTTP/2 cookie miss 6,919 6,720 -199
HTTP/1.1 stale cookie 5,824 5,622 -202
HTTP/2 stale cookie 7,450 7,243 -207

Async header-policy controls varied from -14 to +40 B/request in the Kestrel run, consistent with no material allocation improvement for that path.

Timing was noisy and BDN's 5% equivalence test reported the main cookie and Kestrel comparisons as Same. No throughput or latency improvement is claimed. The supported result is the allocation reduction above.

Allocation trace

Exact-binary EventPipe traces sampled the parent SessionAffinityMiddleware.<InvokeInternal> state-machine box 1,747 times on misses and 2,467 times on hits. Final hit/miss traces sampled it zero times. Disabled traces sampled it zero times on both sides.

Correctness

Synchronous exceptions are converted back into returned tasks using ExceptionDispatchInfo, preserving the original async method behavior. Canceled operations remain canceled tasks. Faulted/canceled ValueTasks still take the await path, and each ValueTask is consumed once.

Tests cover:

  • cookie/header hit, miss, invalid, and stale keys;
  • destination reassignment and middleware ordering;
  • synchronous and asynchronous custom policies/failure policies;
  • exceptions from policies and downstream;
  • cancellation before and after suspension;
  • null downstream tasks, concurrency, and no double execution.

Validation:

  • 2,000 ReverseProxy unit tests passed per target framework and configuration
  • 75 affinity tests passed per target framework and configuration
  • 269 functional tests per target framework: 235 passed, 34 expected skips, 0 failures
  • Cross-platform CI passed at head d3627b95676a7f28c812616e4bb7ca652e2916a6 / merge 34b835fa480590f04c2d9dbc6eacc533a7cc5093: Windows (NetCore-Public 529), Ubuntu (Azure Pipelines 146), and macOS latest (Azure Pipelines 56); Docker, lint, and CLA checks also passed
  • No unresolved review threads or comments

Evidence and limitations

The standalone benchmark is a session artifact rather than a product-tree benchmark. It references the exact parent/final YARP DLLs and can run on any platform supported by BDN and YARP.

  • BDN source/report manifest SHA-256: 3549b8ad0eb4f28d5355683c145ec3a7c9e0d63f63b277d2b8f8fd95261a9590
  • BDN evidence archive SHA-256: 1a7477d2de7f6d214ead3a45be68ed36cb00fbc189716bac6610a0daea4caaa6
  • Component full JSON SHA-256: ae1c361dde8e58afb9535410beb56c7aff7ea497639234458da1c2b874b7b215
  • Kestrel full JSON SHA-256: 358cea3e7df9225f2967f244dcc075eab82351dcd805d549f217f1b87b055103

Cookie parsing and HttpContext.Items allocations are unchanged. This PR removes only the additional session-affinity middleware async frame.

Art Leonard added 2 commits July 12, 2026 13:56
Use completion-aware fast paths for synchronous affinity and failure policies while preserving asynchronous provider behavior and exception semantics.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 72d2ca2f-41bd-4159-aafd-717a139efea2
Cover cancellation that occurs after a custom affinity provider suspends, ensuring the middleware returns a canceled task.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 72d2ca2f-41bd-4159-aafd-717a139efea2
@artl93 artl93 changed the title WIP: DO NOT REVIEW Reduce session-affinity async allocations Reduce session-affinity async allocations Jul 19, 2026

@MihaZupan MihaZupan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I do not believe adding duplication/complexity like this is worth it, especially as runtime async is changing tradeoffs, potentially making it a deoptimization to manually split methods and check task.IsCompleted.

@artl93

artl93 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Thanks, @MihaZupan - closing out.

@artl93 artl93 closed this Jul 21, 2026
@artl93
artl93 deleted the artl93-reduce-affinity-allocations branch July 21, 2026 22:50
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