メドピア開発者ブログ

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

CIで稀にSegmentation faultが起きてRubyが死ぬ問題と対応

CTO室SREの@sinsokuです。

先日、弊社のCIで稀によく Segmentation fault が起きるようになりました。

f:id:sinsoku:20200313181753p:plain

_人人人人人人_
> 突然の死 <
 ̄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 で相談した

f:id:sinsoku:20200313181953p:plain

相談したら、少し経ってシンプルな再現コードが見つかった。ありがたい。

f:id:sinsoku:20200313182030p:plain

その後、笹田さんが原因と100%再現するコードを投稿。(すごい)

f:id:sinsoku:20200313182043p:plain

そして、いつの間にか笹田さんが問題を解決するパッチをコミットし、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