diff options
author | Luke Kanies <luke@madstop.com> | 2009-02-11 16:49:40 -0600 |
---|---|---|
committer | James Turnbull <james@lovedthanlost.net> | 2009-02-12 17:08:48 +1100 |
commit | af3f3ae93ba1e43ac8231e4cf4e5a1f0ed8f3acb (patch) | |
tree | 61167f4c608377aefc5af1a5a3d057fa6f1b3f98 | |
parent | f0ac3aef53a08e271a5c243f17785cdb58f1f5ef (diff) | |
download | puppet-af3f3ae93ba1e43ac8231e4cf4e5a1f0ed8f3acb.tar.gz puppet-af3f3ae93ba1e43ac8231e4cf4e5a1f0ed8f3acb.tar.xz puppet-af3f3ae93ba1e43ac8231e4cf4e5a1f0ed8f3acb.zip |
Refactoring the XMLRPC::Client error-handling
I split it all into smaller, manageable chunks,
and used methods for each step, instead
of having one huge call.
Note that I made all of the tests first, then
refactored the code, so I'm confident there's no
behavior change.
I don't know that this is actually a lot cleaner,
but it seems that way to me. I'm open to
skipping this, but I think it makes the whole thing
a lot cleaner.
Signed-off-by: Luke Kanies <luke@madstop.com>
-rw-r--r-- | lib/puppet/network/xmlrpc/client.rb | 164 | ||||
-rwxr-xr-x | spec/unit/network/xmlrpc/client.rb | 150 |
2 files changed, 254 insertions, 60 deletions
diff --git a/lib/puppet/network/xmlrpc/client.rb b/lib/puppet/network/xmlrpc/client.rb index 37ace2101..91bb03e1f 100644 --- a/lib/puppet/network/xmlrpc/client.rb +++ b/lib/puppet/network/xmlrpc/client.rb @@ -36,57 +36,7 @@ module Puppet::Network interface.methods.each { |ary| method = ary[0] newclient.send(:define_method,method) { |*args| - Puppet.debug "Calling %s.%s" % [namespace, method] - begin - call("%s.%s" % [namespace, method.to_s],*args) - rescue OpenSSL::SSL::SSLError => detail - if detail.message =~ /bad write retry/ - Puppet.warning "Transient SSL write error; restarting connection and retrying" - self.recycle_connection - retry - end - ["certificate verify failed", "hostname was not match", "hostname not match"].each do |str| - if detail.message.include?(str) - Puppet.warning "Certificate validation failed; consider using the certname configuration option" - end - end - raise XMLRPCClientError, - "Certificates were not trusted: %s" % detail - rescue ::XMLRPC::FaultException => detail - raise XMLRPCClientError, detail.faultString - rescue Errno::ECONNREFUSED => detail - msg = "Could not connect to %s on port %s" % - [@host, @port] - raise XMLRPCClientError, msg - rescue SocketError => detail - Puppet.err "Could not find server %s: %s" % - [@host, detail.to_s] - error = XMLRPCClientError.new( - "Could not find server %s" % @host - ) - error.set_backtrace detail.backtrace - raise error - rescue Errno::EPIPE, EOFError - Puppet.warning "Other end went away; restarting connection and retrying" - self.recycle_connection - retry - rescue Timeout::Error => detail - Puppet.err "Connection timeout calling %s.%s: %s" % - [namespace, method, detail.to_s] - error = XMLRPCClientError.new("Connection Timeout") - error.set_backtrace(detail.backtrace) - raise error - rescue => detail - if detail.message =~ /^Wrong size\. Was \d+, should be \d+$/ - Puppet.warning "XMLRPC returned wrong size. Retrying." - retry - end - Puppet.err "Could not call %s.%s: %s" % - [namespace, method, detail.inspect] - error = XMLRPCClientError.new(detail.to_s) - error.set_backtrace detail.backtrace - raise error - end + make_rpc_call(namespace, method, *args) } } @@ -97,13 +47,117 @@ module Puppet::Network @clients[handler] || self.mkclient(handler) end + class ErrorHandler + def initialize(&block) + metaclass.define_method(:execute, &block) + end + end + + # Use a class variable so all subclasses have access to it. + @@error_handlers = {} + + def self.error_handler(exception) + if handler = @@error_handlers[exception.class] + return handler + else + return @@error_handlers[:default] + end + end + + def self.handle_error(*exceptions, &block) + handler = ErrorHandler.new(&block) + + exceptions.each do |exception| + @@error_handlers[exception] = handler + end + end + + handle_error(OpenSSL::SSL::SSLError) do |client, detail, namespace, method| + if detail.message =~ /bad write retry/ + Puppet.warning "Transient SSL write error; restarting connection and retrying" + client.recycle_connection + return :retry + end + ["certificate verify failed", "hostname was not match", "hostname not match"].each do |str| + if detail.message.include?(str) + Puppet.warning "Certificate validation failed; consider using the certname configuration option" + end + end + raise XMLRPCClientError, "Certificates were not trusted: %s" % detail + end + + handle_error(:default) do |client, detail, namespace, method| + if detail.message.to_s =~ /^Wrong size\. Was \d+, should be \d+$/ + Puppet.warning "XMLRPC returned wrong size. Retrying." + return :retry + end + Puppet.err "Could not call %s.%s: %s" % [namespace, method, detail.inspect] + error = XMLRPCClientError.new(detail.to_s) + error.set_backtrace detail.backtrace + raise error + end + + handle_error(OpenSSL::SSL::SSLError) do |client, detail, namespace, method| + if detail.message =~ /bad write retry/ + Puppet.warning "Transient SSL write error; restarting connection and retrying" + client.recycle_connection + return :retry + end + ["certificate verify failed", "hostname was not match", "hostname not match"].each do |str| + if detail.message.include?(str) + Puppet.warning "Certificate validation failed; consider using the certname configuration option" + end + end + raise XMLRPCClientError, "Certificates were not trusted: %s" % detail + end + + handle_error(::XMLRPC::FaultException) do |client, detail, namespace, method| + raise XMLRPCClientError, detail.faultString + end + + handle_error(Errno::ECONNREFUSED) do |client, detail, namespace, method| + msg = "Could not connect to %s on port %s" % [client.host, client.port] + raise XMLRPCClientError, msg + end + + handle_error(SocketError) do |client, detail, namespace, method| + Puppet.err "Could not find server %s: %s" % [@host, detail.to_s] + error = XMLRPCClientError.new("Could not find server %s" % client.host) + error.set_backtrace detail.backtrace + raise error + end + + handle_error(Errno::EPIPE, EOFError) do |client, detail, namespace, method| + Puppet.warning "Other end went away; restarting connection and retrying" + client.recycle_connection + return :retry + end + + handle_error(Timeout::Error) do |client, detail, namespace, method| + Puppet.err "Connection timeout calling %s.%s: %s" % [namespace, method, detail.to_s] + error = XMLRPCClientError.new("Connection Timeout") + error.set_backtrace(detail.backtrace) + raise error + end + + def make_rpc_call(namespace, method, *args) + Puppet.debug "Calling %s.%s" % [namespace, method] + begin + call("%s.%s" % [namespace, method.to_s],*args) + rescue Exception => detail + retry if self.class.error_handler(detail).execute(self, detail, namespace, method) == :retry + end + end + def http unless @http - @http = Puppet::Network::HttpPool.http_instance(@host, @port, true) + @http = Puppet::Network::HttpPool.http_instance(host, port, true) end @http end + attr_reader :host, :port + def initialize(hash = {}) hash[:Path] ||= "/RPC2" hash[:Server] ||= Puppet[:server] @@ -135,7 +189,11 @@ module Puppet::Network # or we've just downloaded certs and we need to create new http instances # with the certs added. def recycle_connection - @http = Puppet::Network::HttpPool.http_instance(@host, @port, true) # reset the instance + if http.started? + http.finish + end + @http = nil + self.http # force a new one end def start diff --git a/spec/unit/network/xmlrpc/client.rb b/spec/unit/network/xmlrpc/client.rb index a0a2e77fb..76ef5c799 100755 --- a/spec/unit/network/xmlrpc/client.rb +++ b/spec/unit/network/xmlrpc/client.rb @@ -2,12 +2,148 @@ Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } -describe Puppet::Network do - it "should raise an XMLRPCClientError if a generated class raises a Timeout::Error" do - http = mock 'http' - Puppet::Network::HttpPool.stubs(:http_instance).returns http - file = Puppet::Network::Client.file.new({:Server => "foo.com"}) - http.stubs(:post2).raises Timeout::Error - lambda { file.retrieve }.should raise_error(Puppet::Network::XMLRPCClientError) +describe Puppet::Network::XMLRPCClient do + describe "when performing the rpc call" do + before do + @client = Puppet::Network::Client.report.xmlrpc_client.new + @client.stubs(:call).returns "foo" + + end + + it "should call the specified namespace and method, with the specified arguments" do + @client.expects(:call).with("puppetreports.report", "eh").returns "foo" + @client.report("eh") + end + + it "should return the results from the call" do + @client.expects(:call).returns "foo" + @client.report("eh").should == "foo" + end + + describe "when returning the http instance" do + it "should use the http pool to create the instance" do + @client.instance_variable_set("@http", nil) + @client.expects(:host).returns "myhost" + @client.expects(:port).returns "myport" + Puppet::Network::HttpPool.expects(:http_instance).with("myhost", "myport", true).returns "http" + + @client.http.should == "http" + end + + it "should reuse existing instances" do + @client.http.should equal(@client.http) + end + end + + describe "when recycling the connection" do + it "should close the existing instance if it's open" do + http = mock 'http' + @client.stubs(:http).returns http + + http.expects(:started?).returns true + http.expects(:finish) + + @client.recycle_connection + end + + it "should force creation of a new instance" do + Puppet::Network::HttpPool.expects(:http_instance).returns "second_http" + + @client.recycle_connection + + @client.http.should == "second_http" + end + end + + describe "and an exception is raised" do + it "should raise XMLRPCClientError if XMLRPC::FaultException is raised" do + error = XMLRPC::FaultException.new("foo", "bar") + + @client.expects(:call).raises(error) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should raise XMLRPCClientError if Errno::ECONNREFUSED is raised" do + @client.expects(:call).raises(Errno::ECONNREFUSED) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log and raise XMLRPCClientError if Timeout::Error is raised" do + Puppet.expects(:err) + @client.expects(:call).raises(Timeout::Error) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log and raise XMLRPCClientError if SocketError is raised" do + Puppet.expects(:err) + @client.expects(:call).raises(SocketError) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log, recycle the connection, and retry if Errno::EPIPE is raised" do + @client.expects(:call).times(2).raises(Errno::EPIPE).then.returns "eh" + + Puppet.expects(:warning) + @client.expects(:recycle_connection) + + @client.report("eh") + end + + it "should log, recycle the connection, and retry if EOFError is raised" do + @client.expects(:call).times(2).raises(EOFError).then.returns "eh" + + Puppet.expects(:warning) + @client.expects(:recycle_connection) + + @client.report("eh") + end + + it "should log and retry if an exception containing 'Wrong size' is raised" do + error = RuntimeError.new("Wrong size. Was 15, should be 30") + @client.expects(:call).times(2).raises(error).then.returns "eh" + + Puppet.expects(:warning) + + @client.report("eh") + end + + it "should raise XMLRPCClientError if OpenSSL::SSL::SSLError is raised" do + @client.expects(:call).raises(OpenSSL::SSL::SSLError) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log and raise XMLRPCClientError if OpenSSL::SSL::SSLError is raised with certificate issues" do + error = OpenSSL::SSL::SSLError.new("hostname was not match") + @client.expects(:call).raises(error) + + Puppet.expects(:warning) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + + it "should log, recycle the connection, and retry if OpenSSL::SSL::SSLError is raised containing 'bad write retry'" do + error = OpenSSL::SSL::SSLError.new("bad write retry") + @client.expects(:call).times(2).raises(error).then.returns "eh" + + @client.expects(:recycle_connection) + + Puppet.expects(:warning) + + @client.report("eh") + end + + it "should log and raise XMLRPCClientError if any other exception is raised" do + @client.expects(:call).raises(RuntimeError) + + Puppet.expects(:err) + + lambda { @client.report("eh") }.should raise_error(Puppet::Network::XMLRPCClientError) + end + end end end |