RubyのTempfileオブジェクトがGCによって消えるのを観察する
たまーにTempfileを使って作ったファイルが消えるのでなんでだろなーと調べていたのだけど、どうやらGCによって消えてしまう可能性があるようなコードになっていたことがわかった(参考:RubyでTempfileを使う際の注意)。原因はおそらくこれだろう、ということで、コードの直し方もいろいろ思いつくわけだが、本当にそれが原因なのかな?とか、修正したコードで発生しなくなるのを確認するにはどうすればいいかな?と思い、社内メンバーの力を借りて実際にTempfileが消えるのを観察した。
さて、GCによって消えるのを観察するためにはGCが発生しなければならない。GCを開始する方法として、RubyではGC.startを使うことができる。コードとしてはこのようなものを使う。
require 'tempfile'
foo = Tempfile.open('nyah.yml') do |f|
f.write 'hello world'
f.path
end
puts "GCされていないオブジェクト: #{ObjectSpace.each_object(Tempfile).map {|f| [f, f.path].inspect }}"
GC.start
puts "GCしたよ"
puts "GCされていないオブジェクト: #{ObjectSpace.each_object(Tempfile).map {|f| [f, f.path].inspect }}"
p File.read(foo)
変数fooにはTempfile.openで作成した一時ファイルのパス名(文字列)が代入される。つまり、Tempfileオブジェクトはどこからも参照されておらず、GC時に削除される対象となりえる。また、GC.startの前後で、Tempfileオブジェクトの一覧を表示し、GCされたかどうかが見えるようにした。このコードを実行すると、2回目のTempfileオブジェクト一覧表示では空配列が出力され、File.read(foo)の実行時にNo such file or directoryエラーとなるはずである。さっそく実行してみよう。
➤ ruby test.rb
GCされていないオブジェクト: ["[#<Tempfile:/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-92812-q6sfs7 (closed)>, \"/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-92812-q6sfs7\"]"]
GCしたよ
GCされていないオブジェクト: ["[#<Tempfile:/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-92812-q6sfs7 (closed)>, \"/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-92812-q6sfs7\"]"]
"hello world"
あれ?GCしたはずなのに、GCされずに読み込めている…これはどういうことだろうか。いろいろ試した結果、Rubyのバージョンによって挙動が違うようだということがわかった。上記の結果となった時、Rubyのバージョンは2.5.1を使っていたのだが、古いバージョンのRuby(v2.3系?)だとGCされているようだ。
2.3.7を使った場合、GCされてNo such file or directoryエラーとなる。GC.startの後ではTempfileオブジェクト一覧が空配列となっていることからもわかる。
➤ ruby -v
ruby 2.3.7p456 (2018-03-28 revision 63024) [x86_64-darwin16]
➤ ruby test.rb
GCされていないオブジェクト: ["[#<Tempfile:/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-19219-i3fzb (closed)>, \"/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-19219-i3fzb\"]"]
GCしたよ
GCされていないオブジェクト: []
test.rb:12:in `read': No such file or directory @ rb_sysopen - /var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-19219-i3fzb (Errno::ENOENT)
from test.rb:12:in `<main>'
なお、2.4.4ではGCされない模様。
➤ ruby -v
ruby 2.4.4p296 (2018-03-28 revision 63013) [x86_64-darwin16]
➤ ruby test.rb
GCされていないオブジェクト: ["[#<Tempfile:/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-42992-vhubds (closed)>, \"/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-42992-vhubds\"]"]
GCしたよ
GCされていないオブジェクト: ["[#<Tempfile:/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-42992-vhubds (closed)>, \"/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-42992-vhubds\"]"]
"hello world"
また、Tempfileオブジェクトが参照されている状態となるようにコードを修正すれば、2.3.7でもちゃんとGCされないことも確認しておいた。
require 'tempfile'
foo = Tempfile.open('nyah.yml') do |f|
f.write 'hello world'
f
end
puts "GCされていないオブジェクト: #{ObjectSpace.each_object(Tempfile).map {|f| [f, f.path].inspect }}"
GC.start
puts "GCしたよ"
puts "GCされていないオブジェクト: #{ObjectSpace.each_object(Tempfile).map {|f| [f, f.path].inspect }}"
p File.read(foo.path)
2.3.7で実行してもGCされず、読み込める。
➤ ruby -v
ruby 2.3.7p456 (2018-03-28 revision 63024) [x86_64-darwin16]
➤ ruby test.rb
GCされていないオブジェクト: ["[#<Tempfile:/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-43111-1cnlm5u (closed)>, \"/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-43111-1cnlm5u\"]"]
GCしたよ
GCされていないオブジェクト: ["[#<Tempfile:/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-43111-1cnlm5u (closed)>, \"/var/folders/cj/m99zsmyj70b_6qhw534435_9k6j57z/T/nyah.yml20180425-43111-1cnlm5u\"]"]
"hello world"
たまに発生するというやっかいな問題だが、意図的にGCを実行することで再現させられることがわかった。また、修正して発生しなくなることも確認できて便利。