CTO室SREの@sinsokuです。
先日、弊社のCIで稀によく Segmentation fault が起きるようになりました。
_人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y ̄
調べてみた
最初は気づかなかったけど、画像の右端のダウンロードっぽいアイコンをクリックすると、実行結果のログを全文見ることができます。
[BUG] Segmentation fault at 0x000056529cd6d5e0 ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-linux] -- Control frame information ----------------------------------------------- c:0059 p:---- s:0312 e:000311 CFUNC :[] c:0058 p:0016 s:0307 e:000306 METHOD /home/circleci/*******/vendor/bundle/gems/activesupport-5.2.3/lib/active_support/execution_wrapper.rb:105 c:0057 p:0004 s:0303 e:000302 METHOD /home/circleci/*******/vendor/bundle/gems/activesupport-5.2.3/lib/active_support/execution_wrapper.rb:83 c:0056 p:0008 s:0298 e:000297 METHOD /home/circleci/*******/vendor/bundle/gems/activesupport-5.2.3/lib/active_support/reloader.rb:72 c:0055 p:0011 s:0294 e:000293 BLOCK /home/circleci/*******/vendor/bundle/gems/activejob-5.2.3/lib/active_job/railtie.rb:27 [FINISH] c:0054 p:---- s:0289 e:000288 CFUNC :instance_exec c:0053 p:0145 s:0283 e:000282 BLOCK /home/circleci/*******/vendor/bundle/gems/activesupport-5.2.3/lib/active_support/callbacks.rb:118 (途中略) c:0008 p:0022 s:0029 e:000028 BLOCK /home/circleci/*******/spec/jobs/conference_mail_sending_job_spec.rb:89 c:0007 p:0003 s:0026 e:000025 BLOCK /home/circleci/*******/spec/support/multithreaded.rb:5 (以下略)
どうやらマルチスレッドに関する何かで問題が起きているっぽい。
再現した
とりあえず、手元で20回くらい実行しても稀に死ぬのは分かった。
$ for n in $(seq 1 20); do \ bundle exec rspec spec/jobs/conference_mail_sending_job_spec.rb:96; \ done
調べてみた
- 100%再現させる方法が分からない
- エラーの起きるActiveSupport::ExecutionWrapperに怪しいコードはない
- Thread についてあまり詳しくない
調べたけど、何も分からない...
ruby-jp で相談した
相談したら、少し経ってシンプルな再現コードが見つかった。ありがたい。
その後、笹田さんが原因と100%再現するコードを投稿。(すごい)
そして、いつの間にか笹田さんが問題を解決するパッチをコミットし、Issueの登録も済んでいた。(すごい)
https://bugs.ruby-lang.org/issues/16676
どうやら、 #hash
の実行中に他スレッドから同じHashを弄ると問題が起きるようです。
テストコードの修正
すでにRubyのmasterブランチでは修正されていますが、CIで2.8.0-devを使うわけにもいきません。
Rubyの新しいバージョンがリリースされるまでのワークアラウンドとして、Railsにパッチを当てる事で対応します。
まず、以下の内容で lib/patches/fix_execution_wrapper.rb
を作成します。
# frozen_string_literal: true raise('Consider removing this patch') if RUBY_VERSION != '2.6.5' module Patches # Rubyが稀にSegmentation faultでエラーになる問題を修正するパッチ。 # # `Thread.current` の代わりに `Thread.current.object_id` を使うこと # で#hashの実行時に他スレッドがテーブルを弄るのを避けます。 # # 元の実装は以下を参照してください。 # ref: https://github.com/rails/rails/blob/v6.0.2.1/activesupport/lib/active_support/execution_wrapper.rb # # ## Issue # # `#hash` can change Hash object from ar_table to st_table # ref: https://bugs.ruby-lang.org/issues/16676 module FixExecutionWrapper def self.active? @active[Thread.current.object_id] end def run! self.class.active[Thread.current.object_id] = true run_callbacks(:run) end def complete! run_callbacks(:complete) ensure self.class.active.delete Thread.current.object_id end end end ActiveSupport::ExecutionWrapper.prepend(Patches::FixExecutionWrapper)
このパッチを config/environments/test.rb
で読み込む。
Rails.application.configure do # 中略 end require 'patches/fix_execution_wrapper'
これで本当に直るかは自信なかったのですが、1週間くらいCIの様子をみていても Segmentation fault が起きませんでした。
たぶん大丈夫です。
まとめ
自分1人で調べていても全く再現コードを作れなかったし、再現コードを見ても原因に検討もつきませんでした。
- ruby-jpに感謝
- 笹田さんすごい
- ThreadとRubyは難しい
もしスレッド周りで同じ問題を踏んだとき、この記事が参考になれば幸いです。
メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!
■募集ポジションはこちら
https://medpeer.co.jp/recruit/entry/
■開発環境はこちら
https://medpeer.co.jp/recruit/workplace/development.html