Pykka 2.0 released with better ergonomics and performance improvements

I’ve finally released Pykka 2.0, the first major update to Pykka in almost six years.

Pykka is a Python implementation of the actor model. The actor model introduces some simple rules to control the sharing of state and cooperation between execution units, which makes it easier to build concurrent applications.

Pykka 2.0 is a major release because it is backwards incompatible in several minor ways. However, the backwards incompatible changes should only affect quite narrow use cases. Mopidy and its extensions, which is the largest open source ecosystem I know of that uses Pykka, runs unmodified on Pykka 2.0.

In this blog post I’ll go through some of the more important improvements in 2.0.

Actor messages no longer restricted to be dicts

Up until now, Pykka has had the in retrospect quite odd limitation that messages sent from an actor to another had to be a dict. The only reason for this was that Pykka added a couple of pykka_ prefixed keys to the dict to keep track of things like the reply future.

In Pykka 2.0, messages are instead wrapped in a lightweight Envelope object that keeps track of Pykka’s metadata. With the Envelope, there are no longer any constraints on the type of object you use as a message, and you are free to use plain strings or instances of your own classes as messages.

from collections import namedtuple
import time

import pykka

# namedtuples are used as messages
Start = namedtuple('Start', ['target'])
Ping = namedtuple('Ping', ['source'])
Pong = namedtuple('Pong', ['source'])

class Pinger(pykka.ThreadingActor):
    def on_receive(self, message):
        if isinstance(message, Start):
            print('Starting...')
            message.target.tell(Ping(source=self.actor_ref))
        elif isinstance(message, Pong):
            time.sleep(0.1)
            print('Ping')
            message.source.tell(Ping(source=self.actor_ref))

class Ponger(pykka.ThreadingActor):
    def on_receive(self, message):
        if isinstance(message, Ping):
            time.sleep(0.1)
            print('Pong')
            message.source.tell(Pong(source=self.actor_ref))

# Start both actors
pinger_ref = Pinger.start()
ponger_ref = Ponger.start()

# Ask the pinger to ping the ponger
pinger_ref.tell(Start(target=ponger_ref))

# Let them ping-pong for a short while
time.sleep(2)

# Clean up and exit
pinger_ref.stop()
ponger_ref.stop()

Actor properties are not accessed when creating an actor proxy

As of Pykka 2.0, properties on actors are no longer accessed when introspecting the actor as part of creating a proxy to the actor. For actors that have properties that do non-trivial work, this is a major performance improvement.

This change alone decreased the runtime of Mopidy’s test suite from 30s to 14s on one computer, without any changes at all to the Mopidy code. Due to Mopidy’s legacy properties, due to be removed in Mopidy 3.0, which are doing lots of non-trivial work, including network access, this change will also greatly reduce Mopidy’s startup time on computers with slow network connections.

import time

import pykka

class SlowPropertiesActor(pykka.ThreadingActor):
    @property
    def foo(self):
        time.sleep(1)
        return 'foo'

actor_ref = SlowPropertiesActor.start()

Using Pykka 1.2.1:

In [2]: %timeit actor_ref.proxy()
1 s ± 1.24 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Using Pykka 2.0.0:

In [2]: %timeit actor_ref.proxy()
184 µs ± 575 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

That’s a 5400x speedup in this example. With more properties and maybe even network access in the properties, the speedup can be a lot larger.

Compatibility with mocks

The actor proxy introspection has been improved to work nicely with mocks. It will now behave as expected when you mock out methods and properties when testing your actors.

Also, an initial section on testing actors have been added to the docs.

Waiting on futures with await

If using Python 3.5+, you can now use the await keyword to get the result from a future. Note that if you need a timeout when waiting for a future result, you still need to use the get() method.

# Waiting forever for a future result with `.get()`
result = future.get()
# Equivalent, using `await`
result = await future

# Waiting for a future with a timeout
result = future.get(timeout=2)

Function for marking objects as traversable

Before Pykka 2.0, an object was marked as traversable by the actor proxy by adding a magic pykka_traversable attribute to the traversable object:

import pykka

class AnActor(pykka.ThreadingActor):
    playback = Playback()

class Playback(object):
    pykka_traversable = True

    def play(self):
        return True

proxy = AnActor.start().proxy()
assert proxy.playback.play().get() is True

This still works in Pykka 2.0, but it has several drawbacks:

  • It pollutes your class, which might otherwise be totally agnostic to Pykka’s existence.
  • In the actor class, it is not visible what objects are traversable, as it is defined elsewhere.
  • It is prone to typos which are not caught by linters because any attribute name is valid. I’ve more than once experienced time consuming bugs simply because I’ve typoed pykka_traversable.

Pykka 2.0 adds a function pykka.traversable() which can be used either as a marker function in the actor class:

class AnActor(pykka.ThreadingActor):
    playback = pykka.traversable(Playback())

class Playback(object):
    def play(self):
        return True

Or it can be used as a decorator on the class of the traversable object:

class AnActor(pykka.ThreadingActor):
    playback = Playback()

@pykka.traversable
class Playback(object):
    def play(self):
        return True

Upgrading to 2.0

The changelog has all the details on the backwards incompatibilities, but in general you should be able to upgrade to Pykka 2.0 without any changes to your code, simply by running:

pip install --upgrade pykka

Pykka 2.0 brings better ergonomics and in some cases improved performance. It addresses all of the minor bugs reported over the last few years. All in all, it should be a clear step up from Pykka 1.2.

Please give it a try and let me know in GitHub issues if you run into any problems.