Search code examples
pythonpykka

Pykka's behaviour with @property setters


I'm playing with pykka's actor model, and found some funny behaviour. Here's a demo that

  1. Launches an actor
  2. Gets a proxy to it
  3. Sets one of its @properties

Here's the code:

import time
import pykka

from sys import version_info as python_version
if python_version > (3, 0):
    from _thread import get_ident
else:
    from thread import get_ident

startTime = time.time()

def debug(msg, prefix='MSG'):
    msgProc = "%s (thread #%s @ t = %.2fs): %s" % (prefix,get_ident(), time.time() - startTime, msg)
    print(msgProc)

def mainThread():

    debug('Launching support actor...', prefix='MAIN')
    supportRef = supportThread.start()

    debug('Getting support proxy...', prefix='MAIN')
    supportProxy = supportRef.proxy()

    debug('Getting myVal obj...', prefix='MAIN')
    obj = supportProxy.myVal
    debug(obj, prefix='MAIN')

    debug('Setting myVal obj...', prefix='MAIN')
    supportProxy.myVal = 2

    debug('Setting myVal obj...', prefix='MAIN')
    supportProxy.myVal = 3

    supportProxy.stop()


class supportThread(pykka.ThreadingActor):

    def __init__(self):
        super(supportThread, self).__init__()

        self._myVal = 0

    @property
    def myVal(self):
       debug("Getting value", prefix='SUPPORT')
       return self._myVal

    @myVal.setter
    def myVal(self, value):

       debug("Setting value: processing for 1s...", prefix='SUPPORT')
       time.sleep(1)

       debug("Setting value: done", prefix='SUPPORT')
       self._myVal = value

mainThread()

The output looks like this:

MAIN (thread #16344 @ t = 0.00s): Launching support actor...
MAIN (thread #16344 @ t = 0.00s): Getting support proxy...
SUPPORT (thread #16344 @ t = 0.00s): Getting value
MAIN (thread #16344 @ t = 0.00s): Getting myVal obj...
MAIN (thread #16344 @ t = 0.00s): <pykka.threading.ThreadingFuture object at 0x0000000002998518>
MAIN (thread #16344 @ t = 0.00s): Setting myVal obj...
SUPPORT (thread #16248 @ t = 0.00s): Getting value
SUPPORT (thread #16248 @ t = 0.00s): Setting value: processing for 1s...
SUPPORT (thread #16248 @ t = 1.00s): Setting value: done
MAIN (thread #16344 @ t = 1.00s): Setting myVal obj...
SUPPORT (thread #16248 @ t = 1.01s): Setting value: processing for 1s...
SUPPORT (thread #16248 @ t = 2.01s): Setting value: done
[Finished in 2.3s]

I have a couple of questions here.

  1. Why does the getter supportThread.myVal() get called in the main thread's context when .proxy() is called?
  2. Why do the lines supportProxy.myVal = <a new value> result in the main thread waiting for the actor to complete?

This seems like a bug to me: I thought that the proxy should only block execution if .get() is called on a ThreadingFuture. Or is this intended?


Solution

  • Disclaimer: I'm the author of Pykka.

    Aside: Pykka isn't dead, it just works quite well for what it was made for: providing an concurrency abstraction for the Mopidy music server and its 100+ extensions.

    Pykka's behavior with properties isn't optimal, but there's a reason it is the way it is.

    1. To create a proxy object Pykka must introspect the API of the target object. When testing if the attributes available on the target object are callables, attributes, or "traversible attributes", getattr() is called once on each attribute. This causes the property getter to be called. See Proxy._get_attributes() and Actor._get_attribute_from_path() for .

    2. As there is no way in Python to capture a return value from a property setter, property setting on Pykka proxy takes the safe default of waiting on the setter to complete so that any exceptions raised in the setter can be reraised on the call site in mainThread(). The alternative would be to leave any exceptions raised by property setters unhandled. See Proxy.__setattr__() for details.

    In summary, properties "works" with Pykka, but you get more control by using method calls, as you then always get a future back and can decide yourself if you need to wait for the result or not.

    Whether you are using Pykka or not, I find it good practice to not do any expensive work in property getters, but instead use proper methods to do "expensive" work.

    API design directly affects how your users will use the API:

    • An object with a property invites to repeated use of the same property, and thus repeated recalculation. Keep properties dead simple and cheap.
    • An object exposing a method returning a result will usually lead to a caller to keep the result in a variable and reuse the same result instead of calling the method multiple times. Use methods for any non-trivial work. If it is really expensive, consider another prefix than get_, for example load_, fetch_, or calculate_, further indicating that the user should keep a handle on the result and reuse it.

    To follow up on these principles myself, Mopidy's core API migrated from using lots of properties to using getters and setters a long time ago. In the next major release, all the properties will be removed from Mopidy's core API. Due to the way proxy creation works, this cleanup will reduce the startup time of Mopidy quite a bit.