Bug #2732

rubyspec: ObjectSpace.define_finalizer doesn't call self-referencing finalizers FAILED

Added by Yusuke Endoh about 5 years ago. Updated almost 4 years ago.

[ruby-dev:40382]
Status:Closed
Priority:High
Assignee:-
ruby -v:ruby 1.9.2dev (2010-02-10 trunk 26632) Backport:

Description

=begin
なかださんかまつもとさん
遠藤です。

以下のようにすると T_ZOMBIE が見えてしまうのはバグでしょうか。

$ ./ruby -ve '
obj = "test"
ObjectSpace.define_finalizer(obj, proc { begin; p obj; ensure; p $!; end })
'
ruby 1.9.2dev (2010-02-10 trunk 26632) [i686-linux]
#

で、obj への reference があるからこれは回収
されないようなことが書かれていますが、結局回収されるように
なっているようです。

でバグとして登録されていますが、どっちが
正しいのかよくわかりません。
rubyspec に回収されないことを確認する spec が入っていて、失敗
しています。

回収されるのが正解だとしたら、例外メッセージから unexpected は
消すべきだと思います。
見えないのが正解だとしたら、バグなんでしょう。

--
Yusuke ENDOH mame@tsg.ne.jp
=end

History

#1 Updated by Nobuyoshi Nakada about 5 years ago

  • Category set to core
  • Priority changed from Low to High
  • Target version set to 2.0.0
  • ruby -v set to ruby 1.9.2dev (2010-02-10 trunk 26632)

=begin

=end

#2 Updated by Yusuke Endoh about 5 years ago

=begin
遠藤です。

2010年2月11日0:29 Yusuke ENDOH mame@tsg.ne.jp:

以下のようにすると T_ZOMBIE が見えてしまうのはバグでしょうか。

$ ./ruby -ve '
obj = "test"
ObjectSpace.define_finalizer(obj, proc { begin; p obj; ensure; p $!; end })
'
ruby 1.9.2dev (2010-02-10 trunk 26632) [i686-linux]
#

「T_ZOMBIE が見えたらバグ」というなかださんとささださんの認定を
頂きました。GC に興味ないので自信はないですが、一応調べました。

ruby 終了時に実行されそうな rb_objspace_call_finalizer の中で、
/* run finalizers */ という箇所があり、chain_finalized_object
してから run_final をする、というようなコードがあります。

chain_finalized_object の中では FL_FINALIZE なオブジェクトは無
条件に T_ZOMBIE にされ、dfree が 0 にされます。そして run_final
の中では T_ZOMBIE になったオブジェクトの finalizer を呼んでしま
うようです。(間違ってたらすみません)

問題点 (疑問点) は 2 つあります。

1) dfree が 0 にされてしまったら、free が呼べなくなる気がする
けれどいいのか? (どうせ終了直前だからいい? MVM になったら
困るかも)

2) 対象オブジェクトが強制的に T_ZOMBIE になってから finalizer
が呼ばれるので当然 T_ZOMBIE なオブジェクトが finalizer から
見えるけれど、どうするのが正しい?

GC に興味のある人に考えてほしいです。

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#3 Updated by Yusuke Endoh about 5 years ago

=begin
遠藤です。

2010年2月11日19:08 Yusuke ENDOH mame@tsg.ne.jp:

以下のようにすると T_ZOMBIE が見えてしまうのはバグでしょうか。
snip
GC に興味のある人に考えてほしいです。

期待していた wanabe さんに「見ない宣言」をされてしまったので、
自分でいい加減に考えてみました。

r18398 で runs finalizers with the object terminated. という
変更が入っていたので、これを revert するような感じでパッチを
書いてみました。T_ZOMBIE は見えなくなったようです。

$ ./ruby -e '
obj = "test"
ObjectSpace.define_finalizer(obj, proc { begin; p obj; ensure; p $!; end })
'

また、make check でも objectspace のエラーが 1 つ増えるだけの
ようです。

けれど、以下の 2 つのよくわからない挙動に悩まされています。

1) 対象オブジェクトをトップレベルのローカル変数に代入すると
ファイナライザが実行されない

# 回収されない
$ ./ruby -e '
def foo; proc { p :foo }; end
s = "foo"
ObjectSpace.define_finalizer(s, foo)
'

# ローカル変数に入れなければ回収される
$ ./ruby -e '
def foo; proc { p :foo }; end
ObjectSpace.define_finalizer("foo", foo)
'
:foo

# トップレベルでなければ回収される
$ ./ruby -e '
class C
def self.foo; proc { p :foo }; end
s = "foo"
ObjectSpace.define_finalizer("foo", foo)
end
'

ファイナライザの proc からトップレベルのローカル変数がなぜか
マークされるんでしょうか。
test/ruby/test_objectspace.rb がこの挙動に依存して失敗する
ようです。

2) Enumerator#next を使うと回収されない

# 回収されない
$ ./ruby -e '
module M
def self.callback
proc { p "finalized" }
end
def self.run
@enum = 1.enum_for(:upto, 3)
@enum.next
ObjectSpace.define_finalizer("foo", callback)
end
end
M.run
'

# next を使わなければ回収される
$ ./ruby -e '
module M
def self.callback
proc { p "finalized" }
end
def self.run
@enum = 1.enum_for(:upto, 3)
#@enum.next
ObjectSpace.define_finalizer("foo", callback)
end
end
M.run
'
"finalized"

# Enumerator をインスタンス変数に入れなければ回収される
$ ./ruby -e '
module M
def self.callback
proc { p "finalized" }
end
def self.run
enum = 1.enum_for(:upto, 3)
enum.next
ObjectSpace.define_finalizer("foo", callback)
end
end
M.run
'
"finalized"

おそらく Fiber のマシンスタックの中に参照があるのではないかと
思いますが、どう対処したものかわかりません。
rubyspec がこの挙動に依存して失敗します (Enumerator の spec と
あわせて実行したときだけ失敗する) 。

もう少し考えてみますが、それでも T_ZOMBIE が見えるよりはいいと
思うので、反対がなければとりあえずコミットしようと思います。

diff --git a/gc.c b/gc.c
index 4cfc23c..32b7436 100644
--- a/gc.c
+++ b/gc.c
@@ -2652,7 +2652,7 @@ static int
chain_finalized_object(st_data_t key, st_data_t val, st_data_t arg)
{
RVALUE p = (RVALUE *)key, **final_list = (RVALUE *)arg;
- if (p->as.basic.flags & FL_FINALIZE) {
+ if ((p->as.basic.flags & (FL_FINALIZE|FL_MARK)) == FL_FINALIZE) {
if (BUILTIN_TYPE(p) != T_ZOMBIE) {
p->as.free.flags = FL_MARK | T_ZOMBIE; /* remain marked */
RDATA(p)->dfree = 0;
@@ -2661,9 +2661,7 @@ chain_finalized_object(st_data_t key, st_data_t
val, st_data_t arg)
*final_list = p;
return ST_CONTINUE;
}
- else {
- return ST_DELETE;
- }
+ return ST_DELETE;
}

void
@@ -2681,15 +2679,16 @@ rb_objspace_call_finalizer(rb_objspace_t *objspace)

  /* run finalizers */
  if (finalizer_table) {
  • finalize_deferred(objspace);
  • while (finalizer_table->num_entries > 0) {
  • do {
  • finalize_deferred(objspace);
  • mark_tbl(objspace, finalizer_table, 0); st_foreach(finalizer_table, chain_finalized_object,
  • (st_data_t)&final_list);
  • if (!(p = final_list)) break;
  • do {
  • final_list = p->as.free.next;
  • run_final(objspace, (VALUE)p);
  • } while ((p = final_list) != 0);
  • (st_data_t)&deferred_final_list);
  • } while (deferred_final_list);
  • if (finalizer_table->num_entries) {
  • rb_warning("%d finalizer%s left not-invoked due to self-reference",
  • finalizer_table->num_entries,
  • finalizer_table->num_entries > 1 ? "s" : ""); } st_free_table(finalizer_table); finalizer_table = 0;

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#4 Updated by Yusuke Endoh about 5 years ago

=begin
遠藤です。

2010年2月12日22:32 Yusuke ENDOH mame@tsg.ne.jp:

2010年2月11日19:08 Yusuke ENDOH mame@tsg.ne.jp:

以下のようにすると T_ZOMBIE が見えてしまうのはバグでしょうか。
snip
もう少し考えてみますが、それでも T_ZOMBIE が見えるよりはいいと
思うので、反対がなければとりあえずコミットしようと思います。

誰にも相手にされず寂しい限りですが、続報です。

このパッチをあてると、Enumerator#next と Tempfile を組み合わせた時に
Tempfile が消されなくなりました。これは実用上弊害がありそうです。

require "tempfile"
e = 1.enum_for(:upto, 3)
e.next
t = Tempfile.new("foo")
p t.path

これ自体はひょっとしたらバグなのかもしれませんが、一般的に言って、
conservative GC だとファイナライザが予期せずオブジェクト自体に参照を
持ってしまう可能性があるので、self-referencial なファイナライザを
実行しないという方針だと、ファイナライザが不可解に実行されない事態の
発生を避けられないという結論に至りました。

終了時には、対象オブジェクトを回収する前に必ずファイナライザを走らせる
というのはどうでしょうか。一応、make check と make test-rubyspec が
完走することは確かめています。

反対がなければコミットして、rubyspec 側も修正します。

diff --git a/gc.c b/gc.c
index 4cfc23c..759b14e 100644
--- a/gc.c
+++ b/gc.c
@@ -2588,11 +2588,29 @@ run_single_final(VALUE arg)
}

static void
-run_final(rb_objspace_t *objspace, VALUE obj)
+run_finalizer(rb_objspace_t *objspace, VALUE obj, VALUE objid, VALUE table)
{
long i;
int status;
- VALUE args[3], table, objid;
+ VALUE args[3];
+
+ args[1] = 0;
+ args[2] = (VALUE)rb_safe_level();
+ if (!args[1] && RARRAY_LEN(table) > 0) {
+ args[1] = rb_obj_freeze(rb_ary_new3(1, objid));
+ }
+ for (i=0; i<RARRAY_LEN(table); i++) {
+ VALUE final = RARRAY_PTR(table)[i];
+ args[0] = RARRAY_PTR(final)[1];
+ args[2] = FIX2INT(RARRAY_PTR(final)[0]);
+ rb_protect(run_single_final, (VALUE)args, &status);
+ }
+}
+
+static void
+run_final(rb_objspace_t *objspace, VALUE obj)
+{
+ VALUE table, objid;
RUBY_DATA_FUNC free_func = 0;

  objid = rb_obj_id(obj);   /* make obj into id */

@@ -2610,17 +2628,7 @@ run_final(rb_objspace_t *objspace, VALUE obj)

  if (finalizer_table &&
st_delete(finalizer_table, (st_data_t*)&obj, &table)) {
  • args[1] = 0;
  • args[2] = (VALUE)rb_safe_level();
  • if (!args[1] && RARRAY_LEN(table) > 0) {
  • args[1] = rb_obj_freeze(rb_ary_new3(1, objid));
  • }
  • for (i=0; i<RARRAY_LEN(table); i++) {
  • VALUE final = RARRAY_PTR(table)[i];
  • args[0] = RARRAY_PTR(final)[1];
  • args[2] = FIX2INT(RARRAY_PTR(final)[0]);
  • rb_protect(run_single_final, (VALUE)args, &status);
  • }
  • run_finalizer(objspace, obj, objid, table); } }

@@ -2652,18 +2660,33 @@ static int
chain_finalized_object(st_data_t key, st_data_t val, st_data_t arg)
{
RVALUE p = (RVALUE *)key, **final_list = (RVALUE *)arg;
- if (p->as.basic.flags & FL_FINALIZE) {
+ if ((p->as.basic.flags & (FL_FINALIZE|FL_MARK)) == FL_FINALIZE) {
if (BUILTIN_TYPE(p) != T_ZOMBIE) {
p->as.free.flags = FL_MARK | T_ZOMBIE; /* remain marked /
RDATA(p)->dfree = 0;
}
p->as.free.next = *final_list;
*final_list = p;
- return ST_CONTINUE;
- }
- else {
- return ST_DELETE;
}
+ return ST_CONTINUE;
+}
+
+struct force_finalize_list {
+ VALUE obj;
+ VALUE table;
+ struct force_finalize_list *next;
+};
+
+static int
+force_chain_object(st_data_t key, st_data_t val, st_data_t arg)
+{
+ struct force_finalize_list *
prev = (struct force_finalize_list **)arg;
+ struct force_finalize_list *curr = ALLOC(struct force_finalize_list);
+ curr->obj = key;
+ curr->table = val;
+ curr->next = *prev;
+ *prev = curr;
+ return ST_DELETE;
}

void
@@ -2681,15 +2704,22 @@ rb_objspace_call_finalizer(rb_objspace_t *objspace)

  /* run finalizers */
  if (finalizer_table) {
  • finalize_deferred(objspace);
  • while (finalizer_table->num_entries > 0) {
  • do {
  • finalize_deferred(objspace);
  • mark_tbl(objspace, finalizer_table, 0); st_foreach(finalizer_table, chain_finalized_object,
  • (st_data_t)&final_list);
  • if (!(p = final_list)) break;
  • do {
  • final_list = p->as.free.next;
  • run_final(objspace, (VALUE)p);
  • } while ((p = final_list) != 0);
  • (st_data_t)&deferred_final_list);
  • } while (deferred_final_list);
  • /* force to run finalizer */
  • while (finalizer_table->num_entries) {
  • struct force_finalize_list *list = 0;
  • st_foreach(finalizer_table, force_chain_object, (st_data_t)&list);
  • while (list) {
  • struct force_finalize_list *curr = list;
  • run_finalizer(objspace, curr->obj, rb_obj_id(curr->obj), curr->table);
  • list = curr->next;
  • xfree(curr);
  • } } st_free_table(finalizer_table); finalizer_table = 0;

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#5 Updated by Yusuke Endoh about 5 years ago

=begin
遠藤です。

2010年2月13日10:40 Tanaka Akira akr@fsij.org:

2010年2月13日10:31 Yusuke ENDOH mame@tsg.ne.jp:

これ自体はひょっとしたらバグなのかもしれませんが、一般的に言って、
conservative GC だとファイナライザが予期せずオブジェクト自体に参照を
持ってしまう可能性があるので、self-referencial なファイナライザを
実行しないという方針だと、ファイナライザが不可解に実行されない事態の
発生を避けられないという結論に至りました。

そういえば、1.8 の tempfile.rb は、ファイナライザとして
Proc じゃなくて普通のオブジェクトを使うようにして、
変な参照を持たないようにしてあるんですが、
そうするのは関係ありますかね。

のパッチ + 単純に Tempfile::Remover を復活させてみた
だけでは、やっぱり消えないようでした。どこかで不要な mark をしてるの
かなあ。

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#6 Updated by Yusuke Endoh about 5 years ago

=begin
遠藤です。

2010年2月16日11:25 KOSAKI Motohiro kosaki.motohiro@jp.fujitsu.com:

忘れそうになるので、IRCの議論を転記

  1. objectはreferenceがある限り回収されない
  2. finalizerはobjectが回収される時に呼ばれる
  3. プロセス終了時はfinalizerが絶対呼ばれる

の1,2と3が両立不可能という仕様バグの話だと思っています。

はい。

プロセス終了時に
まだ参照されているオブジェクトは下記A)、B)どちらでもルール違反

A) 参照されていても強制的にファイナライザを呼ぶ
(現状こちら。かつファイナライザ呼ぶ直前に)
B) プロセス終了時にまだ参照されているオブジェクトはファイナライザ呼ばない
(のようにtempfileが消えなくなる)

ので、仕様明確化が必要でしょう。

  1. object は reference がある限り回収されない
  2. finalizer は object が回収される時 またはプロセス終了時 に呼ばれる

という仕様でいいと思います。抽象的に言うなら、「それより先に object に
対する操作を行う機会がなくなる時点」とかですかね。「それより先」の定義が
微妙ですが。

そのようにするパッチが です。大きな反対はなかったので
一旦コミットします。問題が起きたら言ってください。

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#7 Updated by Yusuke Endoh about 5 years ago

  • Status changed from Open to Closed
  • % Done changed from 0 to 100

=begin
This issue was solved with changeset r26684.
Yusuke, thank you for reporting this issue.
Your contribution to Ruby is greatly appreciated.
May Ruby be with you.

=end

Also available in: Atom PDF