メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

DroidKaigi 2024に参加してきました!

はじめに

こんにちは!メドピアにてモバイルアプリエンジニアをしている佐藤です。
今年のDroidKaigiに、弊社はサポーターとして協賛し、総勢3名のモバイルアプリエンジニアがオフラインにて参加しました。
実際に足を運んだセッションを中心に、DroidKaigi 2024の参加レポートをお届けいたします。

セッションについて

Android ViewからJetpack Composeへ 〜Jetpack Compose移行のすゝめ〜

syarihuさんによるセッションです。

youtu.be

このセッションではJetpack Composeへの移行の進め方が中心のお話となります。
弊社では複数のスマホアプリをリリースしておりますが、ほとんどのアプリがAndroidViewで実装されている為、今後移行を進めていく際の参考になる内容がてんこ盛りでした。
すでに動いているアプリではFullCompose化を目指すのはデグレのリスク面から腰が重くなってしまう部分がありますが、UIの描画だけをCompose化する方法や実例を基にした移行のお話などがあり、弊社アプリでも移行を進める際の戦略に取り入れて進めていきたいと思いました。

2024年のナビゲーション・フォーカス対応:Composeでキーボード・ナビゲーションをサポートしよう

Tiphaine (ティフェン)さんによるセッションです。

youtu.be

ナビゲーション・フォーカスの説明から実装方法、デバッグのやり方について触れられていました。
私はナビゲーション・フォーカス対応の実装経験がなかった為、とても興味深く勉強になる内容でした。
知識として知っているというだけでもアプリ改善の際の提案の幅が広がるので、本セッションを通じてAndroidアプリエンジニアとして一段階成長することが出来たと思います。

また、余談にはなりますが本セッションのスピーカーであるTiphaineさんは弊社の元社員であり、MedPeer時代には多くのことを学ばせていただきました。
セッション後には弊社モバイルアプリエンジニアと記念撮影も行い、ブログへの掲載も快諾してくださいました。
Tiphaineさんの多大なるご厚情に心より感謝申し上げます。

要約:
あざす〜!!!!!!!!!!!!!!!
今度みんなで飲みに行きましょ〜🍻

Tiphaineさん(写真中央)と弊社MBアプリエンジニアの王(写真左)と佐藤(写真右)

KSPの導入・移行を前向きに検討しよう!

shxun6934さんによるセッションです。

youtu.be

KSPとkaptの説明、移行のメリットなどについて触れられていました。
弊社のアプリの中にはDataBidingを多用しているアプリもあり、DataBindingはKSPに非対応であることからKSPとkaptを併用するとビルド時間が遅くなるというのはまさに直面しそうな内容だった為、本セッションで予め知れて良かったです。
今後K2への移行に伴いKSPとkapt周りは触れる部分になってきますので、このセッションの内容はタイムリーでとても参考になりました。

電池寿命を考えた位置情報の監視方法を考える(Geofence)

はるちろさんによるセッションです。

youtu.be

Geofenceの説明、自作のGeofenceとGMSの比較などについて触れられていました。
Geofenceの機能を扱うアプリはあまり多くはないと思いますが、弊社のリリースしているアプリの中にはGeofenceを用いているアプリもあった為とても親和性を感じるセッションでした。
私自身もGeofence周りのコードに触れたことはありますが、私が1から開発したのではなく前任者から引き継いだアプリの為Geofenceの理解は浅瀬だった為、本セッションを通じて理解を深められ本当に良かったです。
余談ですがこのセッションを聞いた当日にGeofenceに関連するissueが立てられた為、「楽しい〜!」ってなりながら対応を進められました!笑

仕組みから理解する!Composeプレビューを様々なバリエーションでスクリーンショットテストしよう

Sumio Toyama (sumio_tym)さんによるセッションです。

speakerdeck.com

各ライブラリの説明やテストの実装方法について触れられていました。
コードを中心にお話があり、本セッションを通じて実装方法が参考になるのはもちろんのこと、スクリーンショットテストへの理解もとても深まる内容でした。
本セッションのお話でもあったような特定のバリエーションにおけるUIのバグ周りはまさに後回しにしがちだったりナイトモード自体にまだ対応していなかったりだった為、今後取り入れていきたい内容が満載のセッションでした。

アプリをリリースできる状態に保ったまま段階的にリファクタリングするための戦略と戦術

Yuki Anzaiさんによるセッションです。

youtu.be

リファクタリングを進める上での戦略から実際にどのようにリファクタリングを行うかをコードを交えてのお話でした。
参考になるリファクタリング例がてんこ盛りで勉強になったのはもちろんのこと、品質を維持したままリファクタリングを進める上での戦略がとてもわかりやすく説明されておりました。
特に戦略①であった環境を整えるの部分については改めてとても大切だと実感しましたので、弊社モバイルアプリエンジニア内においても周知を行いたいと思います。

Jetpack ComposeにおけるShared Element Transitionsの実例と導入方法 またその仕組み

hyogaさんによるセッションです。

youtu.be

Shared Element Transitionsの説明から実装方法についてのお話でした。
Shared Element Transitionsへの理解が深まったのはもちろんのこと、サンプルアプリも素晴らしくこんな素敵なアプリのコードがGitHub上で公開されていることに感謝が堪えません。
特に遷移アニメーション中のHorizontalPagerのスクロールを無効にするバグ対応の部分は結構な躓きポイントだと思います。もし私が実装していたら余裕で頭を悩ませていた自信があります笑
これらの解消方法含め知ることが出来てとても面白いセッションでした。

分析に裏打ちされたアプリウィジェット開発 - Jetpack Glanceとともに

Miyabi Goujiさん、Yuri Oguraさんによるセッションです。

youtu.be

ウィジェットに関する説明、ウィジェットを設置することによるKPIへの貢献や効果検証、実装方法などについて触れられていました。
個人的にはこのセッションが一番面白かったです。
特にデータ分析周りにも触れられており、ウィジェットが事業に対してどのように貢献し、そして認知度の少ないウィジェット機能をどのように訴求したかについても話されており、本セッションの内容はモバイルアプリエンジニアだけでなくアプリに携わるチームメンバーに対しても知っておいてもらいたい内容でした。
YouTubeにアップロードされた後は早速社内のチームメンバーにもシェアしました!

2024年最新版!Android開発で役立つ生成AI徹底比較

Nishimyさん、wiroha(ゐろは)さんによるセッションです。

youtu.be

Gemini、GitHub Copilot、ChatGPTの比較から活用事例について触れられていました。
生成AIは今では開発においてなくてはならないツールの一つであり、弊社においてもCopilotは希望するエンジニア全員に、ChatGPTも社内版ChatGPTの構築が行われエンジニア以外にも多くのメンバーが活用しております。
各生成AIの実務レベルでの活用例を本セッションで知ることが出来、弊社においてもより一層生成AIの活用を促進していきたいと思える素晴らしいセッションでした。

社内版ChatGPTの構築に関しては記事も公開されていますので、興味のある方はご参照ください。

tech.medpeer.co.jp

使って知るCustomLayout. vs DailyScheduler

Saiki Iijimaさんによるセッションです。

youtu.be

CustomLayoutに関する説明からDailySchedulerの実装方法について触れられていました。
ゴリゴリのJetpackComposeセッションでCustomLayoutに関する理解が深まったのはもちろんのこと、8:28辺りにある各関数の使い分けが出来るフローチャートはわかりやすくて最高です。
レイアウト周りは触れることの多い箇所なので、本セッションを理解することでレイアウト構築における選択肢の幅が広がる素晴らしいセッションでした。

AndroidアプリのUIバリエーションをあの手この手で確認する

Nozomi Takumaさんによるセッションです。

youtu.be

タイトル通りUIバリエーションの確認に関する様々な説明について触れられていました。
多くの確認方法の説明がありとても勉強になりました。特にフォルダブル端末の確認方法については疎い部分がありましたので、フォルダブル端末の確認やVirtual sensors活用例の部分は本当に助かる情報でした。

実践!難読化ガイド

みっちゃんさんによるセッションです。

youtu.be

タイトル通り丸々難読化に関するお話です。
難読化周りはたまにしか触れないので若干苦手意識があったのですが、このセッションのお陰で難読化面白い!となりました。
特に原理原則に基づいて立ち向かおう!のお話はコアな内容でありながらもとてもわかりやすくまとめられておりめちゃくちゃ面白かったです。

余談にはなりますが弊社では定期的に勉強会を実施しているのですが、現在はOWASP Mobile Top 10に関する勉強会を実施しており、このセッションを聞く2日前に M7: Insufficient Binary Protection Threat Agents の章に関する勉強会を実施してリバースエンジニアリングに関して語った後でしたのでより一層難読化セッションが楽しめました。
難読化のセッションと合わせてOWASP Mobile Top 10のこちらの章を読むことでより一層理解が深まると思います。

スポンサーブースについて

スポンサーブースにも多くの人がいて賑わいを見せており、すべては回りきれませんでしたが私も多くのスポンサーブースに足を運ばせていただきました!
ブースに足を運ぶことで各社に対する理解が深まり、ブースへ足を運んだことをきっかけに数多くのアプリをインストールしました!
話を聞いた後に実際にその会社のアプリのUI/UXに触れるのはリアルで交流した後ということもありめちゃくちゃ面白いです。

話を聞いてサービス面で特に面白かったのはMagicPodさんです。
一部弊社のサービスでもコア機能となる箇所はEspressoを用いてUIテストの導入を進めたりもしましたが、コア機能ゆえに変更が入り壊れることが多々ありメンテナンスには苦労しているのですが、何とMagicPodさんではAIが自動でスクリプト修正を行なってくれるとのこと!!!
UIテストの導入コストが下がるだけでなくメンテナンスコストも下げられる素敵なサービスだと思いました!

magicpod.com

また、ノベルティも多くいただきました!
どれも素敵なノベルティばかりでしたが、個人的に特に嬉しかったのがタイミーさんとnewmoさんのノベルティです。

タイミーさんのブースではガチャを回しタンブラーが当たったのですが、飲み物を美味しく飲むのはもちろん、ロゴがとても可愛いのでMTGでちょっとした雑談の際のネタにもなりそうなので重宝しております。

newmoさんではフェイスタオルをいただきました。私は趣味で毎週スーパー銭湯に通い月一ペースで遠征も行なっており、新しい銭湯施設に行った際は温泉タオルを収集している位温泉タオルが好きなので、このノベルティは本当に嬉しかったです。
このフェイスタオルを持ってDroidKaigiの翌日にスーパー銭湯で早速活用させていただきました!
切実に販売もして欲しい…!笑

最後に

DroidKaigi 2024はセッションによる勉強、ブースでの交流と得ることが多くとても有意義な大会でした!
このような素敵な大会を提供してくださったDroidKaigi運営の方々・スピーカーの皆様・多くの企業・DroidKaigiに関わるすべての方々に感謝を申し上げます。
また来年も参加できることを楽しみにしております!

弊社MBアプリエンジニア小林(写真右)と佐藤(写真左)


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

YJIT有効化後にUnicornワーカーを増やした場合の各メトリクスの推移について

はじめに

こんにちは。サーバーサイドエンジニアの冨家(@asahi05020934)です。現在は、全国の医師が経験やナレッジを 「集合知」として共有し合う医師専用コミュニティサイト「MedPeer」の開発を行っています。

Ruby 3.2からYJITが実用段階になりました。「MedPeer」でもパフォーマンスを改善するためにYJITを有効化することになりました。

本番環境でYJITを有効化後、ECSサービスのMemory Utilizationに余裕があったので、ECSサービスのUnicornワーカー数を増やすことを検討しました。 しかし、YJIT有効化後のUnicornワーカー数とメトリクスに関してまとめられた記事が少なく、どのように推移していくのか予想を立てるのは簡単ではありませんでした。YJIT有効化後にUnicornワーカー数を増やすと各メトリクスはどのように増減していくのでしょうか。

そこで、今回は「MedPeer」の本番環境でYJIT有効化後にUnicornワーカー数を増やしていった場合の各メトリクスの増減を明らかにしていきます。

計測した環境

以下の環境をもとに計測を行いました。

  • ECSタスク数: 8
  • Rubyのバージョン: 3.3
  • Railsのバージョン: 7.0.8.4
  • ECSタスクが使用するメモリ量: 8192MiB
  • ECSタスクが使用するCPUユニット数: 4096

手順

以下の手順に沿って計測していきました。

  1. Unicornワーカー数が96の日の0時0分から23時59分までのメトリクスを計測する
  2. Unicornワーカー数を96から104に増やす
  3. Unicornワーカー数が104の日の0時0分から23時59分までのメトリクスを計測する
  4. Unicornワーカー数を104から112に増やす
  5. Unicornワーカー数が112の日の0時0分から23時59分までのメトリクスを計測する
  6. 1と3と5の計測した結果を比較する

それぞれの計測日を「MedPeer」のサービスの1つである「Web講演会」の全配信の日程に合わせることで、リクエスト数やリクエストの内容に大きな差異が起きないように工夫しました。

計測すると、Unicornワーカー数112の時点でECSサービスのMemory Utilization Maxの最大値が84.23%まで上昇しました。 これ以上Unicornワーカー数を増やすと本番環境がダウンする可能性を危惧し、Unicornワーカー数を112まで検証することにしました。

結果

表1は、Unicornワーカー数96、Unicornワーカー数104、Unicornワーカー数112の各メトリクスの平均値を表したものです。

Unicornワーカー数96 Unicornワーカー数104 Unicornワーカー数112
ECSサービスのCPU Utilization Avgの平均(%) 5.19 3.33 5.38
ECSサービスのMemory Utilization Avgの平均(%) 35.10 35.50 36.70
ALBのResponse Time p50の平均(ms) 50.00 51.00 48.00
ALBのResponse Time p90の平均(ms) 193.00 210.00 210.00
ALBのResponse Time p95の平均(ms) 317.00 380.00 327.00
ALBのResponse Time p99の平均(ms) 643.00 735.00 647.00
RDSのCPU Utilizationの平均(%) 5.32 4.15 5.87
レイテンシー p50の平均(ms)   41.10 40.80 39.90
レイテンシー p75の平均(ms)   68.40 68.00 68.50
レイテンシー p90の平均(ms)   150.30 159.00 154.00
レイテンシー p95の平均(ms)   255.50 289.30 253.10

表1 Unicornワーカー数96、Unicornワーカー数104、Unicornワーカー数112の各メトリクスの平均値

表2は、表1をもとに各メトリクスの変化率を計算したものです。

96から104の変化率(%) 104から112の変化率(%)
ECSサービスのCPU Utilization Avgの平均 -35.84 61.56
ECSサービスのMemory Utilization Avgの平均 1.14 3.38
ALBのResponse Time p50の平均 2.00 -5.88
ALBのResponse Time p90の平均 8.81 0.00
ALBのResponse Time p95の平均 19.87 -13.95
ALBのResponse Time p99の平均 14.31 -11.97
RDSのCPU Utilizationの平均 -21.99 41.45
レイテンシー p50の平均   -0.73 -2.21
レイテンシー p75の平均   -0.58 0.74
レイテンシー p90の平均   5.79 -3.14
レイテンシー p95の平均   13.23 -12.51

表2 Unicornワーカー数96、Unicornワーカー数104、Unicornワーカー数112の各メトリクスの変化率

考察

ECSサービスのCPU Utilization Avgの平均

計測結果

96から104の変化率は-35.84%であるのに対し、104から112の変化率は61.56%であることがわかりました。このことから、ワーカー数を上げることでECSサービスのCPU Utilization Avgは上がるとは限らないことがわかりました。

考察

直感に反する結果となりました。リクエストの負荷自体が少ない場合、ワーカーを増やしても処理するタスクが少ないため、CPU使用率はあまり上がらなかった可能性があると考えました。

ECSサービスのMemory Utilization Avgの平均

計測結果

ワーカー数を増やすたびに35.10%、35.50%、36.70%と増えています。このことから、ワーカー数を増やす程ECSサービスのメモリがより使われていくことがわかりました。

考察

直感通りの結果になりました。YJITはメタデータにもメモリを使用します。そのため、ワーカーを増やしてメタデータが増えたことで、メモリ使用量が増えることが考えられます。

ALBのResponse Time

計測結果

p50の平均はワーカー数を増やす度に50.00ms、51.00ms、48.00msとレスポンス速度が速くなっていることがわかりました。しかし、p90の平均、p95の平均、p99の平均はワーカー数を上げる度に速度が上がっていないことがわかりました。このことから、ワーカー数を上げることで、基本的なレスポンス速度は速くなるが、遅めのレスポンスは速度が速くなるとは限らないことがわかりました。

考察

p50の平均以外は、直感に反する結果となりました。p90以上ではネットワークの遅延が影響し、一部のリクエストが遅くなる可能性があると考えました。

RDSのCPU Utilizationの平均

計測結果

96から104の変化率は-21.99%であるのに対して、104から112の変化率は41.45%であることがわかりました。このことから、ワーカー数を上げることで、RDSのCPU Utilizationの平均は上がるとは限らないことがわかりました。

考察

直感に反する結果となりました。ワーカー数を増やしても、発行されるクエリがシンプルで、CPUを大量に使用しない場合、RDSのCPU利用率は上がらなかった可能性があると考えました。

レイテンシー

計測結果

p50の平均は、ワーカー数を上げるたびに41.10ms、40.80ms、39.90msと速くなっていることがわかりました。しかし、p75の平均、p90の平均、p95の平均はワーカー数を上げる度に速度が速くなっていないことがわかりました。このことから、ワーカー数を上げることで基本的なレイテンシーは速くなるが、遅めのレイテンシーは速くなるとは限らないことがわかりました。

考察

p50の平均以外は直感に反する結果となりました。高パーセンタイルのレイテンシーには、ネットワークの遅延や一時的な輻輳が影響することがあるので、順当に速くなっていかなかった可能性があると考えました。

結論

以上、「MedPeer」の本番環境でYJIT有効化後にUnicornワーカー数を増やした場合の各メトリクスの増減を明らかにしていきました。その結果、ワーカー数を増やすことで、「ECSサービスのMemory Utilization Avgの平均」は順当に増えていき、「ALBのResponse Time p50の平均」と「レイテンシー p50の平均」は順当に速くなることがわかりました。これは、Unicornワーカー数を増やすことでアプリケーションのメモリ使用量は増えていき、基本的な速度は改善していくということでしょう。

しかし、他のメトリクスはワーカーを増やすことによって順当に変化していきませんでした。より正確な原因については明らかになっていないので、今後の課題にしたいと思います。

最後に

今回の記事の他にもYJITに関連がある記事を執筆しているので、興味がある方はぜひ読んでみてください。


是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

iOSDC Japan 2024に参加しました

みなさん、こんにちは!アプリエンジニアのオウです。

先日、iOSDC Japan 2024に参加してきました!今年、メドピアはシルバースポンサーとしてiOSDC Japanをサポートいたしました。会場はたくさんの参加者で賑わい、とても充実した時間を過ごすことができました。いくつか印象に残ったセッションを紹介したいと思います。

Appleウォレット / Googleウォレットに チケットを保存する方法 speakerdeck.com

このセッションでは、Apple WalletとGoogle Walletにパスを保存する方法についての説明です。実は今回のiOSDCで初めてチケットをApple Walletに保存したんですが、流れがスムーズで印象的でした。普段はWallet関連の開発には関わらないんですが、これを機にちょっと調べてみました。iOSでパスを新規作成するのはシンプルなんですが、更新フローになるとWeb APIやプッシュ通知を使うので結構複雑。今後のプロジェクトで使えたらいいなと思っています。

メインスレッドをブロックさせないためのSwift Concurrencyクイズ speakerdeck.com

このセッション、個人的には結構面白かったです。Swift Concurrencyのクイズを通して実践的な知識が身につく感じがしました。これまで意識していなかったんですが、SwiftのバージョンによってConcurrencyのルールが変わることもあるんですね。ActorとかGlobal Actor周りのルールは覚えなくても、コードを書くときにメインスレッドをブロックしないよう意識することが大事だと思います。ちなみに、5問中4問正解しました!!

Meet BrowserEngineKit speakerdeck.com

BrowserEngineKitについてのセッションでした。これはiOS17.4から使えるようになるみたいで、実際のプロジェクトで利用できるようになるのは少し先ですが、カスタマイズ性が高いので今後使ってみたいなと思っています。ただ、EUの制約に対する対応が必要だったりと、ストア公開にはいくつかハードルがあるみたいです。セキュリティ面の制限も厳しそうです。XPC frameworkも興味深いです。シンプルなAPIインターフェイスでプロセス間の安全なデータやり取りができるのが特徴です。

StoreKit2によるiOSのアプリ内課金のリニューアル speakerdeck.com

StoreKitからStoreKit2に移行する話についてのセッションでした。自分もいくつかのプロジェクトでStoreKitを使っているんですが、トランザクション処理が結構面倒だなと感じていました。StoreKit2ではトランザクション処理が簡単になり、非同期コードもより直感的に書けるようになっているのがいいですね。StoreKit Testingや返金対応も便利になっているので、今後のプロジェクトでも活用したいです。

すべてのヘルスケアデータを紐解く speakerdeck.com

iOS 17からヘルスケアデータをエクスポートできるようになったということで、どんなデータが取れるのかを紹介するセッションでした。普段あまり触らない分野なので、細かくデータの中身を説明してもらえて勉強になりました。そもそもCDAという標準規格があるのも知らなかったです。医療業界でこういったデータが活用される場面が増えるのかもしれません。

リョムキャットのパーフェクトSwiftネーミング教室 speakerdeck.com

このセッションでは、Swiftにおける命名規則についてのベストプラクティスを学びました。個人開発だとあまり気にしないかもしれませんが、チーム開発では読みやすくて保守しやすいコードを書くための命名ルールが本当に重要だなと感じました。

所感

今年のiOSDCは9年目の開催ということで、運営もとてもスムーズで、快適に参加することができました。会場の配置や時間管理、会場の気温など、細部にまで気を配った運営には感心しました。

イベント中に他の参加者や企業の方々と交流することで、普段は知ることのできない知識を得ることができ、とても有意義な時間を過ごせました。特にiOS関連の深い話題や、日常業務ではなかなか触れない技術について学べたのが大きな収穫でした。

ただ、残念だったのはAIに関する話題があまり取り上げられなかったことです。私は、今後のアプリ開発においてAIが非常に大きな影響を与えると考えています。AI技術の進展により、私たちの開発プロセスや提供するユーザー体験がどう変わっていくのか、自分自身でもリサーチしてみようと思います。

今回のiOSDCで得た新しい技術や他の開発者とのつながりは、今後の仕事にも役立つと感じました。来年もまた参加できることを楽しみにしています。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

RailsアプリケーションにThrusterを導入する

こんにちは。サーバーサイドエンジニアの @atolix_です。

今回は37signalsが公開しているThrusterを、メドピアで本番運用をしているアプリケーションのkakariに導入してみました。

kakari.medpeer.jp

Thrusterとは

Thrusterはアセット配信やX-Sendfileのサポートといった機能を持ったGo言語製のHTTP/2プロキシです。

詳細な設定不要でRails標準のPumaサーバーと組み合わせて動作して、nginxを構成に加えた時と同様のリクエスト処理を行うことが可能です。

github.com

www.publickey1.jp

導入前の状況

導入以前のkakariは以下のような構成を取っていました。 アセット配信の為にALBの背後にnginxを置いています。

今回は管理コストを下げる為に、nginxを剥がしてThrusterからアセット配信を行えるように構成を変更していきます。

※Cloudfront等のCDNを使用する選択肢もありましたが、本サービスのクライアントへの影響を抑える為に、ドメイン及びインフラ構成の変更を最小限に留めることを優先して今回はThrusterを選定しています。

移行の前にnginxでやっていた処理と同等の事がThrusterでも可能かチェックします。

チェック項目 Thrusterで可能か
Body Sizeの制限 MAX_REQUEST_BODYというオプションで設定可能。
gzip圧縮 js/css ファイルは自動的に圧縮されて配信される(content-encoding: gzipが付与される)
配信するアセットのキャッシュと最大サイズ指定 キャッシュ可能。最大サイズはMAX_CACHE_ITEM_SIZEで指定。

移行手順

安全に切り替えを行う為に今回はECS serviceごと差し替える方法を取りました。

1. Rails側にThrusterを導入する

一時的にnginxでリクエストを受けるECS serviceと、Thrusterでリクエストを受けるECS serviceを並行に稼動させたいので、Thruster用のポートを8080番で開けます。

加えてnginxの設定と同等のカスタム設定を記述していきます。Thrusterのオプションに用いる環境変数は接頭辞にTHRUSTER_を付けても認識されるので、他の環境変数と区別しやすいように今回は付け加えています。

また、ThrusterはPumaをラップして起動するのでPumaの起動コマンドの頭にthrustを付け加えます。

# Dockerfile
ENV THRUSTER_HTTP_PORT 8080 # デフォルトでは80番ですが、nginxの設定とぶつからないように8080に変更
ENV THRUSTER_MAX_REQUEST_BODY 104857600 # リクエストの最大サイズ
ENV THRUSTER_MAX_CACHE_ITEM_SIZE 3145728 # キャッシュの最大サイズ
ENV THRUSTER_HTTP_READ_TIMEOUT 300 # READのタイムアウト秒数
ENV THRUSTER_HTTP_WRITE_TIMEOUT 300 # WRITEのタイムアウト秒数
...
CMD ["thrust", "bundle", "exec", "puma", "-C", "config/puma.rb"]

EXPOSE 3000 8080 # 3000番にリクエストを投げるとThrusterを無視してpumaにリクエストが飛ぶ

2. Thrusterでリクエストを受けるECS serviceを追加して並行稼動させる

コンテナ定義では先ほど開けた8080番をRailsコンテナのポートマッピングに指定しておきます。

# container_definition.json

{
    "name": "rails",
    "image": "${rails_image}",
    "portMappings": [
      {
        "hostPort": 8080,
        "protocol": "tcp",
        "containerPort": 8080
      }
    ],
    ...

ALBのTarget Groupでも8080番に対応したリソースを追加して、一旦はダミーのListener Ruleを追加します。

# app_alb.tf

resource "aws_alb_target_group" "app" {
  for_each = toset(["blue", "green"])

  name              = "${local.prefix}-app-tg-${each.key}"
  port              = 8080
  protocol          = "HTTP"
  vpc_id            = aws_vpc.main.id
  target_type       = "ip"
  proxy_protocol_v2 = false
  ...
}

resource "aws_alb_listener_rule" "app_listener_https_thruster" {
  listener_arn = aws_alb_listener.app_listener_https.arn
  ...
  condition {
    host_header {
      values = ["dummy.example.com"]
    }
  }
# ecs_service_app.tf

resource "aws_ecs_service" "app" {
  name    = "${local.prefix}-app"
  ...

  load_balancer {
    target_group_arn = aws_alb_target_group.app["blue"].id
    container_name   = "rails"
    container_port   = "8080"
  }

この段階ではThruster駆動のECS serviceにアクセスが飛ぶことはない状態です。

3. Thruster駆動のECS serviceにリクエストを流す

nginx駆動のserviceとThruster駆動のserviceそれぞれのTarget Groupのconditionを入れ替えて、リクエストの向きを切り替えます。

ここまででnginxをThrusterに差し替えることが出来ました。

実際のリクエストを見てしっかりキャッシュヒットしている事を確認しました。

移行後の影響

パフォーマンス面

Thruster切り替え後のパフォーマンスをDatadogで一週間分計測しました。

App Latency

パーセンタイル nginx駆動時(avg) Thruster駆動時(avg)
p50 15.3ms 13.4ms
p90 69.3ms 79.5ms
p95 125.7ms 133.5ms
p99 197.6ms 203.2ms

レイテンシは微妙に上がっています。 nginxよりパフォーマンスは劣ることは予想していましたが、そこまで差は大きくなさそうです。

App Task CPU

nginx駆動時(avg) Thruster駆動時(avg)
7.23% 8.35%

タスク全体ではやや増加していました(新たにgzip圧縮の処理が入っているのでその分上がっているかもしれません)

Rails Container CPU

nginx駆動時(avg) Thruster駆動時(avg)
4.42% 8.01%

ThrusterはRailsコンテナ内で実行されるのでこちらもCPU使用率が上がっていますね。

若干のパフォーマンス低下は見られましたが、現状は問題なくアプリケーションは動作しています。

まとめ

Thrusterに切り替えることでリクエストの処理をRailsコンテナ内で完結させる事が出来る為、nginxの管理コストを減らせる点はとても良いと感じました。

一方で勿論Railsコンテナのパフォーマンスにマイナスの影響が出る場合があるので、移行をする際は今までnginxでやっていた事と同等の処理が出来るか調査することが必須になりそうです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

SolidQueue解体新書

こんにちは。サーバーサイドエンジニアの三村(@t_mimura39)です。

さて、Railsエンジニアの皆さんは非同期処理にどのようなライブラリを利用していますか?
ちなみに弊社では Sidekiq を利用するプロジェクトが多いです。

tech.medpeer.co.jp

今回はRailsでの非同期処理ライブラリの新たな選択肢として誕生した「SolidQueue」について解説します。

github.com

目次

🆕 更新履歴 🆕

2024/09/12

SolidQueue v0.9.0のリリースに伴い大きく変更が入ったため以下の点を加筆・修正しました。

  • デフォルトの設定ファイルリネーム config/solid_queue.yml -> config/queue.yml
  • RecurringTaskの設定が config/recurring.yml に切り出された
  • RecurringTaskの設定で command を指定できるようになり、Jobクラスを作成せずとも簡易的なスケジュール実行が実現できるようになった
  • RecurringTaskのスケジューリング管理がDispatcherから新設されたSchedulerに切り出された

🙋 はじめに 🙋

本記事では SolidQueueの内部実装の簡単な解説 と、それらを理解するために必要な SolidQueueの簡単な概要 について記述します。 SolidQueueの使い方や各種設定値についてはREADMEを参照してください。

本記事執筆時点でのSolidQueueの最新バージョンは v0.9.0 です。 現在SolidQueueはv1リリースに向けて活発に追加開発されているため、記述内容の一部が古くなっている可能性がありますがご留意ください。

📝 SolidQueueとは 📝

Railsの非同期処理を実装するためのフレームワークである ActiveJob はバックエンドのライブラリを選択できる仕組みとなっています。

例えば以下はRails 公式ドキュメントで紹介されているバックエンドライブラリです。

ActiveJobの詳細は公式ドキュメント参照してください。 https://guides.rubyonrails.org/active_job_basics.html

SolidQueue はここに新たに仲間入りするActiveJobバックエンドの一つです。 また、次期Railsのバージョン(Rails v8)ではこのSolidQueueがActiveJobのバックエンドライブラリのデフォルトになりました

🚀 SolidQueueの特徴 🚀

引用元 https://github.com/rails/solid_queue/blob/v0.9.0/README.md#solid-queue

Solid Queue is a DB-based queuing backend for Active Job, designed with simplicity and performance in mind.

Solid Queue は DB ベースの Active Job 用キューイングバックエンドで、シンプルさとパフォーマンスを念頭に設計されています。

DBベースのActiveJobバックエンドライブラリはこれまでにもDelayedJobGoodJob などがありました。 GoodJobは近年出てきたバックエンドライブラリとして注目されていますがPostgreSQL専用なので利用シーンが限定されています。

Solid QueueはMySQL, PostgreSQL or SQLiteの全てをサポート ※1 し、尚且つデータベースの比較的新しい機能である 「FOR UPDATE SKIP LOCKED」を活用することで高パフォーマンスを実現 しています。

SolidQueueの誕生経緯はSolidQueueの開発メンバーによるこちらの紹介記事をご参照ください。

dev.37signals.com

🔓 「FOR UPDATE SKIP LOCKED」 とは 🔓

簡単に説明すると「FOR UPDATEロック付きでSELECTをするが、すでにロックがかかっているレコードはスキップする(ロック待ちが発生しない)」という機能です。

SolidQueueではこれを 「複数のWorkerがJobテーブルから安全かつ高速に1件のレコードを取得する」という用途で利用しています。

この機能がSolidQueueの重要なポイントとなりますが、以下のような形でシンプルに実装されているためSolidQueueの内部実装を理解するための必須知識とはならなそうです。

https://github.com/rails/solid_queue/blob/v0.9.0/app/models/solid_queue/record.rb#L9-L15

# SolidQueue::Record (SolidQueueが提供するモデルの基底クラス)
def self.non_blocking_lock
  if SolidQueue.use_skip_locked
    lock(Arel.sql("FOR UPDATE SKIP LOCKED"))
  else
    lock
  end
end

https://github.com/rails/solid_queue/blob/v0.9.0/app/models/solid_queue/ready_execution.rb#L32-L34

# SolidQueue::ReadyExecution.rb 実行可能なJobの取得処理(抜粋)
def select_candidates(queue_relation, limit)
  queue_relation.ordered.limit(limit).non_blocking_lock.select(:id, :job_id)
end

📝「FOR UPDATE SKIP LOCKED」の詳細については TechRachoさんの記事がとても詳しいので興味のある方はこちらを参照ください。

techracho.bpsinc.jp

🍀 4種類のアクターについて 🍀

SolidQueueは以下の4種類の「アクター」により構成されています。

  • Dispatcher: Jobの実行タイミング管理や同時実行可能数制限などを担当
  • Worker: 実行待ちJobをポーリング的に取得し処理を実行する
  • Scheduler: 後述する RecurringTask のスケジュール管理を担当
  • Supervisor: 設定内容に基づき上記2つを起動させ稼働状況の監視をする

このようにそれぞれの役割が明確に分かれているため、「比較的処理が軽量なDispatcherは1つだけ立ち上げ、Workerは複数立ち上げる」のような調整が柔軟にできるようになっています。

起動方法

以下のコマンドを実行することで、設定ファイルの内容に基づいた種類・個数の「アクター」を起動することができます。 まず最初に「Supervisor」が1つ起動し、そのSupervisorにぶら下がる形 で「Dispatcher」と「Worker」がN個、必要に応じて「Scheduler」が一つ起動する作りとなっています。

# SolidQueueが提供しているコマンド
bin/jobs start           # Starts Solid Queue supervisor to dispatch and perform enqueued jobs. Default command.
# config/queue.yml
# (設定例) Dispatcherが1つ、Workerが2つでぞれぞれ対象とするキューなどの設定を分けることが可能
production:
  dispatchers:
    - polling_interval: 1
      batch_size: 500
      concurrency_maintenance_interval: 300
  workers:
    - queues: "*"
      threads: 3
      polling_interval: 2
    - queues: [ real_time, background ]
      threads: 5
      polling_interval: 0.1
      processes: 3

(おまけ)二つの起動モードについて

SolidQueueはSupervisorのプロセスフォークとしてDispatcherとWorkerを起動する作りとなっています。
しかし、プロセスフォークではなくスレッドで起動する起動モードが実装予定とされていました。 前者のプロセスフォークの実装は「forkモード」、後者のスレッドの実装は「async」と呼称されていました。

ただし、「asyncモード」は完全に削除され「forkモード」に一本化されることとなりました。 SolidQueueの内部構造を理解する上でこの歴史的経緯について把握する必要はありませんが、現在まさに開発過渡期のためコードリーディング時に本件を頭の片隅に置いておくと混乱を回避できます。

本件についての詳細を知りたい方は以下のPullRequestをご参照ください。

🚶 SolidQueue実装の歩き方 🚶

ここまではSolidQueueのコードを読む上で初めに理解しておいた方が良い前提知識でした。
それでは実際にSolidQueueのコードを読んでいきましょう。

SolidQueueは完全にActiveRecordとActiveJobに依存しています。 そのため素直なRailsアプリケーションを読む感覚で全体を読み解くことが出来ます。

まず大きく分けて以下の二つを理解していきましょう。

モデル

DBの取り扱いを含むビジネスロジックは全てモデル(app/models/solid_queue)として定義されています。 まずはここを読むことで各モデルにどのようなメソッドが定義されているか(≒ どのようなデータ操作があるか)が理解できます。

アクター

前述した4つのアクターの動きは lib/solid_queue に定義されています。 「dispatcher.rb」「worker.rb」「scheduler.rb」「supervisor.rb」のようにアクター毎にファイルが分かれているためその点を念頭に読むと理解が早いです。

コードを読む上での些細な注意事項ですが、37signals ※2が開発するシステムによくある特徴である「ConcernによるModule分割」が各所に多用されています。 慣れない人にとってはメソッドの定義箇所を追うのが大変かもしれませんが、処理を抽象化し綺麗に共通化・分離されている様は参考になると思います。

また、concurrent-ruby が時折利用されているため一定ここの知識がある方がリーディングは捗ります。

github.com

🥞 SolidQueueのモデル(テーブル)🥞

SolidQueueはデータベースをバックエンドとしているため、素直なActiveRecordを用いたモデルが提供されています。

本記事執筆時点では以下のテーブル群で構成されています。

  • solid_queue_jobs
  • solid_queue_scheduled_executions
  • solid_queue_ready_executions
  • solid_queue_claimed_executions
  • solid_queue_blocked_executions
  • solid_queue_failed_executions
  • solid_queue_pauses
  • solid_queue_processes
  • solid_queue_semaphores
  • solid_queue_recurring_executions
  • solid_queue_recurring_tasks

最新の情報は db/migrate を参照してください。

Jobの状態遷移

まずはJobにはどのような状態があるのかを確認しましょう。
以下が状態遷移図となります。

SolidQueue Jobの状態遷移(全て)

一見複雑な状態遷移に見えますが、基本的には 「Scheduled -> Ready -> Claimed -> Finished」 の一方通行です。
上記の遷移図から「同時実行数制限」「Job処理失敗・再実行」などを除いたシンプルな遷移図がこちらになります。

SolidQueue Job状態遷移(抜粋)

この基本の状態遷移を頭に入れた上でコードリーディングしていきましょう。

状態の表現方法

Jobモデルに対してhas_one関連のexecutionが紐づく形でJobの状態が管理されています。

※ 「Finished(処理完了)」に関しては「jobs.finished_at が設定されている」又は「Jobレコードが削除されている」という形で表現されています。(この二つの表現方法は preserve_finished_jobs 設定によって切り替わります)

https://github.com/rails/solid_queue/blob/v0.9.0/app/models/solid_queue/job/executable.rb#L86-L109

# SolidQueue::Job::Executable (JobにincludeされるConcernモジュール)
def finished?
  finished_at.present?
end

def status
  if finished?
    :finished
  elsif execution.present?
    execution.type
  end
end

def execution
  %w[ ready claimed failed ].reduce(nil) { |acc, status| acc || public_send("#{status}_execution") }
end

モデルの実装例

全てのモデルの実装について解説してしまうと日が暮れてしまうためここでは二つの例のみを挙げます。

まず「実行待ちのJobの中から実行対象のJobをロックしつつ取得し実行中状態に遷移させる SolidQueue::ReadyExecution.claim」 の実装を見てみましょう。 https://github.com/rails/solid_queue/blob/v0.9.0/app/models/solid_queue/ready_execution.rb

module SolidQueue
  class ReadyExecution < Execution # 「Ready(実行待ち状態)」を表現するJobのhas_oneモデル
    class << self
      def claim(queue_list, limit, process_id)
        # QueueSelector.new(queue_list, self).scoped_relations
        # => 指定されたキューに対応するReadyExecutionのRelationを返却
        QueueSelector.new(queue_list, self).scoped_relations.flat_map do |queue_relation|
          select_and_lock(queue_relation, process_id, limit).tap do |locked|
            limit -= locked.size
          end
        end
      end

      private
        def select_and_lock(queue_relation, process_id, limit)
          return [] if limit <= 0

          transaction do
            candidates = select_candidates(queue_relation, limit)
            lock_candidates(candidates, process_id)
          end
        end

        # 指定件数分の実行待ち状態のJobを「FOR UPDATE SKIP LOCKED」を指定しつつ取得する
        def select_candidates(queue_relation, limit)
          queue_relation.ordered.limit(limit).non_blocking_lock.select(:id, :job_id)
        end

        def lock_candidates(executions, process_id)
          return [] if executions.none?

          # SolidQueue::ClaimedExecution = 実行中状態
          # 実行中状態として登録しJobが重複実行されないようにした上で実行待ち状態を解除する。
          SolidQueue::ClaimedExecution.claiming(executions.map(&:job_id), process_id) do |claimed|
            ids_to_delete = executions.index_by(&:job_id).values_at(*claimed.map(&:job_id)).map(&:id)
            where(id: ids_to_delete).delete_all
          end
        end

次に「Job作成時の初期ステータス決定処理」を見てみましょう。

https://github.com/rails/solid_queue/blob/v0.9.0/app/models/solid_queue/job/executable.rb

# SolidQueue::Job::Executable (JobにincludeされるConcernモジュール)
after_create :prepare_for_execution

def prepare_for_execution
  # 実行予定日時を超過(due?)の場合はディスパッチ処理(後述)を実行
  if due? then dispatch
  else
    # 実行予定日時が未来の場合は「Scheduled(実行予約)」状態に遷移
    schedule
  end
end

def dispatch
  # 同時実行可能数制限ロックを取得できた場合は「Ready(実行待ち)」状態に遷移
  if acquire_concurrency_lock then ready
  else
    # ロックを取得できなかった場合は「Blocked(実行制限中)」状態に遷移
    block
  end
end

いかがでしょうか?
多少コメントで補足をいれていますが、ActiveRecordが使われた素直なRailsアプリケーションとして自然に読めますね。

他のモデル(app/models/solid_queue)にもこのような形で処理が定義されているため自身で読んでみてください。

詳細は割愛しますが Queue のようにデータベースで管理されていない概念についても綺麗にモデリングされている点も参考になりますね。

次は「Supervisor」「Dispatcher」「Worker」の3つのアクターがそれぞれどのような処理をしているかについて見ていきましょう。 ※ Schedulerに関しては、「Recurring tasks」の項で解説します。

😎 Supervisor 😎

Dispatcher・Workerアクターの起動

https://github.com/rails/solid_queue/blob/v0.9.0/lib/solid_queue/supervisor.rb

Supervisorが起動されると、設定ファイルに基づいてDispatcherとWorkerアクターを起動します。

プロセスの死活監視

SolidQueue::Processモデルを活用し稼働しているプロセス(DispatcherとWorker、Scheduler)の死活監視をします。

https://github.com/rails/solid_queue/blob/v0.9.0/app/models/solid_queue/process.rb

# SolidQueue::Process
create_table "solid_queue_processes" do |t|
  t.string "kind", null: false
  t.datetime "last_heartbeat_at", null: false
  t.bigint "supervisor_id"
  t.integer "pid", null: false
  t.string "hostname"
  t.text "metadata"
  t.datetime "created_at", null: false
  t.string "name", null: false
  t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at"
  t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
  t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id"
end

各プロセスは一定間隔でハートビートイベントを発火( process.last_heartbeat_at のタイムスタンプ更新)しています。

https://github.com/rails/solid_queue/blob/v0.9.0/lib/solid_queue/processes/registrable.rb#L39-L46

# SolidQueue::Processes::Registrable (3アクターのプロセスクラスにincludeされるConcernモジュール)
def launch_heartbeat
  # 本処理の本質とは関係ありませんが `Concurrent::TimerTask` がとてもうまく活用されていて綺麗ですね。
  @heartbeat_task = Concurrent::TimerTask.new(execution_interval: SolidQueue.process_heartbeat_interval) do
    wrap_in_app_executor { heartbeat }
  end
  # 省略...
end

def heartbeat
  process.heartbeat
end

https://github.com/rails/solid_queue/blob/v0.9.0/app/models/solid_queue/process.rb#L22-L24

class SolidQueue::Process
  def heartbeat
    touch(:last_heartbeat_at)
  end
end

Supervisorはハートビートが一定間隔停止しているプロセスは停止したとみなし事後処理を実行します。

https://github.com/rails/solid_queue/blob/v0.9.0/lib/solid_queue/supervisor/maintenance.rb

# SolidQueue::Supervisor::Maintenance (SupervisorにincludeされるConcernモジュール)
def launch_maintenance_task
  @maintenance_task = Concurrent::TimerTask.new(run_now: true, execution_interval: SolidQueue.process_alive_threshold) do
    prune_dead_processes
  end

  @maintenance_task.execute
end

def prune_dead_processes
  # プロセスの停止処理を実行
  wrap_in_app_executor { SolidQueue::Process.prune }
end

🏹 Dispatcher 🏹

ポーリング

Dispatcherと後述するWorkerはポーリング処理を実行し続けます。
ポーリング処理は以下のように抽象化されたConcernとして実装されています。

https://github.com/rails/solid_queue/blob/v0.9.0/lib/solid_queue/processes/poller.rb#L24-L42

# SolidQueue::Processes::Poller (間接的に後述のDispatcherとWorkerにincludeされるConcernモジュール)
def start_loop
  loop do
    break if shutting_down?

    wrap_in_app_executor do
      unless poll > 0
        interruptible_sleep(polling_interval)
      end
    end
  end
ensure
  SolidQueue.instrument(:shutdown_process, process: self) do
    run_callbacks(:shutdown) { shutdown }
  end
end

def poll
  raise NotImplementedError
end

実行予約中Jobを監視&ディスパッチ(実行待ちに移動)

Dispatcherのメイン作業です。
deliver_laterなどで実行予約されたJobが実行タイミングを迎えたタイミングでディスパッチします。 あくまでディスパッチまでで、実際のJobを処理するのは後述のWorkerです。

https://github.com/rails/solid_queue/blob/v0.9.0/lib/solid_queue/dispatcher.rb#L26-L35

# SolidQueue::Dispatcher
def poll
  batch = dispatch_next_batch
  batch.size
end

def dispatch_next_batch
  with_polling_volume do
    # 実行予定日時(scheduled_at)が過去のJobを抽出しディスパッチする
    ScheduledExecution.dispatch_next_batch(batch_size)
  end
end

他にも「同時実行可能数制限のメンテナンス」もDispatcherの責務ですが、一旦解説を後回しとします。

🕺 Worker 🕺

実行待ち状態のJobをポーリング的に取得しJobを実行(perform)

Workerの責務はこれだけです。シンプルですね。

https://github.com/rails/solid_queue/blob/v0.9.0/lib/solid_queue/worker.rb#L21-L35

# SolidQueue::Worker
def poll
  claim_executions.then do |executions|
    executions.each do |execution|
      # Pool#postの中でJobがperformされる
      pool.post(execution)
    end

    executions.size
  end
end

def claim_executions
  with_polling_volume do
    # 実行待ち状態のJobを取得
    SolidQueue::ReadyExecution.claim(queues, pool.idle_threads, process_id)
  end
end

以上が3つのアクターの主な処理内容です。
アクターとして責務が分割されていることにより、それぞれの処理が単純化されていて理解しやすいですね。

これからはここまで言及しなかったSolidQueueの機能について簡単に紹介します。

🚧 Jobの同時実行可能数制限について 🚧

機能概要

Sidekiq Enterpriseの「Rate Limiting」のようなものです。

引用元 https://github.com/rails/solid_queue/blob/v0.9.0/README.md#concurrency-controls

Solid Queue extends Active Job with concurrency controls, that allows you to limit how many jobs of a certain type or with certain arguments can run at the same time. When limited in this way, jobs will be blocked from running, and they'll stay blocked until another job finishes and unblocks them, or after the set expiry time (concurrency limit's duration) elapses. Jobs are never discarded or lost, only blocked.

Solid Queue は Active Job に同時実行制御機能を拡張し、特定のタイプや引数を持つジョブを同時に実行できる数を制限することができます。 このように制限された場合、ジョブは実行がブロックされ、他のジョブが終了してブロックが解除されるまで、または設定された有効期限 (同時実行の制限時間) が経過するまでブロックされたままになります。 ジョブが破棄されたり失われたりすることはなく、ブロックされるだけです。

class MyJob < ApplicationJob
  limits_concurrency to: max_concurrent_executions, key: ->(arg1, arg2, **) { ... }, duration: max_interval_to_guarantee_concurrency_limit, group: concurrency_group

  def perform(arg1, arg2, arg3)
end

# 例: 同一アカウントに対してのお知らせ配信処理を同タイミングで最大2つまでに制限。しかし、Jobの実行が5分を超過するとその制限が解放される。
class DeliverAnnouncementToContactJob < ApplicationJob
  limits_concurrency to: 2, key: ->(contact) { contact.account }, duration: 5.minutes

  def perform(contact)

内部実装

その名の通り「セマフォ」を表現するモデルである SolidQueue::Semaphore(solid_queue_semaphore) というモデル(テーブル)が活用されていいます。

これは同時実行数制限対象(key)に対して、現時点で残り何件(value)のJobが実行可能かを表現しています。

SolidQueue::Semaphore
create_table "solid_queue_semaphores" do |t|
  t.string "key", null: false
  t.integer "value", default: 1, null: false
  t.datetime "expires_at", null: false
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at"
  t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value"
  t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
end

細かな処理内容については Semaphoreモデル に綺麗に定義されているためここを読み解くと良いです。

主に以下のような使われ方をしています。

# 同時実行数制限ロックの取得(booleanを返却)
# DispatcherがJobをReadyに移動するタイミングで呼び出される。
# ロックが取得できない場合はBlocked状態となる。
def acquire_concurrency_lock
  return true unless concurrency_limited?

  Semaphore.wait(job) # 指定したjobが実行可能か確認し、実行可能な場合は同時実行可能数(semaphores.value)を-1します。
end

# 同時実行数制限ロックの解放
# Jobの実行終了時などに呼び出される。
def release_concurrency_lock
  return false unless concurrency_limited?

  Semaphore.signal(job)
end

また、有効期限切れのSemaphoreを解放する処理がDispatcherの責務の一つとして定義されています。 詳細はここでは割愛しますが、興味がある方は SolidQueue::Dispatcher::ConcurrencyMaintenance をご参照ください。

⏰ Recurring tasks (cronでのスケジュール定義) ⏰

機能概要

引用元 https://github.com/rails/solid_queue/blob/v0.9.0/README.md#recurring-tasks

Solid Queue supports defining recurring tasks that run at specific times in the future, on a regular basis like cron jobs. These are managed by the scheduler process and are defined in their own configuration file.

Solid Queue は、cron ジョブのように将来の特定の時間に定期的に実行されるタスクの定義をサポートします。 これらはスケジューラー プロセスによって管理され、独自の設定ファイルで定義されます。

# config/recurring.yml
a_periodic_job:
  class: MyJob
  args: [ 42, { status: "custom_status" } ]
  schedule: every second
a_cleanup_task:
  command: "DeletedStuff.clear_all"
  schedule: every day at 9am

上記の通り、専用のJobクラスを定義せずに command を指定するだけで簡易的なスケジュール実行が実現できるのはとても便利そうですね。

内部実装

以下2つのモデルで管理されています。

  • SolidQueue::RecurringExecution
    • RecurringTaskが重複実行されないように排他制御するためのモデル(テーブル)です。
  • SolidQueue::RecurringTask
    • cronで定義されているスケジュールの設定値が格納されています。
    • 本機能を実装する上では本来不要ですが、後述のMissionControlの実装都合で追加されたモデル(テーブル)です。
# SolidQueue::RecurringExecution
create_table "solid_queue_recurring_executions" do |t|
  t.bigint "job_id", null: false
  t.string "task_key", null: false
  t.datetime "run_at", null: false # 実際に実行された日時ではなくcronで算出された日時が設定される
  t.datetime "created_at", null: false
  t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
  t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
end

# SolidQueue::RecurringTask
create_table "solid_queue_recurring_tasks" do |t|
  t.string "key", null: false
  t.string "schedule", null: false
  t.string "command", limit: 2048
  t.string "class_name"
  t.text "arguments"
  t.string "queue_name"
  t.integer "priority", default: 0
  t.boolean "static", default: true, null: false
  t.text "description"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true
  t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static"
end

Scheduler起動時に全てのRecurringTask定義に対して以下の処理が実行されます。 https://github.com/rails/solid_queue/blob/v0.9.0/lib/solid_queue/scheduler/recurring_schedule.rb

# SolidQueue::Scheduler::RecurringSchedule (DuspatcherにincludeされるConcernモジュール)
def schedule(task)
  # Concurrent::ScheduledTaskを利用し、cronに基づき「次回実行日時」に処理をスケジューリング
  scheduled_task = Concurrent::ScheduledTask.new(task.delay_from_now, args: [ self, task, task.next_time ]) do |thread_schedule, thread_task, thread_task_run_at|
    # 再帰的に本メソッドを呼び出し次回分のスケジュールを登録
    thread_schedule.schedule_task(thread_task)

    wrap_in_app_executor do
      # Jobを実行待ち状態で登録
      thread_task.enqueue(at: thread_task_run_at)
    end
  end
  # 省略...
end

注意点

以上の通りSolidQueueのRecurringTaskは「cron定義に基づいて次回実行を予約する」といったアプローチです。 そのため、cronによるJob実行予定日時が「SolidQueueが停止状態」となっていた場合にSolidQueue停止中はもちろんの事、次回SolidQueueが起動した際にも遡ってJobを実行することはありません。

(具体例)
cronが「毎朝7時(0 7 * * *) にDummyJob実行」と定義されている。 何かしらの事情により朝の6:55〜7:05の間SolidQueueが停止されていた場合、本日分のDummyJobは実行されない(エラーにもならない)。

Sidekiqのcron実装である SidekiqCron は、Sidekiq起動時に一定期間内であれば過去を遡ってJobを実行してくれる仕組みがあります。(詳細は こちら を参照ください)

どちらも一長一短ですが、Sidekiqなどの他のライブラリからSolidQueueに移行する際にはこういった細かな挙動の違いにも気をつける必要がありますね。

⛔ キューの一時停止 ⛔

他のライブラリと同様にSolidQueueにも特定のキューを停止する機能が備わっています。

Rails Consoleによる手作業( SolidQueue::Queue#pause )または、後述のUIダッシュボードでの操作によりキューを停止することが可能です。
停止されたキューは以下のテーブルに格納されます。

# SolidQueue::Pause
create_table "solid_queue_pauses" do |t|
  t.string "queue_name", null: false
  t.datetime "created_at", null: false
  t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true
end

このPauseはWorkerが処理対象Jobを抽出する際に参照され、当該キューに属するJobは処理対象から除外されます。

本記事では紹介していませんが、処理対象のキューを選択する処理もモデルとして表現されており「staging* のようなワイルドカード指定によるキュー名の前方一致」や「キューの優先順位」がシンプルに実装されているため読んでいて面白いです。
こちらも興味があれば覗いてみてください。 SolidQueue::QueueSelector

🔁 処理失敗したJobの再実行 🔁

Sidekiqのような「Job実行時に例外が発生し捕捉されなかった場合は自動再実行」のような仕組みはSolidQueue本体にはありません。

Jobを再実行するには以下のいずれかを選択する必要があります。

  • ActiveJobのretry_onを利用
    • ApplicationJobのような基底クラスでStandardErrorをretry_on対象とすることで実質的にSidekiqのような自動リトライを再現します。が、リトライ対象の例外は明示的に尚且つ限定的に指定することをオススメします。
  • 自前で特定の例外を捕捉し再度キューイング
  • 後述のUIダッシュボードで手動再実行

🖼️ UIダッシュボード 🖼️

SolidQueueと同様にRails公式ツールとして Mission Control — Jobs が開発されています。

github.com

※ 以下は本記事執筆時点最新バージョン v0.3.1 時点の内容です。

キューやジョブの一覧管理、失敗したジョブの再実行など基本的な機能は揃っています。 SolidQueue専用ではなく Resque にも対応しているようです。

SolidQueueと同様にこちらも絶賛開発が進行しているので今後の追加機能に期待です。

missin-control

導入時の注意点

Mission Controlは以下のGemが依存として定義されています。
rails/propshaft のようにかなり新しいアセット関連のGemが依存として定義されているため、プロジェクトによっては導入に一定のハードルがあるかもしれません。

https://github.com/rails/mission_control-jobs/blob/v0.3.1/mission_control-jobs.gemspec#L20-L23

# mission_control-jobs.gemspec
spec.add_dependency "rails", ">= 7.1"
spec.add_dependency "propshaft"
spec.add_dependency "importmap-rails"
spec.add_dependency "turbo-rails"
spec.add_dependency "stimulus-rails"
spec.add_dependency "irb", "~> 1.13"

🔥 パフォーマンスやDB負荷 🔥

Sidekiqを多用しているプロジェクトは「パフォーマンス(≒ Jobを捌く速度)」と「DB負荷」が気になるのではないでしょうか。
スタートアップな小さなサービスならまだしも、大量なJobを処理するようなサービスでSolidQueueを安全に利用できるのかは一定の不安があります。

一方、HEY ※3 では非同期処理の全てでSolidQueueが利用されているようです。

Solid Queueは現在、HEYのためだけに毎日2000万近くのジョブを実行しています。 すべてのアプリケーションの移行が完了したら、Solid Queue で 1 日あたり約 1 億件のジョブを処理することになります。 とてもSOLIDな1.0になるでしょう。 rosapolisによる素晴らしい仕事

ソリッド・キューがついに全HEYを制覇。 Resqueにさようなら! Resqueは長年にわたって私たちによく貢献してくれましたが、蓄積された複雑さをすべて圧縮できるクリーンシートの実装が必要な時期が来ていました。 何千万もの毎日のHEYジョブは、今ではSQだけで実行されている!

データベースのスペックやWorker数などは不明ですが、かなりの規模のプロジェクトでも十分に使えるポテンシャルは持っていそうです。

👋 まとめ 👋

簡単ではありますがSolidQueueの内部構造の解説でした。 普段から馴染みのあるActiveRecordで実装されているためとても理解がしやすかったです。

SolidQueueを採用する予定がない方にとっても、「Railsで開発されたジョブキューシステム」として捉えると良い学習対象となるのではないでしょうか。

本記事がきっかけで少しでもSolidQueueに興味を持っていただけたら幸いです。

👍 参考資料 👍

SolidQueueはまだまだ若いライブラリのため参考にできる資料が少ない印象です。
そんな中でもTechRachoさんの翻訳ブログはとても参考にさせていただきました。ありがとうございます。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp


  1. 「FOR UPDATE SKIP LOCKED」 をサポートしているデータベース(MySQL 8以上、またはPostgreSQL 9.5以上)で利用することが推奨されています。 これは「FOR UPDATE SKIP LOCKED」を利用できないと複数のWorkerが実行された際にロック待ちが発生しパフォーマンスが落ちてしまうためです。
  2. Ruby on Railsの作者であるDHHがCTOを務めるシステム開発会社。SolidQueueに関しても37signalsのメンバーが中心に開発をしている。
  3. 37signalsが提供しているメール・カレンダーサービス

成功循環モデルから学ぶ、チーム力を向上させた取り組み

こんにちは。エンジニアの保立(@purunkaoru) です。

先日、弊社のMVPに、開発チームのリーダーをしている四方さん(@shikatadesu)が選ばれました。

style.medpeer.co.jp

近くで見て、僕が勉強になった点を「成功循環モデル」をもとに紹介いたします。 チームビルディングで悩まれている方の参考や気付きになれば嬉しいです。

成功循環モデルは、「関係の質」、「思考の質」、「行動の質」、「結果の質」という順序で質が向上していく考え方です。各段階の質が高まることで次の段階の質も向上し、良い結果が循環するとされています。詳しくは以下のリンクをご参照ください。

「成功循環モデル」とは、組織として成果や結果を上げ続けるために必要な要素とサイクルを示した組織開発フレームです。組織の成果や結果を継続的に上げるためには、組織に所属する従業員同士の関係性が重要です。 Schoo for Business: 成功循環モデルとは?活用するメリットや方法を解説

実は、僕自身が最近までこの考え方にピンときてなくて、深く考えたり、実践することがありませんでした。特に、メンバー間の関係が良好ならば思考や行動、そして結果の質が自動的に向上するかというと、そうとは限りません。例えば、社内の飲み会やチーム旅行が直接的な成果向上につながるとは感じにくいですよね。また、組織の一体感が思考や行動の質を高めるという考えには納得できるものの、その一体感が具体的にどのような形で結果につながるのか、長らく疑問に思っていました。

四方さんが表彰された際に、チームの成功要因を分析したところ、この成功循環モデルで説明がつきそうだなと思ったので、その成果を開発者ブログに書きました。

以下は、チームの関係の質を向上させて、成功循環を生むために工夫された点です。

1. リーダーシップ領域の明確化

このチームでは、必要な時に必要な人がリーダーシップを発揮できる、シェアド・リーダーシップができるチームビルディングを目指していました。そのために、まずは全員がどこでリーダーシップを発揮するのかを明確にして、そのためにどのような知識を増やし、どのような行動を取るのかを考えたうえで、チーム内の共有を行いました。これにより、特定のリーダーのみにタスクや責任が集中することを防ぐとともに、メンバー同士が相互に信頼できる環境ができました。

2. タスクの質と期限の厳守

各メンバーの得意領域が明確になっても、タスクの質が低かったり、決められた期限が破られると、チーム内の信頼関係が落ちる要因となります。PdMもエンジニアも、自身のタスクへの質の向上や期限を守ることへの意識がとても高く、それが信頼関係を構築できる要因となりました。

3. タスクの背景共有

毎日の朝会では、「こぼれ話」というコーナーを設けて、PdMから業界についての共有やユーザーヒアリングの結果など、PdMがインプットしている情報の共有をしていました。この透明性がエンジニアにもタスクの背景を理解させ、より具体的な要件や分析手法の提案を促しました。

これらの取り組みによって「関係の質」が向上したことで、「行動の質」にも好影響を与えました。 以下に具体的な事例を示します。

  • エンジニアの業界理解の深化:エンジニアが業界に関する知識を深めたことで、要件定義やユーザー分析に関してもPdMと共に積極的に提案できるようになりました。
  • テスト工数と手戻りの削減:チーム全体でテスト工数や手戻りをどう減らすかについて議論しました。その結果、要件定義のドキュメントフォーマットやレビュー観点を見直し、テスト工数を半減することができました。
  • タスクの負担分散:PdMが急なタスクで忙しくなると、エンジニアが積極的に業務を引き受けて進捗遅れを防ぎました。これにより、PdMの負担が軽減され、プロジェクト全体の滞りない進行が保たれました。

今回の事例は、チームが各個人の特性やリーダーシップを活かし、チーム内の「関係の質」を高めた結果、「行動の質」や「結果の質」が上がった良い例でした。朝会で趣味やランチの話をするのもいいですが、それだけの関係にとどめず、行動や結果の質を上げるための関係作りをしているのが印象的でした。

僕はまだ読んでいませんが、このチームで成果を出しているメンバーからは、「GitLabに学ぶ 世界最先端のリモート組織のつくりかた」でも成功循環モデルについて触れられていると伺いました。

リーダーがメンバーのパフォーマンスを十分に発揮させるためには、マネージャーは価値観や性格の異なるメンバーやパフォーマンスが出ていないメンバーに対しても積極的に関係性を構築する必要があることがわかります。
(中略)
高いパフォーマンスを発揮するメンバーに対しては質の高い関係性を構築しやすいですが、パフォーマンスが低いメンバーに対しては冷淡になり、関係の質が低下しやすくなります。その他にも、質の良い関係性が構築されていたとしても、メンバー自身が自分の能力を過小評価していると高いOBSE(Organization-Based Self-Esteem)を獲得することは難しく、挑戦的な行動につながりづらいこともあります。

この本では、成功循環サイクルとメンバーの自己効力感を上げることが必要と書かれているようですね。自己効力感については、社内で行なっている施策があるので、別の機会に紹介したいと思います。

メドピアでは、チームビルディングや開発生産性を、個人の感性や勘に委ねるアートから、誰もが再現できるサイエンスにできるように、成功・失敗事例の振り返りに力を注いでいます。もし興味を持っていただけたら、開発者ブログをブックマークしていただけると嬉しいです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

HTTP API Clientライブラリの自作を手助けするGemを公開しました

こんにちは。サーバーサイドエンジニアの三村(@t_mimura39)です。 育休明け早々猛暑の熱気にやられ部屋に閉じこもっています。

今回はとあるGemを作成したので、そちらの紹介をさせていただきます。

目次

前フリ

Webアプリケーションを開発されている皆さん。 外部のHTTP APIを呼び出すような要件が発生したらどのように実装されますか?

まずはHTTP Clientライブラリ(Gem)の選定からですよね。 無難なところでfaraday、最近?だとhttpなんかも選択肢にありますし、Gemを利用せずにRuby標準の Net::HTTP を直接使うなんてこともありますよね。

これらのいずれかを採用した後はどのような実装をされますか?

お行儀が良い方は以下のような形で、専用のHTTP API Clientクラスを作成しその中にfaradayなどGemの依存を閉じ込めるのではないでしょうか

class BookStoreClient
  class Error < StandardError; end
  class ClientError < Error; end
  class ServerError < Error; end

  def books(limit:, offset: 0)
    connection = Faraday.new("https://example.com") do |builder|
      builder.response :raise_error
    end

    connection.get("/books")
  rescue Faraday::Error::ClientError
    raise ClientError
  rescue Faraday::Error::ServerError
    raise ServerError
  rescue StandardError
    raise Error
  end
end

こうすることのメリットは主に二つあります。

一つ目は 「faradayなどの外部gemの影響範囲を狭めることで、外部gemのアップデート時の対応が容易になる」です。

これは分かりやすいですね。
上記の例で言うとfaradayに関する定数やメソッドの利用箇所がClient内部に限定されるのでfaradayに非互換な変更が発生した場合はここだけを修正すれば良くなります。

二つ目は「複数サービスのAPIを同一箇所で呼び出す際に、Clientクラス単位で例外が分かれているため捕捉制御がしやすくなる」です。

これに関してもHTTP Clientに限定された話ではありませんが、例外が表す意味が大きくならないように適切に捕捉し別の例外に置き換えるべきです。という話です。

別の例を挙げるとActiveRecordの ActiveRecord::RecordNotFound があります。 これは「"何か" のレコードが見つからなかった」としか例外レベルでは表現されていないため、例外を捕捉する際に「何のレコードが見つからなかったのか」を特定するには例外オブジェクトの中身まで参照する必要があります。 faradayの例外に関しても全て Faraday::Error のサブクラスとして定義されているため同種の問題があります。

このような問題に対処するために「ライブラリが発生する汎用的な例外を適切に捕捉し、意味が限定的になるような個別の例外に置き換える」といった制御を私は良くやります。(皆さんもやりますよね?)

こういった実装は私の好みなので、今までも散々実装してきましたし今後も継続して推進しています。

が、しかし、しかしですよ。

HTTP APIを呼び出す処理なんてもう何回も実装して、その内の大半は同じような「例外の定義」「ステータスコードによる例外制御」をコピペ実装していませんか? 私はしてきました。ツライ

と言うことで本題です。

Gemの概要

上記のようなツラミを解消するGemを作成しました。

github.com

詳細はREADMEに記載しているためそちらをご参照ください。 と言いたいところですが、拙い英語でニュアンスが伝わるかも怪しいのでこの場を借りて母国語で解説したいと思います。

まず、本Gemの利用方法からです。

READMEに記載しているコード片をそのまま転記します。

class BookStoreClient
  include Ueki::HttpClient.new('https://example.com')

  # Class Method
  def self.delete_book(id)
    delete("/books/#{id}")
  end

  # Instance Method
  def books(limit:, offset: 0)
    get('/books', params: { limit: limit, offset: offset })
  end

  private

  def _default_headers
    h = super
    h['X-Request-Id'] = Current.request_id if Current.request_id.present?
    h
  end
end

BookStoreClient.new.post('/books', params: { title: 'Programming Ruby' })
#=> { id: 1, title: 'Programming Ruby' }

BookStoreClient.new.books(limit: 5)
#=> { books: [{ id: 1, title: 'Programming Ruby' }] }

BookStoreClient.delete_book(1)
#=> nil

BookStoreClient.get('/books/1')
#=> BookStoreClient::NotFoundError (status: 404, body: { message: 'Not Found' })

お察しの良い皆さまならこれだけで理解できるかと思います。

Ueki::HttpClient.new('https://example.com') で "良い感じ" のmoduleが作成されるので、これを自作のHTTP API Clientクラスにincludeするだけです。
そうすることで、getやpostなどのリクエストメソッドが定義され、レスポンスステータスに応じた例外ハンドリングも勝手に制御してくれます。

以上です!

あれ? 例外ハンドリングはfaradayの RaiseErrorMiddleware で十分と思ったそこのあなた! Uekiのウリはここからです。

Uekiがraiseする例外クラスは以下の階層構造となっています。

BookStoreClient
└── Error
    ├── RequestError
    │   ├── TimeoutError
    │   └── UnexpectedError
    └── UnsuccessfulResponseError
        ├── BadRequestError
        │   ├── UnauthorizedError
        │   ├── ForbiddenError
        │   ├── NotFoundError
        │   ├── RequestTimeoutError
        │   ├── ConflictError
        │   ├── UnprocessableEntityError
        │   └── TooManyRequestsError
        └── ServerError

こういったツリー構造って美しいですよね。

ですが、重要なのはそこではありません! これらのトップレベルには自作のHTTP API Clientクラスである BookStoreClient があります。

はい、もうお分かりの通りUekiは Faraday::Error のような汎用例外クラスではなく、includeしたクラスそれぞれに例外クラスも自動定義します。(これが欲しかった!)

これほど丁寧に例外ハンドリングをしてくれているので本Clientクラスの呼び出し側は安心して rescue BookStoreClient::NotFoundError のように例外を捕捉することができます。

ここまでをまとめると、Uekiは大きく分けて以下の二つの機能を提供します。

  • 例外クラスの自動定義
    • 400系, 500系などを考慮した捕捉制御のしやすい階層構造の例外
  • シンプルなget,postなどのリクエストメソッドの自動定義
    • ステータスコードに応じた例外ハンドリング機能付き

いかがでしょうか? とても薄い機能ですが痒い所に手が届くような機能提供となっているのではないでしょうか。

カスタマイズ性について

本Gemは元々弊社内の特定プロジェクトに依存した内部ライブラリとして開発していました。 そのため完全にfaradayに依存しており、faradayのミドルウェア設定なんかもビジネス要件に合わせたものを設定していました。 今回GemとしてOSS公開するにあたり、この点のカスタマイズ性について検討しました。

そこで私の中で出た結論としては「HTTPリクエスト処理に関してはUekiの責任でないから勝手に改変して良いと言う姿勢にしよう」です。

本件の詳細についてもREADMEに記載していますが、こちらでも解説します。

Uekiではデフォルトでfaradayの Net::HTTP adapter を利用し、ログ出力も デフォルトで有効にしています

例えば「Keep-Aliveを有効にしたい。そしてログ出力は不要だから抑制したい」のようなニーズがあったとします。 その場合は思い切ってメソッドを上書きして対処しましょう。

faradayのadapterを Faraday::NetHttpPersistent に変更し、好きなfaradayミドルウェアを自由に設定することが可能です。

class BookStoreClient
  include Ueki::HttpClient.new('https://example.com')

  private

  def _initialize_faraday_connection(request_options)
    Faraday.new(url: self.class::ENDPOINT, headers: _default_headers, request: request_options) do |builder|
      builder.adapter :net_http_persistent, pool_size: 5 do |http|
        http.idle_timeout = 100
      end
    end
  end
end

「faradayのどのadapterを利用しどのミドルウェアを適用するか」というのはビジネス要件次第で組み合わせが膨大になります。 設定変更用のDSLを提供することも検討しましたが、中途半端なものになってしまいかねないため思い切って「勝手にメソッドを上書きして」を公式の対処法と明言することとしました。

意味のある細かい粒度でメソッドを分割しているため、比較的メソッドの上書きはしやすいのではないかなと考えています。

ここでもう一つのニーズについて考えてみます。 「ウチのプロジェクトではfaradayは利用していない。Gemに頼らずNet::HTTPでリクエストしたい。それに逐一各クラスでメソッドの上書きをするのも面倒だ。」

ありそうな話ですよね。 これについてもUekiは "そこそこ" 対応しています。

Uekiの姿勢は「自作のRequesterモジュールを作成し、それをUekiで利用するように設定しましょう」です。 以下のようなイメージです。

class BookStoreClient
  include Ueki::HttpClient.new('https://example.com', requester: CustomRequester)
end

「最強のHTTP リクエスト処理」をUekiは目指しているわけではありません。 "皆さん" が思う「最強のHTTP リクエスト処理」を各々が実装すれば良いだけの話です。 Uekiは少しだけそこのお手伝いをします。

CustomRequester の作成方法など丁寧なドキュメントはまとめていませんが、おそらくここを自作したいと動く人は勝手にコードを読んで察するだろうと考えています。

また、Uekiがこの「HTTPリクエスト処理」に対しての想いが強くない表れとし、ueki.gemspecのdependencyには「faraday」を "あえて" 定義していません。 「faraday」の利用に関しても完全にオプショナルであるということです。

以上のように非常に柔軟で大胆なアプローチでUekiのカスタマイズ性を実現しています。

正直なところUekiに標準実装されている DefaultRequester に私が満足しているためここのカスタマイズ性に関してのモチベーションが低いです。 より洗練されたI/Fなど思いつく方いましたらIssueやPullRequestをお待ちしております。

まとめ

というわけで、今回は「HTTP API Clientライブラリの自作を手助けするGem」について紹介させていただきました。 このGemを利用せずとも、こういった考え方もあるのだなと思えていただけたら幸いです。

皆様のHTTP API人生に幸あれ

おまけ

class BookStoreClient
  include Ueki::HttpClient.new('https://example.com')

少し特殊なI/Fですよね。 includeできるのはmoduleだけなのに、 Ueki::HttpClient クラスとは何者なのかと。

これは 認証Gemのshrine なんかでも利用されているModule Builderパターンという形で実装しています。

こちらの連載記事が詳しいので最後の紹介しておきます。 techracho.bpsinc.jp

Uekiのコードも少しは整理されているものとなっているので、Rubyビギナーの方なんかは勉強がてら読んでみると面白いかと思います。

github.com


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp