事象発生
「画面に表示される金額がおかしい」とクレームがあったのが先月。
該当の画面は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年前に報告されて今までほったらかし、ドキュメントだけしれっと修正という対応を見ると、今まで踏む人がいなかったんだろうな。