Share
Embed
Transcript
41 slides
-
やりたいこと • サーバー/クライアント ◦ 共通リクエスト/レスポンスの送受信 ◦ 暗号化 • クライアント ◦
共通エラーハンドリング ◦ リトライ ◦ デバッグ出力 などなど…… 4 -
サーバーサイド 2通りのやりかた • MagicOnionFilter ◦ MagicOnionに用意された機能 • gRPC Interceptor ◦
gRPC本来の機能 どちらもRPCの前後に処理を挟むもの 6 -
MagicOnionFilter - 実装方法 MagicOnionFilterAttributeを継承する public class SampleFilterAttribute : MagicOnionFilterAttribute {
public override async ValueTask Invoke(ServiceContext context) { try { /* 前処理 */ await Next(context); /* 後処理 */ } catch { /* エラー処理 */ throw; } finally { /* on finally */ } } } 7 -
MagicOnionFilter - 適用方法 ①特定のクラス全体・または1つのメソッドに適用する場合 クラスまたはメソッドに属性として付与する // クラスに付けるとクラス全体に適用される [SampleFilter] public class
SampleService : ServiceBase<ISampleService>, ISampleService { // メソッドに付けるとメソッドだけに適用される [SampleFilter] public async UnaryResult<Nil> HogeAsync() { } } 8 -
MagicOnionFilter - 適用方法 ②全ての通信に適用する場合 MagicOnionOptions で指定する var options = new
MagicOnionOptions { GlobalFilters = new[] { new SampleFilterAttribute() }, }); var service = MagicOnionEngine.BuildServerServiceDefinition(options); 9 -
gRPC Interceptor - 実装方法 Grpc.Core.Interceptors.Interceptor を継承する サーバ側・クライアント側それぞれについて、RPC 種類ごとにメ ソッドが用意されているので、必要なものだけを override
して実 装する 今回は UnaryServerHandler のみ説明 11 -
gRPC Interceptor - 実装方法 public class SampleInterceptor : Interceptor {
// ※MagicOnionを使っている限り TRequest, TResponse は byte[] 固定 public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, // 生リクエストデータ ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation ) { /* 前処理 */ var response = await continuation(request, context); /* 後処理 */ return response; } } 12 -
gRPC Interceptor - 適用方法 MagicOnionServiceDefinition.ServerServiceDefinition から Interceptor を刺した ServerServiceDefinition を生成する
var service = MagicOnionEngine.BuildServerServiceDefinition(); var serverService = service.ServerServiceDefinition.Intercept( new SampleInterceptor() ); 13 -
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 14 -
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.Items がリクエストごとに固有 のストレージになっている 15 -
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.RequestHeaders と context.WriteResponseHeadersAsync を使う 16 -
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.CallContext で gRPC Interceptor 側の context にアクセス可能 17 -
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 18 -
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.ForceSetRawUnaryResult で書き換えは可能 元の値は取得不可 19 -
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 20 -
比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯
✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 21 要件に合う方を選んで使うのが重要 -
サンプル - 共通リクエスト/レスポンスの送受信 • 要件 ◦ メソッド本体との値のやり取り ◦ リクエストヘッダの読みとり ◦
レスポンスヘッダの書き換え MagicOnionFilter 22 -
public class CommonFilterAttribute : MagicOnionFilterAttribute { // ヘッダにバイナリを詰める時は Metadata.BinaryHeaderSuffix を付けないといけない
public const string Key = "base" + Metadata.BinaryHeaderSuffix; public override async ValueTask Invoke(ServiceContext context) { var header = context.CallContext.RequestHeaders.First(x => x.Key == Key); var commonRequest = LZ4MessagePackSerializer.Deserialize<CommonRequest>(header.ValueBytes); /* 共通リクエストに対する処理 */ context.Items[Key] = commonRequest; // メソッドで使うために詰めておく await Next(context); var commonResponse = new CommonResponse { /* 共通レスポンス作成 */ }; await context.CallContext.WriteResponseHeadersAsync(new Metadata { { Key, LZ4MessagePackSerializer.Serialize(commonResponse) } }); } } 23 -
サンプル - 暗号化 • 要件 ◦ リクエストボディの書き換え ◦ レスポンスボディの書き換え ◦
クライアントと共通化 gRPC Interceptor 24 -
public class CryptoInterceptor : Interceptor { public const string Key
= "暗号鍵"; public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation ) { request = Decrypt(request as byte[]) as TRequest; var response = await continuation(request, context); return Encrypt(response as byte[]) as TResponse; } private byte[] Encrypt(byte[] rawData) => 実装略; private byte[] Decrypt(byte[] encryptedData) => 実装略; } 25 -
クライアントサイド - 実装方法 • 基本的に gRPC Interceptor 一択 • MagicOnionClient.Create
に Interceptor を挟んだ CallInvoker を渡す var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure); var invoker = channel.Intercept(new HogeInterceptor()); var client = MagicOnionClient.Create<IHogeService>(invoker); 27 -
クライアントサイド - 実装方法 28 public class SampleInterceptor : Interceptor {
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation ) { /* 前処理 */ var call = continuation(request, context); return call; } } -
クライアントサイド - 実装方法 29 public class SampleInterceptor : Interceptor {
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation ) { /* 前処理 */ var call = continuation(request, context); return call; } } 戻り値が Task(or task-like) じゃない →async/awaitが使えない →後処理どうするの? -
クライアントサイド - 実装方法 30 var call = continuation(request, context); return
new AsyncUnaryCall<TResponse>( call.ResponseAsync.ContinueWith(res => { /* 後処理 */ return res.Result; }), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose ); • 新しい AsyncUnaryCall を作る -
クライアントサイド - 実装方法 31 var call = continuation(request, context); return
new AsyncUnaryCall<TResponse>( call.ResponseAsync.ContinueWith(async res => { /* 後処理 */ return res.Result; }).Unwrap(), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose ); • 後処理で await したい場合 -
エラーハンドリング - 落とし穴 • 例外が発生した場合 ◦ gRPC 的には RpcException が発生するはず
◦ 実際は AggregateException が発生 ▪ Task 内部で発生した例外は AggregateException に 集約される ▪ 元の例外は .InnerExceptions で取得 34 -
エラーハンドリング - 落とし穴 • 例外が発生した場合 ◦ Interceptor を重ねるとその数だけ AggregateException でラップされる
▪ めちゃくちゃ取り出しづらい ▪ 本来は機能単位で Interceptor を分けるべきだが、例 外が扱いづらくなるため1クラスにまとめたほうが良い かもしれない • 結局サーバーと共通化できない 35 -
エラーハンドリング - リトライ 37 // InvokeWithErrorAsync 経由で呼ぶ MagicOnionClient.Create<IHogeService>().InvokeWithErrorAsync(x => x.HogeAsync());
// 通常はこう // MagicOnionClient.Create<IHogeService>().HogeAsync(); • イメージ (めんどくさい……) -
リトライ - 実装方法 38 // IService からメソッドチェーンしたいので型引数が2つになる public static async
UniTask<T> InvokeWithErrorAsync<T, X>(this X self, Func<X, UnaryResult<T>> connector) where X : IService<X> { while (true) { try { return await connector(self); } // AggregateException から RpcException を抜き取る catch (AggregateException e) when (e.InnerException is RpcException rpc) { // 次のスライドで... } } } -
リトライ - 実装方法 39 catch (AggregateException e) when (e.InnerException is
RpcException rpc) { // ここの条件は要件次第 switch (rpc.StatusCode) { case StatusCode.Unavailable: if (await リトライかタイトルに戻るかを聞く() == "リトライ") continue; SceneManager.LoadSceneAsync("Title"); // OperationCanceledException を投げて処理を打ち切ったことを通知 throw new OperationCanceledException("タイトルに戻りました", rpc); } // その他の例外はそのまま投げる throw; } -
まとめ • サーバー ◦ MagicOnionFilter と gRPC Interceptor のうち 要件に合う方を選んで使う
• クライアント ◦ gRPC Interceptor を使う ▪ 複数挟むと例外が扱いづらくなるので1クラスで ◦ gRPC Interceptor でできないことは泥臭く実装 41