Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MagicOnionでの共通処理の挟み方

pasta
June 04, 2019

 MagicOnionでの共通処理の挟み方

pasta

June 04, 2019
Tweet

Other Decks in Programming

Transcript

  1. 共通処理の挟み方
    2019/06/04 MagicOnion勉強会
    ぱすた(@p_a_sta)
    1

    View full-size slide

  2. だれ?
    ぱすた(@p_a_sta)
    株式会社Donuts 新規開発部
    音ゲーつくったり基盤フレームワークつくったり
    サーバサイド初心者
    2

    View full-size slide

  3. 注意
    今回は非ストリーミングAPIしか扱いません。
    (ごめんなさい)
    3

    View full-size slide

  4. やりたいこと
    ● サーバー/クライアント
    ○ 共通リクエスト/レスポンスの送受信
    ○ 暗号化
    ● クライアント
    ○ 共通エラーハンドリング
    ○ リトライ
    ○ デバッグ出力
    などなど……
    4

    View full-size slide

  5. サーバーサイド
    5

    View full-size slide

  6. サーバーサイド
    2通りのやりかた
    ● MagicOnionFilter
    ○ MagicOnionに用意された機能
    ● gRPC Interceptor
    ○ gRPC本来の機能
    どちらもRPCの前後に処理を挟むもの
    6

    View full-size slide

  7. MagicOnionFilter - 実装方法
    MagicOnionFilterAttributeを継承する
    public class SampleFilterAttribute : MagicOnionFilterAttribute
    {
    public override async ValueTask Invoke(ServiceContext context)
    {
    try {
    /* 前処理 */
    await Next(context);
    /* 後処理 */
    } catch {
    /* エラー処理 */
    throw;
    } finally {
    /* on finally */
    }
    }
    }
    7

    View full-size slide

  8. MagicOnionFilter - 適用方法
    ①特定のクラス全体・または1つのメソッドに適用する場合
    クラスまたはメソッドに属性として付与する
    // クラスに付けるとクラス全体に適用される
    [SampleFilter]
    public class SampleService : ServiceBase, ISampleService
    {
    // メソッドに付けるとメソッドだけに適用される
    [SampleFilter]
    public async UnaryResult HogeAsync() { }
    }
    8

    View full-size slide

  9. MagicOnionFilter - 適用方法
    ②全ての通信に適用する場合
    MagicOnionOptions で指定する
    var options = new MagicOnionOptions
    {
    GlobalFilters = new[] { new SampleFilterAttribute() },
    });
    var service = MagicOnionEngine.BuildServerServiceDefinition(options);
    9

    View full-size slide

  10. MagicOnionFilter - 概要図
    10

    View full-size slide

  11. gRPC Interceptor - 実装方法
    Grpc.Core.Interceptors.Interceptor を継承する
    サーバ側・クライアント側それぞれについて、RPC 種類ごとにメ
    ソッドが用意されているので、必要なものだけを override して実
    装する
    今回は UnaryServerHandler のみ説明
    11

    View full-size slide

  12. gRPC Interceptor - 実装方法
    public class SampleInterceptor : Interceptor
    {
    // ※MagicOnionを使っている限り TRequest, TResponse は byte[] 固定
    public override async Task UnaryServerHandler(
    TRequest request, // 生リクエストデータ
    ServerCallContext context,
    UnaryServerMethod continuation
    ) {
    /* 前処理 */
    var response = await continuation(request, context);
    /* 後処理 */
    return response;
    }
    }
    12

    View full-size slide

  13. gRPC Interceptor - 適用方法
    MagicOnionServiceDefinition.ServerServiceDefinition から
    Interceptor を刺した ServerServiceDefinition を生成する
    var service = MagicOnionEngine.BuildServerServiceDefinition();
    var serverService = service.ServerServiceDefinition.Intercept(
    new SampleInterceptor()
    );
    13

    View full-size slide

  14. 比較
    MagicOnion
    Filter
    gRPC
    Interceptor
    部分適用 ◯ ✕
    メソッド本体との値のやり取り ◯ ✕
    リクエストヘッダの読みとり
    レスポンスヘッダの書き換え
    ◯ ◯
    リクエストボディの書き換え ✕ ◯
    レスポンスボディの書き換え △ ◯
    クライアントと共通化 ✕ ◯
    14

    View full-size slide

  15. 比較
    MagicOnion
    Filter
    gRPC
    Interceptor
    部分適用 ◯ ✕
    メソッド本体との値のやり取り ◯ ✕
    リクエストヘッダの読みとり
    レスポンスヘッダの書き換え
    ◯ ◯
    リクエストボディの書き換え ✕ ◯
    レスポンスボディの書き換え △ ◯
    クライアントと共通化 ✕ ◯
    context.Items がリクエストごとに固有
    のストレージになっている
    15

    View full-size slide

  16. 比較
    MagicOnion
    Filter
    gRPC
    Interceptor
    部分適用 ◯ ✕
    メソッド本体との値のやり取り ◯ ✕
    リクエストヘッダの読みとり
    レスポンスヘッダの書き換え
    ◯ ◯
    リクエストボディの書き換え ✕ ◯
    レスポンスボディの書き換え △ ◯
    クライアントと共通化 ✕ ◯
    context.RequestHeaders と
    context.WriteResponseHeadersAsync を使う
    16

    View full-size slide

  17. 比較
    MagicOnion
    Filter
    gRPC
    Interceptor
    部分適用 ◯ ✕
    メソッド本体との値のやり取り ◯ ✕
    リクエストヘッダの読みとり
    レスポンスヘッダの書き換え
    ◯ ◯
    リクエストボディの書き換え ✕ ◯
    レスポンスボディの書き換え △ ◯
    クライアントと共通化 ✕ ◯
    context.CallContext で gRPC Interceptor 側の
    context にアクセス可能
    17

    View full-size slide

  18. 比較
    MagicOnion
    Filter
    gRPC
    Interceptor
    部分適用 ◯ ✕
    メソッド本体との値のやり取り ◯ ✕
    リクエストヘッダの読みとり
    レスポンスヘッダの書き換え
    ◯ ◯
    リクエストボディの書き換え ✕ ◯
    レスポンスボディの書き換え △ ◯
    クライアントと共通化 ✕ ◯
    18

    View full-size slide

  19. 比較
    MagicOnion
    Filter
    gRPC
    Interceptor
    部分適用 ◯ ✕
    メソッド本体との値のやり取り ◯ ✕
    リクエストヘッダの読みとり
    レスポンスヘッダの書き換え
    ◯ ◯
    リクエストボディの書き換え ✕ ◯
    レスポンスボディの書き換え △ ◯
    クライアントと共通化 ✕ ◯
    context.ForceSetRawUnaryResult
    で書き換えは可能
    元の値は取得不可
    19

    View full-size slide

  20. 比較
    MagicOnion
    Filter
    gRPC
    Interceptor
    部分適用 ◯ ✕
    メソッド本体との値のやり取り ◯ ✕
    リクエストヘッダの読みとり
    レスポンスヘッダの書き換え
    ◯ ◯
    リクエストボディの書き換え ✕ ◯
    レスポンスボディの書き換え △ ◯
    クライアントと共通化 ✕ ◯
    20

    View full-size slide

  21. 比較
    MagicOnion
    Filter
    gRPC
    Interceptor
    部分適用 ◯ ✕
    メソッド本体との値のやり取り ◯ ✕
    リクエストヘッダの読みとり
    レスポンスヘッダの書き換え
    ◯ ◯
    リクエストボディの書き換え ✕ ◯
    レスポンスボディの書き換え △ ◯
    クライアントと共通化 ✕ ◯
    21
    要件に合う方を選んで使うのが重要

    View full-size slide

  22. サンプル - 共通リクエスト/レスポンスの送受信
    ● 要件
    ○ メソッド本体との値のやり取り
    ○ リクエストヘッダの読みとり
    ○ レスポンスヘッダの書き換え
    MagicOnionFilter
    22

    View full-size slide

  23. 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(header.ValueBytes);
    /* 共通リクエストに対する処理 */
    context.Items[Key] = commonRequest; // メソッドで使うために詰めておく
    await Next(context);
    var commonResponse = new CommonResponse { /* 共通レスポンス作成 */ };
    await context.CallContext.WriteResponseHeadersAsync(new Metadata {
    { Key, LZ4MessagePackSerializer.Serialize(commonResponse) }
    });
    }
    } 23

    View full-size slide

  24. サンプル - 暗号化
    ● 要件
    ○ リクエストボディの書き換え
    ○ レスポンスボディの書き換え
    ○ クライアントと共通化
    gRPC Interceptor
    24

    View full-size slide

  25. public class CryptoInterceptor : Interceptor
    {
    public const string Key = "暗号鍵";
    public override async Task UnaryServerHandler(
    TRequest request, ServerCallContext context,
    UnaryServerMethod 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

    View full-size slide

  26. クライアントサイド
    26

    View full-size slide

  27. クライアントサイド - 実装方法
    ● 基本的に gRPC Interceptor 一択
    ● MagicOnionClient.Create に Interceptor を挟んだ
    CallInvoker を渡す
    var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure);
    var invoker = channel.Intercept(new HogeInterceptor());
    var client = MagicOnionClient.Create(invoker);
    27

    View full-size slide

  28. クライアントサイド - 実装方法
    28
    public class SampleInterceptor : Interceptor
    {
    public override AsyncUnaryCall AsyncUnaryCall(
    TRequest request,
    ClientInterceptorContext context,
    AsyncUnaryCallContinuation continuation
    ) {
    /* 前処理 */
    var call = continuation(request, context);
    return call;
    }
    }

    View full-size slide

  29. クライアントサイド - 実装方法
    29
    public class SampleInterceptor : Interceptor
    {
    public override AsyncUnaryCall AsyncUnaryCall(
    TRequest request,
    ClientInterceptorContext context,
    AsyncUnaryCallContinuation continuation
    ) {
    /* 前処理 */
    var call = continuation(request, context);
    return call;
    }
    }
    戻り値が Task(or task-like) じゃない
    →async/awaitが使えない
    →後処理どうするの?

    View full-size slide

  30. クライアントサイド - 実装方法
    30
    var call = continuation(request, context);
    return new AsyncUnaryCall(
    call.ResponseAsync.ContinueWith(res =>
    {
    /* 後処理 */
    return res.Result;
    }),
    call.ResponseHeadersAsync,
    call.GetStatus,
    call.GetTrailers,
    call.Dispose
    );
    ● 新しい AsyncUnaryCall を作る

    View full-size slide

  31. クライアントサイド - 実装方法
    31
    var call = continuation(request, context);
    return new AsyncUnaryCall(
    call.ResponseAsync.ContinueWith(async res =>
    {
    /* 後処理 */
    return res.Result;
    }).Unwrap(),
    call.ResponseHeadersAsync,
    call.GetStatus,
    call.GetTrailers,
    call.Dispose
    );
    ● 後処理で await したい場合

    View full-size slide

  32. クライアントサイド - 実装方法
    32
    ● 前処理で await したい場合
    ○ 不明……

    View full-size slide

  33. エラーハンドリング
    33

    View full-size slide

  34. エラーハンドリング - 落とし穴
    ● 例外が発生した場合
    ○ gRPC 的には RpcException が発生するはず
    ○ 実際は AggregateException が発生
    ■ Task 内部で発生した例外は AggregateException に
    集約される
    ■ 元の例外は .InnerExceptions で取得
    34

    View full-size slide

  35. エラーハンドリング - 落とし穴
    ● 例外が発生した場合
    ○ Interceptor を重ねるとその数だけ AggregateException
    でラップされる
    ■ めちゃくちゃ取り出しづらい
    ■ 本来は機能単位で Interceptor を分けるべきだが、例
    外が扱いづらくなるため1クラスにまとめたほうが良い
    かもしれない
    ● 結局サーバーと共通化できない
    35

    View full-size slide

  36. エラーハンドリング - リトライ
    ● 一定の例外だったらリトライしたい
    ● Interceptor でリトライをする方法はなさそう(※独自調べ)
    拡張メソッドを作って、必ずそこ経由で呼ぶようにするしかなさそう
    36

    View full-size slide

  37. エラーハンドリング - リトライ
    37
    // InvokeWithErrorAsync 経由で呼ぶ
    MagicOnionClient.Create().InvokeWithErrorAsync(x => x.HogeAsync());
    // 通常はこう
    // MagicOnionClient.Create().HogeAsync();
    ● イメージ
    (めんどくさい……)

    View full-size slide

  38. リトライ - 実装方法
    38
    // IService からメソッドチェーンしたいので型引数が2つになる
    public static async UniTask InvokeWithErrorAsync(this X self,
    Func> connector) where X : IService
    {
    while (true)
    {
    try { return await connector(self); }
    // AggregateException から RpcException を抜き取る
    catch (AggregateException e) when (e.InnerException is RpcException rpc)
    {
    // 次のスライドで...
    }
    }
    }

    View full-size slide

  39. リトライ - 実装方法
    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;
    }

    View full-size slide

  40. まとめ
    ● サーバー
    ○ MagicOnionFilter と gRPC Interceptor のうち
    要件に合う方を選んで使う
    ● クライアント
    ○ gRPC Interceptor を使う
    ■ 複数挟むと例外が扱いづらくなるので1クラスで
    ○ gRPC Interceptor でできないことは泥臭く実装
    41

    View full-size slide