Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: Rails 6.1を7.0にアップグレードしてみた、PostgreSQLでジョブキューほか(20220208前編)

こんにちは、hachi8833です。

つっつきボイス:「このSUSEのYouTubeチャネルがめちゃくちゃ面白い↓」「お〜、英語の替え歌がどっさりですね」「映像も凝っていてクォリティが半端ない」

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下の更新の中からChangelogが更新されたものを中心に見繕いました。引き続きドキュメントの修正が多いようです。

🔗 TestCase#stub_constを追加

以下のようにブロック内で定数の値を変更する。

# World::List::Import::LARGE_IMPORT_THRESHOLD = 5000

stub_const(World::List::Import, :LARGE_IMPORT_THRESHOLD, 1) do
  assert_equal 1, World::List::Import::LARGE_IMPORT_THRESHOLD
end

assert_equal 5000, World::List::Import::LARGE_IMPORT_THRESHOLD = 5000

World::List::Import::LARGE_IMPORT_THRESHOLD = 5000を無理やり設定する代わりにこのメソッドを使えば、warningも出力されなくなり、そのテストが完了すれば元の値に戻る。
同PRより


つっつきボイス:「お、このstub_constは便利そう: 個人的に好き👍」

「これと似たような感じで、テストコードでクラスインスタンス変数をスタブする必要が生じることがたまにあるんですが、直接スタブできないので、たとえばクラスインスタンス変数の値にアクセスするメソッドを追加してそれをスタブした覚えがあります」「あ、なるほど」

参考: クラス変数とクラスインスタンス変数を理解する - Qiita

🔗 PostgreSQL generated column関連の修正

# activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L666
-           default_function = extract_default_function(default_value, default)
+
+           if attgenerated.present?
+             default_function = default
+           else
+             default_function = extract_default_function(default_value, default)
+           end

つっつきボイス:「この修正はissueを見るのがわかりやすそう↓」「schema.rbでPostgreSQLのgenerated columnが常にas: nilになってしまっていたんですね」「これは修正が必要なヤツ」

参考: 5.3. 生成列 -- PostgreSQL 13.1

🔗 Websocketsが接続中に閉じないようにする

これは、接続状態のWebSocketを閉じると永久に壊れてしまう問題(Safariによるバグ)を解決する。
この問題は、最近の Safari(15.1+) のNSURLSession Websockets と呼ばれる新しいWebSocketsの実装で発生した。この問題が発生するとWebSocketsが完全におかしくなり、修正するにはSafariを完全に再起動する必要がある(リロードだけでは修正できない)。

この問題はBasecampで発生したが、このパッチで問題の修正を確認できた。このパッチは単に接続状態にあるWebSocketを閉じないようにするもの。他のユーザーからも同様の問題が報告されている(これこれ)。ActionCableに限らないが、Action Cableの監視システムはstaleコネクション検出ロジックの一部で接続状態のコネクションを閉じることがあったためにこの問題が発生した。
Safari固有の問題ではあるものの、Action Cableを使うすべてのアプリに影響するため、パッチをRailsにアップストリームすることにした。この問題はトラブルシューティングが非常に難しい問題でもある (この問題を追いかけるのに数週間かかった)。他のブラウザでは問題を起こさないので、ブラウザ検出を行わずにシンプルに対応した。最悪、接続状態にあるWebsocketsコネクションが閉じていない状態で接続される可能性もあり、その場合ブラウザに2つのコネクションができてしまうが、Action Cableで監視されることも使われることもないので何もせずに勝手に死んでくれる。
Safariが近いうちにこの問題を解決して、この変更を元に戻すことを検討できるようになればと思う。
同PRより


つっつきボイス:「Safariのバグを回避する修正か」「プルリクに書かれているリンクが切れていてSafariのどのバグだかわからない...」「あらま」

🔗 非推奨化されたurlsafe_csrf_tokensを削除

関連PR: #43817
通常なら非推奨化されたコードを削除するのはもっと先になるが、#44283を進めるのに邪魔なので、今削除する方がよいと判断した。
同PRより


つっつきボイス:「お、少し前に非推奨化されたurlsafe_csrf_tokensコンフィグが早くも削除された(ウォッチ20211221)」「Shopifyからのプルリクですね」

🔗 PostgreSQLとMySQLの修正1件ずつ


つっつきボイス:「PostgreSQLにクエリをキャンセルする機能があるとは知らなかった」

# activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb#L121
        def exec_rollback_db_transaction # :nodoc:
+         @connection.cancel unless @connection.transaction_status == PG::PQTRANS_IDLE
+         @connection.block
          execute("ROLLBACK", "TRANSACTION")
        end

「ところでテストコード:<という顔文字っぽい書き方があるけど↓、Rubyでこんな書き方ができるんですね」「ちょっとぎょっとする感じ」「Rubyのシンボルでしょうか?」「どうやらシンボルですね」

# activerecord/test/cases/adapters/postgresql/transaction_test.rb#195
    assert_operator duration, :<, 5

「こちらはMySQLアダプタです」「マルチステートメントのバッチで、abandon_results!を最後に呼んでいたのをバッチごとに呼ぶように修正したのね」

# activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb#L99
          def execute_batch(statements, name = nil)
            statements = statements.map { |sql| transform_query(sql) }
            combine_multi_statements(statements).each do |statement|
              raw_execute(statement, name)
+             @connection.abandon_results!
            end
-           @connection.abandon_results!
          end

max_allowed_packet設定を超えるフィクスチャをbulk insertするとMysql2::Error: Commands out of sync; you can't run this command nowが発生していたのを修正。
同PRより

🔗 番外: Railsガイドの旧ガイドリンクにdata-turbolinks="false"を追加


つっつきボイス:「こちらはRailsガイドのテンプレートにあるリンクの修正だそうです」「過去のガイドへのリンクだから、data-turbolinks="false"を追加して完全にページ遷移する必要があるんでしょうね」「なるほど」「デフォルトでTurbolinksがオンになっていると<a>タグの挙動が変わるんだった、気をつけよう」

# guides/source/_welcome.html.erb#L18
<p>
The guides for earlier releases:
-<a href="https://guides.rubyonrails.org/v7.0/">Rails 7.0</a>,
-<a href="https://guides.rubyonrails.org/v6.1/">Rails 6.1</a>,
-<a href="https://guides.rubyonrails.org/v6.0/">Rails 6.0</a>,
-<a href="https://guides.rubyonrails.org/v5.2/">Rails 5.2</a>,
-<a href="https://guides.rubyonrails.org/v5.1/">Rails 5.1</a>,
-<a href="https://guides.rubyonrails.org/v5.0/">Rails 5.0</a>,
-<a href="https://guides.rubyonrails.org/v4.2/">Rails 4.2</a>,
-<a href="https://guides.rubyonrails.org/v4.1/">Rails 4.1</a>,
-<a href="https://guides.rubyonrails.org/v4.0/">Rails 4.0</a>,
-<a href="https://guides.rubyonrails.org/v3.2/">Rails 3.2</a>,
-<a href="https://guides.rubyonrails.org/v3.1/">Rails 3.1</a>,
-<a href="https://guides.rubyonrails.org/v3.0/">Rails 3.0</a>, and
-<a href="https://guides.rubyonrails.org/v2.3/">Rails 2.3</a>.
+<a href="https://guides.rubyonrails.org/v7.0/" data-turbolinks="false">Rails 7.0</a>,
+<a href="https://guides.rubyonrails.org/v6.1/" data-turbolinks="false">Rails 6.1</a>,
+<a href="https://guides.rubyonrails.org/v6.0/" data-turbolinks="false">Rails 6.0</a>,
+<a href="https://guides.rubyonrails.org/v5.2/" data-turbolinks="false">Rails 5.2</a>,
+<a href="https://guides.rubyonrails.org/v5.1/" data-turbolinks="false">Rails 5.1</a>,
+<a href="https://guides.rubyonrails.org/v5.0/" data-turbolinks="false">Rails 5.0</a>,
+<a href="https://guides.rubyonrails.org/v4.2/" data-turbolinks="false">Rails 4.2</a>,
+<a href="https://guides.rubyonrails.org/v4.1/" data-turbolinks="false">Rails 4.1</a>,
+<a href="https://guides.rubyonrails.org/v4.0/" data-turbolinks="false">Rails 4.0</a>,
+<a href="https://guides.rubyonrails.org/v3.2/" data-turbolinks="false">Rails 3.2</a>,
+<a href="https://guides.rubyonrails.org/v3.1/" data-turbolinks="false">Rails 3.1</a>,
+<a href="https://guides.rubyonrails.org/v3.0/" data-turbolinks="false">Rails 3.0</a>, and
+<a href="https://guides.rubyonrails.org/v2.3/" data-turbolinks="false">Rails 2.3</a>.
</p>

🔗Rails

🔗 Rails 6.1を7.0にアップグレードしてみた(Ruby Weeklyより)


つっつきボイス:「7.0へのアップグレードをやってみた記事がそろそろ出始めてますね」「Railsはアップグレードガイドが整備されているので、それに従っていれば基本的な部分は大丈夫なはず👍」

参考: Rails アップグレードガイド - Railsガイド

「アップグレードで問題になるのはむしろgemでしょうね」「たしかに...」

🔗 que: PostgreSQLをジョブキューにする(Ruby Weeklyより)

que-rb/que - GitHub


つっつきボイス:「Ruby Weeklyで見かけたんですが、このプルリクにも登場していたのが気になりました↓」

「★は2000超えか」「queはケイと発音するらしいけど何語だろう?」「これはPostgreSQLをジョブキューとして使えるようにするようですね: 以前も似たようなgemを見たことがあるかも」

# 同リポジトリより
# app/jobs/charge_credit_card.rb
class ChargeCreditCard < Que::Job
  # Default settings for this job. These are optional - without them, jobs
  # will default to priority 100 and run immediately.
  self.run_at = proc { 1.minute.from_now }

  # We use the Linux priority scale - a lower number is more important.
  self.priority = 10

  def run(credit_card_id, user_id:)
    # Do stuff.
    user = User.find(user_id)
    card = CreditCard.find(credit_card_id)

    User.transaction do
      # Write any changes you'd like to the database.
      user.update charged_at: Time.now

      # It's best to destroy the job in the same transaction as any other
      # changes you make. Que will mark the job as destroyed for you after the
      # run method if you don't do it yourself, but if your job writes to the DB
      # but doesn't destroy the job in the same transaction, it's possible that
      # the job could be repeated in the event of a crash.
      destroy

      # If you'd rather leave the job record in the database to maintain a job
      # history, simply replace the `destroy` call with a `finish` call.
    end
  end
end

「たしかにぽすぐれならジョブキューもやれる」「Redisを立てたくない人が欲しくなるのかも」「新しめのPostgreSQLでないとできないでしょうけど」


以下の記事では方法こそ違うものの、PostgreSQLをジョブキューの永続化に使っています↓。

RailsのPostgreSQL上でマルチテナントのジョブキューシステムを独自構築する(翻訳)

🔗 パスワード強度をエントロピーで算出する(Ruby Weeklyより)


つっつきボイス:「パスワードの強度を根拠のある形で算出するためにstrong_password gemを使ってエントロピーベースのパスワードチェックを実装した記事、なるほど」「画面ではauto-check-elementを使っているそうです」

bdmac/strong_password - GitHub

github/auto-check-element - GitHub

# 同記事より
def create
  checker = User.password_checker
  entropy = checker.calculate_entropy(params[:value] || "")
  percentage = (entropy / STRONG_ENTROPY) * 100

  percentage = 100 if percentage > 100

  render(partial: "users/shared/password_strength_meter", locals: { strength: percentage.to_i })
end

「いつも言っていますが、セキュリティに関連する実装では専門家が推奨するライブラリを使うべき」「セキュリティコアの手作りは避けたいですね」

🔗 rpush: Rubyでプッシュ通知と連携(Ruby Weeklyより)

rpush/rpush - GitHub


つっつきボイス:「Apple Push Notification ServiceやAmazon Device Messagingなどのプッシュ通知を共通の方法で書けるgemのようですね」「なるほど」「こういうのは自分で実装するよりもよさそう👍」

app = Rpush::Apnsp8::App.new
app.name = "ios_app"
app.apn_key = File.read("/path/to/sandbox.p8")
app.environment = "development" # APNs environment.
app.apn_key_id = "APN KEY ID" # This is the Encryption Key ID provided by apple
app.team_id = "TEAM ID" # the team id - e.g. ABCDE12345
app.bundle_id = "BUNDLE ID" # the unique bundle id of the app, like com.example.appname
app.connections = 1
app.save!

「プッシュ通知ってあまり好きじゃないかも...」「プッシュ通知はユーザーがオプトアウトできますし、通知を増やしすぎないようにするのがコツでしょうね」

🔗 Sidekiqのコンカレンシーを制御する(Ruby Weeklyより)


つっつきボイス:「タイトルを見たときはSidekiqのEnterprise版の機能を自前で実装するのかと思ったけど、Redisのロック周りやアトミック操作などの話をしてるな: どうやら特定のジョブがコンカレントに実行されないことを保証するようにしたということみたい」「あ、concurrency controlはそっちの意味なんですね」「特定のジョブが実行されているときに特定のジョブが同時に実行されないようにしたいというのはたしかにありそう」

🔗 その他Rails


つっつきボイス:「寿司ビール問題↓が今になってまたバズっているのはともかく、とっくにサポート終了したMySQL 5.6を今の時代に使っている方がヤバいのでは」「ですよね」「以下の記事↓を見た限りでもExtened Supportすら1年前に終了している」

参考: MySQLのEOL - 雑記帳

MySQLのencodingをutf8からutf8mb4に変更して寿司ビール問題に対応する


前編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Rubygems Adoptionフォームが開設、JetBrains Gateway、NGINX Unitほか(20220201後編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。