Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

build:
name: Basic Tests
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:

- name: Check out code
Expand All @@ -36,7 +36,7 @@ jobs:

grpc_web:
name: gRPC-Web Tests
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:

- name: Check out code
Expand Down
5 changes: 2 additions & 3 deletions src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

#endregion

using System.Collections.Concurrent;
using System.Diagnostics;
using Grpc.Core;
#if SUPPORT_LOAD_BALANCING
Expand Down Expand Up @@ -51,7 +50,7 @@ public sealed partial class GrpcChannel : ChannelBase, IDisposable
internal const long DefaultMaxRetryBufferPerCallSize = 1024 * 1024; // 1 MB

private readonly object _lock;
private readonly ConcurrentDictionary<IMethod, GrpcMethodInfo> _methodInfoCache;
private readonly ThreadSafeLookup<IMethod, GrpcMethodInfo> _methodInfoCache;
private readonly Func<IMethod, GrpcMethodInfo> _createMethodInfoFunc;
private readonly Dictionary<MethodKey, MethodConfig>? _serviceConfigMethods;
private readonly bool _isSecure;
Expand Down Expand Up @@ -109,7 +108,7 @@ public sealed partial class GrpcChannel : ChannelBase, IDisposable
internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(address.Authority)
{
_lock = new object();
_methodInfoCache = new ConcurrentDictionary<IMethod, GrpcMethodInfo>();
_methodInfoCache = new ThreadSafeLookup<IMethod, GrpcMethodInfo>();

// Dispose the HTTP client/handler if...
// 1. No client/handler was specified and so the channel created the client itself
Expand Down
104 changes: 104 additions & 0 deletions src/Grpc.Net.Client/Internal/ThreadSafeLookup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using System.Collections.Concurrent;

internal sealed class ThreadSafeLookup<TKey, TValue> where TKey : notnull
{
// Avoid allocating ConcurrentDictionary until the threshold is reached.
// Looking up a key in an array is as fast as a dictionary for small collections and uses much less memory.
internal const int Threshold = 10;

private KeyValuePair<TKey, TValue>[] _array = Array.Empty<KeyValuePair<TKey, TValue>>();
private ConcurrentDictionary<TKey, TValue>? _dictionary;

/// <summary>
/// Gets the value for the key if it exists. If the key does not exist then the value is created using the valueFactory.
/// The value is created outside of a lock and there is no guarentee which value will be stored or returned.
/// </summary>
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
if (_dictionary != null)
{
return _dictionary.GetOrAdd(key, valueFactory);
}

if (TryGetValue(_array, key, out var value))
{
return value;
}

var newValue = valueFactory(key);

lock (this)
{
if (_dictionary != null)
{
_dictionary.TryAdd(key, newValue);
}
else
{
// Double check inside lock if the key was added to the array by another thread.
if (TryGetValue(_array, key, out value))
{
return value;
}

if (_array.Length + 1 > Threshold)
{
// Array length exceeds threshold so switch to dictionary.
var newDict = new ConcurrentDictionary<TKey, TValue>();
foreach (var kvp in _array)
{
newDict.TryAdd(kvp.Key, kvp.Value);
}
newDict.TryAdd(key, newValue);

_dictionary = newDict;
_array = Array.Empty<KeyValuePair<TKey, TValue>>();
}
else
{
// Add new value by creating a new array with old plus new value.
var newArray = new KeyValuePair<TKey, TValue>[_array.Length + 1];
Array.Copy(_array, newArray, _array.Length);
newArray[newArray.Length - 1] = new KeyValuePair<TKey, TValue>(key, newValue);

_array = newArray;
}
}
}

return newValue;
}

private static bool TryGetValue(KeyValuePair<TKey, TValue>[] array, TKey key, out TValue value)
{
foreach (var kvp in array)
{
if (EqualityComparer<TKey>.Default.Equals(kvp.Key, key))
{
value = kvp.Value;
return true;
}
}

value = default!;
return false;
}
}
69 changes: 69 additions & 0 deletions test/Grpc.Net.Client.Tests/ThreadSafeLookupTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

namespace Grpc.Net.Client.Tests;

[TestFixture]
public class ThreadSafeLookupTests
{
[Test]
public void GetOrAdd_ReturnsCorrectValueForNewKey()
{
var lookup = new ThreadSafeLookup<int, string>();
var result = lookup.GetOrAdd(1, k => "Value-1");

Assert.AreEqual("Value-1", result);
}

[Test]
public void GetOrAdd_ReturnsExistingValueForExistingKey()
{
var lookup = new ThreadSafeLookup<int, string>();
lookup.GetOrAdd(1, k => "InitialValue");
var result = lookup.GetOrAdd(1, k => "NewValue");

Assert.AreEqual("InitialValue", result);
}

[Test]
public void GetOrAdd_SwitchesToDictionaryAfterThreshold()
{
var addCount = (ThreadSafeLookup<int, string>.Threshold * 2);
var lookup = new ThreadSafeLookup<int, string>();

for (var i = 0; i <= addCount; i++)
{
lookup.GetOrAdd(i, k => $"Value-{k}");
}

var result = lookup.GetOrAdd(addCount, k => $"NewValue-{addCount}");

Assert.AreEqual($"Value-{addCount}", result);
}

[Test]
public void GetOrAdd_HandlesConcurrentAccess()
{
var lookup = new ThreadSafeLookup<int, string>();
Parallel.For(0, 1000, i =>
{
var value = lookup.GetOrAdd(i, k => $"Value-{k}");
Assert.AreEqual($"Value-{i}", value);
});
}
}