diff --git a/lib/puppet/util/file_locking.rb b/lib/puppet/util/file_locking.rb index 8b194ed83..bd0181733 100644 --- a/lib/puppet/util/file_locking.rb +++ b/lib/puppet/util/file_locking.rb @@ -1,44 +1,47 @@ require 'puppet/util' module Puppet::Util::FileLocking module_function # Create a shared lock for reading def readlock(file) raise ArgumentError, "#{file} is not a file" unless !File.exists?(file) or File.file?(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) raise Puppet::DevError, "Cannot create #{file}; directory #{File.dirname(file)} does not exist" unless FileTest.directory?(File.dirname(file)) raise ArgumentError, "#{file} is not a file" unless !File.exists?(file) or File.file?(file) 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| + File.open(file, File::Constants::CREAT | File::Constants::WRONLY, mode) do |rf| rf.lock_exclusive do |lrf| + # poor's man open(2) O_EXLOCK|O_TRUNC + lrf.seek(0, IO::SEEK_SET) + lrf.truncate(0) yield lrf end end end end end diff --git a/spec/integration/util/file_locking_spec.rb b/spec/integration/util/file_locking_spec.rb index 20c61d3d5..50613448b 100755 --- a/spec/integration/util/file_locking_spec.rb +++ b/spec/integration/util/file_locking_spec.rb @@ -1,37 +1,57 @@ #!/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") - filepath = file.path - file.close!() - file = filepath - 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) } + before :each do + @file = Tempfile.new("puppetspec") + filepath = @file.path + @file.close!() + @file = filepath + @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) } + end + it "should be able to keep file corruption from happening when there are multiple writers threads" do 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).should == data + Puppet::Util::FileLocking.readlock(@file) { |f| + YAML.load(f.read).should == @data } } sleep 0.01 sync.synchronize(Sync::EX) { - Puppet::Util::FileLocking.writelock(file) { |f| - f.puts YAML.dump(data) + Puppet::Util::FileLocking.writelock(@file) { |f| + f.puts YAML.dump(@data) } } } } } threads.each { |th| th.join } end + + it "should be able to keep file corruption from happening when there are multiple writers processes" do + unless Process.fork + 50.times { |b| + Puppet::Util::FileLocking.writelock(@file) { |f| + f.puts YAML.dump(@data) + } + sleep 0.01 + } + Kernel.exit! + end + + 50.times { |c| + Puppet::Util::FileLocking.readlock(@file) { |f| + YAML.load(f.read).should == @data + } + } + end end diff --git a/spec/unit/util/file_locking_spec.rb b/spec/unit/util/file_locking_spec.rb index 10051060c..476aa2caa 100755 --- a/spec/unit/util/file_locking_spec.rb +++ b/spec/unit/util/file_locking_spec.rb @@ -1,156 +1,174 @@ #!/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 before do File.stubs(:exists?).with('/file').returns true File.stubs(:file?).with('/file').returns true end it "should use a global shared mutex" do @sync = mock 'sync' @sync.expects(:synchronize).with(Sync::SH).once 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.stubs(: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 it "should only work on regular files" do File.expects(:file?).with('/file').returns false proc { Puppet::Util::FileLocking.readlock('/file') }.should raise_error(ArgumentError) end it "should create missing files" do @sync = mock 'sync' @sync.stubs(:synchronize).yields Puppet::Util.expects(:sync).with('/file').returns @sync File.expects(:exists?).with('/file').returns false File.expects(:open).with('/file').once Puppet::Util::FileLocking.readlock('/file') end end describe "when acquiring a write lock" do before do @sync = mock 'sync' Puppet::Util.stubs(:sync).returns @sync @sync.stubs(:synchronize).yields File.stubs(:file?).with('/file').returns true File.stubs(:exists?).with('/file').returns true end it "should fail if the parent directory does not exist" do FileTest.expects(:directory?).with("/my/dir").returns false File.stubs(:file?).with('/my/dir/file').returns true File.stubs(:exists?).with('/my/dir/file').returns true 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) + File.expects(:open).with("/file", File::Constants::CREAT | File::Constants::WRONLY , :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) + File.expects(:open).with("/file", File::Constants::CREAT | File::Constants::WRONLY, 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) + File.expects(:open).with("/file", File::Constants::CREAT | File::Constants::WRONLY, 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 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) + lfh.stubs(:seek) + lfh.stubs(:truncate) lfh.expects(:print).with "foo" Puppet::Util::FileLocking.writelock('/file') do |f| f.print "foo" end end + it "should truncate the file under an exclusive lock" do + fh = mock 'fh' + File.expects(:open).yields fh + + lfh = mock 'locked_filehandle' + fh.expects(:lock_exclusive).yields(lfh) + + lfh.expects(:seek).with(0, IO::SEEK_SET) + lfh.expects(:truncate).with(0) + lfh.stubs(:print) + + Puppet::Util::FileLocking.writelock('/file') do |f| + f.print "foo" + end + end + it "should only work on regular files" do File.expects(:file?).with('/file').returns false proc { Puppet::Util::FileLocking.writelock('/file') }.should raise_error(ArgumentError) end it "should create missing files" do @sync = mock 'sync' @sync.stubs(:synchronize).yields Puppet::Util.expects(:sync).with('/file').returns @sync File.expects(:exists?).with('/file').returns false - File.expects(:open).with('/file', 'w', 0600).once + File.expects(:open).with('/file', File::Constants::CREAT | File::Constants::WRONLY, 0600).once Puppet::Util::FileLocking.writelock('/file') end end end