Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
MagicOnionでの共通処理の挟み方
Search
pasta
June 04, 2019
Programming
4k
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
MagicOnionでの共通処理の挟み方
MagicOnion勉強会
https://connpass.com/event/127369/
pasta
June 04, 2019
Other Decks in Programming
See All in Programming
AIは賢い。でも実行環境は? CLIおじさんがAI時代に伝えたいこと ~ CLIおじさんがAI時代に伝えたいこと ~
curekoshimizu
1
240
The Good Stuff, Not the Slop: Engineering High-Quality Android Apps with Modern AI Tooling
danybony
1
250
How I Stole PSI from Android Studio - DroidKaigi2026
worker8
0
130
Are APIs Still Relevant in the AI Era?
soyuka
0
240
テストを司るデーモンに会いに行く 〜隔離した仮想マシンでテストを通すまで〜
h1d3mun3
1
430
【高い買い物LT会】初任給で話題の国産フィジカルAIを買った話
akagami
PRO
0
160
WebAssembly in Android Apps 〜 WASMはJNIの夢を見るか
keiji
1
110
App Intentsのビルドプロセスを支える技術
kntkymt
0
380
AHC070解法紹介
eijirou
0
120
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
150
Vibes Containers 〜AIで変わるコンテナ設計と運用〜
tkikuc
3
550
AWS DevOps Agentで インシデント対応をAIに任せたい
honmarkhunt
7
3k
Featured
See All Featured
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
287
14k
YesSQL, Process and Tooling at Scale
rocio
174
15k
Fantastic passwords and where to find them - at NoRuKo
philnash
52
3.8k
Lessons Learnt from Crawling 1000+ Websites
charlesmeaden
PRO
1
1.6k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
17k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.6k
Context Engineering - Making Every Token Count
addyosmani
9
1.1k
Taking LLMs out of the black box: A practical guide to human-in-the-loop distillation
inesmontani
PRO
3
2.4k
Data-driven link building: lessons from a $708K investment (BrightonSEO talk)
szymonslowik
1
1.3k
[SF Ruby Conf 2025] Rails X
palkan
3
1.4k
Impact Scores and Hybrid Strategies: The future of link building
tamaranovitovic
0
440
The Mindset for Success: Future Career Progression
greggifford
PRO
0
500
Transcript
共通処理の挟み方 2019/06/04 MagicOnion勉強会 ぱすた(@p_a_sta) 1
だれ? ぱすた(@p_a_sta) 株式会社Donuts 新規開発部 音ゲーつくったり基盤フレームワークつくったり サーバサイド初心者 2
注意 今回は非ストリーミングAPIしか扱いません。 (ごめんなさい) 3
やりたいこと • サーバー/クライアント ◦ 共通リクエスト/レスポンスの送受信 ◦ 暗号化 • クライアント ◦
共通エラーハンドリング ◦ リトライ ◦ デバッグ出力 などなど…… 4
サーバーサイド 5
サーバーサイド 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
MagicOnionFilter - 概要図 10
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
クライアントサイド 26
クライアントサイド - 実装方法 • 基本的に 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 したい場合
クライアントサイド - 実装方法 32 • 前処理で await したい場合 ◦ 不明……
エラーハンドリング 33
エラーハンドリング - 落とし穴 • 例外が発生した場合 ◦ gRPC 的には RpcException が発生するはず
◦ 実際は AggregateException が発生 ▪ Task 内部で発生した例外は AggregateException に 集約される ▪ 元の例外は .InnerExceptions で取得 34
エラーハンドリング - 落とし穴 • 例外が発生した場合 ◦ Interceptor を重ねるとその数だけ AggregateException でラップされる
▪ めちゃくちゃ取り出しづらい ▪ 本来は機能単位で Interceptor を分けるべきだが、例 外が扱いづらくなるため1クラスにまとめたほうが良い かもしれない • 結局サーバーと共通化できない 35
エラーハンドリング - リトライ • 一定の例外だったらリトライしたい • Interceptor でリトライをする方法はなさそう(※独自調べ) 拡張メソッドを作って、必ずそこ経由で呼ぶようにするしかなさそう 36
エラーハンドリング - リトライ 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; }
まとめ 40
まとめ • サーバー ◦ MagicOnionFilter と gRPC Interceptor のうち 要件に合う方を選んで使う
• クライアント ◦ gRPC Interceptor を使う ▪ 複数挟むと例外が扱いづらくなるので1クラスで ◦ gRPC Interceptor でできないことは泥臭く実装 41