Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 2, 2025

ConcurrentDictionary enumeration throws when the dictionary is modified during iteration. The Compact() method was enumerating with LINQ's OrderBy() while concurrent Set() calls could modify the dictionary, causing intermittent ArgumentException in CI.

Changes

Snapshot the dictionary before ordering to isolate the LINQ enumeration from concurrent modifications:

// Before
var kvps = _dict.OrderBy(x => x.Value.LastAccess).ToArray();

// After  
var kvps = _dict.ToArray().OrderBy(x => x.Value.LastAccess).ToArray();

The initial ToArray() creates a point-in-time snapshot of the dictionary entries, preventing the enumeration exception while maintaining intended compaction behavior.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 2kmvsblobprodcus39.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • 2zrvsblobprodcus388.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • 7devsblobprodcus323.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • 7tjvsblobprodcus341.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • bcnvsblobprodcus378.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • frdvsblobprodcus327.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • ibzvsblobprodcus369.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • imzvsblobprodcus368.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • josvsblobprodcus372.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • kgfvsblobprodcus314.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • kh4vsblobprodcus325.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • kijvsblobprodcus387.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • l49vsblobprodcus358.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • s4uvsblobprodcus326.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)
  • u3hvsblobprodcus371.vsblob.vsassets.io
    • Triggering command: dotnet build src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Race Condition in MemoryCache.Compact() Causes Intermittent Test Failures</issue_title>
<issue_description># Race Condition in MemoryCache.Compact() Causes Intermittent Test Failures

Summary

The MemoryCache<TKey, TValue>.Compact() method has a race condition that causes intermittent test failures when the cache is accessed concurrently. The method enumerates a ConcurrentDictionary while it can be modified by other threads, violating thread-safety guarantees.

Environment

Steps to Reproduce

  1. Clone the repository
  2. Run ./restore.sh
  3. Run ./build.sh -test
  4. Observe intermittent test failure in Microsoft.CodeAnalysis.Razor.Workspaces.Test

Note: This is a race condition, so it may not reproduce consistently.

Expected Behavior

The ConcurrentSets_DoesNotThrow test should pass consistently.

Actual Behavior

Test intermittently fails with:

System.ArgumentException : The index is equal to or greater than the length of the array, or the number of elements in the dictionary is greater than the available space from index to the end of the destination array.

Stack Trace

at System.Collections.Concurrent.ConcurrentDictionary`2.System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>.CopyTo(KeyValuePair`2[] array, Int32 index)
at System.Linq.Enumerable.ICollectionToArray[TSource](ICollection`1 collection)
at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
at System.Linq.Enumerable.OrderedIterator`1.ToArray()
at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
at Microsoft.CodeAnalysis.Razor.Utilities.MemoryCache`2.Compact()
at Microsoft.CodeAnalysis.Razor.Utilities.MemoryCache`2.Set(TKey key, TValue value)

Test Results

Before fix:

Microsoft.CodeAnalysis.Razor.Workspaces.Test  Total: 635, Errors: 0, Failed: 1, Skipped: 13

After fix:

Build succeeded in 683.8s (all tests pass)

Root Cause

In src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Utilities/MemoryCache2.csat line 67, theCompact()method enumerates theConcurrentDictionary` with LINQ:

protected virtual void Compact()
{
    var kvps = _dict.OrderBy(x => x.Value.LastAccess).ToArray();

    for (var i = 0; i < _sizeLimit / 2; i++)
    {
        _dict.Remove(kvps[i].Key);
    }
}

The problem:

  1. Multiple threads call Set() concurrently
  2. Set() can trigger Compact() when the cache is full
  3. Compact() enumerates the ConcurrentDictionary with .OrderBy().ToArray()
  4. While enumerating, another thread modifies the dictionary via Set()
  5. The enumeration throws ArgumentException

While ConcurrentDictionary is thread-safe for individual operations, enumerating it while it's being modified can throw exceptions. This is documented behavior of ConcurrentDictionary.

Solution

Snapshot the dictionary before ordering to prevent enumeration exceptions:

protected virtual void Compact()
{
    var kvps = _dict.ToArray().OrderBy(x => x.Value.LastAccess).ToArray();

    for (var i = 0; i < _sizeLimit / 2; i++)
    {
        _dict.Remove(kvps[i].Key);
    }
}

Fix

File: src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Utilities/MemoryCache2.cs`
Line: 67

Before:

var kvps = _dict.OrderBy(x => x.Value.LastAccess).ToArray();

After:

var kvps = _dict.ToArray().OrderBy(x => x.Value.LastAccess).ToArray();

The initial .ToArray() creates a snapshot of the dictionary at that moment, preventing concurrent modifications from affecting the LINQ enumeration.

Additional Context

  • This is a flaky/intermittent test - it depends on thread timing
  • The test ConcurrentSets_DoesNotThrow is specifically designed to test concurrent access
  • The fix adds minimal overhead (one array allocation) but ensures thread-safety during compaction
  • The issue may be more apparent on certain systems/runtimes depending on scheduling

Impact

  • Current: Intermittent test failures that can block CI/development
  • After Fix: Consistent test passes
  • Risk: Low - the change properly handles concurrent access as intended by the test

References

Comments on the Issue (you are @copilot in this section)

@davidwengier This one fails on my machine regularly, but it's easy enough to ignore. Interestingly I've never seen it fail in CI on our Ubuntu legs.

I presume this was gener...


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

…y before ordering

Co-authored-by: davidwengier <754264+davidwengier@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix race condition in MemoryCache.Compact() method Fix race condition in MemoryCache.Compact() enumeration Nov 2, 2025
Copilot AI requested a review from davidwengier November 2, 2025 05:59
@davidwengier davidwengier deleted the copilot/fix-race-condition-memorycache branch December 11, 2025 02: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.

Race Condition in MemoryCache.Compact() Causes Intermittent Test Failures

2 participants