views:

935

answers:

4

I'm new to Ruby, so I'm having some trouble understanding this weird exception problem I'm having. I'm using the ruby-aaws gem to access Amazon ECS: http://www.caliban.org/ruby/ruby-aws/. This defines a class Amazon::AWS:Error:

module Amazon
  module AWS
    # All dynamically generated exceptions occur within this namespace.
    #
    module Error
      # An exception generator class.
      #
      class AWSError
        attr_reader :exception

        def initialize(xml)
          err_class = xml.elements['Code'].text.sub( /^AWS.*\./, '' )
          err_msg = xml.elements['Message'].text

          unless Amazon::AWS::Error.const_defined?( err_class )
            Amazon::AWS::Error.const_set( err_class,
                    Class.new( StandardError ) )
          end

          ex_class = Amazon::AWS::Error.const_get( err_class )
          @exception = ex_class.new( err_msg )
        end
      end
    end
  end
end

This means that if you get an errorcode like AWS.InvalidParameterValue, this will produce (in its exception variable) a new class Amazon::AWS::Error::InvalidParameterValue which is a subclass of StandardError.

Now here's where it gets weird. I have some code that looks like this:

begin
  do_aws_stuff
rescue Amazon::AWS::Error => error
  puts "Got an AWS error"
end

Now, if do_aws_stuff throws a NameError, my rescue block gets triggered. It seems that Amazon::AWS::Error isn't the superclass of the generated error - I guess since it's a module everything is a subclass of it? Certainly if I do:

irb(main):007:0> NameError.new.kind_of?(Amazon::AWS::Error)
=> true

It says true, which I find confusing, especially given this:

irb(main):009:0> NameError.new.kind_of?(Amazon::AWS)
=> false

What's going on, and how am I supposed to separate out AWS errors from other type of errors? Should I do something like:

begin
  do_aws_stuff
rescue => error
  if error.class.to_s =~ /^Amazon::AWS::Error/
    puts "Got an AWS error"
  else
    raise error
  end
end

That seems exceptionally janky. The errors thrown aren't class AWSError either - they're raised like this:

error = Amazon::AWS::Error::AWSError.new( xml )
raise error.exception

So the exceptions I'm looking to rescue from are the generated exception types that only inherit from StandardError.

To clarify, I have two questions:

  1. Why is NameError, a Ruby built in exception, a kind_of?(Amazon::AWS::Error), which is a module?
    Answer: I had said include Amazon::AWS::Error at the top of my file, thinking it was kind of like a Java import or C++ include. What this actually did was add everything defined in Amazon::AWS::Error (present and future) to the implicit Kernel class, which is an ancestor of every class. This means anything would pass kind_of?(Amazon::AWS::Error).

  2. How can I best distinguish the dynamically-created exceptions in Amazon::AWS::Error from random other exceptions from elsewhere?

A: 

Well, from what I can tell:

Class.new( StandardError )

Is creating a new class with StandardError as the base class, so it is not going to be a Amazon::AWS::Error at all. It is just defined in that module, which is probably why it is a kind_of? Amazon::AWS::Error. It probably isn't a kind_of? Amazon::AWS because maybe modules don't nest for purposes of kind_of? ?

Sorry, I don't know modules very well in Ruby, but most definitely the base class is going to be StandardError.

UPDATE: By the way, from the ruby docs:

obj.kind_of?(class) => true or false

Returns true if class is the class of obj, or if class is one of the superclasses of obj or modules included in obj.

Mike Stone
Yeah, I know the base class is `StandardError`. What drives me nuts is that `NameError.new.kind_of?(Amazon::AWS::Error)` is true. Why would that be?
BRH
Check out my update... that probably explains it ;-)
Mike Stone
No, I'm not convinced. ;-) Amazon::AWS::Error is a module. It's not the same class as NameError. It's not a superclass of NameError. And NameError doesn't, to my knowledge, include the Amazon::AWS::Error module.
BRH
but it is being created within that module, so that may implicitly include the module
Mike Stone
No, [NameError](http://ruby-doc.org/core-1.8.7/classes/NameError.html) is a built-in Ruby exception, not created within Amazon::AWS::Error.
BRH
Hmmm, yeah I was a little confused there... plus I did some tests and couldn't reproduce the behavior... very weird... maybe some monkey patching somewhere else might be causing it? That or it's too late for me to see the obvious ;-)
Mike Stone
See my comment on Jean's answer... I think that is the correct answer (IE, definitely looks like monkey patching)
Mike Stone
+3  A: 

Ok, I'll try to help here :

First a module is not a class, it allows you to mix behaviour in a class. second see the following example :

module A
  module B
    module Error
      def foobar
        puts "foo"
      end
    end
  end
end

class StandardError
  include A::B::Error
end

StandardError.new.kind_of?(A::B::Error)
StandardError.new.kind_of?(A::B)
StandardError.included_modules #=> [A::B::Error,Kernel]

kind_of? tells you that yes, Error does possess All of A::B::Error behaviour (which is normal since it includes A::B::Error) however it does not include all the behaviour from A::B and therefore is not of the A::B kind. (duck typing)

Now there is a very good chance that ruby-aws reopens one of the superclass of NameError and includes Amazon::AWS:Error in there. (monkey patching)

You can find out programatically where the module is included in the hierarchy with the following :

class Class
  def has_module?(module_ref)
    if self.included_modules.include?(module_ref) and not self.superclass.included_modules.include?(module_ref)                      
        puts self.name+" has module "+ module_ref.name          
    else
      self.superclass.nil? ? false : self.superclass.has_module?(module_ref)
    end        
  end
end
StandardError.has_module?(A::B::Error)
NameError.has_module?(A::B::Error)

Regarding your second question I can't see anything better than

begin 
#do AWS error prone stuff
rescue Exception => e
  if Amazon::AWS::Error.constants.include?(e.class.name)
    #awsError
  else
    whatever
  end 
end

(edit -- above code doesn't work as is : name includes module prefix which is not the case of the constants arrays. You should definitely contact the lib maintainer the AWSError class looks more like a factory class to me :/ )

I don't have ruby-aws here and the caliban site is blocked by the company's firewall so I can't test much further.

Regarding the include : that might be the thing doing the monkey patching on the StandardError hierarchy. I am not sure anymore but most likely doing it at the root of a file outside every context is including the module on Object or on the Object metaclass. (this is what would happen in IRB, where the default context is Object, not sure about in a file)

from the pickaxe on modules :

A couple of points about the include statement before we go on. First, it has nothing to do with files. C programmers use a preprocessor directive called #include to insert the contents of one file into another during compilation. The Ruby include statement simply makes a reference to a named module. If that module is in a separate file, you must use require to drag that file in before using include.

(edit -- I can't seem to be able to comment using this browser :/ yay for locked in platforms)

Jean
I think you are right.... I tried the exact code presented in the question and it doesn't yield true... so there must be monkey patching going on....
Mike Stone
Hm, not exactly. So that AWS stuff is definitely in the heirarchy for *everything*. I suspect I screwed up by saying "include Amazon::AWS::Error" at the top of my file. How does that work? It just includes that module in the base of everything?
BRH
Unfortunately (as I just clarified in my question) the library is throwing the generated exception, not AWSError. So the exceptions come out looking like Amazon::AWS::Error::InvalidParameterValue.
BRH
Thanks for all the help. I'll mail the library author and ask if he can switch to using a common base class, and use your proposed code until then.
BRH
+1  A: 

Just wanted to chime in: I would agree this is a bug in the lib code. It should probably read:

      unless Amazon::AWS::Error.const_defined?( err_class )
        kls = Class.new( StandardError )
        Amazon::AWS::Error.const_set(err_class, kls)
        kls.include Amazon::AWS::Error
      end
0124816
Even better would be to have an AWSException class that was a subclass of StandardError, i.e Amazon::AWS::AWSException < StandardError. You could then use "kls = Class.new(Amazon::AWS::AWSException)" after which you could just check whether the error was an "Amazon::AWS::AWSException".
Peter Wagenet
A: 

One issue you're running into is that Amazon::AWS::Error::AWSError is not actually an exception. When raise is called, it looks to see if the first parameter responds to the exception method and will use the result of that instead. Anything that is a subclass of Exception will return itself when exception is called so you can do things like raise Exception.new("Something is wrong").

In this case, AWSError has exception set up as an attribute reader which it defines the value to on initialization to something like Amazon::AWS::Error::SOME_ERROR. This means that when you call raise Amazon::AWS::Error::AWSError.new(SOME_XML) Ruby ends up calling Amazon::AWS::Error::AWSError.new(SOME_XML).exception which will returns an instance of Amazon::AWS::Error::SOME_ERROR. As was pointed out by one of the other responders, this class is a direct subclass of StandardError instead of being a subclass of a common Amazon error. Until this is rectified, Jean's solution is probably your best bet.

I hope that helped explain more of what's actually going on behind the scenes.

Peter Wagenet