2021年5月28日(金)に行われたイベント『事業成長を加速させたエンジニアリングのウラ側』の登壇資料です。
API、普通に動かしたいじゃないですか。動かしたいですよね?でも普通って簡単じゃないよね...という話をします。 フィッツプラスでは異なるスキーマのAPI開発を同じRailsアプリケーション上で行っています。 その際に気づいたAPI開発のお作法や罠についてお話します。
- URL: https://medpeer.connpass.com/event/211745/
RailsのAPIを普通に動かしたい話2021 / 05 / 28~ 事業成長を加速させたエンジニアリングのウラ側 ~@terry_i_MedPeer, inc. - Engineer
View Slide
Agenda自己紹介導入背景起きたこと原因と解決まとめ
誰やお前
福本 晃之Teruhisa FukumotoMedPeerWeb Developer (2019/10~)f-teruhisa @terry_i_
Twitter#medpeer
導入
特定保健指導 プラットフォーム開発
関連技術 / アーキテクチャ『特定保健指導"フィッツプラス"事業を支えるモノリシック Rails + VIPER Swift アーキテクチャ』: https://tech.medpeer.co.jp/entry/2020/06/22/121153
フィッツプラスのRails API● モノリシックRailsを中心とした構成● 同じRails上に異なるスキーマのAPIが複数存在する● 各APIスキーマのコンテキストが微妙に異なる○ スマホ/Webアプリ, 社内/外向け, toB/toC etc...
そこでタイトルの話題ですよ
RailsのAPIを普通に動かしたい話
“普通” is 何??● スキーマ定義通りにレスポンスを返すこと● エラー検知できること、通常動作は検知しないこと● テストが可能なこと、テストしやすいこと
普通が意外と難しい※APIを開発している現場のイメージです
● API開発でまんまとハマった罠● 最低限気をつけといたほうがよさそうな話● やった工夫とか小さいTipsとか今日する話「普通を目指すために」≠API開発のベストプラクティスの話
起きたこと
今回取り上げるサービスのAPIスマホアプリ ⇔ サーバー ⇔ 自社のRails(★)
1日の食事写真をスマホからアップロードスマホアプリ ⇔ サーバー ⇔ 自社のRails(★)
リリース前テストもバッチリ書いてるしstaging環境での動作検証も問題ない。通知されたエラーも全部潰したし、勝ちゲーや!!俺はエライ!!
リリース後に問題が...
同じエラーの通知が100回鳴る
ユニークなはずのレコードが2つ作られる
なんでや
サンプルコード※プロダクトコードから改変しています
● トランザクション● 並列度の高いAPIリクエスト● ActiveJobによる非同期処理この辺の複合的な要因
ちょっとずつ見ていきます
原因と解決
1. トランザクション2. 並列度の高いAPIリクエスト3. ActiveJobによる非同期処理原因
あっ(凡ミス)
find_or_create_by 自体がトランザクションを張るトランザクション内でカジュアルに使うhttps://railsguides.jp/active_record_querying.html#find-or-create-by
解決するなら...??● find_or_create_byしない(簡単)● トランザクションをネスト(require: true)させる○ トランザクションの状態を読み解きづらくあんまやりたくない● エラー時にトランザクションを抜けてリトライさせる● (create_withとかでINSERT時にlock外したりしてもOKぽいけど、コード上でロック状態を読み解かせるのはツライ気がしている)参考になるやつ『Rails APIドキュメント: Active Recordのトランザクション(翻訳)』 : https://techracho.bpsinc.jp/hachi8833/2020_11_30/101160
トランザクションのミスだけならエラーになって処理されないだけでは...??
ログを漁ってみた結果...※リクエスト元の管理画面のAPI連携ログの画面
ログを漁ってみた結果※リクエスト元の管理画面のAPI連携ログの画面同じエンドポイントのAPIに対して中身の違う複数のリクエストが同時に来てた※同じ日の食事が複数投稿/更新されたら、溜めてJobで一気に送ってたみたい
APIリクエストの”並列度”を考えてなかった● そもそも同時にAPIを呼ばれる考えがなかった● どのようにAPIが叩かれるかを把握してなかった● 並列度が高い状態でのトランザクションの扱い...??○ 何がいつロックされるのかよくわからん
● 検索(find)と更新(create / update)の分ける○ 検索はトランザクション外で実行● APIの処理全体を明示的にリトライさせる○ リトライ時に他のトランザクションのCOMMITを拾い、DBから検索できるように解決した際の方針
原因1. トランザクション2. 並列度の高いAPIリクエスト3. ActiveJobによる非同期処理
S3への画像アップロードをJobに渡している
Jobはこんな感じ
詳細(今回は割愛)https://zenn.dev/t_fukumoto/articles/fe3598ea0a128d93c8cd
起きたこと(再掲)ActiveJob::DeserializationErrorが起きまくった
● Jobの引数に配列を渡していた○ タイミングによってGlobalIDのシリアライズに失敗するっぽい● (エラーで)死んだJobが残る○ 失敗してもJob自体が破棄されない○ Sidekiqが律儀にリトライしようとする原因参考になるやつ『Rails: Active Jobスタイルガイド(翻訳)』 : https://techracho.bpsinc.jp/hachi8833/2020_09_30/96694
はいリトライで盛られていたので、実際には100回起きたわけじゃなさそう
Jobに渡すときのポイント● 引数に単一のインスタンスを渡す● JobではSidekiqのリトライ機能を使う(sidekiq6系~)● discard_onでエラーを指定し明示的にJobを破棄● トランザクション内でJobは呼ばない○ 今回はやらなかったけど、やりがちなので気をつける
🎉めでたし🎉※その後Jobで再度リクエストし直してもらい事なきを得る(重複データはRakeで滅尽🔪)
おまけ(FIXME)
並列度の高いRspecのテスト....??引用『RSpec での悲観ロックのテスト』 : https://tech.actindi.net/2015/11/09/3656032930
まとめ
● API開発は奥行きを意識する(トランザクション, 並列度)● APIを叩く側のコンテキストにも関心を持つ● 検索と更新 / 作成を分け、うまくリトライさせる● Jobは実行タイミングなど通常の処理との違いを理解するまとめ
Thank you!!@terry_i_MedPeer, inc. - Engineer