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 @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
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
Despite of what is usually said, it's possible to unrequire/unload packages using this process.
d:/foo.rb
with this simple content :class Foo
end
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
$"
, 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'}
=> []
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>
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With