STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

開発版のRubyを用いてCIを定期実行する試みとその成果

こんにちは。本記事は ykpythemindmameko1 が共著しています。

今回は開発版のRuby(Ruby head)を用いてSTORESのRailsアプリケーションのCIを定期実行していることと、それによってRuby本体の開発にフィードバックをしていることについてお話します。

モチベーション

STORES はRubyを用いて10年選手のRails製アプリケーションを複数開発・運用しており、事業の柱となる技術として大きく投資をしています。
具体的にはRubyのフルタイムコミッター2名がジョインしていたり、RubyKaigiを始めとする各カンファレンスへ協賛をしております。

product.st.inc

product.st.inc

product.st.inc

Rubyをとりまく環境として非常に恵まれた会社であることは間違いないでしょう。その中で、普段からアプリケーションコードを書いているチームとしてよりRubyに対して貢献したい、というのがこの施策のモチベーションです。 (ykpy)

何をしているのか

毎日、Ruby の trunk の headを用いて既存Railsアプリケーションのテストコードを実行することで、Ruby本体の新機能・変更による既存のRubyコードへの影響を観測しています。 2023年末頃から実施しはじめ、今では社内の4つのRailsアプリにて毎日CIにより実行されています。 (ykpy)

実行結果が流れてくるチャンネルを作っている

成果

開発版の Ruby を用いて CI を動かすことによって、非互換や不具合を事前にキャッチすることができました。これにより、我々 STORES アプリでの Ruby のアップデートがかなりやりやすくなると思っています。また、Ruby 自体に「こういうケースで動かないな」とか、「この変更はやっぱまずいんじゃないか」といったフィードバックすることで、Ruby 開発に貢献することができました。実際、これらのフィードバックは Ruby 3.4 の改善に生かされています。

ここでは、わかりやすい成果をいくつかご紹介します。 (mame/ko1)

Rubyの非互換に前もって対応できた

最近のRubyは非互換にかなり配慮しているつもりですが、こまごまとした非互換は日々どうしても入ります。意図しない非互換から、意図した非互換までいろいろ。動かすためにはどこかを直す必要がありますが、例えばこんな感じでなおしました。

  • Ruby をなおした
    • マイナーなC API(rb_gc_force_recycle)が削除された影響でddtraceがビルドできなくなった(C APIをダミーで復活させて対応した)
    • メジャーなC API(rb_io_t)が削除された影響でunicornがビルドできなくなった(C APIの削除を一旦巻き戻して対応した
  • Gem をなおした
  • アプリをなおした
    • Hash#inspectの返り値が変わったのでテストが失敗するようになった(アプリのテストを修正した)
    • csv gemやdrb gemがbundled gemsに移行した影響で、Gemfileに追加する必要があった(Gemfileに追加した)

ひとつひとつは小さくても(いや、全然小さくないやつもありますが)、一気にアップデートしようとして問題が大量噴出するとつらいですよね。技術的負債に利息が付く前に返せる感じ。

Rubyのバグが見つかった

Rubyに入った最適化の影響でsegfaultするようになったことが検知でき、リリース前に修正することができました。

↓のPRがその修正です。(この背景を全然書いてないけど)

github.com

事前に気づかなかったら、Ruby 3.4にアップデートして「謎のsegfaultが発生する……」となって一回休みになってたところでしょう。

ちなみに、この最適化のバグはShopifyのCIをパスしていたと思われます(実装者がShopifyの人なので)。各自のアプリでテストすることの大切さがわかりますね。

Rubyの非互換の大きさを測定できた

たとえば Ruby 3.4 のHash#inspectの返り値の非互換は意図したものでしたが、テストの期待値の修正程度で済むと予想されていました。そして、(少なくともSTORESのアプリケーションでは)実際そのとおりだったことが確認できました。

他には、Ruby 3.4ではデフォルトのパーサがPrismに変わるという(内部実装上の)大変更がありましたが、その影響がないことが事前に確認できたのも助かりました。

非互換の大きさが想定通りであることを確かめるのはとてもむずかしいのでありがたかったですし、STORESにとっても予想外の影響がないことが確認できてよかったです。

定期実行してみたくなったあなたへ

我々はGitHub Actionsを用いてCIを実行しており、いくつかの工夫をしています。(ykpy)

composite actionを用いて通常のpush時のCI実行とは別の文脈でテストを実行できるようにする

# .github/composite/backend-test/action.yml と保存

inputs:
  custom-ruby-version:
    default: ''
    description: 'ruby head用'

runs:
  using: "composite"
  steps:
    # HACK: 無理やり.ruby-versionを書き換えつつ、Gemfileの Ruby versionの指定を消す
    - run: |
        echo '${{ inputs.custom-ruby-version }}' > .ruby-version
        sed -i "/^ruby '/d" Gemfile
      shell: bash
      if: ${{ inputs.custom-ruby-version != '' }}

    - name: setup Ruby
      uses: ruby/setup-ruby@v1

    # デバッグにわかりやすいようにバージョン情報を出力
    - run: ruby -v
      if: ${{ inputs.custom-ruby-version != '' }}

    # 以下は普通のRails のテストを実行 ....

以上のような設定を用意し、Actionsのcron機能でテストを実行します。

name: ruby head test

on:
  schedule:
    - cron: '0 1 * * 1-5' # 平日のJST10時

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  ruby-head-test:
    steps:
      - uses: actions/checkout@v4

      - uses: ./.github/composite/backend-test
        with:
          custom-ruby-version: 'debug'

Ruby の Debug バージョンを使う

setup-ruby actionには .ruby-version ファイルを見てそれにしたがって Rubyをインストールしてくれる便利な機能があるので利用します。 setup-rubyにおいてdebugというバージョンを指定することで、Rubyのheadをdebugビルド 1 したRubyをインストールすることができます。

1つ注意点としては通常のRuby debug版は遅いです。普段のRubyで実行するよりも4倍くらい時間がかかるので Actionsのtimeout-minutes 設定を伸ばしておきましょう。 (ykpy)

もしもバグを見つけたら

CI上で開発版のRubyを用いることであなたが開発しているRuby製のプロダクトでバグが見つかるかもしれません。 そのときは、https://bugs.ruby-lang.org/issues/new でバグ報告をしましょう。いきなりバグ報告をするのが躊躇されるようなら、Xやruby-jp Slack#ruby チャンネルなどを徘徊しているRubyコミッタを探して相談してみるのもいいと思います。(尻込みして誰にも伝えないのが一番よくない!) (mame)

まとめ

今回はRuby本体の開発に対して、実際に運用しているアプリケーションから貢献している事例を紹介しました。 STORES ではアプリケーションエンジニアとRubyのフルタイム開発者の距離が近く、実際にRubyが改善されていくさまを見ることができ、とても楽しいです。ぜひみなさんのアプリケーションでも開発版Rubyをお試しください!


  1. Ruby には -DRUBY_DEBUG というデバッグ用ビルドオプションがあり、これを有効にしてビルドすることにより、様々なアサーションが有効になり「この処理はこうなるだろう」ということを動的に(病的に)チェックするようになります。インタプリタが意図通り正しく動作していることを確認できますが、その代わりに実行がだいぶ遅くなります。 (ko1)