Skip to content

Commit

Permalink
Load needed (dynamic) dependencies for provisioners at creation time.
Browse files Browse the repository at this point in the history
The need for this change was initated with #293 which runs every
instance action in a thread of execution, even in serial mode. The issue
is that on the Mac platform MRI Ruby 1.9 and 2.0 (not confirmed for
2.1), dynamic (i.e. native) code needs to be loaded in the main thread
otherwise causing a "Trace/BPT trap: 5" crash.

References:

* http://stackoverflow.com/questions/9933969/matlab-ruby-gem-doesnt-work-when-called-from-thread#answer-12319780
* https://bugs.launchpad.net/sbcl/+bug/1014409

For Berkshelf 2.0.x at least, there are multiple transitive dependencies
that *could* load native code, meaning that `require 'berkshelf'` could
very well lead to a VM crash if performed in a thread other than main.

This commit pushes library loading code ahead to Provisioner creation
time. In other words, by the time you have a Provisioner object
reference either Berkshelf, Librarian-Chef, or nothing has been required
(assuming a Chef provisioner).

A drawback to this approach is that these dynamic dependencies will be
eagerly loaded when Test Kitchen is booting for trivial CLI commands
such as `kitchen list` and `kitchen diagnose`. Further work is needed to
ensure that `kitchen diagnose` remains viable even in the face of a
failure to load these dependencies.

Finally, the loaded version of Berkshelf or Librarian-Chef will be
displayed to the user on INFO for converge action and DEBUG on code
loading for troubleshooting.

Closes #357

/cc @reset, @ivey, @sethvargo
  • Loading branch information
fnichol committed Feb 12, 2014
1 parent 644f080 commit 954a3ff
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 12 deletions.
3 changes: 3 additions & 0 deletions lib/kitchen/provisioner/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def initialize(config = {})
def instance=(instance)
@instance = instance
expand_paths!
load_needed_dependencies!
end

# Returns the name of this driver, suitable for display in a CLI.
Expand Down Expand Up @@ -128,6 +129,8 @@ def expand_paths!
end
end

def load_needed_dependencies! ; end

def logger
instance ? instance.logger : Kitchen.logger
end
Expand Down
22 changes: 16 additions & 6 deletions lib/kitchen/provisioner/chef/berkshelf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,15 @@ def initialize(berksfile, path, logger = Kitchen.logger)
@logger = logger
end

def self.load!(logger = Kitchen.logger)
load_berkshelf!(logger)
end

def resolve
info("Resolving cookbook dependencies with Berkshelf...")
version = ::Berkshelf::VERSION
info("Resolving cookbook dependencies with Berkshelf #{version}...")
debug("Using Berksfile from #{berksfile}")

load_berkshelf!

::Berkshelf.ui.mute do
if ::Berkshelf::Berksfile.method_defined?(:vendor)
# Berkshelf 3.0 requires the directory to not exist
Expand All @@ -60,10 +63,17 @@ def resolve

attr_reader :berksfile, :path, :logger

def load_berkshelf!
require 'berkshelf'
def self.load_berkshelf!(logger)
first_load = require 'berkshelf'

version = ::Berkshelf::VERSION
if first_load
logger.debug("Berkshelf #{version} library loaded")
else
logger.debug("Berkshelf #{version} previously loaded")
end
rescue LoadError => e
fatal("The `berkshelf' gem is missing and must be installed" +
logger.fatal("The `berkshelf' gem is missing and must be installed" +
" or cannot be properly activated. Run" +
" `gem install berkshelf` or add the following to your" +
" Gemfile if you are using Bundler: `gem 'berkshelf'`.")
Expand Down
23 changes: 17 additions & 6 deletions lib/kitchen/provisioner/chef/librarian.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,22 @@ class Librarian

include Logging


def initialize(cheffile, path, logger = Kitchen.logger)
@cheffile = cheffile
@path = path
@logger = logger
end

def self.load!(logger = Kitchen.logger)
load_librarian!(logger)
end

def resolve
info("Resolving cookbook dependencies with Librarian-Chef")
version = ::Librarian::Chef::VERSION
info("Resolving cookbook dependencies with Librarian-Chef #{version}...")
debug("Using Cheffile from #{cheffile}")

load_librarian!

env = ::Librarian::Chef::Environment.new(
:project_path => File.dirname(cheffile))
env.config_db.local["path"] = path
Expand All @@ -54,12 +58,19 @@ def resolve

attr_reader :cheffile, :path, :logger

def load_librarian!
require 'librarian/chef/environment'
def self.load_librarian!(logger)
first_load = require 'librarian/chef/environment'
require 'librarian/action/resolve'
require 'librarian/action/install'

version = ::Librarian::Chef::VERSION
if first_load
logger.debug("Librarian-Chef #{version} library loaded")
else
logger.debug("Librarian-Chef #{version} previously loaded")
end
rescue LoadError => e
fatal("The `librarian-chef' gem is missing and must be installed" +
logger.fatal("The `librarian-chef' gem is missing and must be installed" +
" or cannot be properly activated. Run" +
" `gem install librarian-chef` or add the following to your" +
" Gemfile if you are using Bundler: `gem 'librarian-chef'`.")
Expand Down
10 changes: 10 additions & 0 deletions lib/kitchen/provisioner/chef_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ def create_sandbox

protected

def load_needed_dependencies!
if File.exists?(berksfile)
debug("Berksfile found at #{berksfile}, loading Berkshelf")
Chef::Berkshelf.load!(logger)
elsif File.exists?(cheffile)
debug("Cheffile found at #{cheffile}, loading Librarian-Chef")
Chef::Librarian.load!(logger)
end
end

def format_config_file(data)
data.each.map { |attr, value|
[attr, (value.is_a?(Array) ? value.to_s : %{"#{value}"})].join(" ")
Expand Down

0 comments on commit 954a3ff

Please sign in to comment.