You're right on all counts, Jacob. I've squashed the following changes into my existing commit (which is available at http://github.com/stereotype441/puppet/tree/ticket/2.6.x/4771):
diff --git a/lib/puppet/parser/type_loader.rb b/lib/puppet/parser/type_loader.rb index 33c5304..bae5603 100644 --- a/lib/puppet/parser/type_loader.rb +++ b/lib/puppet/parser/type_loader.rb @@ -20,13 +20,20 @@ class Puppet::Parser::TypeLoader # already executing it, wait for it to finish. If this thread is # already executing it, return immediately without executing the # block. + # + # Note: the reason for returning immediately if this thread is + # already executing the block is to handle the case of a circular + # import--when this happens, we attempt to recursively re-parse a + # file that we are already in the process of parsing. To prevent + # an infinite regress we need to simply do nothing when the + # recursive import is attempted. def do_once(file) need_to_execute = synchronize do case @state[file] when :doing if @thread[file] != Thread.current @cond_var[file].wait - end + end false when :done false @@ -34,6 +41,7 @@ class Puppet::Parser::TypeLoader @state[file] = :doing @thread[file] = Thread.current @cond_var[file] = new_cond + true end end if need_to_execute On Wed, Sep 22, 2010 at 11:57 AM, Jacob Helwig <ja...@puppetlabs.com> wrote: > On Mon, 20 Sep 2010 15:59:19 -0700, Paul Berry wrote: > > > > The function import_if_possible, which was supposed to be responsible > > for making sure that no two threads tried to import the same file at > > the same time, was not making this decision based on the full pathname > > of the file, since it was being invoked before pathnames were > > resolved. As a result, if we attempted to import two distinct files > > with the same name at the same time (either in two threads or in a > > single thread due to recursion), one of the files would not always get > > imported. > > > > Fixed this problem by moving the thread-safety logic to happen after > > filenames are resolved to absolute paths. This made it possible to > > simplify the thread-safety logic significantly. > > > > Signed-off-by: Paul Berry <p...@puppetlabs.com> > > --- > > lib/puppet/parser/parser_support.rb | 2 +- > > lib/puppet/parser/type_loader.rb | 90 > ++++++++++++++++------------------ > > spec/lib/puppet_spec/files.rb | 1 + > > spec/unit/parser/type_loader_spec.rb | 19 +------ > > 4 files changed, 47 insertions(+), 65 deletions(-) > > > > diff --git a/lib/puppet/parser/parser_support.rb > b/lib/puppet/parser/parser_support.rb > > index c0fd371..4f3a4dd 100644 > > --- a/lib/puppet/parser/parser_support.rb > > +++ b/lib/puppet/parser/parser_support.rb > > @@ -111,7 +111,7 @@ class Puppet::Parser::Parser > > end > > > > def import(file) > > - known_resource_types.loader.import_if_possible(file, @lexer.file) > > + known_resource_types.loader.import(file, @lexer.file) > > end > > > > def initialize(env) > > diff --git a/lib/puppet/parser/type_loader.rb > b/lib/puppet/parser/type_loader.rb > > index 09aa636..33c5304 100644 > > --- a/lib/puppet/parser/type_loader.rb > > +++ b/lib/puppet/parser/type_loader.rb > > @@ -3,25 +3,48 @@ require 'puppet/node/environment' > > class Puppet::Parser::TypeLoader > > include Puppet::Node::Environment::Helper > > > > - class Helper < Hash > > + # Helper class that makes sure we don't try to import the same file > > + # more than once from either the same thread or different threads. > > + class Helper > > include MonitorMixin > > - def done_with(item) > > - synchronize do > > - delete(item)[:busy].signal if self.has_key?(item) and > self[item][:loader] == Thread.current > > - end > > + def initialize > > + super > > + # These hashes are indexed by filename > > + @state = {} # :doing or :done > > + @thread = {} # if :doing, thread that's doing the parsing > > + @cond_var = {} # if :doing, condition var that will be signaled > when done. > > end > > - def owner_of(item) > > - synchronize do > > - if !self.has_key? item > > - self[item] = { :loader => Thread.current, :busy => > self.new_cond} > > - :nobody > > - elsif self[item][:loader] == Thread.current > > - :this_thread > > + > > + # Execute the supplied block exactly once per file, no matter how > > + # many threads have asked for it to run. If another thread is > > + # already executing it, wait for it to finish. If this thread is > > + # already executing it, return immediately without executing the > > + # block. > > I was a bit confused by the "this thread is already executing it" part, > until it was brought to my attention that there might be circular > imports. Might be worth explicitly mentioning that to save people a few > brain cycles in the future? > > > + def do_once(file) > > + need_to_execute = synchronize do > > + case @state[file] > > + when :doing > > + if @thread[file] != Thread.current > > + @cond_var[file].wait > > + end > > Indentation on 'end'? > > > + false > > + when :done > > + false > > else > > - flag = self[item][:busy] > > - flag.wait > > - flag.signal > > - :another_thread > > + @state[file] = :doing > > + @thread[file] = Thread.current > > + @cond_var[file] = new_cond > > Should this return new_cond in the "true" case, or explicitly return > true? Do we care? > > > + end > > + end > > + if need_to_execute > > + begin > > + yield > > + ensure > > + synchronize do > > + @state[file] = :done > > + @thread.delete(file) > > + @cond_var.delete(file).broadcast > > + end > > end > > end > > end > > @@ -51,8 +74,7 @@ class Puppet::Parser::TypeLoader > > unless file =~ /^#{File::SEPARATOR}/ > > file = File.join(dir, file) > > end > > - unless imported? file > > - @imported[file] = true > > + @loading_helper.do_once(file) do > > parse_file(file) > > end > > end > > @@ -60,27 +82,20 @@ class Puppet::Parser::TypeLoader > > modname > > end > > > > - def imported?(file) > > - @imported.has_key?(file) > > - end > > - > > def known_resource_types > > environment.known_resource_types > > end > > > > def initialize(env) > > self.environment = env > > - @loaded = {} > > - @loading = Helper.new > > - > > - @imported = {} > > + @loading_helper = Helper.new > > end > > > > def load_until(namespaces, name) > > return nil if name == "" # special-case main. > > name2files(namespaces, name).each do |filename| > > modname = begin > > - import_if_possible(filename) > > + import(filename) > > rescue Puppet::ImportError => detail > > # We couldn't load the item > > # I'm not convienced we should just drop these errors, but this > > @@ -96,10 +111,6 @@ class Puppet::Parser::TypeLoader > > nil > > end > > > > - def loaded?(name) > > - @loaded.include?(name) > > - end > > - > > def name2files(namespaces, name) > > return [name.sub(/^::/, '').gsub("::", File::SEPARATOR)] if name =~ > /^::/ > > > > @@ -126,21 +137,4 @@ class Puppet::Parser::TypeLoader > > parser.file = file > > parser.parse > > end > > - > > - # Utility method factored out of load for handling thread-safety. > > - # This isn't tested in the specs, because that's basically impossible. > > - def import_if_possible(file, current_file = nil) > > - @loaded[file] || begin > > - case @loading.owner_of(file) > > - when :this_thread > > - nil > > - when :another_thread > > - import_if_possible(file,current_file) > > - when :nobody > > - @loaded[file] = import(file,current_file) > > - end > > - ensure > > - @loading.done_with(file) > > - end > > - end > > end > > diff --git a/spec/lib/puppet_spec/files.rb > b/spec/lib/puppet_spec/files.rb > > index cab4a1e..52ed903 100644 > > --- a/spec/lib/puppet_spec/files.rb > > +++ b/spec/lib/puppet_spec/files.rb > > @@ -1,4 +1,5 @@ > > require 'fileutils' > > +require 'tempfile' > > > > # A support module for testing files. > > module PuppetSpec::Files > > diff --git a/spec/unit/parser/type_loader_spec.rb > b/spec/unit/parser/type_loader_spec.rb > > index 83006b3..02d543b 100644 > > --- a/spec/unit/parser/type_loader_spec.rb > > +++ b/spec/unit/parser/type_loader_spec.rb > > @@ -77,13 +77,6 @@ describe Puppet::Parser::TypeLoader do > > @loader.load_until(["foo"], "bar") { |f| false }.should be_nil > > end > > > > - it "should know when a given name has been loaded" do > > - @loader.expects(:name2files).returns %w{file} > > - @loader.expects(:import).with("file",nil) > > - @loader.load_until(["foo"], "bar") { |f| true } > > - @loader.should be_loaded("file") > > - end > > - > > it "should set the module name on any created resource types" do > > type = Puppet::Resource::Type.new(:hostclass, "mytype") > > > > @@ -113,7 +106,8 @@ describe Puppet::Parser::TypeLoader do > > describe "when importing" do > > before do > > Puppet::Parser::Files.stubs(:find_manifests).returns ["modname", > %w{file}] > > - @loader.stubs(:parse_file) > > + Puppet::Parser::Parser.any_instance.stubs(:parse) > > + Puppet::Parser::Parser.any_instance.stubs(:file=) > > end > > > > it "should return immediately when imports are being ignored" do > > @@ -154,16 +148,9 @@ describe Puppet::Parser::TypeLoader do > > @loader.import("myfile", "/current/file") > > end > > > > - it "should know when a given file has been imported" do > > - Puppet::Parser::Files.expects(:find_manifests).returns ["modname", > %w{/one}] > > - @loader.import("myfile") > > - > > - @loader.should be_imported("/one") > > - end > > - > > it "should not attempt to import files that have already been > imported" do > > Puppet::Parser::Files.expects(:find_manifests).returns ["modname", > %w{/one}] > > - @loader.expects(:parse_file).once > > + Puppet::Parser::Parser.any_instance.expects(:parse).once > > @loader.import("myfile") > > > > # This will fail if it tries to reimport the file. > > -- > > 1.7.2 > > > > -- > > You received this message because you are subscribed to the Google Groups > "Puppet Developers" group. > > To post to this group, send email to puppet-...@googlegroups.com. > > To unsubscribe from this group, send email to > puppet-dev+unsubscr...@googlegroups.com<puppet-dev%2bunsubscr...@googlegroups.com> > . > > For more options, visit this group at > http://groups.google.com/group/puppet-dev?hl=en. > > > > -- > Jacob Helwig > > -----BEGIN PGP SIGNATURE----- > Version: GnuPG v1.4.10 (GNU/Linux) > > iQGcBAEBAgAGBQJMmlF6AAoJEHJabXWGiqEB3qYL/1OXRqPuasD7rLUwBbWOQcth > kgpnM6oPKG63xgIThi9DnUwUDTOtRRGZi8bCtfHHWvEf789ZK7MKMEGcQwPZRB2r > R1t7hDkSnZ/cBPN2ashqGRaqWkHN9eCGWFpSoMNPsXTynXDFjWLn0FUDgZ2WNI8u > /BC0tX8F9sBP2g9uC0zUEyrVculCeXXi13guVDanOTllGk/UkKMg+kU017IrmhHV > 4Xdrt+0JBO68G4Bu1J1uxsHhNla7wjN1LC7MgMbezSV3VwDze/XZ1H6A6+uVRehL > HIrYKqUiylGg7EsQJ56YBSc0/Hl0/AMJaaxSMCBwTycpVvwGXOxw3XdB7JjA1XZX > nkdp1497IBYMolyLC/n/Pj0GRiiiz/9CeSZFa59t7yLiJAVBrzbViUhq+tSU09e7 > w9GpXWV7lx4RjfTxIjHqzNRJ6V7co5iGW2slxQLnb1b/c+3wR3OOuqSTCmPZ74yt > 9kFYf1KXcwMm9MmW8gbZfSCo6JHKOh5LMLaZ3r1kkQ== > =Ojg0 > -----END PGP SIGNATURE----- > > -- You received this message because you are subscribed to the Google Groups "Puppet Developers" group. To post to this group, send email to puppet-...@googlegroups.com. To unsubscribe from this group, send email to puppet-dev+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en.