Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to 'unload' ('un-require') a Ruby library?

I'm looking to load a few libraries, have them do some work, and then do the opposite of require to avoid compatibility errors later. I don't want to have to dump to a file and restart a shell, as the objects created (such as data) could be processed well by my other libraries, just not in the presence of the early ones I'm seeking to unload.

Anyone got any suggestions or know if this is possible? A conversation from 2006 didn't come to much conclusion-wise other than that 'it looks like Webrick manages to do this somehow'.

The libraries in question are Google_drive and Nokogiri (the spreadsheet processing library Roo depends on Google_drive for online spreadsheet reading/writing, as described at that link).

like image 609
Louis Maddox Avatar asked Nov 03 '13 12:11

Louis Maddox


4 Answers

Like @Alex said, you could use the Kernel#fork to create a new ruby process where you will require your libraries. The new forked process will have access to data loaded in the parent process:

def talk(msg)
  # this will allow us to see which process is
  # talking
  puts "#{Process.pid}: #{msg}"
end

# this data was loaded on the parent process
# and will be use in the child (and in the parent)
this_is_data = ["a", "b", "c"]

talk "I'm the father process, and I see #{this_is_data}"

# this will create a new ruby process
fork{
  talk "I'm another process, and I also see #{this_is_data}"
  talk "But when I change `this_is_data`, a new copy of it is created"
  this_is_data << "d"
  talk "My own #{this_is_data}"
}

# let's wait and give a chance to the child process
# finishes before the parent
sleep 3

talk "Now, in the father again, data is: #{this_is_data}"

The result of this execution will vary in your machine, the Process.id will return different values, but it will be like these:

23520: I'm the father process, and I see ["a", "b", "c"]
23551: I'm another process, and I also see ["a", "b", "c"]
23551: But when I change `this_is_data`, a new copy of it is created
23551: My own ["a", "b", "c", "d"]
23520: Now, in the father again, data is: ["a", "b", "c"]

And this is good! Each process created by fork is an O.S. level process and run in it's own memory space.

Another thing you can do to somehow manage the globals created by loading a file, is replace the use of require by load. This approach doesn't solve all the problems already pointed, but really can help. See the following specs:

require "minitest/autorun"

describe "Loading files inside a scope" do

  def create_lib_file(version)
    libfile = <<CODE
      class MyLibrary#{version}
        VERSION = "0.0.#{version}"
      end

      class String
        def omg_danger!
        end
      end

      puts "loaded \#{MyLibrary#{version}::VERSION}"
    CODE

    File.write("my_library.rb", libfile)
  end

  after do
    File.delete("my_library.rb") if File.exists?("my_library.rb")
  end

  describe "loading with require" do
    it "sees the MyLibrary definition" do
      create_lib_file("1")
      require_relative "my_library.rb"
      MyLibrary1::VERSION.must_be :==, "0.0.1"
      "".respond_to?(:omg_danger!).must_be :==, true
    end
  end

  describe "loading with #load " do
    describe "without wrapping" do
      it "sees the MyLibrary definition" do
        create_lib_file("2")
        load "my_library.rb"
        MyLibrary2::VERSION.must_be :==, "0.0.2"
        "".respond_to?(:omg_danger!).must_be :==, true
      end
    end

    describe "using anonymous module wraping" do
      it "doesn't sees MyLibrary definition" do
        create_lib_file("3")
        load "my_library.rb", true
        ->{ MyLibrary3 }.must_raise NameError
        "".respond_to?(:omg_danger!).must_be :==, false
      end
    end
  end
end

And the result of execution:

Run options: --seed 16453

# Running tests:

loaded 0.0.3
.loaded 0.0.2
.loaded 0.0.1
.

Finished tests in 0.004707s, 637.3486 tests/s, 1274.6973 assertions/s.

3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
like image 126
23 revs, 14 users 49% Avatar answered Sep 17 '22 13:09

23 revs, 14 users 49%


I'm not aware of any way to unload a file, but you can reset handpicked global variables to nil and undefine constants (which is close enough):

class Foo; end
Object.constants.include?(:Foo)
Object.send(:remove_const, :Foo)
Object.constants.include?(:Foo)
Foo                              # NameError: uninitialized constant Foo

Depending on what your conflicts are, you could also temporarily rename the conflicting classes:

Bar = Foo
Object.send(:remove_const, :Foo)
do_stuff
Foo = Bar
like image 33
Denis de Bernardy Avatar answered Sep 17 '22 13:09

Denis de Bernardy


Despite of what is usually said, it's possible to unrequire/unload packages using this process.

  1. Assuming that the file required is stored as d:/foo.rb with this simple content :
class Foo

end
  1. As any Class, Module or Method is defined as a constant in Ruby you can first unlink it :
irb(main):001:0> require 'd:/foo.rb'
=> true
irb(main):002:0> defined? Foo
=> "constant"
irb(main):003:0> Object.send(:remove_const, :Foo)
=> Foo
irb(main):004:0> defined? Foo
=> nil
  1. The files already required/loaded are recorded in the global var $", you then need to purge what you've already required from it :
irb(main):005:0> $".select{|r| r.include? 'foo.rb'}
=> ["d:/foo.rb"]
irb(main):006:0> $".delete('d:/foo.rb')
=> "d:/foo.rb"
irb(main):007:0> $".select{|r| r.include? 'foo.rb'}
=> []
  1. Now you can require your file again and everything will be refreshed and available.
irb(main):008:0> require 'd:/foo.rb'
=> true
irb(main):009:0> $".select{|r| r.include? 'foo.rb'}
=> ["d:/foo.rb"]
irb(main):010:0> defined? Foo
=> "constant"
irb(main):011:0> Foo.new
=> #<Foo:0x000000033ff8d8>
like image 33
Milmandre Avatar answered Sep 21 '22 13:09

Milmandre


Unfortunately, a couple characteristics of Ruby are at ends with your desire to cleanly "unload" a library. First, "loading" a Ruby library can run arbitrary Ruby code. Second, existing constants and methods can be dynamically redefined in Ruby.

If a Ruby library only defines new classes and modules, you can simply undefine them as @Denis pointed out. However, in that case, "compatibility errors" are very unlikely to occur even if you just leave them as is. If a library monkey-patches core Ruby classes, creates signal handlers, or sets up tracing hooks or at_exit hooks, it will be very, very difficult to trace out everything which has changed and reverse the changes cleanly.

Your best bet would be to first load your data, then use something like Process#fork to fork off a new shell, then load the libraries. When you are finished, kill the child shell and go back to the parent. Your data will still be there.

https://github.com/burke/zeus and https://github.com/jonleighton/spring use similar techniques to avoid repeatedly waiting for Rails to load. Perhaps you can adapt some pieces of their work.

like image 28
Alex D Avatar answered Sep 19 '22 13:09

Alex D