[rspec-users] Specifying a Celluloid actor is still alive without a negative error expectation

Ash Moran ash.moran at patchspace.co.uk
Tue Aug 13 10:58:05 UTC 2013


Hi all

I've recently become aware that  `expect { … }.to_not raise_error(SomeError)` has been deprecated. I've found a few cases where this has been frustrating, but I've hit one I'm completely stumped by. 

I'm writing a Celluloid actor which invokes some user-provided procs with (separately) user-provided values. As an example of something that might go wrong, the object providing a value may provide the wrong number of arguments for a given callback, and so an ArgumentError will be raised. Here's a cut-down version of the code:

    class Result
      include Celluloid

      # … lots of stuff omitted …

      def on(handlers)
        # … more stuff omitted …
        handlers.fetch(@message_type).call(*@message_args)
      end
    end

Normally, if an error is raised in an actor method, that actor is killed. As you can see, `call` is wide open to an ArgumentError. However, as this is the client object's fault, not ours, I don't want the actor to crash. Celluloid provides an `abort` method[1] for this purpose. My problem is in specifying the behaviour. Naively, you might try to check if the actor is still alive:

    it "doesn't crash the actor" do
      expect {
        actor.method_that_raises_an_error
      }.to_not change { actor.alive? }.to(false)
    end

Unfortunately, as the actor is running asynchronously, the example checks the actor's alive state before it completely dies. One way to approach this is to use Celluloid's actor guarantees and send it another message, which will be handled synchronously after the first is processed, and will raise a DeadActorError:

    it "doesn't crash the actor" do
      actor.method_that_raises_an_error rescue nil

      expect {
        actor.method_that_raises_an_error
      }.to_not raise_error(Celluloid::DeadActorError)
    end

Unfortunately, this prints a deprecation warning into the spec output, and presumably this will actually fail in a future version of RSpec.

Now, it's obvious from the code that changing the expectation to just `to_not raise_error()` is wrong, as we are expecting the method to raise an error – we just want to make sure it's not a Celluloid::DeadActorError. I could rescue specific errors inside the expect block, but now I'm duplicating knowledge from other examples just to make this one run.

So another way to approach this is my waiting on the actor's thread. Again, naively this is a starting point:

    it "doesn't crash the actor" do
      expect {
        actor.method_that_raises_an_error rescue nil
        Celluloid::Actor.join(actor)
      }.to_not change { actor.alive? }.to(false)
    end

There are two problems with this. The first is that it sets up a race condition, because any delay in the RSpec thread, eg:

    it "doesn't crash the actor" do
      expect {
        actor.method_that_raises_an_error rescue nil
        sleep 0.1
        Celluloid::Actor.join(actor)
      }.to_not change { actor.alive? }.to(false)
    end

Produces this error in Actor.join:

    An error occurred in an after hook
      Celluloid::DeadActorError: actor already terminated

Since the purpose of Celluloid / actors is to avoid this sort of problem, I don't consider it an acceptable approach.

The second (much bigger) problem, is that it only helps describe the failure case, once the actor is fixed to use `abort` rather than `raise`, it doesn't crash, and so Actor.join blocks the RSpec thread indefinitely.

The only option remaining I can think of is to re-implement the negative error expectation with begin/rescue, which is what I've done in other places I find this pattern useful.

Does anybody have any suggestions that may help? I've exhausted my own ideas.

Thanks
Ash


[1] https://github.com/celluloid/celluloid/wiki/Frequently-Asked-Questions#q-how-can-i-raise-an-exception-in-the-caller-without-crashing-the-receiver


-- 
http://www.patchspace.co.uk/
http://www.linkedin.com/in/ashmoran

-------------- next part --------------
A non-text attachment was scrubbed...
Name: smime.p7s
Type: application/pkcs7-signature
Size: 4837 bytes
Desc: not available
URL: <http://rubyforge.org/pipermail/rspec-users/attachments/20130813/8d79f646/attachment.bin>


More information about the rspec-users mailing list