diff options
-rw-r--r-- | lib/puppet/event_manager.rb | 182 | ||||
-rwxr-xr-x | spec/unit/event_manager.rb | 266 |
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 |