sendException() rather than throw when sending to shutdown actors
In 1.2, if an actor has been stopped at the moment you send a message, you get an immediate "you need to start the actor" ActorInitializationException.
If it gets stopped just a moment after you send, then you get a timeout on the future.
In 2.0, it looks like #894 fixes it so you get an ActorKilledException rather than waiting for the timeout. However, just looking at the source (full disclosure, I did not test), I think you still get the ActorInitializationException if you send the message the millisecond after instead of the millisecond before the actor is stopped.
To handle this I think you have to write some thing like:
I think it would be convenient if any and all exceptions ended up in the Future.
It might be even nicer if sending to a not-yet-started actor is treated as a programmer mis-use of the API so throws right away as it does now, but sending to a started-then-stopped actor is treated the same as having the actor stopped before it processes the message.
In the non-future case it may be more consistent if sending to a shutdown actor silently does nothing... that would look like this I guess since sendException does nothing on non-Future?
and then
I guess one problem with this change is that the
However, my impression is that if you're relying on getting the
If it gets stopped just a moment after you send, then you get a timeout on the future.
In 2.0, it looks like #894 fixes it so you get an ActorKilledException rather than waiting for the timeout. However, just looking at the source (full disclosure, I did not test), I think you still get the ActorInitializationException if you send the message the millisecond after instead of the millisecond before the actor is stopped.
To handle this I think you have to write some thing like:
def tryAsk(message: Any) = {
// "?" will throw by default on a stopped actor; we want to put an exception
// in the future instead to avoid special cases
try {
actor ? message
} catch {
case e: ActorInitializationException =>
val f = new DefaultCompletableFuture[Any]()
f.completeWithException(new ActorKilledException("Actor was not running"))
f
}
}
I think it would be convenient if any and all exceptions ended up in the Future.
It might be even nicer if sending to a not-yet-started actor is treated as a programmer mis-use of the API so throws right away as it does now, but sending to a started-then-stopped actor is treated the same as having the actor stopped before it processes the message.
In the non-future case it may be more consistent if sending to a shutdown actor silently does nothing... that would look like this I guess since sendException does nothing on non-Future?
def postMessageToMailbox(message: Any, channel: UntypedChannel): Unit =
if (isRunning) dispatcher dispatchMessage new MessageInvocation(this, message, channel)
else if (isShutdown) channel.sendException(new ActorKilledException("Actor is stopped"))
else throw new ActorInitializationException("Actor has not been started")
and then
postMessageToMailboxAndCreateFutureResultWithTimeout
would have the analogous change except sendException would succeed instead of no-op.I guess one problem with this change is that the
tryTell
vs. tell
distinction becomes pretty pointless, since tryTell
could only be used to paper over what's essentially a bug (sending messages to an actor you didn't start).However, my impression is that if you're relying on getting the
ActorInitializationException
your code is already broken with fire-and-forget (unless the actor is immortal), since the actor could be stopped before it processes the message anyway... so it's almost always right to use tryTell
and assume messages can be lost. And if you're relying on ActorInitializationException
with ask
then you have to be handling ActorKilledException
set on the future anyway, so ActorInitializationException
is just an extra duplicate codepath.
Leave a comment
Thank you very much for this thorough write-up and analysis! Internally, we have been discussing how to make the semantics more consistent for quite some time now, and we will likely end up doing something like:
There are a few tickets involved in doing this, hopefully I will find them all and link them to this one.
- `tell` will never throw an exception, because
- actors are always fully started from the moment they are born and
- sending to a dead actor fails silently (there is no guaranteed processing anyway)
- `ask` will be identical to `tell`, but it will create a local temporary actor which is used as sender reference, and this actor will complete the Future
- the local temporary actor will monitor the life-cycle of the `ask` target and complete it with `ActorKilledException` upon death
There are a few tickets involved in doing this, hopefully I will find them all and link them to this one.
Hey Havoc,
Roland is spot on, thanks for the great analysis.
This will be fixed as Roland says for 2.x
Roland is spot on, thanks for the great analysis.
This will be fixed as Roland says for 2.x