メドピア開発者ブログ

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

Kaigi on Rails 2023 セッションレポート #kaigionrails

こんにちは!サーバーサイドエンジニアの近藤です。

Kaigi on Rails 2023 が、10月27日から10月28日にかけて開催されました。 2020年から続くKaigi on Railsですが、今年が初のオンライン&オフライン同時のハイブリット開催となります。 メドピアはゴールドスポンサーとして協賛し、15名ほどのメンバーが現地で参加しました。

当記事では、現地で聴いたセッションのうち印象に残った発表をピックアップしてご紹介したいと思います。

Exceptional Rails

speakerdeck.com

メドピアで技術顧問をしていただいている前島さんによるセッションです。 Railsにおいて例外をいかにして扱うか、というお話でした。

Railsコード中の例外処理ひとつ取っても、考えることは数多くあります。

  • いつ例外を発生させるか
  • 発生した例外をいつどのようにrescueするのか
  • 通知が必要な例外をどのように開発者に通知するのか
  • エラーページをユーザにどう表示するのか

エラーを扱う際のこれらの観点について、方針とその理由を身近な具体例を交えながら解説していました。

個人的に印象的だった話題は、外部サービス起因のエラーの通知についての話題です。 エラーを通知するか否かについて

  1. エラーが起きる状況を考えてみる
  2. そのエラーが開発者側では対応できないこと(外部サービスの不具合など)が原因で起きるものであれば(即時の)通知はしない
    • 対応できないエラーが来てもノイズになるため
    • レベルをwarningなどに設定し、短期間に頻発するようであれば通知するのはあり

といった判断基準を紹介しており、通知に対する考え方の面で学びになったお話でした。

全体的に、今後例外を扱う際にぜひ見返したくなるようなセッションでした。

Update Billion Records

speakerdeck.com

レコード数が50~60億という巨大なテーブルに対してカラムを追加し埋める際に、なにを考えどのような手段を取ったかについてのお話です。

この巨大なテーブルを更新するにあたって、
「更新中にサービスを止めてはいけない」
「更新処理はいつでも中断・再開できなければならない」
などの要件がありました。

適切な更新手段を見つけるために、最初は既存手法の踏襲から始め、

(要件や現状の)整理 →(手段の)発案 → 検証

というサイクルを繰り返し、最終的にたどり着いた手段は「更新対象をqueueで管理し非同期jobで少しずつ更新する」というものでした。

発表の後半で、スピーカーのtokutomiさんは今回の改修を次のように振り返っています。

最終的な構成も参考になるものでしたが、それと同じくらい、 最終的な構成に至るまでの改善プロセスも参考になる発表でした。

Turbolinksアレルギー患者に捧げるTurbo & Stimulusでの時短実装術

speakerdeck.com

弊社エンジニアの草分さんが「Turbolinksアレルギー患者に捧げるTurbo & Stimulusでの時短実装術」というタイトルで登壇しました。

[草分さん]「Hotwireという名前をご存知でしょうか?」

[会場] 「👏 👏 」

[草分さん]「では、Hotwireを本番環境等でちゃんと使っていましたでしょうか?」

[会場] 「......」

[草分さん]「🤯」

という笑い溢れる導入から始まったこのセッションは、 なにかと避けられがちなHotwireを実プロダクトで採用した話と、 その際にどのようにHotwireを使い時短を実現したかについての発表でした。

タイトルにもある「時短」について、

  • create (= フォームのsubmit) 前の確認画面
  • フォームの動的切り替え

という2つの状況を例に、 Hotwireを使った時短実装術をデモと共に紹介していました。

確認画面をモーダルで出すことでユーザー入力の再描画処理を省けたり、 Hotwireを使うことでリッチなUIもお手軽に実装できたとのことです。

スライドの左半分がデモ画面になっている超絶プレゼン技術でした

Hotwireをオススメできる状況として

  • 画面側がHotwireで十分実現できるとき
  • 社内向けの管理画面
  • バックエンドエンジニアが多いプロジェクト

を挙げており、実際にヤクチエの開発で採用した実例も紹介していました。

スライドの所々に小ネタが散りばめられており、会場から笑い声が絶えないセッションでした。

32個のPRでリリースした依存度の高いコアなモデルの安全な弄り方

speakerdeck.com

既存システムを動かしたままREAD/WRITEの多いテーブルのカラムを置き換えるために、MySQLのオンラインDDL機能(ロックを取得せずにマイグレーションを行える機能)を利用したお話です。

一言で「オンラインDDL機能を利用」といっても、考慮すべき点は数多くあります。

たとえばオンラインDDLそのものに対する注意点として、

  • 本来はオンラインDDLの対象になる操作であっても、場合によってはそうならない場合がある
  • オンラインDDLは一部の期間でメタデータロックというロックを取得するため、ロックを取る他の処理の確認が必要

などの注意点を紹介していました。

また、リリースフローに関する注意として、 コンテナを用いてBlue/Greenデプロイをしているような環境では、
「マイグレーションの完了タイミング」と
「コンテナの入れ替わりタイミング」にずれが生じてしまいます。

これにより、古いアプリが新しいデータベースを見に行ってしまう状況が発生するため、 単純に「カラム名の変更」をするのではなく「カラムの追加 → 役割の置き換え → 古いカラムの削除」という手順を踏んでリリースしたとのことです。

実際のリリースでは下図のような段階的な対応を行ったとのことで、このリリースに万全の体制で臨んだことが伺えます。

現在私が携わっているプロダクトでも大きなテーブルのマイグレーションには苦しんでおり、オンラインDDLの注意点や段階的なリリース手法など、ぜひ今後の参考にしたいセッションでした。

おわりに

全体的なセッションの傾向として、実際のプロダクト開発に関連する内容のものが多くを占めており、 drinkupや懇親会の会場でもプロダクト開発の話題が数多く飛び交っていたように思えます。 Kaigi on Railsを軸にして、Railsまわりの知見の交換が行われているのを感じました。

今回のイベントで得た知見を、私も日々のプロダクト開発に活かしたいなと思いました。 Kaigi on Railsへの参加は初めてだったのですが、とても充実した2日間でした!

Afterイベントやります!

connpass.com

11/9(木)の19時から、メドピア株式会社、株式会社スマートバンクさん、株式会社マイベストさんの3社合同で、アフターイベント「After Kaigi on Rails LT Night」を行います。

各社から1名ずつのLTと公募によるLTが3つの、計6つのLTが行われる予定です。 会場はメドピアのオフィスで、最寄り駅は東銀座駅となります。 RubyやRailsの関連技術について、食事とともにお話できる場になればなと思います。

ご興味ありましたら上記リンクからぜひご参加ください!


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


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

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp

Vue 2 の EOL まで 2 ヶ月ですが進捗どうですか?~Vue Fes Japan 2023 ゴールドスポンサーとして登壇します~

フロントエンドの小林和弘です。

遂にオフラインでの開催となった Vue Fes Japan 2023 が今月末に迫っています。
2019 年は台風、2020 年、2021 年は新型コロナウィルス感染症による開催見送り、去年は感染予防のためオンライン開催となっていました。
2018 年以来、実に 5 年ぶりのオフラインカンファレンスです(めでたい)🎉🎉🎉

vuefes.jp

メドピアは例年に引き続き Vue Fes Japan 2023 にゴールドスポンサーとして協賛しています。
(去年に引き続き、個人スポンサーもさせてもらいました)

その他にセッションルームネーミングライツスポンサー、スペシャルランチスポンサーとしても協賛しています。
タイムテーブル上の 4 つのトラックのうちのひとつが「メドピアトラック」という名前になっていて、さらにランチタイムでは「メドピアトラック」にてスペシャルランチセッションのお時間をいただいています。 そこでは Vue3 の migration についてのお話をする予定です。

セッションの概要

メドピアではいくつもの医療系サービスを開発・運用しています。そのほとんどのサービスで Vue を使わせてもらっています。

Vue は Single File Component で双方向データバインディングができるため Multi Page Application で部分的にリッチな UI が簡単につくれたり、Nuxt を使って Single Page Application が構築できたりする非常に便利なフレームワークだと思っています。

その Vue ですが Vue2 が 2023 年 12 月 31 日に LTS が終了して EOL を迎えてしまいます。Nuxt2 も同日に EOL になります。
年始にも Vue の開発者の Evan You がブログのポストで EOL についてリマインドのアナウンスを行っていました。今年は Vue2 / Nuxt2 の migration に追われたプロジェクトがいくつもあると思います。

Vue Fes Japan 2023 のランチセッションでは、メドピアで利用している Vue2 / Nuxt2 の migration をどのようにして行っているのか紹介したいと思います。
migration 中のプロジェクト、migration を考えているプロジェクトの一助になれば幸いです。

vuefes.jp

ご興味がある方は、Vue Fes Japan 2023 のランチセッションに足を運んでみてください。

ブース出展について

会場ではブースも出展します。

ブースに足を運んでいただくと、お酒との相性をセルフチェックできる「アルコールパッチテスト」をお試しいただけます。アフターパーティーで飲酒される方はぜひ試してみてください🍻
「アルコールパッチテスト」をお試しいただいて、X(旧 Twitter)で結果投票に参加いただくと大きなオリジナルバッグがもらえます 👜

アルコールパッチテストをするとオリジナルバッグがもらえます

また、メドピアのノベルティグッズも配布する予定です。
LINE スタンプにもなっているメドピア公式キャラクター「メドベア」がプリントされたかわいいノベルティグッズです。ご興味がある方は会場でゲットしてみてください。

メドベアのノベルティグッズと現場猫の VPoT

アフターイベントについて

Vue Fes Japan 2023 の 2 週間後、2023 年 11 月 7 日(火)にメドピア株式会社、MNTSQ 株式会社、及び株式会社 hacomono で Vue Fes Japan 2023 After Meetup を開催します。
Vue.js 日本ユーザーグループ代表の kazupon さんを特別ゲストとして迎え、セッションやパネルディスカッションを行います。

Vue Fes Japan 2023 と同じく、アフターイベントもオフライン開催を予定しています。
会場はメドピアのオフィスで、最寄り駅は東銀座駅になっています。

medpeer.connpass.com

こちらのイベントもご興味がある方はぜひ参加してみてください。


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


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

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp

A/BテストツールとしてCloudWatch Evidentlyを導入してみた

サーバーサイドエンジニアの熊木(@k_kumaki_)です。

先日、私が担当しているヤクメドにA/BテストツールとしてAWSのサービスであるCloudWatch Evidentlyを導入したので、その経緯や内容についてまとめます。

目次

CloudWatch Evidentlyとは

AWS上でA/Bテストやフィーチャーフラグの管理ができるサービスです。 比較的新しいサービスのため、馴染みのない方も多いかもしれません。

aws.amazon.com

導入に至った経緯

私が開発しているサービスであるヤクメドでは、UX向上のためA/Bテストを実施しています。

そのA/Bテストツールとして、Google Optimizeを使用していましたが、この度2023年9月30日をもってサポートが終了することが発表されました。そのため、A/Bテストの代替案を探す必要がありました。

導入理由

一番の理由は低価格であることです。 CloudWatch EvidentlyではEvidentlyイベントとEvidently分析ユニットに基づいて課金が発生する仕組みになっています。 これをもとに料金を試算すると、他のサービスに比べてかなり低額でA/Bテストを実施できるためCloudWatch Evidentlyを採用することとしました。

docs.aws.amazon.com

導入方法

構成図

1. プロジェクトの作成

ここではプロジェクト名と説明を設定するのみです。 また、データストアとしてS3かCloudWatch Logsが選択できるので、今回はS3を選択しました。 発生したイベントをデータストアに保存して、他のサービスと組み合わせることでより柔軟な分析が可能になります。

ヤクメドではTerraformでAWSリソースの管理をしており、以下のコードで作成しました。

resource "aws_evidently_project" "evidently" {
  name        = "${local.prefix}-evidently"
  description = "${local.prefix}-evidently"

  data_delivery {
    s3_destination {
      bucket = aws_s3_bucket.evidently_logs.id
      prefix = "${local.prefix}-evidently"
    }
  }

  tags = {
    Name = "${local.prefix}-evidently"
  }

  depends_on = [aws_s3_bucket_policy.evidently_logs]
}

2. 機能の追加

次に作成したプロジェクトに機能を追加します。 どういった分岐をするか、ユーザートラフィックを何%ずつ割り振るかなどの設定を行います。

3. アプリ側の対応

ヤクメドではバックエンドはRailsで書かれているため、CloudWatch EvidentlyのSDKを導入しました。

docs.aws.amazon.com

どこからでも使用できるA/Bテスト用のモジュールを作成し、以下のようなリクエストをCloudWatch Evidentlyに送信します。

evidently_client.evaluate_feature(
  {
    entity_id: entity_id, # ユーザーを特定する一意のID
    feature: feature, # 設定した機能名
    project: project, # 設定したプロジェクト名
  }
)

レスポンスとして、以下のような形式で返ってきます。 今回は機能でブール値を設定したためvalueとしてbool_value が返却されており、これをもとにアプリ内で分岐をさせることができます。

#<struct Aws::CloudWatchEvidently::Types::EvaluateFeatureResponse
 details="{}",
 reason="DEFAULT",
 value=#<struct Aws::CloudWatchEvidently::Types::VariableValue::BoolValue bool_value=true, double_value=nil, long_value=nil, string_value=nil, unknown=nil>,
 variation="LP">

よかった点

自分で細かな定義が可能

  • アプリ側で設定している情報に対して細かく制御が可能
    • 例えば、特定の地域のユーザーやChromeを使用しているユーザーなど細かく分けられます

参考: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Evidently-segments.html

導入が簡単

  • この記事にも書いた通り、元々AWSを使っているサービスであればかなり簡単に導入することが可能です

終わりに

今回は、シンプルな構成での導入をご紹介しました。

CloudWatch Evidentlyを導入してみてまだ数週間ですが、Google Optimizeと遜色なくA/Bテストを行うことができています。 この記事が、A/Bテストツールでお悩み中の方の参考になれば幸いです。


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


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

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp

Kaigi on Rails 2023に@lni_Tが登壇します & メドピアが協賛します #kaigionrails

皆様こんにちは、メドピアのサーバーサイドエンジニアの草分( @lni_T )です。

この度、2023/10/27(金)-28(土)の2日間で開催される「Kaigi on Rails 2023」に登壇させていただくこととなりました!
タイトルは「Turbolinksアレルギー患者に捧げるTurbo & Stimulusでの時短実装術」となります。

kaigionrails.org

スケジュールは、
Day1 16:50 〜 17:05 / Room B
を予定しています。 ぜひセッションにお越しくださいませ。

9月には大阪ruby会議03松江Ruby会議10 にて登壇させていただきましたが、それに続いて今月は東京での登壇となります。
先月のセッションも聞いていただいた方は、今月もぜひよろしくお願いします!

協賛について

メドピアはGold SponsorsとしてKaigi on Rails 2023に協賛しております!

kaigionrails.org

登壇内容について

セッションでは以下の内容をお伝えします。

皆様、Railsのフロントエンドツール「Hotwire」は使っていますか?
Rails 7からはデフォルトで採用されており、「Turbo」や「Stimulus」といったライブラリが利用できます。

ですが、利用者があまり多くなかった「Turbolinks」のイメージに影響されて、利用を避けている方は居ませんか?

このトークでは、実際のバックエンドリプレイス案件において、
Turbo & Stimulusを採用することで開発工数を大幅に削減できた事例、および実装方法についてご紹介します。

Hotwireは、JavaScriptをあまり書かずにモダンなWebアプリケーションを実現するフレームワークで、JSONではなくHTMLをやりとりすることが特徴です。
RailsのController/Viewのみでは手間がかかる画面の実装において、Hotwireは大きな効力を発揮してくれます。

セッションでは、そんなHotwireの活用例やノウハウを皆様に紹介します。

見どころは?

公式ページにも記載した通り、Hotwireを実際の開発案件での活用事例をお話しします。

背景となったメドピアの事業はこちらです。

medpeer.co.jp

「事業譲受」をおこなったため、システムもそのまま移行したかのように見えますが、実はバックエンド側については、ほぼ全てをRailsアプリでリプレイスしています。

限られた時間でリプレイスを進めるにあたり、画面実装だけに時間を割いている余裕はありません。
そんな中でも、Hotwireの「少ない労力でフロントエンドが実装できる」利点を活かすことで、無事にサービスをリリースすることができました。

今回のセッションでは、そんな【鉄火場での活用事例】をお伝えできればと考えています。

おわりに

Kaigi on Railsとしては初のオフライン/オンライン同時開催とのことで、どのような会になるのか非常に楽しみですね。
チケット購入がまだの方はこちらよりお求めください。 kaigionrails.doorkeeper.jp

それでは皆様、お会いできることを楽しみにしております!


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


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

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp

【Nuxt 3移行】ユニットテストをNuxt 2から移行し、実行速度が4倍速くなった話

こんにちは。フロントエンドエンジニアの相澤 ( @ttt3pu ) です。

みなさま、Nuxt 2 から Nuxt 3 へのアップグレードは順調でしょうか。

メドピアでは、2023年末のVue 2のEOLへ向けて、
各プロダクトで積極的にNuxt 3へのアップグレードを進めています。

現在私の担当しているプロダクトでは、マイグレーション作業自体はほぼ完了しており、
残すはQAテストなどを行うのみと言う段階で、本番リリースまであと一歩というところまで進んでおります! 🎉

マイグレーションの事例も徐々に増え始めてきて、Nuxt 3のリリース当初よりも段々と移行はしやすくなってきましたが、
個人的に結構大変だったのがユニットテストのマイグレーション作業でした。

当記事では、マイグレーションに当たっての Tips と、ついでに Vitest を導入したことにより、
実行速度が 約 10分 -> 約2分30秒 ( 約 4倍 ! ) まで跳ね上がった話をご紹介します。

目次

対象プロジェクトのユニットテスト周りのライブラリ構成

まず、今回マイグレーション作業を行なったプロジェクトは、以下のようなライブラリ構成で構築されていました。

移行後は、以下のようなライブラリ構成となりました。

Jest は廃止し、 Vitest へ移行しています。

vue-test-utilsについては、Vue 3対応しているのがv2以降となるため、バージョンが上がっています。

今回肝となったのが「nuxt-vitest」の導入でした。
Nuxt 3 + Vitest の実行環境を用意してくれるライブラリです。
詳しい内容については後述させていただきます。

移行完了までのステップ

プロダクトの実装状況によって多少順番が変わってくるとは思いますが、
テストが全て通るまでに、以下のステップで作業を行いました。

  1. Jest から Vitest へ移行
  2. nuxt-vitestを導入
  3. vue-test-utils の v1 から v2 へのマイグレーション作業を実施
  4. 落ちているテストごとの個別修正
  5. Vitest の設定を調整し速度改善

ここから先は、それぞれのステップで行なった作業について、詳しくご紹介させていただきます。

Jest から Vitest への移行

一番最初のステップとして、nuxt-vitestを導入する前準備として、JestからVitestへの移行を行いました。

Jestとは互換性が高いので、導入はほとんど苦になりませんでした。

ざっくりとまとめると、以下の作業を行うだけで移行が完了しました。

  • jest.config.ts の各設定を vitest.cofig.ts へ移行する
  • jest メソッドを vi へ置換する
    • 例: jest.fn -> vi.fnjest.spyOn-> vi.spyOn など
  • npm scripts等にCLIの記述があれば変更する

詳細については、公式の Migration Guide をご参照ください!

vitest.dev

nuxt-vitest の導入

続いて nuxt-vitest を導入します。

nuxt-vitest は、Nuxt の開発コアメンバーである Daniel さんによって開発されている、Nuxt 3 + Vitest の実行環境を用意してくれるライブラリです。

github.com

導入することより、自分で実装しようとするとかなり複雑になってしまうような設定を、ある程度自動で行なってくれるようになります。
コアメンバーが開発しているということもあり、メンテナンスもNuxt本体のアップグレードに合わせて頻繁に行われているので安心です。

導入方法もとても簡単です。
nuxt-vitest をインストール後、 nuxt.config.tsvitest.config.ts にそれぞれ読み込ませたら完了です!

※ バージョンによって導入方法が変わる可能性があるため、詳細は公式の README をご参照ください。

github.com

// nuxt.config.ts の設定例
export default defineNuxtConfig({
  // ...
  modules: [
    'nuxt-vitest'
  ]
})

// vitest.config.ts の設定例
import { defineVitestConfig } from 'nuxt-vitest/config'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
  },
});

実はこの時点でこのライブラリの恩恵をかなり受けることができており、
vitest.config.ts 内の記述をかなりスッキリさせることができています。

この時点で nuxt-vitest なしで実行しようとした場合、
useNuxtApp や useRoute 等の、 #importsから使用するメソッド類 の import 周りだったり・・・
composables と component のAuto Import 周りだったり・・・
等の問題が出てきてコケてしまいます。

この問題を手動で直すのはかなり難易度が高いため、自動で補完してくれるのはかなり助かりました。

また、 nuxt-vitest には、環境構築用の module だけでなく、
mockNuxtImport 等のユニットテスト用のヘルパーも用意されています。

これらの詳しい使い方については後述させていただきます。

vue-test-utils のマイグレーション作業を実施

ひとまずこの時点で vitest コマンドが正常に実行できる状態にはなるはずなので、
ここから先はspecファイル内の記述の修正を行なっていきます。

公式で Migration Guide が用意されているため、これに沿って実施を行なっていきます。

test-utils.vuejs.org

変更内容としてはそこまで難しくはないのですが、変更量はどうしても多くなってしまう感じでした。

大きいところだと stubsplugins 等のオプションを global の中に入れる必要が出てきたこと等でしょうか。
これが地味に結構大変でした・・。

// before
const wrapper = mount(Component, {
  stubs: {
    ...
  },
})

// after
const wrapper = mount(Component, {
  global: {
    stubs: {
      ...
    },
  },
})

落ちているテストごとの個別修正

この時点である程度テストは通るようになったと思います。
ここから先はテストごとに個別修正をしていきます。

ここから先はプロダクトによって臨機応変に対応を行う必要がありそうですが、
個人的に大変だった部分をいくつかピックアップして記載させていただきます。

Composition API 内で setData メソッドを使用している箇所の修正

vue-test-utils で用意されている、 data の値を変更するメソッドとして setData というものがあります。

test-utils.vuejs.org

このメソッドは v1 の時はComposition API を使用しているコンポーネントでも動いていてくれたのですが、 v2 からはうまく動作がしなくなってしまいました。

そのため、以下のような形で代用することで対応しました。

// before
const wrapper = mount(Component);
await wrapper.setData({ count: 1 });  
expect(wrapper.html()).toContain('Count: 1')

// after
const wrapper = mount(Component);
wrapper.vm.count = 1;
await flushPromises();
expect(wrapper.html()).toContain('Count: 1')

useRoute などの #imports から使用するメソッドのモック化

色々やり方はあると思いますが、
私たちのプロダクトでは、useRoute のモック化を Nuxt 2 環境では以下のような形で行なっていました。

// hoge.vue
<script>
const useRoute();
...
</script>

// hoge.spec.ts
const wrapper = mount(Component, {
  mocks: {
    $route: {
        path: '/hoge',
    },
  },
}) 
...

Nuxt 3 ではこの方法ではモック化ができなくなるため、修正を行う必要があります。

ここで nuxt-vitest に用意されているヘルパーメソッドが活躍します。
mockNuxtImport を使用することによって、以下のような記述で mock 化を行うことができます。

import { mockNuxtImport } from 'nuxt-vitest/utils'

mockNuxtImport('useRoute', () => {
  return () => {
    return {
      path: '/hoge',
    },
  }
})

ここも nuxt-vitest を使わなかったら更にひと工夫がいるであろう箇所のため、かなりの助かりポイントでした。

useRoute 以外のメソッドに関しても、同様の方法で対応を行うことができます。

Vitest の設定を調整し速度改善

なんとかテストが全て通るようになりましたが、
ここでせっかく Jest から Vitest に移行したのにあまり速度が変わっていないことに気づきます。

詳しく調べてみると --single-thread と言うオプションがあることがわかり、
このオプションを有効にしたところ、
冒頭に記載した通り速度が 10分 -> 2分30秒 ( 約 4倍 ! ) にまで跳ね上がりました!

vitest.dev

# コマンドの実行例
yarn vitest --single-thread --coverage

このオプションは名前の通り、シングルスレッドでテストを実行させるオプションで、
ざっくりとまとめると、

  • 無効にした場合 -> テストごとに別々の環境を作成した上で実行される
  • 有効にした場合 -> 同一の環境でテストが実行されるため、初期化を繰り返すコストを回避できる

という挙動を実現してくれるようです。

今回対応を行なったプロダクトは弊社内では規模が大きめのもので、 テストファイル総数 178件、テスト総数 854件 と実行される数も多いです。
そんな中、しかもcoverage付きでこの速度というのはなかなか感動しました・・・ ✨

ただし、公式の Docs に記載されている通り、実行する環境によっては、このオプションを使用した場合うまくいかない場合もあるようなので、そこに関しては注意が必要そうです。

WARNING

Even though this option will force tests to run one after another, this option is different from Jest's --runInBand. Vitest uses workers not only for running tests in parallel, but also to provide isolation. By disabling this option, your tests will run sequentially, but in the same global context, so you must provide isolation yourself.

This might cause all sorts of issues, if you are relying on global state (frontend frameworks usually do) or your code relies on environment to be defined separately for each test. But can be a speed boost for your tests (up to 3 times faster), that don't necessarily rely on global state or can easily bypass that.

最後に

Nuxt 3 本体のマイグレーションについては事例が徐々に出てきてはいるものの、テスト周りはまだ情報が少なくなかなか大変でした。
この記事がみなさまの Nuxt 3 アップグレードの助けになれば幸いです!


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


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

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp

Raspberry Pi + ChatGPT で会話ロボットを作った

会話の間が長く、スムーズでなくてすいません💦

長期休みに入る子供対策 R&Dの一環で子供達と会話してくれる AI ロボットを作りました。

github.com

会話の例

日々このような会話が繰り広げられる様になりました。

例1

  • 🤖「こんにちは。今日はどんなサッカー練習をしましたか?」
  • 👦「今日はダイレクトシュートの練習をしました」
  • 🤖「それは良いですね。ダイレクトシュートは難しいですよね。どんなダイレクトシュートの練習ですか?」
  • 👦「インサイドキックです」
  • 🤖「サッカーの試合で使われるキックの7割がインサイドキックと言われていますね。試合でもインサイドキックをしますか?」

例2

  • 🤖「こんにちは。どんなサッカー選手が好きですか?」
  • 👦「ドリブルやパスが上手な選手です。」
  • 🤖「どの選手が好きですか?」
  • 👦「モドリッチです。」

などなど、普段親子での会話では出ない様な回答が引き出せて面白いです。

ロボットには敬語使うんだなというのも興味深いです。

システム概要図

*1

大まかな処理の流れ

  1. モーションセンサー検知
  2. OpenAI に会話開始テキストを送信し、レスポンスを音声変換しスピーカーから再生
  3. 以下繰り返し
    1. ビープ音再生
    2. RPi 録音開始
    3. 子供しゃべる
    4. 録音内容を音声ファイルに保存
    5. 音声ファイルを Cloud Speech to Text API でテキスト変換*2
    6. テキストを OpenAI に送信しレスポンスを音声変換しスピーカーから再生

工夫点

  • 会話のきっかけはロボットから
    • 「話しかけられた」方が子供は嬉しいと思ったので 👦
  • 子供の話すターンの終了は無言3秒経過したら
    • 「最初は録音開始から10秒以内に話す」ルールにしたが、話したいことが言えなかった
    • pyaudio の無言時間計測できる仕様があったのでそれを利用した
  • 子供が話して良いタイミングにはビープ音を鳴らす
    • 話して良い合図がないとロボットとの会話は難しかった
    • ビープ音と一緒に話している間は LED を光らせる様に視覚的にも会話期間を認識させる
  • 会話は返答無視で自動終了
    • ある程度遊んだらロボットの返事待たずどこかへいってしまう為
    • 返答を 5秒無視で会話終了
      • 再度モーションセンサーが検知したら会話開始となる
  • 日本語・英語どちらでも話せる様にした
    • 私個人の英語学習の為に英会話もさせたかった

ChatGPT にお願いする会話の制約

英語・日本語で制約を指定しています。

先の動画では以下のような指示をしています。

ja:
  system_content: >-
    #指示:
    以下の制約の下でチャットしてください。

    #制約条件:
    私は日本のサッカーが大好きな小学生2年生です。
    会話のトピックはどんなサッカー選手が好きか、とか、今日はどんなサッカーの練習をしたとか、をお願いします。
    回答は極力短くお願いします。

使ったもの

  • RPi 4B
    • OS: Raspbian GNU/Linux 10 (buster)
  • OpenAI API
  • Cloud Speech to Text API
  • モーションセンサー
  • スピーカー
  • マイク

諸々購入しましたが、弊社エンジニアは実質 0 円です😄
弊社のテックサポート制度で購入をサポートしている為です 🎉

ブレッドボードの設定

SunFounder で各種モジュールの設定方法があったので参考にしました。

https://docs.sunfounder.com/projects/kepler-kit/en/latest/

難しかったこと

3歳の舌足らずなところはうまく聞き取ってもらえなかったです。 でも会話が継続されているのでこれはこれで良いのかもしれない😅

また、こちらの話終わりを「無言になってから3秒経過したら」としましたが、ノイズを拾ってしまい3秒以上かかることがありました。

マイクの性能にも依存するのかもしれませんが、スマートホームのようなスムーズな会話を実現するには録音ライブラリの微調整と機材の向上は必要かと感じました。*3

もしくは、視覚的にローディング待ち状態がわかる仕掛けがあると良いかなと思いました。

子供達の反応

  • 長男:
    • もっと自分に合ってるサッカーの練習方法を教えて欲しい⚽️
    • これいつまで話せばいいの?
  • 次男:
    • ロボットになりたい🤖

カスタマーからストイックな要求も飛び出し、改良の余地がありました。
会話を続ける限り続けられてしまうので質問攻めの苦痛もあった様です。*4
次男のサイボーグ化は家族会議します。

まとめ

シンプルにモジュールと API を繋ぎ合わせて Raspberry Pi + ChatGPT で会話ロボットを作ってみました。

今後、さらに会話した感が増すように唇が動いたり、表情筋を再現できるような仕組みを作りたいと思っています。(怖い)

以上
参考になれば幸いです。


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


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

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp

*1:シーケンス図は https://github.com/kenzo0107/raspi-talk#system-overview

*2:音声のテキスト変換について、julius: 日本語音声を認識できるものの、辞書の作成や各種ライブラリのインストールが必要で手間でした。 pocketsphinx はインストールの難易度が高く、解決しなかったので見送りました。最終的に音声ファイルからテキストに変換する際の精度と導入の簡易さから Cloud Speech to Text を選択しました。他良い選択があれば是非ともご教示ください🙇‍♂️

*3:動画では途中早送りしてます

*4:数ターンで会話終了する仕組みを入れても良さそう

お財布に優しいCI改善小ネタ集

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

主に保険薬局と患者さまを繋ぐ「かかりつけ薬局」化支援アプリ kakariのサーバーサイド開発(Ruby on Rails)を担当しています。

今回はRailsシステムのCI時間をコスト追加なしで半減した話をします。

目次

前提

対象プロジェクト

まず、今回対象となるRailsシステムの規模感について簡単にご紹介します。

kakariは2019年にリリースされ、今現在も活発に開発が進んでいるプロジェクトです。 大まかに以下のような構成となっています。

  • バックエンド Ruby on Rails
    • フロントエンドとモバイルアプリ向けのAPIを提供
    • 一部システムではwebpackを利用しVue.jsが組み込まれているRails Viewも提供
  • フロントエンド Vue.js
    • Rails Viewに組み込まれている
  • モバイルアプリ Kotlin, Swift
    • ※ 今回のスコープ外

今回はこれらのうち「Ruby on Railsとそれに組み込まれているVue.js」を管理しているGit リポジトリのCIを改善した話となります。

$ rails stats
+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |  25114 |  18962 |     700 |    2768 |   3 |     4 |
| Helpers              |    336 |    240 |       1 |      42 |  42 |     3 |
| Jobs                 |      6 |      3 |       1 |       0 |   0 |     0 |
| Models               |  68048 |  46214 |    1537 |    5532 |   3 |     6 |
| Mailers              |   1128 |    871 |      60 |     117 |   1 |     5 |
| Views                |    182 |    172 |       0 |       0 |   0 |     0 |
| Libraries            |   1473 |   1140 |      16 |      77 |   4 |    12 |
| Controller specs     |    459 |    389 |       0 |       2 |   0 |   192 |
| Decorator specs      |    135 |    111 |       0 |       0 |   0 |     0 |
| Haml_lint specs      |     39 |     26 |       0 |       0 |   0 |     0 |
| Helper specs         |    320 |    240 |       0 |       1 |   0 |   238 |
| Lib specs            |   1126 |    936 |       0 |       2 |   0 |   466 |
| Mailer specs         |   6797 |   6046 |       0 |       3 |   0 |  2013 |
| Model specs          |  77120 |  61488 |       3 |     140 |  46 |   437 |
| Push_notifier specs  |   2547 |   2173 |       0 |       6 |   0 |   360 |
| Request specs        |  54132 |  45976 |       0 |     407 |   0 |   110 |
| Serializer specs     |   1820 |   1509 |       0 |       0 |   0 |     0 |
| System specs         |  28542 |  22830 |       0 |      20 |   0 |  1139 |
| Task specs           |    203 |    169 |       0 |       0 |   0 |     0 |
| Validator specs      |   1355 |    957 |      20 |       6 |   0 |   157 |
| Worker specs         |   7776 |   6388 |       0 |      25 |   0 |   253 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                | 278658 | 216840 |    2338 |    9148 |   3 |    21 |
+----------------------+--------+--------+---------+---------+-----+-------+
  Code LOC: 67602     Test LOC: 149238     Code to Test Ratio: 1:2.2

CIの状況

kakariでは主にCircleCIを利用しており、以下のようなJobを実行しています。

  • Ruby on Railsのテスト(RSpec
    • 各種モデル群の単体テスト
    • APIのRequest Spec
    • Rails Viewを提供しているエンドポイントに対してのSystem Spec
  • JavaScriptのテスト(Jest
    • JavaScriptで完結する各種Vue.js Componentの単体テストなど
  • 静的解析

他にも細かなJobが幾つかありますが、いずれも1分前後の小さなJobのため今回は割愛します。 また、Playwrightを活用したE2Eテストもありますが、これに関しても実行環境・タイミングなどが特殊なため今回の対象外とします。

改善結果

細かな改善内容はさておき、具体的な改善結果をお見せします。

改善前のCI Workflow 14分18秒

改善後のCI Workflow 6分35秒

「14分」から「6分半」への大幅な改善を達成することができました 🎉

また、 「お金💰の力で並列数を増加」のような対応はとらずに一定改善できたため副次的効果ですがコストの削減にも繋がりました。

改善内容

前提知識: CIのキャッシュ機能

具体的な改善内容紹介の前に、ここで「CIのキャッシュ機能」について前提知識として理解しておきましょう。

CircleCI公式ドキュメントが丁寧のためそちらをご参照ください。 circleci.com

例示されている通り、yarn packageやruby gemsなどのライブラリのインストール処理にてキャッシュを活用し、各Job内でのインストール時間を短縮することが典型的な活用事例かと思います。

また、今回はCircleCIを利用しましたがGitHub ActionGit Lab CI/CDなど大抵のCIサービスには同等の仕組みがあるため適宜読み替えてください。

それでは、改善内容を紹介します。

webpack buildのキャッシュを活用

出落ちになりますが、本改善がかなり大きなものとなります。

前述した通り、本Railsシステムには一部Vue.jsが組み込まれているエンドポイント(画面)が存在します。 そのエンドポイントに対してのSystem Specを動作させるにはwebpackのbuildが必要な構成となっています。

このwebpack buildの実行時間が毎回5分弱ほどでした。

この時間を短縮するためにwebpackのpersitent cachingという仕組みを活用しました。 これは「キャッシュ」という名から推測できる通りビルド結果をキャッシュし、ビルド時間を短縮してくれる仕組みです。

改善効果

「5分弱」から「1分弱」(4~5分)の時間短縮

※ キャッシュが適用される場合

対応方法

以下のようにwebpackの設定を記述してキャッシュ情報をファイル出力し、そのファイルをCircleCIのキャッシュ対象に含めるだけです。

  // webpack.config.js
  cache: {
    type: 'filesystem',
    cacheLocation: path.resolve(__dirname, 'tmp/cache/webpack'),
  },
  snapshot: {
    buildDependencies: { hash: true },
    module: { hash: true },
    resolve: { hash: true },
    resolvebuildDependencies: { hash: true },

ポイントはキャッシュデータ有効性の判定方法を「hash 方式」としている点です。 キャッシュの有効性判定の方法は以下の二種類が選べます。

  • hash(ファイルコンテンツハッシュ) 方式
    • ファイル内容のハッシュ値が同一かどうかで判定
    • 公式原文 Compare content hashes to determine invalidation (more expensive than timestamp, but changes less often).
  • timestamp(ファイルタイムスタンプ)方式 デフォルト
    • ファイルのタイムスタンプ(更新日時)が同一かどうかで判定
    • 公式原文 Compare timestamps to determine invalidation.

CIでは毎回git cloneする都合上ファイルタイムスタンプが更新されます。 そのため、timestamp方式を利用するとキャッシュされているスナップショットが不正と判定されてしまいキャッシュを活用することができなくなってしまいます。

参考資料

RuboCopのキャッシュを活用

RuboCopにも同様のキャッシュの仕組みがあります。

docs.rubocop.org

キャッシュの有効性判定は以下の通りで、「Ruby・RuboCopのバージョン、Cop設定が変わらない限り同一ファイル内容に対してのCop結果は不変」ということから、Cop結果をキャッシュとして記録し次回実行時にキャッシュされているCop結果を流用する仕組みです。

Cache Validity
Later runs will be able to retrieve this information and present the stored information instead of inspecting the file again. This will be done if the cache for the file is still valid, which it is if there are no changes in:

・the contents of the inspected file
・RuboCop configuration for the file
・the options given to rubocop, with some exceptions that have no bearing on which offenses are reported
・the Ruby version used to invoke rubocop
・version of the rubocop program (or to be precise, anything in the source code of the invoked rubocop program)

改善効果

「2分」から「10秒弱」(2分)の時間短縮

※ キャッシュが適用される場合

対応方法

実はRuboCopはデフォルトでキャッシュファイルを生成します。 生成されるキャッシュファイルをCircleCIのキャッシュ対象に含めるだけです。

以下のように --cache-root オプションでキャッシュファイルの生成先を指定することができます。

bundle exec rubocop --cache-root tmp/cache/rubocop

参考資料

ESLintのキャッシュを活用

そろそろキャッシュにも慣れてきた頃でしょうか。
冗長な説明となるため簡単にご紹介します。

eslint.org

改善効果

「30秒前後」から「3秒前後」(30秒)の時間短縮

※ キャッシュが適用される場合

対応方法

eslint app/bundles/javascripts spec/javascripts --ext js,vue,ts \
  --cache \ # キャッシュを有効化
  --cache-location tmp/cache/eslint/ \ # キャッシュファイル生成先を指定
  --cache-strategy content # キャッシュの有効性判定を「ファイル内容方式」とする(理由は上記webpack buildと同様)

参考資料

Jestのキャッシュを活用

キャッシュはもう飽きてきましたね。
これが最後のキャッシュ対応です。

jestjs.io

改善効果

「3分前後」から「2分前後」(1分)の時間短縮

※ キャッシュが適用される場合

対応方法

cacheDirectory を指定し、生成されたキャッシュファイルをCircleCIのキャッシュ対象に含めるだけです。

# jest.config.js
module.exports = {
  cacheDirectory: 'tmp/cache/jest',
};

参考資料

RSpec Jobをテスト特性ごとに分割

RSpec実行Jobを「webpack buildが必要なSystem Spec rspec_with_assets」と「それ以外(Model Specなど) rspec_without_assets」の二つに分割し、「時間がかかるWebPack Buildを待たずに rspec_without_assets を開始」することで全体の総時間を短縮する試みです。
こちらは同チームのサーバーサイドエンジニアの谷(@yuyat137)が対応しました。

RSpecを分割実行しJob依存関係を最適化(before)

RSpecを分割実行しJob依存関係を最適化(after)

改善効果

これまでの改善とは異なり特定のJobが改善されるようなものではないため改善効果の計測が難しいのですが、最大(webpack buildのキャッシュが適用されない場合)は4,5分の時間短縮が期待できます。
また、後述するリソースクラスと並列数の最適化にも繋がります。

対応方法

特別な工夫はなく、単にRSpec実行Jobを分割するだけです。 Jobを分割する分メンテナンスコストは増えるため、期待できる改善効果とのトレードオフで採用・不採用を検討すると良いかと思います。

CircleCIのリソースクラスと並列数の最適化

まず前提知識として、CircleCIには特定のJobを並列稼働させる仕組みがあります。 例えば以下はRSpec実行Jobを8並列で稼働している様子です。

CircleCIにてRSpecの実行Jobを8並列で稼働している様子

このように並列数を上げることでCIの総時間を短縮することができます。 ただし、CircleCIは「サーバー稼働時間の合計 ≒ コスト」となるため、並列数を上げるとその分コストが増えます。(※ Job実行のための事前処理が並列数分実行されるため)

circleci.com

もう一つの前提知識として、CircleCIでは各種Jobのリソースクラス(サーバーのスペックのようなものと解釈してください)を調整することが可能です。 例えばCPUやメモリをあまり消費しないようなJobの場合は小さめの small なんかを指定するとコスト削減に繋がります。

今回の改善は「並列数」と「リソースクラス」の両方を調整し、最適化しました。

端的に言うと「RSpec実行Jobのリソースクラスを下げて並列数を上げた」となります。 実際の最適化内容は以下です。

※ 消費クレジット= ここではCircleCI上のコストの単位と解釈してください。 (参照: https://circleci.com/docs/ja/credits/

最適化前

  • 「Medium(CPU=2, Mem=4, 消費クレジット=10/min)」を8並列

最適化後

前述の「RSpec Jobをテスト特性ごとに分割」が完了していたため、それらのJobの特性に応じてリソースクラスと並列数を最適化。

  • 重ためのSystem Spec系実行Job
    • 「Medium(CPU=2, Mem=4, 消費クレジット=10/min)」を4並列
  • テストケースが多いSystem Spec以外実行Job
    • 「Small(CPU=1, Mem=2, 消費クレジット=5/min)」を8並列

最適化前後で消費クレジットの変化なし(共に80/min)

改善効果

「10分30秒」から「7分弱」(4分弱)の時間短縮

細かなテストが多いRSpec群に対しては「スペック抑えめで並列数を増やす」というアプローチが良さそうでした。

採用しなかった・見送った改善候補

HAML-Lint, Fasterer, Brakemanのキャッシュを活用

RuboCopやESLint同様に静的解析のJobは極力キャッシュを活用したかったのですが、ツール側が機能提供していなかったので断念しました。
OSSコントリビュートチャンスですね。

Stylelintのキャッシュを活用

Stylelintにもキャッシュの仕組みがあるためこれを活用することができたのですが、元々のJob実行時間が10秒未満と短かったため今回は見送りました。

bootsnapを活用

Bootsnapは各種ファイルパス情報などをキャッシュすることで、Rubyアプリケーションの起動を高速化するGemです。
詳細は公式ドキュメントなどを参照ください。

github.com

Bootsnapが生成するファイルをCircleCIのキャッシュ対象に含めるだけでJob内のコマンド実行が高速化する可能性があります。 Bootsnapは巨大なアプリケーションほど効果を発揮しますが、kakariはせいぜい小〜中規模程度なため「Bootsnapで受けられる恩恵 < Jobの複雑さ」と判断し今回は見送りました。

Jestの並列実行化

kakariではJestをシリアル実行していたので、並列で稼働する(デフォルト)の設定に変更してみましたが、逆にCI時間が延びたため今回は見送りました。 JestのCI時間はそこまで支配的でなかったため、深く調べてはいませんが恐らくCPU/メモリリソースが十分に割り当てられていなかったことが原因かと思われます。 jestjs.io

まとめ

以上が最近kakariプロジェクトで取り組んだCI改善の小ネタ集でした。

繰り返しになりますが、追加コストなし でCIの実行時間を「14分」から「6分半」に短縮することができました 🎉

いずれも裏技のような改善ではなく、公式ドキュメントに記載されているような正攻法ばかりでした。
「重たいテストを一つ一つ修正する」ことも大事ですが、まずは落ち着いて公式ドキュメントを読み込むことも大事ですね。

本記事がきっかけで世界で動いているCI時間が少しでも短縮することを願っています。


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


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

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp