diff --git a/lib/puppet/util/file_locking.rb b/lib/puppet/util/file_locking.rb index 80a0b2b0c..101ece308 100644 --- a/lib/puppet/util/file_locking.rb +++ b/lib/puppet/util/file_locking.rb @@ -1,47 +1,44 @@ require 'puppet/util' module Puppet::Util::FileLocking module_function # Create a shared lock for reading def readlock(file) Puppet::Util.sync(file).synchronize(Sync::SH) do File.open(file) { |f| f.lock_shared { |lf| yield lf } } end end # Create an exclusive lock for writing, and do the writing in a # tmp file. def writelock(file, mode = nil) unless FileTest.directory?(File.dirname(file)) raise Puppet::DevError, "Cannot create %s; directory %s does not exist" % [file, File.dirname(file)] end tmpfile = file + ".tmp" unless mode + # It's far more likely that the file will be there than not, so it's + # better to stat once to check for existence and mode. + # If we can't stat, it's most likely because the file's not there, + # but could also be because the directory isn't readable, in which case + # we won't be able to write anyway. begin mode = File.stat(file).mode rescue mode = 0600 end end Puppet::Util.sync(file).synchronize(Sync::EX) do File.open(file, "w", mode) do |rf| rf.lock_exclusive do |lrf| - File.open(tmpfile, "w", mode) do |tf| - yield tf - end - begin - File.rename(tmpfile, file) - rescue => detail - File.unlink(tmpfile) if File.exist?(tmpfile) - raise Puppet::Error, "Could not rename %s to %s: %s; file %s was unchanged" % [file, tmpfile, Thread.current.object_id, detail, file] - end + yield lrf end end end end end diff --git a/spec/integration/util/file_locking.rb b/spec/integration/util/file_locking.rb index f8e21ed6e..680b3d1ed 100755 --- a/spec/integration/util/file_locking.rb +++ b/spec/integration/util/file_locking.rb @@ -1,36 +1,36 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/util/file_locking' describe Puppet::Util::FileLocking do it "should be able to keep file corruption from happening when there are multiple writers" do file = Tempfile.new("puppetspec") file.close!() file = file.path - File.open(file, "w") { |f| f.puts "starting" } - data = {:a => :b, :c => "A string", :d => "another string", :e => %w{an array of strings}} + File.open(file, "w") { |f| f.puts YAML.dump(data) } + threads = [] sync = Sync.new 9.times { |a| threads << Thread.new { 9.times { |b| sync.synchronize(Sync::SH) { Puppet::Util::FileLocking.readlock(file) { |f| - YAML.load(f.read) + YAML.load(f.read).should == data } } sleep 0.01 sync.synchronize(Sync::EX) { Puppet::Util::FileLocking.writelock(file) { |f| f.puts YAML.dump(data) } } } } } threads.each { |th| th.join } end end diff --git a/spec/unit/util/file_locking.rb b/spec/unit/util/file_locking.rb index a8b0c1840..70003faa1 100755 --- a/spec/unit/util/file_locking.rb +++ b/spec/unit/util/file_locking.rb @@ -1,146 +1,115 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/util/file_locking' class FileLocker include Puppet::Util::FileLocking end describe Puppet::Util::FileLocking do it "should have a module method for getting a read lock on files" do Puppet::Util::FileLocking.should respond_to(:readlock) end it "should have a module method for getting a write lock on files" do Puppet::Util::FileLocking.should respond_to(:writelock) end it "should have an instance method for getting a read lock on files" do FileLocker.new.private_methods.should be_include("readlock") end it "should have an instance method for getting a write lock on files" do FileLocker.new.private_methods.should be_include("writelock") end describe "when acquiring a read lock" do it "should use a global shared mutex" do sync = mock 'sync' sync.expects(:synchronize).with(Sync::SH) Puppet::Util.expects(:sync).with("/file").returns sync Puppet::Util::FileLocking.readlock '/file' end it "should use a shared lock on the file" do sync = mock 'sync' sync.expects(:synchronize).yields Puppet::Util.expects(:sync).with("/file").returns sync fh = mock 'filehandle' File.expects(:open).with("/file").yields fh fh.expects(:lock_shared).yields "locked_fh" result = nil Puppet::Util::FileLocking.readlock('/file') { |l| result = l } result.should == "locked_fh" end end describe "when acquiring a write lock" do before do @sync = mock 'sync' Puppet::Util.stubs(:sync).returns @sync @sync.stubs(:synchronize).yields end it "should fail if the parent directory does not exist" do FileTest.expects(:directory?).with("/my/dir").returns false lambda { Puppet::Util::FileLocking.writelock('/my/dir/file') }.should raise_error(Puppet::DevError) end it "should use a global exclusive mutex" do sync = mock 'sync' sync.expects(:synchronize).with(Sync::EX) Puppet::Util.expects(:sync).with("/file").returns sync Puppet::Util::FileLocking.writelock '/file' end it "should use any specified mode when opening the file" do File.expects(:open).with("/file", "w", :mymode) Puppet::Util::FileLocking.writelock('/file', :mymode) end it "should use the mode of the existing file if no mode is specified" do File.expects(:stat).with("/file").returns(mock("stat", :mode => 0755)) File.expects(:open).with("/file", "w", 0755) Puppet::Util::FileLocking.writelock('/file') end it "should use 0600 as the mode if no mode is specified and the file does not exist" do File.expects(:stat).raises(Errno::ENOENT) File.expects(:open).with("/file", "w", 0600) Puppet::Util::FileLocking.writelock('/file') end it "should create an exclusive file lock" do fh = mock 'fh' File.expects(:open).yields fh fh.expects(:lock_exclusive) Puppet::Util::FileLocking.writelock('/file') end - it "should write to a temporary file" do + it "should allow the caller to write to the locked file" do fh = mock 'fh' File.expects(:open).yields fh lfh = mock 'locked_filehandle' - fh.expects(:lock_exclusive).yields lfh + fh.expects(:lock_exclusive).yields(lfh) - tf = mock 'tmp_filehandle' - File.expects(:open).with { |path, *args| path == "/file.tmp" }.yields tf + lfh.expects(:print).with "foo" - result = nil - File.stubs(:rename) - Puppet::Util::FileLocking.writelock('/file') { |f| result = f } - result.should equal(tf) - end - - it "should rename the temporary file to the normal file" do - fh = stub 'fh' - fh.stubs(:lock_exclusive).yields fh - File.stubs(:open).yields fh - - File.expects(:rename).with("/file.tmp", "/file") - Puppet::Util::FileLocking.writelock('/file') { |f| } - end - - it "should fail if it cannot rename the file" do - fh = stub 'fh' - fh.stubs(:lock_exclusive).yields fh - File.stubs(:open).yields fh - - File.expects(:rename).with("/file.tmp", "/file").raises(RuntimeError) - lambda { Puppet::Util::FileLocking.writelock('/file') { |f| } }.should raise_error(Puppet::Error) - end - - it "should remove the temporary file if the rename fails" do - fh = stub 'fh' - fh.stubs(:lock_exclusive).yields fh - File.stubs(:open).yields fh - - File.expects(:rename).with("/file.tmp", "/file").raises(RuntimeError) - File.expects(:exist?).with("/file.tmp").returns true - File.expects(:unlink).with("/file.tmp") - lambda { Puppet::Util::FileLocking.writelock('/file') { |f| } }.should raise_error(Puppet::Error) + Puppet::Util::FileLocking.writelock('/file') do |f| + f.print "foo" + end end end end