When making synchronous calls, exceptions which occur in the context of the
receiver are automatically reraised in the caller just like any other Ruby
object, regardless of if you're using any actor-specific features like
linking or trapping exits. It will also crash the receiver.
That makes sense.
But when making asynchronous calls:
Your parallel map is a perfect example of what I'd actually want here: If
an
I think reraising the original exception in the caller context gives the
caller appropriate context to bail out of whatever they're doing and avoid
making subsequent calls at all. Other threads may be trying to make calls,
and if an exception entirely unrelated to the calls they're making is
raised because the actor is dead, I think that'd be rather confusing.
That makes a lot of sense.
Still, I shouldn't have to create an entire new actor, link it to your actor,
and have it trap errors in order to find the actual exception I caused which
lead to the actor's death. Maybe it's appropriate for bang methods to return
some object which can be used to retrieve an exception?
- How do you handle cycles?
I don't, but they can be detected if you don't mind a bit of a performance
penalty. For that I need to track chains of synchronous calls and detect if
the receiver of a given method exists earlier in the call chain. If so,
Celluloid can raise an exception in the caller context indicating that a
deadlock would occur. This is a bit of a glaring deficiency right now.
That's what I was trying to do, except I wasn't planning to deadlock. I was
planning to allow the call... somehow. Basically, if you had any sort of
pattern where two objects call methods on each other, it should work the way
it does synchronously.
I think this makes sense, semantically. After all, if an actor calls a method
on itself, we don't get any sort of deadlock. If an actor calls a method on
another object running in the same thread, which then calls a method on the
actor, at least with my implementation, this also doesn't deadlock -- and in
yours, if I pass 'self' around, we get the same result. Why should it be
different if I call a method on another _actor_ which then calls a method on
me?
Still, it's tricky to come up with an efficient way to do this, and I never
managed to get anything to work, no matter how inefficient.
First, 'self' wouldn't be an actor reference, it'd be the object itself,
Yes. I provide Celluloid.current_actor to use in lieu of self. This feels a
bit ugly, but I don't know of any way to redefine self (nor do I think
that'd be a particularly good idea either)
So, there is a way, but you probably won't like it...
One experiment I did here was:
- Grab all methods, stuff them in a hash, and undef them.
- When a method is called, intercept it like a proxy, and do whatever I need
to do to get it to the right thread.
- To actually call the method, grab the method object, bind it to self, and
apply.
It's not really redefining self, but it accomplishes what's needed here.
However, I suspect it breaks all kinds of inheritance, unless I also absorb
that kind of functionality -- that is, whenever something inherits from this
class, give it a clone of the hash to start with.
One advantage to this approach is that I could very easily allow some methods
to require the actor thread, and some methods to run in the calling thread --
by default, they run in the actor thread. The obvious application is when a
method really doesn't need to involve the actor:
class Sheen
include Suit
# define a new threadsafe method
threadsafe :status do
:winning
end
end
But maybe you want to anyway:
class Sheen
include Suit
attr_reader :status, :sober
def initialize
@status = :winning
@sober = true
end
def fall_off_wagon!
@status = :WINNING
@sober = false
end
def is_off_wagon?
!sober && status == :WINNING
end
threadsafe :hello do
if is_off_wagon?
puts 'WINNING!!!'
else
puts 'Hi.'
end
end
end
It makes sense that fall_off_wagon! and is_off_wagon? should run on the actor
thread. It makes sense that the 'hello' method doesn't really need to run on
the actor thread, and maybe it's a performance improvement that the Sheen
thread doesn't actually have to talk, or ever wait for output, etc. I'm really
reaching here, because I don't actually have a real application for this, but
I don't think it's entirely unreasonable -- kind of like the Java
'synchronized' keyword, except message-passing behavior is the default.
But notice that the 'threadsafe' call doesn't have to call 'self' at all. In
fact, that syntax is actually syntactic sugar for:
def hello
...
end
threadsafe :hello
I'm still just writing normal methods, but every method call, whether it's to
'self' or not, is still going through the same logic to determine whether or
not it needs to run on the Sheen thread.
I was much more interested in getting the semantics right, to show that it can
be done, rather than making it performant and immediately useful. Like you, I
wanted to use this to sort of prototype those semantics, with the hope that
they would get into something like Reia eventually. (I started this before I
heard of Reia, and probably before Reia was in any way practical, so I wasn't
deliberately reinventing the wheel.)
I don't know of a better solution. This is the same approach Erlang uses.
The only evolution it's seen in recent history is systems like Ulf Wiger's
gproc.
Looking again, maybe the supervisor already does this?
supervisor = Sheen.supervise "Charlie Sheen"
charlie = supervisor.actor
This would solve both problems, right? (Assuming the supervisor is itself
threadsafe.) It could use some sugar, but I'm not entirely sure how.
That's an interesting approach, but a bit different than the one I'm
shooting for in Celluloid, where I want concurrent objects to quack like
normal Ruby objects as much as possible.
And this does quack like a normal Ruby object, unless something goes wrong and
an exception is raised. But I was never quite satisfied with how exceptions
were dealt with. For one thing, it's not OK that someone might ignore a future
and never see the exception.