Skip to content

Commit f29c274

Browse files
authored
Handle errors in service methods (#96)
1 parent 9e63dc0 commit f29c274

33 files changed

+510
-269
lines changed

perf/Grpc.AspNetCore.Microbenchmarks/Internal/MessageHelpers.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,22 @@
2121
using Google.Protobuf;
2222
using Grpc.AspNetCore.Server;
2323
using Grpc.AspNetCore.Server.Internal;
24+
using Microsoft.AspNetCore.Http;
25+
using Microsoft.Extensions.Logging.Abstractions;
2426

2527
namespace Grpc.AspNetCore.Microbenchmarks.Internal
2628
{
2729
internal static class MessageHelpers
2830
{
29-
private static readonly GrpcServiceOptions TestServiceOptions = new GrpcServiceOptions();
31+
private static readonly HttpContextServerCallContext TestServerCallContext = new HttpContextServerCallContext(new DefaultHttpContext(), new GrpcServiceOptions(), NullLogger.Instance);
3032

3133
public static void WriteMessage<T>(Stream stream, T message) where T : IMessage
3234
{
3335
var messageData = message.ToByteArray();
3436

3537
var pipeWriter = new StreamPipeWriter(stream);
3638

37-
PipeExtensions.WriteMessageAsync(pipeWriter, messageData, TestServiceOptions, flush: true).GetAwaiter().GetResult();
39+
PipeExtensions.WriteMessageAsync(pipeWriter, messageData, TestServerCallContext, flush: true).GetAwaiter().GetResult();
3840
}
3941
}
4042
}

perf/Grpc.AspNetCore.Microbenchmarks/UnaryServerCallHandlerBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void GlobalSetup()
4646
{
4747
var marshaller = Marshallers.Create((arg) => MessageExtensions.ToByteArray(arg), bytes => new ChatMessage());
4848
var method = new Method<ChatMessage, ChatMessage>(MethodType.Unary, typeof(TestService).FullName, nameof(TestService.SayHello), marshaller, marshaller);
49-
_callHandler = new UnaryServerCallHandler<ChatMessage, ChatMessage, TestService>(method, new GrpcServiceOptions(), NullLoggerFactory.Instance);
49+
_callHandler = new UnaryServerCallHandler<ChatMessage, ChatMessage, TestService>(method, new GrpcServiceOptions<TestService>(), NullLoggerFactory.Instance);
5050

5151
_trailers = new HeaderDictionary();
5252

src/Grpc.AspNetCore.Server/Internal/ClientStreamingServerCallHandler.cs renamed to src/Grpc.AspNetCore.Server/Internal/CallHandlers/ClientStreamingServerCallHandler.cs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
using Microsoft.Extensions.DependencyInjection;
2424
using Microsoft.Extensions.Logging;
2525

26-
namespace Grpc.AspNetCore.Server.Internal
26+
namespace Grpc.AspNetCore.Server.Internal.CallHandlers
2727
{
2828
internal class ClientStreamingServerCallHandler<TRequest, TResponse, TService> : ServerCallHandlerBase<TRequest, TResponse, TService>
2929
where TRequest : class
@@ -49,35 +49,45 @@ public override async Task HandleCallAsync(HttpContext httpContext)
4949
httpContext.Response.ContentType = "application/grpc";
5050
httpContext.Response.Headers.Append("grpc-encoding", "identity");
5151

52-
// Setup ServerCallContext
53-
var serverCallContext = new HttpContextServerCallContext(httpContext, Logger);
54-
serverCallContext.Initialize();
52+
var serverCallContext = new HttpContextServerCallContext(httpContext, ServiceOptions, Logger);
5553

56-
// Activate the implementation type via DI.
5754
var activator = httpContext.RequestServices.GetRequiredService<IGrpcServiceActivator<TService>>();
58-
var service = activator.Create();
59-
60-
TResponse response;
55+
TService service = null;
6156

57+
TResponse response = null;
6258
try
6359
{
64-
using (serverCallContext)
60+
serverCallContext.Initialize();
61+
62+
service = activator.Create();
63+
64+
response = await _invoker(
65+
service,
66+
new HttpContextStreamReader<TRequest>(httpContext, serverCallContext, Method.RequestMarshaller.Deserializer),
67+
serverCallContext);
68+
69+
if (response == null)
6570
{
66-
response = await _invoker(
67-
service,
68-
new HttpContextStreamReader<TRequest>(httpContext, ServiceOptions, Method.RequestMarshaller.Deserializer),
69-
serverCallContext);
71+
// This is consistent with Grpc.Core when a null value is returned
72+
throw new RpcException(new Status(StatusCode.Cancelled, "Cancelled"));
7073
}
74+
75+
var responseBodyPipe = httpContext.Response.BodyPipe;
76+
await responseBodyPipe.WriteMessageAsync(response, serverCallContext, Method.ResponseMarshaller.Serializer);
77+
}
78+
catch (Exception ex)
79+
{
80+
serverCallContext.ProcessHandlerError(ex, Method.Name);
7181
}
7282
finally
7383
{
74-
activator.Release(service);
84+
serverCallContext.Dispose();
85+
if (service != null)
86+
{
87+
activator.Release(service);
88+
}
7589
}
7690

77-
// TODO(JunTaoLuo, JamesNK): make sure the response is not null
78-
var responseBodyPipe = httpContext.Response.BodyPipe;
79-
await responseBodyPipe.WriteMessageAsync(response, ServiceOptions, Method.ResponseMarshaller.Serializer, serverCallContext.WriteOptions);
80-
8191
httpContext.Response.ConsolidateTrailers(serverCallContext);
8292

8393
// Flush any buffered content

src/Grpc.AspNetCore.Server/Internal/DuplexStreamingServerCallHandler.cs renamed to src/Grpc.AspNetCore.Server/Internal/CallHandlers/DuplexStreamingServerCallHandler.cs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
using Microsoft.Extensions.DependencyInjection;
2424
using Microsoft.Extensions.Logging;
2525

26-
namespace Grpc.AspNetCore.Server.Internal
26+
namespace Grpc.AspNetCore.Server.Internal.CallHandlers
2727
{
2828
internal class DuplexStreamingServerCallHandler<TRequest, TResponse, TService> : ServerCallHandlerBase<TRequest, TResponse, TService>
2929
where TRequest : class
@@ -49,28 +49,34 @@ public override async Task HandleCallAsync(HttpContext httpContext)
4949
httpContext.Response.ContentType = "application/grpc";
5050
httpContext.Response.Headers.Append("grpc-encoding", "identity");
5151

52-
// Setup ServerCallContext
53-
var serverCallContext = new HttpContextServerCallContext(httpContext, Logger);
54-
serverCallContext.Initialize();
52+
var serverCallContext = new HttpContextServerCallContext(httpContext, ServiceOptions, Logger);
5553

56-
// Activate the implementation type via DI.
5754
var activator = httpContext.RequestServices.GetRequiredService<IGrpcServiceActivator<TService>>();
58-
var service = activator.Create();
55+
TService service = null;
5956

6057
try
6158
{
62-
using (serverCallContext)
63-
{
64-
await _invoker(
65-
service,
66-
new HttpContextStreamReader<TRequest>(httpContext, ServiceOptions, Method.RequestMarshaller.Deserializer),
67-
new HttpContextStreamWriter<TResponse>(serverCallContext, ServiceOptions, Method.ResponseMarshaller.Serializer),
68-
serverCallContext);
69-
}
59+
serverCallContext.Initialize();
60+
61+
service = activator.Create();
62+
63+
await _invoker(
64+
service,
65+
new HttpContextStreamReader<TRequest>(httpContext, serverCallContext, Method.RequestMarshaller.Deserializer),
66+
new HttpContextStreamWriter<TResponse>(serverCallContext, Method.ResponseMarshaller.Serializer),
67+
serverCallContext);
68+
}
69+
catch (Exception ex)
70+
{
71+
serverCallContext.ProcessHandlerError(ex, Method.Name);
7072
}
7173
finally
7274
{
73-
activator.Release(service);
75+
serverCallContext.Dispose();
76+
if (service != null)
77+
{
78+
activator.Release(service);
79+
}
7480
}
7581

7682
httpContext.Response.ConsolidateTrailers(serverCallContext);

src/Grpc.AspNetCore.Server/Internal/ServerStreamingServerCallHandler.cs renamed to src/Grpc.AspNetCore.Server/Internal/CallHandlers/ServerStreamingServerCallHandler.cs

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
using Microsoft.Extensions.DependencyInjection;
2424
using Microsoft.Extensions.Logging;
2525

26-
namespace Grpc.AspNetCore.Server.Internal
26+
namespace Grpc.AspNetCore.Server.Internal.CallHandlers
2727
{
2828
internal class ServerStreamingServerCallHandler<TRequest, TResponse, TService> : ServerCallHandlerBase<TRequest, TResponse, TService>
2929
where TRequest : class
@@ -49,35 +49,40 @@ public override async Task HandleCallAsync(HttpContext httpContext)
4949
httpContext.Response.ContentType = "application/grpc";
5050
httpContext.Response.Headers.Append("grpc-encoding", "identity");
5151

52-
// Decode request
53-
var requestPayload = await httpContext.Request.BodyPipe.ReadSingleMessageAsync(ServiceOptions);
54-
var request = Method.RequestMarshaller.Deserializer(requestPayload);
52+
var serverCallContext = new HttpContextServerCallContext(httpContext, ServiceOptions, Logger);
5553

56-
// Setup ServerCallContext
57-
var serverCallContext = new HttpContextServerCallContext(httpContext, Logger);
58-
serverCallContext.Initialize();
59-
60-
// Activate the implementation type via DI.
6154
var activator = httpContext.RequestServices.GetRequiredService<IGrpcServiceActivator<TService>>();
62-
var service = activator.Create();
55+
TService service = null;
6356

6457
try
6558
{
66-
using (serverCallContext)
67-
{
68-
await _invoker(
69-
service,
70-
request,
71-
new HttpContextStreamWriter<TResponse>(serverCallContext, ServiceOptions, Method.ResponseMarshaller.Serializer),
72-
serverCallContext);
73-
}
59+
serverCallContext.Initialize();
60+
61+
// Decode request
62+
var requestPayload = await httpContext.Request.BodyPipe.ReadSingleMessageAsync(serverCallContext);
63+
var request = Method.RequestMarshaller.Deserializer(requestPayload);
64+
65+
service = activator.Create();
66+
67+
await _invoker(
68+
service,
69+
request,
70+
new HttpContextStreamWriter<TResponse>(serverCallContext, Method.ResponseMarshaller.Serializer),
71+
serverCallContext);
72+
}
73+
catch (Exception ex)
74+
{
75+
serverCallContext.ProcessHandlerError(ex, Method.Name);
7476
}
7577
finally
7678
{
77-
activator.Release(service);
79+
serverCallContext.Dispose();
80+
if (service != null)
81+
{
82+
activator.Release(service);
83+
}
7884
}
7985

80-
8186
httpContext.Response.ConsolidateTrailers(serverCallContext);
8287

8388
// Flush any buffered content

src/Grpc.AspNetCore.Server/Internal/UnaryServerCallHandler.cs renamed to src/Grpc.AspNetCore.Server/Internal/CallHandlers/UnaryServerCallHandler.cs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,39 +49,49 @@ public override async Task HandleCallAsync(HttpContext httpContext)
4949
httpContext.Response.ContentType = "application/grpc";
5050
httpContext.Response.Headers.Append("grpc-encoding", "identity");
5151

52-
// Decode request
53-
var requestPayload = await httpContext.Request.BodyPipe.ReadSingleMessageAsync(ServiceOptions);
54-
var request = Method.RequestMarshaller.Deserializer(requestPayload);
52+
var serverCallContext = new HttpContextServerCallContext(httpContext, ServiceOptions, Logger);
5553

56-
// Setup ServerCallContext
57-
var serverCallContext = new HttpContextServerCallContext(httpContext, Logger);
58-
serverCallContext.Initialize();
59-
60-
// Activate the implementation type via DI.
6154
var activator = httpContext.RequestServices.GetRequiredService<IGrpcServiceActivator<TService>>();
62-
var service = activator.Create();
63-
64-
TResponse response;
55+
TService service = null;
6556

57+
TResponse response = null;
6658
try
6759
{
68-
using (serverCallContext)
60+
serverCallContext.Initialize();
61+
62+
var requestPayload = await httpContext.Request.BodyPipe.ReadSingleMessageAsync(serverCallContext);
63+
64+
var request = Method.RequestMarshaller.Deserializer(requestPayload);
65+
66+
service = activator.Create();
67+
68+
response = await _invoker(
69+
service,
70+
request,
71+
serverCallContext);
72+
73+
if (response == null)
6974
{
70-
response = await _invoker(
71-
service,
72-
request,
73-
serverCallContext);
75+
// This is consistent with Grpc.Core when a null value is returned
76+
throw new RpcException(new Status(StatusCode.Cancelled, "Cancelled"));
7477
}
78+
79+
var responseBodyPipe = httpContext.Response.BodyPipe;
80+
await responseBodyPipe.WriteMessageAsync(response, serverCallContext, Method.ResponseMarshaller.Serializer);
81+
}
82+
catch (Exception ex)
83+
{
84+
serverCallContext.ProcessHandlerError(ex, Method.Name);
7585
}
7686
finally
7787
{
78-
activator.Release(service);
88+
serverCallContext.Dispose();
89+
if (service != null)
90+
{
91+
activator.Release(service);
92+
}
7993
}
8094

81-
// TODO(JunTaoLuo, JamesNK): make sure the response is not null
82-
var responseBodyPipe = httpContext.Response.BodyPipe;
83-
await responseBodyPipe.WriteMessageAsync(response, ServiceOptions, Method.ResponseMarshaller.Serializer, serverCallContext.WriteOptions);
84-
8595
httpContext.Response.ConsolidateTrailers(serverCallContext);
8696

8797
// Flush any buffered content
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
using System;
20+
using System.Collections.Generic;
21+
using System.Text;
22+
23+
namespace Grpc.AspNetCore.Server.Internal
24+
{
25+
internal static class ErrorMessageHelper
26+
{
27+
internal static string BuildErrorMessage(string message, Exception exception, bool? includeExceptionDetails)
28+
{
29+
if (includeExceptionDetails ?? false)
30+
{
31+
return $"{message} {exception.GetType().Name}: {exception.Message}";
32+
}
33+
34+
return message;
35+
}
36+
}
37+
}

src/Grpc.AspNetCore.Server/Internal/HttpContextServerCallContext.Log.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ internal sealed partial class HttpContextServerCallContext
3535
private static readonly Action<ILogger, string, Exception> _invalidTimeoutIgnored =
3636
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(2, "InvalidTimeoutIgnored"), "Invalid grpc-timeout header value '{Timeout}' has been ignored.");
3737

38+
private static readonly Action<ILogger, string, Exception> _errorExecutingServiceMethod =
39+
LoggerMessage.Define<string>(LogLevel.Error, new EventId(3, "ErrorExecutingServiceMethod"), "Error when executing service method '{ServiceMethod}'.");
40+
41+
private static readonly Action<ILogger, StatusCode, Exception> _rpcConnectionError =
42+
LoggerMessage.Define<StatusCode>(LogLevel.Information, new EventId(4, "RpcConnectionError"), "Error status code '{StatusCode}' raised.");
43+
3844
public static void DeadlineExceeded(ILogger logger, TimeSpan timeout)
3945
{
4046
_deadlineExceeded(logger, timeout, null);
@@ -44,5 +50,15 @@ public static void InvalidTimeoutIgnored(ILogger logger, string timeout)
4450
{
4551
_invalidTimeoutIgnored(logger, timeout, null);
4652
}
53+
54+
public static void ErrorExecutingServiceMethod(ILogger logger, string serviceMethod, Exception ex)
55+
{
56+
_errorExecutingServiceMethod(logger, serviceMethod, ex);
57+
}
58+
59+
public static void RpcConnectionError(ILogger logger, StatusCode statusCode, Exception ex)
60+
{
61+
_rpcConnectionError(logger, statusCode, ex);
62+
}
4763
}
4864
}

0 commit comments

Comments
 (0)