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 : %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 : %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
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
# 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 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:
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.