Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

loading/unloading/updating class in ruby

I did a little experiments with Ruby class dynamic loading/unloading/updating as implementing plugins infrastructure. I found a few points:

  1. If loading new version of same class without first unloading it, the new one essentially 'top' or 'merge' with previous version. All existent objects created with previous version would get their class definition 'updated'.
  2. Unloading a class does not affect existent objects created with this class. Existent objects stay with whatever version just unloaded. (class cannot be used anymore but not the objects already created)
  3. If loading new version after unloading of previous version, new objects created would be of the new version. However, old objects created before the loading of new version would not be affected and would still be of the old version.

My question is, is there an easy way to make existent object created from the old class version 'switch' to the new version (but not a merged version of the old & new version)? It seems to me the possible way to do it is to re-create the object after unloading/loading, which is not suitable for plugins (don't want it be destroyed).

Update: my intent was to have existent objects updated with new version, without the problem of merging old version with new version (like change of number of arguments, or the removal of a method). Unloading and then reloading again seems to be the cleanest way of doing this, though you must keep track of all such objects and recreate them when needed. Also, expensive objects might not be suitable for re-creation. This leaves me with the second option, prohibiting unexpected merging from happening. As long as no method removed, no method signature changed, the merging should work just fine.

Below is my test program:

$ cat test.rb
load 'v1.rb'
puts "=> 'v1.rb' loaded"
a1 = A.new
puts "=> object a1(#{a1}) created"
a1.common
a1.method_v1
load 'v2.rb'
puts '',"=> class A updated by 'v2.rb'"
a1.common
a1.method_v1
a1.method_v2

a2 = A.new
puts '',"=> object a2(#{a2}) created"
a2.common
a2.method_v1
a2.method_v2

Object.send(:remove_const, 'A')
puts '',"=> class A unloaded"

A.new rescue puts $!

puts '',"=> class A does not exist now"
a1.common
a1.method_v1
a1.method_v2 rescue puts $!
a2.common
a2.method_v1
a2.method_v2

load 'v3.rb'
puts '',"=> 'v3.rb' loaded"
a1.common
a1.method_v1
a1.method_v2 rescue puts $!
a1.method_v3 rescue puts $!
a2.common
a2.method_v1
a2.method_v2
a2.method_v3 rescue puts $!

a3 = A.new
puts '',"=> object a3(#{a3}) create"
a3.common
a3.method_v1 rescue puts $!
a3.method_v2 rescue puts $!
a3.method_v3

The sample output:

$ ruby test.rb
=> 'v1.rb' loaded
=> object a1(#<A:0x1042d4b0>) created
#<A:0x1042d4b0>: common: v1
#<A:0x1042d4b0>: method v1

=> class A updated by 'v2.rb'
#<A:0x1042d4b0>: common: v2
#<A:0x1042d4b0>: method v1
#<A:0x1042d4b0>: method v2

=> object a2(#<A:0x1042cec0>) created
#<A:0x1042cec0>: common: v2
#<A:0x1042cec0>: method v1
#<A:0x1042cec0>: method v2

=> class A unloaded
uninitialized constant A

=> class A does not exist now
#<A:0x1042d4b0>: common: v2
#<A:0x1042d4b0>: method v1
#<A:0x1042d4b0>: method v2
#<A:0x1042cec0>: common: v2
#<A:0x1042cec0>: method v1
#<A:0x1042cec0>: method v2

=> 'v3.rb' loaded
#<A:0x1042d4b0>: common: v2
#<A:0x1042d4b0>: method v1
#<A:0x1042d4b0>: method v2
undefined method `method_v3' for #<A:0x1042d4b0>
#<A:0x1042cec0>: common: v2
#<A:0x1042cec0>: method v1
#<A:0x1042cec0>: method v2
undefined method `method_v3' for #<A:0x1042cec0>

=> object a3(#<A:0x1042c3f8>) create
#<A:0x1042c3f8>: common: v3
undefined method `method_v1' for #<A:0x1042c3f8>
undefined method `method_v2' for #<A:0x1042c3f8>
#<A:0x1042c3f8>: method v3

Below is the 3 versions of class A:

$ cat v1.rb
class A
  def common
    puts "#{self}: common: v1"
  end
  def method_v1
    puts "#{self}: method v1"
  end
end

$ cat v2.rb
class A
  def common
    puts "#{self}: common: v2"
  end
  def method_v2
    puts "#{self}: method v2"
  end
end

$ cat v3.rb
class A
  def common
    puts "#{self}: common: v3"
  end
  def method_v3
    puts "#{self}: method v3"
  end
end
like image 882
bryantsai Avatar asked Nov 06 '22 18:11

bryantsai


2 Answers

Obviously there's a danger in totally replacing the class definition with a new class definition, whether you're merging the new version or deleting the old version and expecting objects to automatically get updated. That danger is in the fact that the old version of the object may be in an invalid state for the new version. (For example, instance variables that the new version of the class initializies in its initialize method may not have been defined by the old version, but there could aso be subtler bugs than this). So care (and a well-planned upgrade path) is needed no matter how you pull this off.

Given that you know what the version you're upgrading from looks like (which you need in order to upgrade sensibly anyway), it's dead simple to have the new version of the class remove unneeded methods from the old version of the class:

class A
  remove_method :foo
end

And I'm not sure what you're talking about when you say there's problems redefining a method to take a different number of parameters. It works fine for me:

class A
  def foo a
    a
  end
end
ainst=A.new
p(ainst.foo 1) rescue puts($!)
p(ainst.foo 1,2) rescue puts($!)

class A
  def foo a,b
    [a,b]
  end
end
p(ainst.foo 1) rescue puts($!)
p(ainst.foo 1,2) rescue puts($!)

The only thing you can't do (AFAIK) is change the class's superclass. That's defined the first time you define the class, and you're not allowed to change it (though you can specify the same ancestor class again).

class A < Object
end
class A < Object
end
class A < String #TypeError: superclass mismatch for class A
end
like image 155
Ken Bloom Avatar answered Nov 12 '22 16:11

Ken Bloom


In short, there is no way to do this without some serious hacking. What I suggest you to do is to make a to_serialized method that returns an array that the initialize method accepts to get the same state. If you simply want to copy all instance variables over, you could do this:

class A
  def initialize(instance_variables)
    instance_variables.each do |key, value|
      self.instance_variable_set(key, value)
    end
  end

  def to_serialized
    iv = {}
    self.instance_variables.each do |key|
      iv[key] = self.instance_variable_get(key)
    end
  end
end

And to reload the method, you could do this:

obj_state = object.to_serialized
Object.send(:remove_const, 'A')
load 'file.rb'
object = A.new(obj_state)

Note that this doesn't nest, so if any of the objects the instance variables refer to is reloaded too, you need to "serialize" them yourself.

like image 22
sarahhodne Avatar answered Nov 12 '22 17:11

sarahhodne