Repository navigation
Fix log buffering race by removing the rule selector cache - #7803
Conversation
โฆCache LogBufferingFilterRuleSelector is a singleton shared by all global and per-request log buffers. Its cache stored pooled List<T> instances that InvalidateCache() returned to the pool (clearing them) while concurrent Select() calls could still be enumerating them. Under load this threw "Collection was modified; enumeration operation may not execute", NullReferenceException, or silently produced wrong buffering decisions. Cache immutable arrays instead, so InvalidateCache() only needs to clear the dictionary and readers can safely finish enumerating evicted values. Fixes #7383 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the timing-based Parallel.For test with a single-threaded test that invalidates the cache while Select() iterates over the cached rules, and assert explicitly that no exception is thrown. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
๐ Good job! The coverage increased ๐
Full code coverage report: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1623526&view=codecoverage-tab |
The cache was cleared at the end of every request, so it rarely produced hits, while it allocated on every Select() call, contended under concurrent load, and ignored the logger category in its key. Select() is now a static, single pass over the category's rules array: no shared state to race on, nothing to invalidate, and no allocations. InvalidateCache(), the selector's DI registration and constructor parameters, and the Dispose methods that only invalidated the cache are removed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
๐ข Approval recommended
The shared mutable state is fully removed, callers are consistently updated, and targeted regression coverage verifies category isolation.
Review effort: Balanced
Findings: None
What changed in this PR
Removes the shared rule-selector cache to eliminate log-buffering races and incorrect cross-category rule reuse.
Changes:
- Replaces cached rule selection with stateless array iteration.
- Removes obsolete selector injection, invalidation, and disposal paths.
- Adds regression coverage for category-specific rules.
| File | Description |
|---|---|
test/โLibraries/โMicrosoft.Extensions.Telemetry.Tests/โBuffering/โLogBufferingFilterRuleTests.cs |
Updates selector calls and adds regression coverage. |
src/โLibraries/โMicrosoft.Extensions.Telemetry/โBuffering/โLogBufferingFilterRuleSelector.cs |
Makes rule selection static and cache-free. |
src/โLibraries/โMicrosoft.Extensions.Telemetry/โBuffering/โGlobalLogBufferManager.cs |
Removes selector dependency. |
src/โLibraries/โMicrosoft.Extensions.Telemetry/โBuffering/โGlobalBufferLoggingBuilderExtensions.cs |
Removes obsolete DI registration. |
src/โLibraries/โMicrosoft.Extensions.Telemetry/โBuffering/โGlobalBuffer.cs |
Uses stateless selection and removes invalidation. |
src/โLibraries/โMicrosoft.AspNetCore.Diagnostics.Middleware/โBuffering/โPerRequestLogBufferManager.cs |
Removes selector propagation. |
src/โLibraries/โMicrosoft.AspNetCore.Diagnostics.Middleware/โBuffering/โIncomingRequestLogBufferHolder.cs |
Removes cache-only disposal behavior. |
src/โLibraries/โMicrosoft.AspNetCore.Diagnostics.Middleware/โBuffering/โIncomingRequestLogBuffer.cs |
Uses static selection and drops cache-only disposal. |
๐ก Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
๐ Good job! The coverage increased ๐
Full code coverage report: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1623814&view=codecoverage-tab |
LogBufferingFilterRuleSelectoris a singleton shared by every global and per-request log buffer. Under concurrent load, per-request log buffering throwsInvalidOperationException: Collection was modified; enumeration operation may not executefromSelect(), which makes the feature unusable in production.Fixes: #7383
Root cause
The selector cached rule candidates per
(LogLevel, EventId)in pooledList<LogBufferingFilterRule>instances.InvalidateCache()returned those lists to the pool, which clears them, while concurrentSelect()calls could still be iterating them.InvalidateCache()runs every time a request ends: the scopedIncomingRequestLogBufferHolderdisposes each per-categoryIncomingRequestLogBuffer, and each of them invalidated the shared cache. Measured withTestServer, the cache was cleared once per request per category, so it rarely helped:Even a cache hit allocated a closure (56 B per log call), and the cache key ignored the logger category, so one category could get candidates computed from another category's rules.
Fix
Remove the cache.
Select()is now a static, single pass over the category's rules (theLogBufferingFilterRule[]each buffer already computes), with the same matching order and result as before. With no shared state there is nothing to race on, nothing to invalidate, and no per-call allocation.InvalidateCache(), the selector's DI registration and constructor parameters, and theDisposemethods that only existed to invalidate the cache are removed.Benchmarks (BenchmarkDotNet MediumRun, .NET 10, Apple M3 Max), per log call:
Alternatives that keep a cache were measured as well. With 8 threads sharing one selector, an immutable-array cache and a pooled-list cache behind a
ReaderWriterLockSlimcost 80-280 ns per call from contention on cache clears, versus 1-8 ns without a cache. The only scenario where a cache won was a warm cache with 10 rules (7.4 ns vs 15.5 ns); with request-end invalidation it lost again (29.3 ns vs 14.6 ns). Taking an array instead ofIList<T>matters: theIList<T>version took 51 ns in the 10-rule request scenario.Testing
SelectsRightRule_WhenCategoriesHaveDifferentRules: the same scenario returned the other category's rule with the cached implementation.Microsoft.Extensions.Telemetry.Tests(341 tests) andMicrosoft.AspNetCore.Diagnostics.Middleware.Tests(188 tests) pass on net9.0 and net10.0.Microsoft Reviewers: Open in CodeFlow