Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 17 additions & 7 deletions src/Grpc.Net.Client/Internal/Configuration/ConfigProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

namespace Grpc.Net.Client.Internal.Configuration
{
internal struct ConfigProperty<TValue, TInner> where TValue : IConfigValue
internal sealed class ConfigProperty<TValue, TInner> where TValue : IConfigValue
{
private TValue? _value;
private readonly Func<TInner?, TValue?> _valueFactory;
Expand All @@ -37,13 +37,23 @@ public ConfigProperty(Func<TInner?, TValue?> valueFactory, string key)
{
if (_value == null)
{
var innerValue = inner.GetValue<TInner>(_key);
_value = _valueFactory(innerValue);

if (_value != null && innerValue == null)
// Multiple threads can get a property at the same time. We want this to be safe.
// Because a value could be lazily initialized, lock to ensure multiple threads
// don't try to update the underlying dictionary at the same time.
lock (this)
{
// Set newly created value
SetValue(inner, _value);
// Double-check locking.
if (_value == null)
{
var innerValue = inner.GetValue<TInner>(_key);
_value = _valueFactory(innerValue);
Copy link

@halter73 halter73 May 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably should have asked this before approving, but can _valueFactory call into user code? Or inner.GetValue/SetValue? This reminds me a little bit of the locks I took in ConfigurationManager that caused dotnet/runtime#61747.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's internal.


if (_value != null && innerValue == null)
{
// Set newly created value
SetValue(inner, _value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the SetValue function also lock since it's public and called by other code?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfigProperty and IConfigValue are both internal so I think it's okay. If this was ever exposed anywhere, locking on this would be bad. But unless it's being exposed as object, I don't see how anyone would easily get a reference.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the SetValue function also lock since it's public and called by other code?

The goal is to make the type thread-safe for reads, e.g. multiple threads can do this without error:

if (serviceConfig.LoadBalancingConfigs.Count > 0)
{
    // ....
}

If someone calls SetValue value by setting a property on another thread, e.g. serviceConfig.LoadBalancingConfigs = new List<LoadBalancingConfig>() then an error is fine.

}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Grpc.Net.Client/Internal/Configuration/Values.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

namespace Grpc.Net.Client.Internal.Configuration
{
internal class Values<T, TInner> : IList<T>, IConfigValue
internal sealed class Values<T, TInner> : IList<T>, IConfigValue
{
internal readonly IList<TInner> Inner;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ public async Task UpdateAddresses_ConnectIsInProgress_InProgressConnectIsCancele

private static ConnectionManager CreateConnectionManager(
ILoggerFactory loggerFactory,
TestResolver resolver,
Resolver resolver,
TestSubchannelTransportFactory transportFactory)
{
return new ConnectionManager(
Expand Down
32 changes: 32 additions & 0 deletions test/Grpc.Net.Client.Tests/ServiceConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,38 @@ public void LoadBalancingConfig_CreateUnderlyingConfig()
Assert.IsNotNull((IDictionary<string, object>)pickFirstConfig[LoadBalancingConfig.PickFirstPolicyName]);
}

[Test]
public async Task ServiceConfig_LoadBalancingConfigs_ConcurrentAccess()
{
// Arrange & Act
for (var i = 0; i < 100; i++)
{
await AccessServiceConfigConcurrently();
}
}

private static async Task AccessServiceConfigConcurrently()
{
var serviceConfig = new ServiceConfig();

List<Task> connectTasks = new List<Task>();
for (var i = 0; i < 100; i++)
{
connectTasks.Add(Task.Run(ConnectAsync));
}

await Task.WhenAll(connectTasks);

void ConnectAsync()
{
// Excuse to access LoadBalancingConfigs
if (serviceConfig.LoadBalancingConfigs.Count > 0)
{
serviceConfig.ToString();
}
}
}

[Test]
public void LoadBalancingConfig_ReadUnderlyingConfig()
{
Expand Down