From ba7386d21e68333c0257277ad74206ade65fbfce Mon Sep 17 00:00:00 2001
From: Victor Lowther <victor.lowther@gmail.com>
Date: Fri, 3 Aug 2012 10:31:11 -0500
Subject: [PATCH 1/6] Ensure that only one chef-client run can happen at a
 time.

This patch wraps do_run in a locking scheme that provides the
following semantics:

 * Only one chef-client run can happen at a time.
 * Only one chef-client can be queued up to run after the run that is
   currently in progress finishes.
 * Any additional chef-client run requests will wait for the queued
   request to finish and then exit.
---
 chef/lib/chef/client.rb | 66 ++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 57 insertions(+), 9 deletions(-)

Index: lib/chef/client.rb
===================================================================
--- lib/chef/client.rb.orig	2014-10-21 11:24:50.532126967 +0200
+++ lib/chef/client.rb	2014-10-21 11:24:59.098126952 +0200
@@ -41,6 +41,7 @@ require 'chef/formatters/doc'
 require 'chef/formatters/minimal'
 require 'chef/version'
 require 'chef/resource_reporter'
+require 'chef/run_lock'
 require 'ohai'
 require 'rbconfig'
 
@@ -385,7 +386,7 @@ class Chef
       runner.converge
       @events.converge_complete
       true
-    rescue Exception => e
+    rescue Exception
       # TODO: should this be a separate #converge_failed(exception) method?
       @events.converge_complete
       raise
@@ -404,8 +405,10 @@ class Chef
     # === Returns
     # true:: Always returns true.
     def do_run
-      run_context = nil
+      runlock = RunLock.new(Chef::Config)
+      runlock.acquire
 
+      run_context = nil
       @events.run_start(Chef::VERSION)
       Chef::Log.info("*** Chef #{Chef::VERSION} ***")
       enforce_path_sanity
@@ -445,8 +448,9 @@ class Chef
         @events.run_failed(e)
         raise
       ensure
-        run_status = nil
+        @run_status = nil
         run_context = nil
+        runlock.release
         GC.start
       end
       true
Index: spec/functional/run_lock_spec.rb
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ spec/functional/run_lock_spec.rb	2014-10-21 11:24:59.099126952 +0200
@@ -0,0 +1,88 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require File.expand_path('../../spec_helper', __FILE__)
+require 'chef/client'
+
+describe Chef::RunLock do
+
+  # This behavior is believed to work on windows, but the tests use UNIX APIs.
+  describe "when locking the chef-client run", :unix_only => true do
+    it "allows only one chef client run per lockfile" do
+      read, write = IO.pipe
+      run_lock = Chef::RunLock.new(:file_cache_path => "/var/chef/cache", :lockfile => "/tmp/chef-client-running.pid")
+      p1 = fork do
+        run_lock.acquire
+        write.puts 1
+        #puts "[#{Time.new.to_i % 100}] p1 (#{Process.pid}) running with lock"
+        sleep 2
+        write.puts 2
+        #puts "[#{Time.new.to_i % 100}] p1 (#{Process.pid}) releasing lock"
+        run_lock.release
+      end
+
+      p2 = fork do
+        run_lock.acquire
+        write.puts 3
+        #puts "[#{Time.new.to_i % 100}] p2 (#{Process.pid}) running with lock"
+        run_lock.release
+      end
+
+      Process.waitpid2(p1)
+      Process.waitpid2(p2)
+
+      write.close
+      order = read.read
+      read.close
+
+      order.should == "1\n2\n3\n"
+    end
+
+    it "clears the lock if the process dies unexpectedly" do
+      read, write = IO.pipe
+      run_lock = Chef::RunLock.new(:file_cache_path => "/var/chef/cache", :lockfile => "/tmp/chef-client-running.pid")
+      p1 = fork do
+        run_lock.acquire
+        write.puts 1
+        #puts "[#{Time.new.to_i % 100}] p1 (#{Process.pid}) running with lock"
+        sleep 1
+        write.puts 2
+        #puts "[#{Time.new.to_i % 100}] p1 (#{Process.pid}) releasing lock"
+        run_lock.release
+      end
+
+      p2 = fork do
+        run_lock.acquire
+        write.puts 3
+        #puts "[#{Time.new.to_i % 100}] p2 (#{Process.pid}) running with lock"
+        run_lock.release
+      end
+      Process.kill(:KILL, p1)
+
+      Process.waitpid2(p1)
+      Process.waitpid2(p2)
+
+      write.close
+      order = read.read
+      read.close
+
+      order.should =~ /3\Z/
+    end
+  end
+
+end
+
Index: spec/unit/client_spec.rb
===================================================================
--- spec/unit/client_spec.rb.orig	2014-10-21 11:24:50.584126967 +0200
+++ spec/unit/client_spec.rb	2014-10-21 11:24:59.099126952 +0200
@@ -124,6 +124,9 @@ shared_examples_for Chef::Client do
       Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(mock_chef_rest_for_node_save)
       mock_chef_rest_for_node_save.should_receive(:put_rest).with("nodes/#{@fqdn}", @node).and_return(true)
 
+      Chef::RunLock.any_instance.should_receive(:acquire)
+      Chef::RunLock.any_instance.should_receive(:release)
+
       # Post conditions: check that node has been filled in correctly
       @client.should_receive(:run_started)
       @client.should_receive(:run_completed_successfully)
Index: spec/unit/run_lock_spec.rb
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ spec/unit/run_lock_spec.rb	2014-10-21 11:24:59.099126952 +0200
@@ -0,0 +1,37 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require File.expand_path('../../spec_helper', __FILE__)
+require 'chef/client'
+
+describe Chef::RunLock do
+
+  describe "when first created" do
+    it "locates the lockfile in the file cache path by default" do
+      run_lock = Chef::RunLock.new(:file_cache_path => "/var/chef/cache", :lockfile => nil)
+      run_lock.runlock_file.should == "/var/chef/cache/chef-client-running.pid"
+    end
+
+    it "locates the lockfile in the user-configured path when set" do
+      run_lock = Chef::RunLock.new(:file_cache_path => "/var/chef/cache", :lockfile => "/tmp/chef-client-running.pid")
+      run_lock.runlock_file.should == "/tmp/chef-client-running.pid"
+    end
+  end
+
+  # See also: spec/functional/run_lock_spec
+
+end
Index: lib/chef/run_lock.rb
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ lib/chef/run_lock.rb	2014-10-21 11:24:59.100126952 +0200
@@ -0,0 +1,61 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+  class RunLock
+    attr_reader :runlock
+    attr_reader :runlock_file
+
+    def initialize(config)
+      @runlock_file = config[:lockfile] || "#{config[:file_cache_path]}/chef-client-running.pid"
+      @runlock = nil
+    end
+
+    def acquire
+      @runlock = File.open(runlock_file,'w+')
+      unless runlock.flock(File::LOCK_EX|File::LOCK_NB)
+        # Another chef client running...
+        runpid = runlock.read.strip.chomp
+        Chef::Log.info("Chef client #{runpid} is running, will wait for it to finish and then run.")
+        runlock.flock(File::LOCK_EX)
+      end
+      # We grabbed the run lock.  Save the pid.
+      runlock.truncate(0)
+      runlock.rewind # truncate doesn't reset position to 0.
+      runlock.write(Process.pid.to_s)
+    end
+
+    def release
+      if runlock
+        runlock.flock(File::LOCK_UN)
+        runlock.close
+        # Don't unlink the pid file, if another chef-client was waiting, it
+        # won't be recreated. Better to leave a "dead" pid file than not have
+        # it available if you need to break the lock.
+        reset
+      end
+    end
+
+    private
+
+    def reset
+      @runlock = nil
+    end
+
+  end
+end
+
Index: lib/chef/config.rb
===================================================================
--- lib/chef/config.rb.orig	2014-10-21 11:24:50.328126967 +0200
+++ lib/chef/config.rb	2014-10-21 11:24:59.099126952 +0200
@@ -158,6 +158,15 @@ class Chef
     # Where chef's cache files should be stored
     file_cache_path platform_specific_path("/var/chef/cache")
 
+    # By default, chef-client (or solo) creates a lockfile in
+    # `file_cache_path`/chef-client-running.pid
+    # If `lockfile` is explicitly set, this path will be used instead.
+    #
+    # If your `file_cache_path` resides on a NFS (or non-flock()-supporting
+    # fs), it's recommended to set this to something like
+    # '/tmp/chef-client-running.pid'
+    lockfile nil
+
     # Where backups of chef-managed files should go
     file_backup_path platform_specific_path("/var/chef/backup")
 
