summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/puppet/event_manager.rb182
-rwxr-xr-xspec/unit/event_manager.rb266
2 files changed, 448 insertions, 0 deletions
diff --git a/lib/puppet/event_manager.rb b/lib/puppet/event_manager.rb
new file mode 100644
index 000000000..50c489673
--- /dev/null
+++ b/lib/puppet/event_manager.rb
@@ -0,0 +1,182 @@
+require 'puppet/external/event-loop'
+
+# Manage events related to starting, stopping, and restarting
+# contained services.
+class Puppet::EventManager
+ include SignalObserver
+
+ attr_reader :services, :threads, :timers
+
+ def initialize
+ @services = []
+ @threads = []
+ @timers = []
+ end
+
+ # Create a new service that we're supposed to run
+ def add_service(service)
+ @services << service
+ end
+
+ def newthread(&block)
+ @threads << Thread.new { yield }
+ end
+
+ # Add a timer we need to pay attention to.
+ # This is only used by Puppet::Agent at the moment.
+ def newtimer(hash, &block)
+ timer = EventLoop::Timer.new(hash)
+ @timers << timer
+
+ if block_given?
+ observe_signal(timer, :alarm, &block)
+ end
+
+ # In case they need it for something else.
+ timer
+ end
+
+ # Reload any services that can be reloaded. Really, this is just
+ # meant to trigger an Agent run.
+ def reload
+ done = 0
+ services.find_all { |service| service.respond_to?(:run) }.each do |service|
+ if service.running?
+ Puppet.notice "Not triggering already-running %s" % service.class
+ next
+ end
+
+ Puppet.notice "Triggering a run of %s" % service.class
+
+ done += 1
+ begin
+ service.run
+ rescue => detail
+ Puppet.err "Could not run service %s: %s" % [service.class, detail]
+ end
+ end
+
+ unless done > 0
+ Puppet.notice "No services were reloaded"
+ end
+ end
+
+ def reopen_logs
+ Puppet::Util::Log.reopen
+ end
+
+ # Relaunch the executable.
+ def restart
+ if client = @services.find { |s| s.is_a? Puppet::Network::Client.master } and client.running?
+ client.restart
+ else
+ command = $0 + " " + self.args.join(" ")
+ Puppet.notice "Restarting with '%s'" % command
+ Puppet.shutdown(false)
+ Puppet::Util::Log.reopen
+ exec(command)
+ end
+ end
+
+ # Trap a couple of the main signals. This should probably be handled
+ # in a way that anyone else can register callbacks for traps, but, eh.
+ def set_traps
+ {:INT => :shutdown, :TERM => :shutdown, :HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs}.each do |signal, method|
+ trap(signal) do
+ Puppet.notice "Caught #{signal}; calling #{method}"
+ send(method)
+ end
+ end
+ end
+
+ # Shutdown our server process, meaning stop all services and all threads.
+ # Optionally, exit.
+ def shutdown(leave = true)
+ Puppet.notice "Shutting down"
+ stop_timers
+
+ stop_services
+
+ stop_threads
+
+ if leave
+ exit(0)
+ end
+ end
+
+ # Start all of our services and optionally our event loop, which blocks,
+ # waiting for someone, somewhere, to generate events of some kind.
+ def start
+ start_services
+
+ start_timers
+
+ EventLoop.current.run
+ end
+
+ def start_services
+ # Starting everything in its own thread. Otherwise
+ # we might have one service stop another service from
+ # doing things like registering timers.
+ @services.dup.each do |svc|
+ begin
+ svc.start
+ rescue => detail
+ if Puppet[:trace]
+ puts detail.backtrace
+ end
+ @services.delete svc
+ Puppet.err "Could not start %s: %s" % [svc.class, detail]
+ end
+ end
+
+ # We need to give the services a chance to register their timers before
+ # we try to start monitoring them.
+ sleep 0.5
+
+ unless @services.length > 0
+ Puppet.notice "No remaining services; exiting"
+ exit(1)
+ end
+ end
+
+ def stop_services
+ # Stop our services
+ services.each do |svc|
+ begin
+ timeout(20) do
+ svc.shutdown
+ end
+ rescue TimeoutError
+ Puppet.err "%s could not shut down within 20 seconds" % svc.class
+ end
+ end
+ end
+
+ # Monitor all of the timers that have been set up.
+ def start_timers
+ timers.each do |timer|
+ EventLoop.current.monitor_timer timer
+ end
+ end
+
+ def stop_timers
+ # Unmonitor our timers
+ timers.each do |timer|
+ EventLoop.current.ignore_timer timer
+ end
+ end
+
+ def stop_threads
+ # And wait for them all to die, giving a decent amount of time
+ threads.each do |thr|
+ begin
+ timeout(20) do
+ thr.join
+ end
+ rescue TimeoutError
+ # Just ignore this, since we can't intelligently provide a warning
+ end
+ end
+ end
+end
diff --git a/spec/unit/event_manager.rb b/spec/unit/event_manager.rb
new file mode 100755
index 000000000..e15461c98
--- /dev/null
+++ b/spec/unit/event_manager.rb
@@ -0,0 +1,266 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+require 'puppet/event_manager'
+require 'puppet/agent'
+
+describe Puppet::EventManager do
+ before do
+ @manager = Puppet::EventManager.new
+ end
+
+ it "should include SignalObserver" do
+ Puppet::EventManager.ancestors.should be_include(SignalObserver)
+ end
+
+ it "should should add the provided service to its list of services when a new service is added" do
+ @manager.add_service("foo")
+ @manager.services.should be_include("foo")
+ end
+
+ it "should create a new thread and add it to its thread list when a new thread is added" do
+ Thread.expects(:new).returns "foo"
+ @manager.newthread {}
+ @manager.threads.should be_include("foo")
+ end
+
+ it "should stop all timers, services, and threads, then exit, when asked to shutdown" do
+ @manager.expects(:stop_services)
+ @manager.expects(:stop_timers)
+ @manager.expects(:stop_threads)
+
+ @manager.expects(:exit)
+
+ @manager.shutdown
+ end
+
+ it "should tell the event loop to monitor each timer when told to start timers" do
+ timer1 = mock 'timer1'
+ timer2 = mock 'timer2'
+
+ @manager.expects(:timers).returns [timer1, timer2]
+
+ EventLoop.current.expects(:monitor_timer).with timer1
+ EventLoop.current.expects(:monitor_timer).with timer2
+
+ @manager.start_timers
+ end
+
+ it "should tell the event loop to stop monitoring each timer when told to stop timers" do
+ timer1 = mock 'timer1'
+ timer2 = mock 'timer2'
+
+ @manager.expects(:timers).returns [timer1, timer2]
+
+ EventLoop.current.expects(:ignore_timer).with timer1
+ EventLoop.current.expects(:ignore_timer).with timer2
+
+ @manager.stop_timers
+ end
+
+ it "should start all services, monitor all timers, and let the current event loop run when told to start" do
+ @manager.expects(:start_services)
+ @manager.expects(:start_timers)
+
+ EventLoop.current.expects(:run)
+
+ @manager.start
+ end
+
+ it "should reopen the Log logs when told to reopen logs" do
+ Puppet::Util::Log.expects(:reopen)
+ @manager.reopen_logs
+ end
+
+ describe "when adding a timer" do
+ before do
+ @timer = mock("timer")
+ EventLoop::Timer.stubs(:new).returns @timer
+
+ @manager.stubs(:observe_signal)
+ end
+
+ it "should create and return a new timer with the provided arguments" do
+ timer = mock("timer")
+ EventLoop::Timer.expects(:new).with(:foo => :bar).returns @timer
+
+ @manager.newtimer(:foo => :bar) {}.should equal(@timer)
+ end
+
+ it "should add the timer to the list of timers" do
+ @manager.newtimer(:foo => :bar) {}
+
+ @manager.timers.should be_include(@timer)
+ end
+
+ it "should set up a signal observer for the timer" do
+ @manager.expects(:observe_signal).with { |timer, signal, block| timer == @timer and signal == :alarm }
+
+ @manager.newtimer(:foo => :bar) {}
+ end
+ end
+
+ describe "when starting services" do
+ before do
+ @service = stub 'service', :start => nil
+ @manager.stubs(:sleep)
+ end
+
+ it "should start each service" do
+ service = mock 'service'
+ service.expects(:start)
+
+ @manager.add_service service
+
+ @manager.start_services
+ end
+
+ it "should not fail if a service fails to start" do
+ service = mock 'service'
+ service.expects(:start).raises "eh"
+
+ @manager.add_service @service
+ @manager.add_service service
+
+ lambda { @manager.start_services }.should_not raise_error
+ end
+
+ it "should delete failed services from its service list" do
+ service = mock 'service'
+ service.expects(:start).raises "eh"
+
+ @manager.add_service @service
+ @manager.add_service service
+
+ @manager.start_services
+
+ @manager.services.should_not be_include(service)
+ end
+
+# it "should start each service in a separate thread" do
+# # They don't expect 'start', because we're stubbing 'newthread'
+# service1 = mock 'service1'
+# service2 = mock 'service2'
+#
+# @manager.add_service service1
+# @manager.add_service service2
+#
+# @manager.expects(:newthread).times(2)
+#
+# @manager.start_services
+# end
+
+ it "should exit if no services were able to be started" do
+ service = mock 'service'
+ service.expects(:start).raises "eh"
+
+ @manager.add_service service
+
+ @manager.expects(:exit).with(1)
+
+ lambda { @manager.start_services }.should_not raise_error
+ end
+ end
+
+ describe "when stopping services" do
+ it "should use a timeout" do
+ @manager.expects(:timeout).with(20)
+ @manager.expects(:services).returns %w{foo}
+
+ @manager.stop_services
+ end
+
+ it "should stop each service" do
+ service = mock 'service'
+ service.expects(:shutdown)
+ @manager.expects(:services).returns [service]
+
+ @manager.stop_services
+ end
+
+ it "should log if a timeout is encountered" do
+ service = mock 'service'
+ service.expects(:shutdown).raises(TimeoutError)
+ @manager.expects(:services).returns [service]
+
+ Puppet.expects(:err)
+
+ @manager.stop_services
+ end
+ end
+
+ describe "when stopping threads" do
+ it "should use a timeout" do
+ @manager.expects(:timeout).with(20)
+ @manager.expects(:threads).returns %w{foo}
+
+ @manager.stop_threads
+ end
+
+ it "should join each thread" do
+ thread = mock 'thread'
+ thread.expects(:join)
+ @manager.expects(:threads).returns [thread]
+
+ @manager.stop_threads
+ end
+
+ it "should not fail if a timeout is encountered" do
+ thread = mock 'thread'
+ thread.expects(:join).raises(TimeoutError)
+ @manager.expects(:threads).returns [thread]
+
+ @manager.stop_threads
+ end
+ end
+
+ describe "when setting traps" do
+ before do
+ @manager.stubs(:trap)
+ end
+
+ {:INT => :shutdown, :TERM => :shutdown, :HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs}.each do |signal, method|
+ it "should log and call #{method} when it receives #{signal}" do
+ @manager.expects(:trap).with(signal).yields
+
+ Puppet.expects(:notice)
+
+ @manager.expects(method)
+
+ @manager.set_traps
+ end
+ end
+ end
+
+ describe "when reloading" do
+ it "should run all services that can be run but are not currently running" do
+ service = Puppet::Agent.new(String)
+
+ @manager.add_service service
+
+ service.expects(:running?).returns false
+ service.expects(:run)
+
+ @manager.reload
+ end
+
+ it "should not run services that are already running" do
+ service = Puppet::Agent.new(String)
+
+ @manager.add_service service
+
+ service.expects(:running?).returns true
+ service.expects(:run).never
+
+ @manager.reload
+ end
+
+ it "should not try to run services that cannot be run" do
+ service = "string"
+ @manager.add_service service
+
+ @manager.reload
+ end
+ end
+end