Browse Source

Fix thread-safety issue causing infinite loops in TypeHelper cache (#20113)

* Initial plan

* Fix thread-safety issue by replacing Dictionary with ConcurrentDictionary

Co-authored-by: grokys <1775141+grokys@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: grokys <1775141+grokys@users.noreply.github.com>

Fix RemoteProtocolTests
release/11.3.10
Copilot 2 months ago
committed by Julien Lebosquain
parent
commit
c20777321d
No known key found for this signature in database GPG Key ID: 1833CAD10ACC46FD
  1. 11
      src/Avalonia.Remote.Protocol/MetsysBson.cs
  2. 48
      tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs

11
src/Avalonia.Remote.Protocol/MetsysBson.cs

@ -30,6 +30,7 @@ Code imported from https://github.com/elaberge/Metsys.Bson without any changes
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@ -695,7 +696,7 @@ namespace Metsys.Bson
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = "Bson uses reflection")]
internal class TypeHelper
{
private static readonly IDictionary<Type, TypeHelper> _cachedTypeLookup = new Dictionary<Type, TypeHelper>();
private static readonly ConcurrentDictionary<Type, TypeHelper> _cachedTypeLookup = new ConcurrentDictionary<Type, TypeHelper>();
private static readonly BsonConfiguration _configuration = BsonConfiguration.Instance;
private readonly IDictionary<string, MagicProperty> _properties;
@ -725,13 +726,7 @@ namespace Metsys.Bson
public static TypeHelper GetHelperForType(Type type)
{
TypeHelper helper;
if (!_cachedTypeLookup.TryGetValue(type, out helper))
{
helper = new TypeHelper(type);
_cachedTypeLookup[type] = helper;
}
return helper;
return _cachedTypeLookup.GetOrAdd(type, t => new TypeHelper(t));
}
public static string FindProperty(LambdaExpression lambdaExpression)

48
tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs

@ -18,6 +18,8 @@ namespace Avalonia.DesignerSupport.Tests
{
public class RemoteProtocolTests : IDisposable
{
private const int TimeoutInMs = 1000;
private readonly List<IDisposable> _disposables = new List<IDisposable>();
private IAvaloniaRemoteTransportConnection _server;
private IAvaloniaRemoteTransportConnection _client;
@ -68,7 +70,7 @@ namespace Avalonia.DesignerSupport.Tests
object TakeServer()
{
var src = new CancellationTokenSource(200);
var src = new CancellationTokenSource(TimeoutInMs);
try
{
return _serverMessages.Take(src.Token);
@ -132,7 +134,7 @@ namespace Avalonia.DesignerSupport.Tests
foreach (var p in t.GetProperties())
p.SetValue(o, GetRandomValue(p.PropertyType, $"{t.FullName}.{p.Name}"));
_client.Send(o).Wait(200);
_client.Send(o).Wait(TimeoutInMs);
var received = TakeServer();
Helpers.StructDiff(received, o);
@ -160,6 +162,48 @@ namespace Avalonia.DesignerSupport.Tests
}
[Fact]
[SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method", Justification = "Sync context is explicitly disabled")]
void BsonSerializationIsThreadSafe()
{
Init();
// This test verifies that concurrent serialization doesn't cause infinite loops
// or corruption in the TypeHelper cache
var messages = Enumerable.Range(0, 100).Select(i => new MeasureViewportMessage
{
Width = i,
Height = i * 2
}).ToArray();
var tasks = new List<Task>();
var exceptions = new ConcurrentBag<Exception>();
// Spawn multiple threads that all try to serialize messages concurrently
for (int i = 0; i < 10; i++)
{
var task = Task.Run(() =>
{
try
{
foreach (var message in messages)
{
_client.Send(message).Wait(TimeoutInMs);
}
}
catch (Exception ex)
{
exceptions.Add(ex);
}
});
tasks.Add(task);
}
Task.WaitAll(tasks.ToArray(), TimeoutInMs * messages.Length * 10);
// Verify no exceptions occurred
Assert.Empty(exceptions);
}
public void Dispose()
{
_disposables.ForEach(d => d.Dispose());

Loading…
Cancel
Save