Skip to content

Commit fedf5ab

Browse files
Make getting lazy properties on ServiceConfig threadsafe (#1752)
Co-authored-by: Brennan <brecon@microsoft.com>
1 parent 1f5186e commit fedf5ab

File tree

4 files changed

+51
-9
lines changed

4 files changed

+51
-9
lines changed

src/Grpc.Net.Client/Internal/Configuration/ConfigProperty.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
namespace Grpc.Net.Client.Internal.Configuration
2222
{
23-
internal struct ConfigProperty<TValue, TInner> where TValue : IConfigValue
23+
internal sealed class ConfigProperty<TValue, TInner> where TValue : IConfigValue
2424
{
2525
private TValue? _value;
2626
private readonly Func<TInner?, TValue?> _valueFactory;
@@ -37,13 +37,23 @@ public ConfigProperty(Func<TInner?, TValue?> valueFactory, string key)
3737
{
3838
if (_value == null)
3939
{
40-
var innerValue = inner.GetValue<TInner>(_key);
41-
_value = _valueFactory(innerValue);
42-
43-
if (_value != null && innerValue == null)
40+
// Multiple threads can get a property at the same time. We want this to be safe.
41+
// Because a value could be lazily initialized, lock to ensure multiple threads
42+
// don't try to update the underlying dictionary at the same time.
43+
lock (this)
4444
{
45-
// Set newly created value
46-
SetValue(inner, _value);
45+
// Double-check locking.
46+
if (_value == null)
47+
{
48+
var innerValue = inner.GetValue<TInner>(_key);
49+
_value = _valueFactory(innerValue);
50+
51+
if (_value != null && innerValue == null)
52+
{
53+
// Set newly created value
54+
SetValue(inner, _value);
55+
}
56+
}
4757
}
4858
}
4959

src/Grpc.Net.Client/Internal/Configuration/Values.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
namespace Grpc.Net.Client.Internal.Configuration
2222
{
23-
internal class Values<T, TInner> : IList<T>, IConfigValue
23+
internal sealed class Values<T, TInner> : IList<T>, IConfigValue
2424
{
2525
internal readonly IList<TInner> Inner;
2626

test/Grpc.Net.Client.Tests/Balancer/ConnectionManagerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ public async Task UpdateAddresses_ConnectIsInProgress_InProgressConnectIsCancele
329329

330330
private static ConnectionManager CreateConnectionManager(
331331
ILoggerFactory loggerFactory,
332-
TestResolver resolver,
332+
Resolver resolver,
333333
TestSubchannelTransportFactory transportFactory)
334334
{
335335
return new ConnectionManager(

test/Grpc.Net.Client.Tests/ServiceConfigTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,38 @@ public void LoadBalancingConfig_CreateUnderlyingConfig()
137137
Assert.IsNotNull((IDictionary<string, object>)pickFirstConfig[LoadBalancingConfig.PickFirstPolicyName]);
138138
}
139139

140+
[Test]
141+
public async Task ServiceConfig_LoadBalancingConfigs_ConcurrentAccess()
142+
{
143+
// Arrange & Act
144+
for (var i = 0; i < 100; i++)
145+
{
146+
await AccessServiceConfigConcurrently();
147+
}
148+
}
149+
150+
private static async Task AccessServiceConfigConcurrently()
151+
{
152+
var serviceConfig = new ServiceConfig();
153+
154+
List<Task> connectTasks = new List<Task>();
155+
for (var i = 0; i < 100; i++)
156+
{
157+
connectTasks.Add(Task.Run(ConnectAsync));
158+
}
159+
160+
await Task.WhenAll(connectTasks);
161+
162+
void ConnectAsync()
163+
{
164+
// Excuse to access LoadBalancingConfigs
165+
if (serviceConfig.LoadBalancingConfigs.Count > 0)
166+
{
167+
serviceConfig.ToString();
168+
}
169+
}
170+
}
171+
140172
[Test]
141173
public void LoadBalancingConfig_ReadUnderlyingConfig()
142174
{

0 commit comments

Comments
 (0)