Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Effectで作る堅牢でスケーラブルなAPIゲートウェイ / Robust and Scala...
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Yuichi Goto
May 11, 2024
Programming
2.5k
9
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Effectで作る堅牢でスケーラブルなAPIゲートウェイ / Robust and Scalable API Gateway Built on Effect
May 11, 2024 @ TSKaigi
Yuichi Goto
May 11, 2024
More Decks by Yuichi Goto
See All by Yuichi Goto
[Teaser] Type-Safe Lightweight DDD with Effect Schema
yasaichi
3
540
Google Cloud を用いたソフトウェア開発の内製化組織の早期立ち上げの実現 / Rapid Establishment of In-House Software Development Teams Using Google Cloud
yasaichi
1
1.7k
[EN] Robust and Scalable API Gateway Built on Effect
yasaichi
3
460
あるRailsエンジニアがビジネスリーダーに転身するまで
yasaichi
9
3.3k
Active Recordから考える次の10年を見据えた技術選定 / Architecture decision for the next 10 years at PIXTA
yasaichi
50
23k
Active Recordから考える次世代のRuby on Railsの方向性 / Directions for the next generation of Ruby on Rails: From the viewpoint of its Active Record
yasaichi
38
22k
ピクスタのエンジニアリングとCircleCI / Software Engineering with CircleCI at PIXTA
yasaichi
1
480
Ruby on Railsの正体と向き合い方 / What is Ruby on Rails and how to deal with it?
yasaichi
146
95k
SSR以後の世界へ / techcamp05
yasaichi
3
1.9k
Other Decks in Programming
See All in Programming
Technical Debt: Understanding it Rightly, Engaging it Rightly #LaravelLiveJP
shogogg
0
190
AI 時代のソフトウェア設計の学び方
masuda220
PRO
29
12k
AIとRubyの静的型付け
ukin0k0
0
540
AIエージェントと協働するCLI開発 — BunとOpenClawで学んだこと
yoshikouki
1
240
Make SRE Operations Easier with Azure SRE Agent
kkamegawa
0
4.1k
SPMマルチモジュールで テストカバレッジを取得する技法
yosshi4486
0
140
並列実装の現場、2ヶ月間実務でAIを使い倒したAIもPCも私も限界が近い
ming_ayami
0
110
プラグインで拡張される Context をtype-safe にする難しさと設計判断
kazupon
2
590
DynamoDBには集計系のクエリがないけどなんとかしたい
musan
1
130
jQueryをバージョンアップする前に使いたいjQuery Migrate
matsuo_atsushi
0
190
「エンジニアインターン、どうやって取った?」準備のリアルを語るLT会 Progate BAR
akiomatic
0
120
AI時代の仕事技芸論 — ソフトウェア開発で「遊ぶように働く」職人的熟達のすすめ
kuranuki
1
610
Featured
See All Featured
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
3.3k
How to train your dragon (web standard)
notwaldorf
97
6.7k
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
71
40k
Product Roadmaps are Hard
iamctodd
PRO
55
12k
Marketing to machines
jonoalderson
1
5.4k
How Software Deployment tools have changed in the past 20 years
geshan
0
34k
Code Review Best Practice
trishagee
74
20k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
12
1.2k
A designer walks into a library…
pauljervisheath
211
24k
Practical Orchestrator
shlominoach
191
11k
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
160
B2B Lead Gen: Tactics, Traps & Triumph
marketingsoph
0
140
Transcript
Effectで作る 堅牢でスケーラブルなAPIゲートウェイ Yuichi Goto (@_yasaichi) May 11,2024 @ TSKaigi
自己紹介 Yuichi Goto @_yasaichi @yasaichi 株式会社EARTHBRAIN
シニアエンジニア 「パーフェクトRuby on Rails」共著者 texta.fm (ex-host) 2 2
[PR] EARTHBRAINは「建設現場のデジタル革命」に挑む企業です 技術的には「ハードとソフトの 高度な融合」に挑んでいる 画像出典: https://www.earthbrain.com/smartconstruction/ 3 3
本発表の背景と目的 背景: あるAPIゲートウェイのTypeScript,Deno,NestJSによるリプレース プロジェクトでの話(リプレース後の実装は検証環境で稼働している) 目的: 本プロジェクトで導入した「Effect」というライブラリを紹介すること 本ライブラリを導入した目的,結果,得られた学びを共有すること 4 4
Agenda 1. Effectの紹介 2. Effectの導入目的と結果 3. まとめ 5 5
とは何か 2024年4月に安定版のv3に到達した TypeScriptの新興ライブラリで, Effect System(※)を実装したもの [1] [2] 実体験から Effect Systemの詳細に立ち入らずとも,ソフトウェア開発の
実務で利用できると感じている (ただし個人差あり,後述) Option/Either,不変データ構造,パターンマッチング,DI,Telemetry等を 提供する標準ライブラリ的な側面もあるが,本発表では言及しない ※ プログラムのEffect(例: 副作用)を説明する形式システムで [3],コンピュータサイエンスにおける研究テーマの1つ 6 6
Effect型の値の定義と生成 型定義: Effect<Success, Error, Requirements> 第2,3引数はそれぞれ「失敗時のエラーの型」「依存の型」を表す 型の観点では 成功時以外の型も表現できるPromise という理解でよい 生成方法(一部抜粋):
関数から: Effect.sync , Effect.tryPromise , Effect.gen それ以外の値から: Effect.succeed , Effect.fail 7 7
Effect型の値の合成と実行 合成方法: pipe と yield* によってEffect型の値を別の処理の中で参照 することで実現(Promiseの then と await
に“見かけ上”似ている) 実行方法(Promiseと異なり,明示的に実行する必要がある) Effect.run(Sync|Promise) : 成功時は値を(非)同期的に返し, 失敗 時はエラーを投げる Effect.run(Sync|Promise)Exit : 成功時は同上で,失敗時はエラーを 値として(非)同期的に返す 8 8
import { Effect } from 'effect'; const remainder = (n:
number, d: number): Effect.Effect<number, Error> => !isFinite(n) || !isFinite(d) || d === 0 ? Effect.fail(new Error('Cannot calculate the remainder')) : Effect.succeed(n % d); const isEven = (n: number): Effect.Effect<boolean, Error> => remainder(n, 2).pipe(Effect.andThen((r) => r === 0)); Effect.runSync(isEven(42)); // true Effect.runSync(isEven(NaN)); // Error: Cannot calculate the remainder Effectを使った同期関数の実装と実行例: 剰余演算と偶数判定 コンパイラがisEvenのエラー型を remainderから推論できている 9 9
Agenda 1. Effectの紹介 2. Effectの導入目的と結果 3. まとめ 10 10
導入目的: ネットワークI/O起因のトレードオフへの対処 Effectを導入したのは,APIゲートウェイの実装に必ず含まれるネットワーク I/Oを伴う処理から生じる次のトレードオフに対処するため。 堅牢性とスケーラビリティ: 主要な関心事(サービス群へのAPIリクエスト, レスポンスの取捨選択と合成)を素直に実装すると犠牲になりがち コードの保守性: 堅牢性やスケーラビリティを考慮して注意深く実装すると 犠牲になりがち
11 11
export class UsersService { constructor( private readonly postApiService: PostApiService, private
readonly userApiService: UserApiService, ) {} async findOne(id: number): Promise<FindUserResponseDto> { try { const user = await this.userApiService.getUserById({ userId: id }); const posts = await this.postApiService.getPosts({ userId: id, limit: 5 }); return { id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), }; } catch (error) { if (error instanceof ApiException && error.code === 404) { throw new NotFoundException('User not found', { cause: error }); } throw error; } } } 素直な実装の例: ユーザーとその最新の投稿を取得し,結果を合成する 素直な実装のため保守性は高いが, 前述の観点で改善できる点がある ※ 実際のコードの理解には建設業のドメイン知識が必要になるため,「ユーザー」と「投稿」に置き換えている 12 12
// Before const user = await this.userApiService.getUserById({ userId: id });
// After const user = await retry( async () => { try { return await this.userApiService.getUserById({ userId: id }); } catch (error) { if (error instanceof ApiException && error.code === 404) { throw new AbortError( new NotFoundException('User not found', { cause: error }), ); } throw error; } }, { retries: 3 }, ); 堅牢性の観点: APIリクエストのリトライによる堅牢性向上の余地がある 13 13
// Before const user = await this.userApiService.getUserById({ userId: id });
const posts = await this.postApiService.getPosts({ userId: id, limit: 5 }); // After const [user, posts] = await Promise.all([ this.userApiService.getUserById({ userId: id }), this.postApiService.getPosts({ userId: id, limit: 5 }), ]); スケーラビリティの観点: APIリクエストの並行化による高速化の余地がある ユーザーIDが判明しているので,2つの APIリクエストを並行処理できる 14 14
async findOne(id: number): Promise<FindUserResponseDto> { const [user, posts] = await
Promise.all([ retry( async () => { try { return await this.userApiService.getUserById({ userId: id }); } catch (error) { if (error instanceof ApiException && error.code === 404) { throw new AbortError( new NotFoundException('User not found', { cause: error }), ); } throw error; } }, { retries: 3 }, ), retry( () => this.postApiService.getPosts({ userId: id, limit: 5 }), { retries: 3 }, ), ]); return { id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), }; } 改善後のコードは,素直な実装と比べて保守性が低下してしまう 1つの関数の中に複数の関心事が 入り組んでしまっていることが原因 15 15
Effectを導入するとどうなるか 16 16
findOne(id: number): Effect.Effect<FindUserResponseDto, NotFoundException> { return Effect.all( [ Effect .tryPromise(()
=> this.userApiService.getUserById({ userId: id })) .pipe(Effect.retry({ until: (error) => error instanceof ApiException && error.code === 404, times: 3, })), Effect .tryPromise(() => this.postApiService.getPosts({ userId: id, limit: 5 })) .pipe(Effect.retry({ times: 3 })), ], { concurrency: 'inherit' }, ).pipe( Effect.catchAll(({ error }) => error instanceof ApiException && error.code === 404 ? Effect.fail(new NotFoundException('User not found', { cause: error })) : Effect.die(error) ), Effect.andThen(([user, posts]) => ({ id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), })), ); } 複数の関心事を対応するコードブロックに分解でき,保守性が向上する 17 17
findOne(id: number): Effect.Effect<FindUserResponseDto, NotFoundException> { Effect .tryPromise(() => this.userApiService.getUserById({ userId:
id })) Effect .tryPromise(() => this.postApiService.getPosts({ userId: id, limit: 5 })) Effect.andThen(([user, posts]) => ({ id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), })), } 主要な関心事: APIリクエスト,レスポンスの取捨選択と合成 18 18
findOne(id: number): Effect.Effect<FindUserResponseDto, NotFoundException> { .pipe(Effect.retry({ until: (error) => error
instanceof ApiException && error.code === 404, times: 3, })), .pipe(Effect.retry({ times: 3 })), Effect.catchAll(({ error }) => error instanceof ApiException && error.code === 404 ? Effect.fail(new NotFoundException('User not found', { cause: error })) : Effect.die(error) } 副次的な関心事1: APIリクエストのリトライ,エラーハンドリング 19 19
findOne(id: number): Effect.Effect<FindUserResponseDto, NotFoundException> { return Effect.all( [ ], {
concurrency: 'inherit' }, ).pipe( ); } 副次的な関心事2: APIリクエストの並行化 20 20
導入結果と得られた学び 結果: 前述のコードに対して他のメンバーから「読みづらい」「難しい」という 反応を受けたため,現在はNestJSの一部レイヤーへの導入に留めている 学び: Effectの理解に加えて, pipe を使ったプログラミングスタイルの 導入も想定より障壁が高いこと 今後:
約2週間前にもう一方のGeneratorを使ったスタイルで素晴らしい 改善(※)が入ったため,こちらを使って全体導入に再挑戦したい ※ 以前は yield* _(effect) のようにEffect型の値にアダプターをかます必要があったが,v3.0.4で不要になった [4] 21 21
// async/await const add = async () => { const
x = await Promise.resolve(1); const y = await Promise.resolve(2); return x + y; }; // function*/yield* const add = Effect.gen(function* () { const x = yield* Effect.succeed(1); const y = yield* Effect.succeed(2); return x + y; }); Generatorを使うとasync/awaitとほぼ同じメンタルモデルで実装できる 22 22
findOne(id: number): Effect.Effect<FindUserResponseDto, NotFoundException> { return Effect.gen(this, function* () {
const [user, posts] = yield* Effect.all( [ Effect.retry( Effect.tryPromise(() => this.userApiService.getUserById({ userId: id })), { until: (error) => error instanceof ApiException && error.code === 404, times: 3, }, ), Effect.retry( Effect.tryPromise(() => this.postApiService.getPosts({ userId: id, limit: 5 })), { times: 3 }, ), ], { concurrency: 'inherit' }, ); return { id: user.id, username: user.username, latestPosts: posts.map((post) => ({ id: post.id, title: post.title })), }; }).pipe( Effect.catchAll(({ error }) => error instanceof ApiException && error.code === 404 ? Effect.fail(new NotFoundException('User not found', { cause: error })) : Effect.die(error) ), ); } 前述のServiceに適用するとmap/andThenがなくなりEasyな見た目に pipeを完全に使わない=Generator内でエラーを扱う ためには,Either型を導入する必要がある(次の論点) 23 23
Agenda 1. Effectの紹介 2. Effectの導入目的と結果 3. まとめ 24 24
まとめ EffectはTypeScriptの新興ライブラリで,開発者はPromiseに似たより型 安全な値を pipe や yield* で合成することで任意の処理を実装する 発表者はあるAPIゲートウェイの開発において,「堅牢性とスケーラビリティ」 「コードの保守性」のトレードオフを解決するため,本ライブラリを導入した 一定の成果が挙げられたが,
pipe を使った実装スタイルの障壁が高いと 判明したため,全体導入には至っていない(Generatorスタイルで再挑戦) 25 25
おわりに: Michael Arnaldi氏(BDFL of Effect)曰く 出典: https://x.com/MichaelArnaldi/status/1661478108447535105 26 26
ご清聴ありがとうございました This presentation is created by Marp. Great thanks @yhatt!
27 27
参考文献 1. Effect 3.0 – Effect Blog,URL: https://effect.website/blog/effect-3.0 2. effect/README.md
at e9875da3732bc67bb62789f2850d34abe7eb873d · Effect-TS/effect,URL: https://g ithub.com/Effect-TS/effect/blob/e9875da3732bc67bb62789f2850d34abe7eb873d/README.md#effect 3. Nielson, F., & Nielson, H.R. (1999). Type and Effect Systems. Correct System Design. 4. Release
[email protected]
· Effect-TS/effect,URL: https://github.com/Effect-TS/effect/releases/tag/effect% 403.0.4 28 28