jodal / pykka

🌀 Pykka makes it easier to build concurrent Python applications.
https://pykka.readthedocs.io
Apache License 2.0
1.19k stars 106 forks source link

Sender attribute? #40

Open drozzy opened 10 years ago

drozzy commented 10 years ago

Is there an equivalent of a sender attribute on the actor? This is to allow me to send messages to the "sender" --- and is drastically different from the "ask" semantics.

Consider, for example, a child actor keeping track of "failures" and sending a message to parent saying "Failure Limit Reached".

In this case the semantics of the parent would be something like this:

  1. Spawn Child actor.
  2. Listen to "Failure Limit Reached" messages and restart or replace child actor with a new one.

This cannot be accomplished with an "ask" --- since once the child is spawned, the parent cares not for it and carries on with its duties.

P.S.: As a side note, documentation seems to favor the "ask" way of doing things (see "replying to messages" in pykka docs), which is not how the general actor system usually functions. The preferred way is to use "tell" and only resort to "ask" in select cases. Akka also prefers tell: http://doc.akka.io/docs/akka/snapshot/scala/actors.html

drozzy commented 5 years ago

This is me from the future - I suggest you guys take a look at Erlang instead of Akka for inspiration. This is what I ended up using instead.

jodal commented 5 years ago

Thanks for the tip!

We now have the Envelope wrapper around messages, so adding extra metadata like a sender attribute is now straight forward. The open question is how to best expose it to on_receive(). I see two alternatives:

I'm open to other suggestions and to hear what expectations you have for this from other actor systems.

Andrei-Pozolotin commented 5 years ago
  • Before calling on_receive(), set self.sender = envelope.sender

please publish a release with this feature, to verify its usability in practice

akatashev commented 4 years ago

In Akka it works the following way:

ActorCell class has a property:

var currentMessage: Envelope = _

Envelope case class has a message and a sender:

final case class Envelope private (val message: Any, val sender: ActorRef)`

sender method looks like this:

  final def sender(): ActorRef = currentMessage match {
    case null                      => system.deadLetters
    case msg if msg.sender ne null => msg.sender
    case _                         => system.deadLetters
  }

Following this pattern, in Pykka it could be something like: In _envelope.py:

class Envelope:
    """
    Envelope to add metadata to a message.

    This is an internal type and is not part of the public API.

    :param message: the message to send
    :type message: any
    :param reply_to: the future to reply to if there is a response
    :type reply_to: :class:`pykka.Future`
    """

    # Using slots speeds up envelope creation with ~20%
    __slots__ = ["message", "sender", "reply_to"]

    def __init__(self, message, sender=None, reply_to=None):
        self.message = message
        self.reply_to = reply_to
        self.sender = sender
...

In _actor.py:

    def _actor_loop(self):
        """
        The actor's event loop.

        This is the method that will be executed by the thread or greenlet.
        """
        try:
            self.on_start()
        except Exception:
            self._handle_failure(*sys.exc_info())

        while not self.actor_stopped.is_set():
            envelope = self.actor_inbox.get()
            self.current_message = envelope
...

and

def _sender(self):
    sender = self.current_message.sender
    if not sender:
        sender = DeadLetters(self)
    return sender

where

class DeadLetters(ActorRef):
    def __repr__(self):
        return f"<DeadLetters of {self}>"

    def tell(self, message):
        logger.warning("%s: Received message %s via 'tell'.", self.__repr__(), message)

    def ask(self, message, block=True, timeout=None):
        logger.warning("%s: Received message %s via 'ask'.", self.__repr__(), message)

or something like this. Just to cover a situation when an actor tries to send something to a sender and this sender is not an actor. This DeadLetters pseudo-ActorRef could be pre-created, but I don't expect lots of messages to end there, so pre-creating it for every actor could be a waste of memory.

I don't mean, that's a great solution, but that's an example of how it's implemented in Akka Classic.

Surely, it would be necessary to modify tell and ask methods a bit. If we forget about Akka ! method and take a look at its tell method, it looks like this:

  final def tell(msg: Any, sender: ActorRef): Unit = this.!(msg)(sender)

So, to have something similar, we would need to do something like:

    def tell(self, message, sender=None):
        """
        Send message to actor without waiting for any response.

        Will generally not block, but if the underlying queue is full it will
        block until a free slot is available.

        :param message: message to send
        :param sender: ActorRef of a sender
        :type message: any

        :raise: :exc:`pykka.ActorDeadError` if actor is not available
        :return: nothing
        """
        if hasattr(sender, "actor_ref"):
            sender = sender.actor_ref

        if not self.is_alive():
            raise ActorDeadError(f"{self} not found")
        self.actor_inbox.put(Envelope(message, sender=sender))

And then use it from an actor like:

def on_receive(self, message):
    if message == "Hello!":
        self._sender().tell("Why, hello, good sir!", self)

That's not pretty, but since we don't have implicit variables right now I don't see how to make it prettier. As an unexpected plus it would give us a chance to send a message "on behalf" of some other actor. This can be useful sometimes.

MaksSieve commented 1 month ago

any updates?