2016-08-30 7 views
4

Я создал класс, который хочу привязать к файловому дескриптору, и закрою его, когда экземпляр GC-ed.Как вы проверите, будет ли вызываться деструктор Ruby?

Я создал класс, который выглядит примерно так:

class DataWriter 
    def initialize(file) 
    # open file 
    @file = File.open(file, 'wb') 
    # create destructor 
    ObjectSpace.define_finalizer(self, self.class.finalize(@file)) 
    end 

    # write 
    def write(line) 
    @file.puts(line) 
    @file.flush 
    end 

    # close file descriptor, note, important that it is a class method 
    def self.finalize(file) 
    proc { file.close; p "file closed"; p file.inspect} 
    end 
end 

Затем я попытался проверить метод деструктора, как так:

RSpec.describe DataWriter do 
    context 'it should call its destructor' do 
    it 'calls the destructor' do 
     data_writer = DataWriter.new('/tmp/example.txt') 
     expect(DataWriter).to receive(:finalize) 
     data_writer = nil 
     GC.start 
    end 
    end 
end 

При выполнении этого теста, даже если «файл закрыт »печатается вместе с файлом. Проверьте, что тест не выполнен со следующим выходом:

1) DataWriter it should call its destructor calls the destructor 
    Failure/Error: expect(DataWriter).to receive(:finalize) 

     (DataWriter (class)).finalize(*(any args)) 
      expected: 1 time with any arguments 
      received: 0 times with any arguments 
    # ./spec/utils/data_writer_spec.rb:23:in `block (3 levels) in <top (required)>' 
+1

Upvote, потому что вы узнали о сборе мусора и его завершении в рубине: D –

+0

@ GavinMiller Я только что узнал об этом, только что вернулся к рубину после набега на землю C++, поэтому я, естественно, начал искать деструктора и научился что они немного необычны в рубине. –

ответ

4

finalize называется в initialize, возвращает процедурный, и никогда не вызывается снова, так что вы не можете ожидать, что она будет называться во время финализации. Это proc, который вызывается, когда экземпляр завершен. Чтобы проверить это, попросите метод proc вызвать метод вместо выполнения самой работы. Это проходит:

class DataWriter 
    # initialize and write same as above 

    def self.finalize(file) 
    proc { actually_finalize file } 
    end 

    def self.actually_finalize(file) 
    file.close 
    end 

end 

RSpec.describe DataWriter do 
    context 'it should call its destructor' do 
    it 'calls the destructor' do 
     data_writer = DataWriter.new('/tmp/example.txt') 
     expect(DataWriter).to receive(:actually_finalize) 
     data_writer = nil 
     GC.start 
    end 
    end 
end 
+1

Обратите внимание, что «GC.start» - это всего лишь * подсказка *, которую GC * может запускать сейчас, не гарантируется, что GC * будет работать. И даже если он работает, нет никаких гарантий *, которые * объекты будут собирать. Например, если это GC реального времени, у него может быть жесткий предел того, как долго ему разрешено работать, и поэтому он может просто не обойтись, чтобы собрать все осиротевшие объекты. Таким образом, есть всевозможные причины, по которым этот тест может потерпеть неудачу, даже если финализатор работает отлично. –

+1

@ JörgWMittag Я подозревал, что это так. У вас есть какие-либо документы, чтобы узнать больше о GC рубина? –

1

даже если «файл закрыт» печатаются вместе с file.inspect, тест не пройден со следующим выходом

Я бросил свой код в один файл и запустил его. Оказывается, что код финализации не быть очищены до RSpec выходов не дали выход я получаю:

Failures: 
F 

    1) DataWriter it should call its destructor calls the destructor 
    Failure/Error: expect(DataWriter).to receive(:finalize) 

     (DataWriter (class)).finalize(*(any args)) 
      expected: 1 time with any arguments 
      received: 0 times with any arguments 
    # /scratch/data_writer.rb:27:in `block (3 levels) in <top (required)>' 

Finished in 0.01066 seconds (files took 0.16847 seconds to load) 
1 example, 1 failure 

Failed examples: 

rspec /scratch/data_writer.rb:25 # DataWriter it should call its destructor calls the destructor 

"file closed" 
"#<File:/tmp/example.txt (closed)>" 

Что касается почему это, я не могу сказать прямо сейчас.Dave is right вы утверждаете, что это уже произошло, поэтому ваш тест никогда не пройдет. Вы можете наблюдать это, изменив тест:

it 'calls the destructor' do 
    expect(DataWriter).to receive(:finalize).and_call_original 
    data_writer = DataWriter.new('/tmp/example.txt') 
    data_writer = nil 
    GC.start 
end 
1

IMHO вы не должны полагаться на финализатор, чтобы работать точно, когда GC работает. В конечном итоге они побегут. Но, возможно, только тогда, когда процесс закончится. Насколько я могу судить, это также зависит от реализации Ruby и реализации GC. 1.8 имеет другое поведение, чем 1.9+, Rubinius и JRuby могут быть разными.

Обеспечение того, чтобы ресурс был выпущен, может быть достигнут блоком, который также будет заботиться о том, чтобы ресурс был освобожден, как только это не было необходимо.

несколько API, имеют один и тот же стиль в Ruby:

File.open('thing.txt', 'wb') do |file| # file is passed to block 
             # do something with file 
end         # file will be closed when block ends 

Вместо того, чтобы делать это (как вы показали в своей сущности)

(1..100_000).each do |i| 
    File.open(filename, 'ab') do |file| 
    file.puts "line: #{i}" 
    end 
end 

Я хотел бы сделать это следующим образом:

File.open(filename, 'wb') do |file| 
    (1..100_000).each do |i| 
    file.puts "line: #{i}" 
    end 
end 
+0

Как мне сделать это со стилем кода, который у меня есть сейчас? Я чувствую, что держаться за имя файла и открывать его каждый раз, когда я хочу писать, будет гораздо более неэффективным, чем просто удерживать дескриптор файла один раз, а затем использовать его, пока я не отброшу свой класс. (Я позорно признаю, что я пока не профилировал это, но был только моим стилем письма по умолчанию). Кроме того, это только проблема для тестирования, для моего кода мне остается только, чтобы все было очищено. Для моего теста я забочусь о том, когда. –

+0

@ MikeH-R добавил небольшой образец. Опора на финализаторы вызовет проблемы, потому что она не определена, когда она будет работать. –

+0

Я думаю, что вам не хватает того, что я говорю, я сделал [этот gist] (https://gist.github.com/mijoharas/f7968a546d4681ab7f5a83115f5bf2d7), чтобы показать, что я имею в виду, первый подход в 9 раз медленнее. –

0

Я переписал свое рабочее решение ниже, бит, я не запускал этот код.

RSpec.describe DataWriter do 
    context 'it should call its destructor' do 
    it 'calls the destructor' do 

     # creating pipe for IPC to get result from child process 
     # after it garbaged 
     # http://ruby-doc.org/core-2.0.0/IO.html#method-c-pipe 
     rd, wr = IO.pipe 

     # forking 
     # https://ruby-doc.org/core-2.1.2/Process.html#method-c-fork 
     if fork  
     wr.close 
     called = rd.read 
     Process.wait 
     expect(called).to eq('/tmp/example.txt') 
     rd.close 
     else 
     rd.close 
     # overriding DataWriter.actually_finalize(file) 
     DataWriter.singleton_class.class_eval do 
      define_method(:actually_finalize) do |arg| 
      wr.write arg 
      wr.close 
      end 
     end 

     data_writer = DataWriter.new('/tmp/example.txt') 
     data_writer = nil 
     GC.start 
     end 
    end 
    end 
end 

Главное, что я поймаю этот GC.start call выполняет реальную работу точно при выходе из процесса. Я пробовал блоки и потоки, но в моем случае (ruby 2.2.4p230 @ Ubuntu x86_64) он работает только при завершении процесса.

Я предлагаю, чтобы существовал лучший способ получить результаты от дочернего процесса, но я использовал межпроцессную связь (IPC).

И у меня нет результата с построением ожидания rspec при вызове метода деструктора в форме, как expect(DataWriter).to receive(:actually_finalize).with('/tmp/example.txt') - Я не знаю почему, но я полагаю, что обертки, созданные Rspec, были обмануты или нарушены до вызова деструктора класс.

Надеюсь, это поможет!