Skip to content

Commit e48a036

Browse files
authored
Resolver has separate error for fetching service config (#1475)
1 parent 00f7601 commit e48a036

File tree

7 files changed

+228
-21
lines changed

7 files changed

+228
-21
lines changed

src/Grpc.Net.Client/Balancer/DnsResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ private async Task RefreshCoreAsync(CancellationToken cancellationToken)
147147

148148
var resolvedPort = _address.Port == -1 ? 80 : _address.Port;
149149
var endpoints = addresses.Select(a => new BalancerAddress(a.ToString(), resolvedPort)).ToArray();
150-
var resolverResult = ResolverResult.ForResult(endpoints, serviceConfig: null);
150+
var resolverResult = ResolverResult.ForResult(endpoints);
151151
_listener(resolverResult);
152152
}
153153
catch (Exception ex)

src/Grpc.Net.Client/Balancer/Internal/ConnectionManager.cs

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ namespace Grpc.Net.Client.Balancer.Internal
3535
{
3636
internal class ConnectionManager : IDisposable, IChannelControlHelper
3737
{
38+
private static readonly ServiceConfig DefaultServiceConfig = new ServiceConfig();
39+
3840
private readonly SemaphoreSlim _nextPickerLock;
3941
private readonly object _lock;
4042
internal readonly Resolver _resolver;
@@ -50,6 +52,7 @@ internal class ConnectionManager : IDisposable, IChannelControlHelper
5052
private Task? _resolveTask;
5153
private TaskCompletionSource<SubchannelPicker> _nextPickerTcs;
5254
private int _currentSubchannelId;
55+
private ServiceConfig? _previousServiceConfig;
5356

5457
internal ConnectionManager(
5558
Resolver resolver,
@@ -148,30 +151,65 @@ private void OnResolverResult(ResolverResult result)
148151
throw new InvalidOperationException($"Load balancer not configured.");
149152
}
150153

154+
var channelStatus = result.Status;
155+
151156
// https://github.com/grpc/proposal/blob/master/A21-service-config-error-handling.md
152-
// 1. Only use resolved service config if not disabled.
153-
// 2. Only use resolved service config if valid. This includes having a factory for one of the load balancing configs.
157+
// Additionally, only use resolved service config if not disabled.
154158
LoadBalancingConfig? loadBalancingConfig = null;
155-
if (result.ServiceConfig != null)
159+
if (!DisableResolverServiceConfig)
156160
{
157-
if (!DisableResolverServiceConfig)
161+
ServiceConfig? workingServiceConfig = null;
162+
if (result.ServiceConfig == null)
158163
{
159-
if (result.ServiceConfig.LoadBalancingConfigs.Count > 0)
164+
// Step 4 and 5
165+
if (result.ServiceConfigStatus == null)
166+
{
167+
// Step 5: Use default service config if none is provided.
168+
_previousServiceConfig = DefaultServiceConfig;
169+
workingServiceConfig = DefaultServiceConfig;
170+
}
171+
else
160172
{
161-
if (!ChildHandlerLoadBalancer.TryGetValidServiceConfigFactory(result.ServiceConfig.LoadBalancingConfigs, LoadBalancerFactories, out loadBalancingConfig, out var _))
173+
// Step 4
174+
if (_previousServiceConfig == null)
162175
{
163-
ConnectionManagerLog.ResolverUnsupportedLoadBalancingConfig(Logger, result.ServiceConfig.LoadBalancingConfigs);
176+
// Step 4.ii: If no config was provided or set previously, then treat resolution as a failure.
177+
channelStatus = result.ServiceConfigStatus.GetValueOrDefault();
178+
}
179+
else
180+
{
181+
// Step 4.i: Continue using previous service config if it was set and a new one is not provided.
182+
workingServiceConfig = _previousServiceConfig;
183+
ConnectionManagerLog.ResolverServiceConfigFallback(Logger, result.ServiceConfigStatus.GetValueOrDefault());
164184
}
165185
}
166186
}
167187
else
188+
{
189+
// Step 3: Use provided service config if it is set.
190+
workingServiceConfig = result.ServiceConfig;
191+
_previousServiceConfig = result.ServiceConfig;
192+
}
193+
194+
195+
if (workingServiceConfig?.LoadBalancingConfigs.Count > 0)
196+
{
197+
if (!ChildHandlerLoadBalancer.TryGetValidServiceConfigFactory(workingServiceConfig.LoadBalancingConfigs, LoadBalancerFactories, out loadBalancingConfig, out var _))
198+
{
199+
ConnectionManagerLog.ResolverUnsupportedLoadBalancingConfig(Logger, workingServiceConfig.LoadBalancingConfigs);
200+
}
201+
}
202+
}
203+
else
204+
{
205+
if (result.ServiceConfig != null)
168206
{
169207
ConnectionManagerLog.ResolverServiceConfigNotUsed(Logger);
170208
}
171209
}
172210

173211
var state = new ChannelState(
174-
result.Status,
212+
channelStatus,
175213
result.Addresses,
176214
loadBalancingConfig,
177215
BalancerAttributes.Empty);
@@ -513,6 +551,9 @@ internal static class ConnectionManagerLog
513551
private static readonly Action<ILogger, Exception?> _pickWaiting =
514552
LoggerMessage.Define(LogLevel.Trace, new EventId(14, "PickWaiting"), "Waiting for a new picker.");
515553

554+
private static readonly Action<ILogger, Status, Exception?> _resolverServiceConfigFallback =
555+
LoggerMessage.Define<Status>(LogLevel.Debug, new EventId(15, "ResolverServiceConfigFallback"), "Falling back to previously loaded service config. Resolver failure when retreiving or parsing service config with status: {Status}");
556+
516557
public static void ResolverRefreshRequested(ILogger logger)
517558
{
518559
_resolverRefreshRequested(logger, null);
@@ -586,6 +627,11 @@ public static void PickWaiting(ILogger logger)
586627
{
587628
_pickWaiting(logger, null);
588629
}
630+
631+
public static void ResolverServiceConfigFallback(ILogger logger, Status status)
632+
{
633+
_resolverServiceConfigFallback(logger, status, null);
634+
}
589635
}
590636
}
591637
#endif

src/Grpc.Net.Client/Balancer/Resolver.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,12 @@ public sealed class ResolverResult
9999
private BalancerAttributes? _attributes;
100100

101101
[DebuggerStepThrough]
102-
private ResolverResult(Status status, IReadOnlyList<BalancerAddress>? addresses, ServiceConfig? serviceConfig)
102+
private ResolverResult(Status status, IReadOnlyList<BalancerAddress>? addresses, ServiceConfig? serviceConfig, Status? serviceConfigStatus)
103103
{
104104
Status = status;
105105
Addresses = addresses;
106106
ServiceConfig = serviceConfig;
107+
ServiceConfigStatus = serviceConfigStatus;
107108
}
108109

109110
/// <summary>
@@ -121,6 +122,11 @@ private ResolverResult(Status status, IReadOnlyList<BalancerAddress>? addresses,
121122
/// </summary>
122123
public ServiceConfig? ServiceConfig { get; }
123124

125+
/// <summary>
126+
/// Gets an optional service config status.
127+
/// </summary>
128+
public Status? ServiceConfigStatus { get; }
129+
124130
/// <summary>
125131
/// Gets metadata attributes.
126132
/// </summary>
@@ -139,19 +145,36 @@ public static ResolverResult ForFailure(Status status)
139145
throw new ArgumentException("Error status code must not be OK.", nameof(status));
140146
}
141147

142-
return new ResolverResult(status, addresses: null, serviceConfig: null);
148+
return new ResolverResult(status, addresses: null, serviceConfig: null, serviceConfigStatus: null);
143149
}
144150

145151
/// <summary>
146152
/// Create <see cref="ResolverResult"/> for the specified addresses.
147153
/// </summary>
148154
/// <param name="addresses">The resolved addresses.</param>
149-
/// <param name="serviceConfig">An optional service config.</param>
150155
/// <returns>A resolver result.</returns>
151156
[DebuggerStepThrough]
152-
public static ResolverResult ForResult(IReadOnlyList<BalancerAddress> addresses, ServiceConfig? serviceConfig)
157+
public static ResolverResult ForResult(IReadOnlyList<BalancerAddress> addresses)
158+
{
159+
return new ResolverResult(Status.DefaultSuccess, addresses, serviceConfig: null, serviceConfigStatus: null);
160+
}
161+
162+
/// <summary>
163+
/// Create <see cref="ResolverResult"/> for the specified addresses and service config.
164+
/// </summary>
165+
/// <param name="addresses">The resolved addresses.</param>
166+
/// <param name="serviceConfig">An optional service config. A <c>null</c> value indicates that the resolver either didn't retreive a service config or an error occurred. The error must be specified using <paramref name="serviceConfigStatus"/>.</param>
167+
/// <param name="serviceConfigStatus">A service config status. The status indicates an error retreiveing or parsing the config. The status must not be <see cref="StatusCode.OK"/> if no service config is specified.</param>
168+
/// <returns>A resolver result.</returns>
169+
[DebuggerStepThrough]
170+
public static ResolverResult ForResult(IReadOnlyList<BalancerAddress> addresses, ServiceConfig? serviceConfig, Status? serviceConfigStatus)
153171
{
154-
return new ResolverResult(Status.DefaultSuccess, addresses, serviceConfig);
172+
if (serviceConfigStatus?.StatusCode == StatusCode.OK && serviceConfig == null)
173+
{
174+
throw new ArgumentException("Service config status code must not be OK when there is no service config.", nameof(serviceConfigStatus));
175+
}
176+
177+
return new ResolverResult(Status.DefaultSuccess, addresses, serviceConfig, serviceConfigStatus);
155178
}
156179
}
157180
}

src/Grpc.Net.Client/Balancer/StaticResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public override Task RefreshAsync(CancellationToken cancellationToken)
5959
throw new InvalidOperationException("Resolver hasn't been started.");
6060
}
6161

62-
_listener(ResolverResult.ForResult(_addresses, serviceConfig: null));
62+
_listener(ResolverResult.ForResult(_addresses));
6363
return Task.CompletedTask;
6464
}
6565

test/FunctionalTests/Balancer/PickFirstBalancerTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using System.Linq;
2424
using System.Net;
2525
using System.Net.Http;
26+
using System.Net.Sockets;
2627
using System.Threading.Tasks;
2728
using FunctionalTestsWebsite;
2829
using Google.Protobuf;
@@ -102,6 +103,46 @@ Task<HelloReply> UnaryMethod(HelloRequest request, ServerCallContext context)
102103
Assert.AreEqual("127.0.0.1:50051", host);
103104
}
104105

106+
[Test]
107+
public async Task UnaryCall_SingleAddressFailure_RpcError()
108+
{
109+
// Ignore errors
110+
SetExpectedErrorsFilter(writeContext =>
111+
{
112+
return true;
113+
});
114+
115+
string? host = null;
116+
Task<HelloReply> UnaryMethod(HelloRequest request, ServerCallContext context)
117+
{
118+
host = context.Host;
119+
return Task.FromResult(new HelloReply { Message = request.Name });
120+
}
121+
122+
// Arrange
123+
using var endpoint = BalancerHelpers.CreateGrpcEndpoint<HelloRequest, HelloReply>(50051, UnaryMethod, nameof(UnaryMethod));
124+
125+
var channel = await BalancerHelpers.CreateChannel(LoggerFactory, new PickFirstConfig(), new[] { endpoint.Address }).DefaultTimeout();
126+
127+
var client = TestClientFactory.Create(channel, endpoint.Method);
128+
129+
// Act
130+
var reply = await client.UnaryCall(new HelloRequest { Name = "Balancer" }).ResponseAsync.DefaultTimeout();
131+
132+
// Assert
133+
Assert.AreEqual("Balancer", reply.Message);
134+
Assert.AreEqual("127.0.0.1:50051", host);
135+
136+
Logger.LogInformation("Ending " + endpoint.Address);
137+
endpoint.Dispose();
138+
139+
var ex = await ExceptionAssert.ThrowsAsync<RpcException>(
140+
() => client.UnaryCall(new HelloRequest { Name = "Balancer" }).ResponseAsync).DefaultTimeout();
141+
142+
Assert.AreEqual(StatusCode.Unavailable, ex.StatusCode);
143+
Assert.IsInstanceOf(typeof(SocketException), ex.Status.DebugException);
144+
}
145+
105146
[Test]
106147
public async Task UnaryCall_UnavailableAddress_FallbackToWorkingAddress()
107148
{

0 commit comments

Comments
 (0)