Movatterモバイル変換


[0]ホーム

URL:


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

Effectで作る堅牢でスケーラブルなAPIゲートウェイ / Robust and Scala...

Avatar for Yuichi Goto Yuichi Goto
May 11, 2024

Effectで作る堅牢でスケーラブルなAPIゲートウェイ / Robust and Scalable API Gateway Built on Effect

May 11, 2024 @ TSKaigi

Avatar for Yuichi Goto

Yuichi Goto

May 11, 2024
Tweet

More Decks by Yuichi Goto

See All by Yuichi Goto

Other Decks in Programming

See All in Programming

Featured

See All Featured

Transcript

  1. Effectで作る 堅牢でスケーラブルなAPIゲートウェイ Yuichi Goto (@_yasaichi) May 11,2024 @ TSKaigi

  2. 自己紹介  Yuichi Goto  @_yasaichi  @yasaichi  株式会社EARTHBRAIN

    シニアエンジニア  「パーフェクトRuby on Rails」共著者  texta.fm (ex-host) 2 2
  3. [PR] EARTHBRAINは「建設現場のデジタル革命」に挑む企業です 技術的には「ハードとソフトの 高度な融合」に挑んでいる 画像出典: https://www.earthbrain.com/smartconstruction/ 3 3

  4. 本発表の背景と目的 背景: あるAPIゲートウェイのTypeScript,Deno,NestJSによるリプレース プロジェクトでの話(リプレース後の実装は検証環境で稼働している) 目的: 本プロジェクトで導入した「Effect」というライブラリを紹介すること 本ライブラリを導入した目的,結果,得られた学びを共有すること 4 4

  5. Agenda 1. Effectの紹介 2. Effectの導入目的と結果 3. まとめ 5 5

  6. とは何か 2024年4月に安定版のv3に到達した TypeScriptの新興ライブラリで, Effect System(※)を実装したもの [1] [2] 実体験から Effect Systemの詳細に立ち入らずとも,ソフトウェア開発の

    実務で利用できると感じている (ただし個人差あり,後述) Option/Either,不変データ構造,パターンマッチング,DI,Telemetry等を 提供する標準ライブラリ的な側面もあるが,本発表では言及しない ※ プログラムのEffect(例: 副作用)を説明する形式システムで [3],コンピュータサイエンスにおける研究テーマの1つ 6 6
  7. Effect型の値の定義と生成 型定義: Effect<Success, Error, Requirements> 第2,3引数はそれぞれ「失敗時のエラーの型」「依存の型」を表す 型の観点では 成功時以外の型も表現できるPromise という理解でよい 生成方法(一部抜粋):

    関数から: Effect.sync , Effect.tryPromise , Effect.gen それ以外の値から: Effect.succeed , Effect.fail 7 7
  8. Effect型の値の合成と実行 合成方法: pipe と yield* によってEffect型の値を別の処理の中で参照 することで実現(Promiseの then と await

    に“見かけ上”似ている) 実行方法(Promiseと異なり,明示的に実行する必要がある) Effect.run(Sync|Promise) : 成功時は値を(非)同期的に返し, 失敗 時はエラーを投げる Effect.run(Sync|Promise)Exit : 成功時は同上で,失敗時はエラーを 値として(非)同期的に返す 8 8
  9. 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
  10. Agenda 1. Effectの紹介 2. Effectの導入目的と結果 3. まとめ 10 10

  11. 導入目的: ネットワークI/O起因のトレードオフへの対処 Effectを導入したのは,APIゲートウェイの実装に必ず含まれるネットワーク I/Oを伴う処理から生じる次のトレードオフに対処するため。 堅牢性とスケーラビリティ: 主要な関心事(サービス群へのAPIリクエスト, レスポンスの取捨選択と合成)を素直に実装すると犠牲になりがち コードの保守性: 堅牢性やスケーラビリティを考慮して注意深く実装すると 犠牲になりがち

    11 11
  12. 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
  13. // 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
  14. // 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
  15. 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
  16. Effectを導入するとどうなるか 16 16

  17. 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
  18. 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
  19. 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
  20. findOne(id: number): Effect.Effect<FindUserResponseDto, NotFoundException> { return Effect.all( [ ], {

    concurrency: 'inherit' }, ).pipe( ); } 副次的な関心事2: APIリクエストの並行化 20 20
  21. 導入結果と得られた学び 結果: 前述のコードに対して他のメンバーから「読みづらい」「難しい」という 反応を受けたため,現在はNestJSの一部レイヤーへの導入に留めている 学び: Effectの理解に加えて, pipe を使ったプログラミングスタイルの 導入も想定より障壁が高いこと 今後:

    約2週間前にもう一方のGeneratorを使ったスタイルで素晴らしい 改善(※)が入ったため,こちらを使って全体導入に再挑戦したい ※ 以前は yield* _(effect) のようにEffect型の値にアダプターをかます必要があったが,v3.0.4で不要になった [4] 21 21
  22. // 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
  23. 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
  24. Agenda 1. Effectの紹介 2. Effectの導入目的と結果 3. まとめ 24 24

  25. まとめ EffectはTypeScriptの新興ライブラリで,開発者はPromiseに似たより型 安全な値を pipe や yield* で合成することで任意の処理を実装する 発表者はあるAPIゲートウェイの開発において,「堅牢性とスケーラビリティ」 「コードの保守性」のトレードオフを解決するため,本ライブラリを導入した 一定の成果が挙げられたが,

    pipe を使った実装スタイルの障壁が高いと 判明したため,全体導入には至っていない(Generatorスタイルで再挑戦) 25 25
  26. おわりに: Michael Arnaldi氏(BDFL of Effect)曰く 出典: https://x.com/MichaelArnaldi/status/1661478108447535105 26 26

  27. ご清聴ありがとうございました This presentation is created by Marp. Great thanks @yhatt!

    27 27
  28. 参考文献 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

[8]ページ先頭

©2009-2025 Movatter.jp