RubyのTempfileオブジェクトがGCによって消えるのを観察する

Pocket

たまーに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を実行することで再現させられることがわかった。また、修正して発生しなくなることも確認できて便利。

Leave a Reply

Your email address will not be published. Required fields are marked *