Gabe Spradlin
Gabe Spradlin

Reputation: 2057

Finalizers in Ruby: is there an equivalent to "__destruct" from PHP?

Within PHP classes the parser deals with the __construct and __destruct methods to instantiate the instance and destroy it when the script exits or you use unset. When you extend a class you simply use parent::__construct and parent::__destruct to run any cleanup code that might need running on the class that was extended.

Now within the context of a class that represents DB data and helps you manipulate that data I'd thought that a __destruct method could be used to compare current values against the originals grabbed from the DB and do an UPDATE when necessary (in some cases just always do an UPDATE so long as the Primary Key value cannot be changed). Implementing this in PHP is pretty straight forward.

The main upside to this approach would be to simply manipulate class variables quickly as needed and then have the class do one big UPDATE at the end. In long scripts that run for minutes it might be nice to create DB instance during __construct, get the data, close the DB connection, and then manipulate class variables only during the minutes long execution. On __destruct, open up a new DB connection make the UPDATEs and then close down the DB connection and clean up anything else that needs cleaning up.

I'm curious what people's thoughts are on whether or not this is a good idea/bad practice but my main question was is this possible in Ruby.

In Ruby you have the initialize method that runs when you instantiate an instance of the class. The Ruby equivalent of parent::__construct is super in Ruby. And there is the ObjectSpace.define_finalize and finalize method for Ruby classes. However, as I understand it, the finalize method isn't supposed to be able to reference the instance calling it. On top of that I can't find any equivalent to parent::__destruct. I suspect that's because there isn't an equivalent since it seems that the finalize method was explicitly designed to prevent referencing itself.

Anyone out there know of way to do this? If not, what's the best practice for dumping Ruby classes to get back resources and prevent data loss? Does everyone have a garbage_collection method they call just before setting a class instance to nil or is there some other way?

Thanks

Upvotes: 5

Views: 6369

Answers (2)

George Koehler
George Koehler

Reputation: 1703

No, Ruby has no equivalent to PHP __destruct, because PHP destroys an object as soon as its reference count reaches zero, but Ruby is slow to destroy objects. Ruby marks and sweeps objects only from time to time. Also, Ruby is conservative when it scans for local variables of C code. Ruby refuses to destroy an object if a C local might point to it.

Ruby has ObjectSpace.define_finalizer but it is difficult to use. An object can have more than one finalizer. If the superclass defined a finalizer, you can define another finalizer on the same object. The problem is that ObjectSpace.undefine_finalizer removes all finalizers from the object. So if the superclass removes its finalizer, it also removes your finalizer.

Ruby runs the finalizer only after destroying the object. The finalizer must not have a reference to the object. If it does, Ruby never destroys the object, and you have a memory leak. The finalizer must not be in a scope where self or a local variable refers to the object.

In the next example, I show how to use a finalizer to update a database. This is a bad idea because Ruby is slow to run finalizers. The database might not get updated for a long time. I call GC.start to force a full garbage collection. My computer's Ruby refuses to destroy one of the objects, so one update doesn't happen yet.

require 'forwardable'
require 'pstore'

# Create a database of people's ages.
People = PStore.new('people.pstore')
People.transaction do
  People['Alice'] = 20
  People['Bob'] = 30
  People['Carl'] = 40
  People['David'] = 50
  People['Eve'] = 60
end

# Shows people in database.  This can show old values if someone
# forgot to update the database!
def show_people(heading)
  People.transaction(true) do
    puts heading
    %w[Alice Bob Carl David Eve].each do |name|
      puts "  #{name} is #{People[name]} years old."
    end
  end
end

show_people("Before birthdays:")

# This is a person in the database.  You can change his or her age,
# but the database is only updated when you call #update or by this
# object's finalizer.
class Person
  # We need a PersonStruct for the finalizer, because Ruby destroys
  # the Person before calling the finalizer.
  PersonStruct = Struct.new(:name, :age, :oage)
  class PersonStruct
    def update(_)
      s = self
      if s.age != s.oage
        People.transaction { People[s.name] = s.oage = s.age }
      end
    end
  end
  private_constant :PersonStruct

  # Delegate name (r) and age (rw) to the PersonStruct.
  extend Forwardable
  def_delegators(:@struct, :name, :age, :age=)

  # Find a person in the database.
  def initialize(name)
    age = People.transaction(true) { People[name] }
    @struct = PersonStruct.new(name, age, age)
    ObjectSpace.define_finalizer(self, @struct.method(:update))
  end

  # Update this person in the database.
  def update
    @struct.update(nil)
  end
end

# Now give everyone some birthdays.
Person.new('Alice').age += 2
Person.new('Bob').age += 1
Person.new('Carl').age += 1
Person.new('David').age += 1
Person.new('Eve').age += 2

# I forgot to keep references to the Person objects and call
# Person#update.  Now I try to run the finalizers.
GC.start

# Did the database get updated?
show_people("After birthdays:")

My computer gives this output:

Before birthdays:
  Alice is 20 years old.
  Bob is 30 years old.
  Carl is 40 years old.
  David is 50 years old.
  Eve is 60 years old.
After birthdays:
  Alice is 22 years old.
  Bob is 31 years old.
  Carl is 41 years old.
  David is 51 years old.
  Eve is 60 years old.

I added 2 years to Eve's age, but there was no update before I checked the database. The C code that interprets Ruby might have left a reference to Person.new('Eve') in some local variable, so Ruby would not destroy the object. The result might change if you use another version of Ruby or a different C compiler. Ruby does run any leftover finalizers when the program exits, so the update did happen, but it was too late.

Upvotes: 6

Najzero
Najzero

Reputation: 3202

as pst noted in his comment you don't need a destructor for ruby. Just set all referring variables to null ( ref = nil ) and the object will get deleted by garbage collection. You can not know exactly when its garbace collected (deleted). In addition you could ( not that I recommend ) write a proc that runs before actuall deletion of that object

ObjectSpace.define_finalizer(self, self.class.method(:finalize).to_proc)

Upvotes: 4

Related Questions