LoginSignup
0
0

Regexp.last_match(1)の地雷のような挙動を踏み抜いてしまった話

Posted at

事象発生

「画面に表示される金額がおかしい」とクレームがあったのが先月。
該当の画面はDBからレコードを取ってきて表示するだけのシンプルなもので、バグりようがない。コードはこんな感じ。

class SomeRecord < ApplicationRecord
  scope :by_code, ->(code) {
    if /abc(\d+)/ =~ code
      where(code: $1)
    else
      none
    end
  }
end

どうせ客の見間違えだろうと放置していたところ、他にも似たような問い合わせがちらほら来るように。
さすがに金額がおかしいのはやばいだろ、来週までに直しといてと言われたので仕方なく調査。

原因

  • 公式ドキュメントに嘘が書いてあった。

どうやらRegexp.last_matchや$1等の変数は、procの中で使った場合
「ローカルスコープかつスレッドローカル」ではないらしい。

P = ->(str) {
  /abc(\d+)/ =~ str
  sleep 0.5
  puts "str=#{str}, Regexp.last_match(1)=#{Regexp.last_match(1)}"
}

def f1
  Thread.new { P.call('abc100') }
  Thread.new { P.call('abc200') }
end

f1
sleep 5

結果

str=abc100, Regexp.last_match(1)=200
str=abc200, Regexp.last_match(1)=200

1回目はstr=abc100, Regexp.last_match(1)=100と出力されるはず。
しかしもう片方のスレッドによって結果が上書きされてstr=abc100, Regexp.last_match(1)=200と出力されてしまっている。

対処方法

正規表現のマッチ結果を一旦変数に入れる

P2 = ->(str) {
  m = str.match(/abc(\d+)/)
  sleep 0.5
  puts "str=#{str}, m[1]=#{m[1]}"
}

def f2
  Thread.new { P2.call('abc100') }
  Thread.new { P2.call('abc200') }
end

f2
sleep 5

結果は期待通り

str=abc100, m[1]=100
str=abc200, m[1]=200

まとめ

rubyの正規表現でlast_matchや$1等は使わず、マッチ結果は一度変数に入れた方が安全。

公式で報告されてないのかなと思ったら以下のチケットが該当する模様。
https://bugs.ruby-lang.org/issues/8444

10年前に報告されて今までほったらかし、ドキュメントだけしれっと修正という対応を見ると、今まで踏む人がいなかったんだろうな。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0